# 第二阶段 · 模块二 · 第四节:自定义工具开发
如何开发自定义工具?buildTool 工厂函数如何使用?MCP 工具的接口是什么?插件系统如何扩展工具?
Claude Code 全局架构
┌─────────────────────────────────────────────────────────────────────┐
│ 工具/服务层 │
│ │
│ tools.ts │
│ ├── 内置工具(Read, Write, Bash...) │
│ ├── MCP 工具 ← 本节 │
│ └── 插件工具 │
└─────────────────────────────────────────────────────────────────────┘
源码位置:`src/Tool.ts` 第 783 行
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>;
}
源码位置:`src/Tool.ts` 第 757 行
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?: unknown) => false,
isReadOnly: (_input?: unknown) => false,
isDestructive: (_input?: unknown) => false,
checkPermissions: (input) =>
Promise.resolve({ behavior: 'allow', updatedInput: input }),
toAutoClassifierInput: (_input?: unknown) => '',
userFacingName: (_input?: unknown) => '',
};
问 1:buildTool 的作用是什么?
// buildTool 提供工具的默认实现
// 开发者只需要覆盖需要自定义的部分
const MyTool = buildTool({
name: 'myTool',
description: 'My custom tool',
async execute(args) {
return { content: 'Hello!' };
},
// 其他属性使用 TOOL_DEFAULTS 的默认值
});
问 2:为什么需要 TOOL_DEFAULTS?
// 问题:如果每个工具都要定义所有属性,会很繁琐
// 解决方案:提供默认值,工具只需覆盖必要的属性
const TOOL_DEFAULTS = {
isEnabled: () => true, // 默认启用
isConcurrencySafe: () => false, // 默认不安全
checkPermissions: () => allow, // 默认允许
// ...
};
问 3:ToolDef 接口包含哪些属性?
interface ToolDef {
name: string;
description: string;
inputSchema: z.ZodSchema;
outputSchema?: z.ZodSchema;
execute(args: unknown, context: ToolContext): Promise<ToolResult>;
// 可选覆盖
isEnabled?: () => boolean;
isConcurrencySafe?: (input?: unknown) => boolean;
isReadOnly?: (input?: unknown) => boolean;
checkPermissions?: (input: unknown) => Promise<PermissionResult>;
}
MCP(Model Context Protocol)是一个允许外部服务为 Claude 提供工具的协议。
┌─────────────────────────────────────────────────────────────────────┐
│ Claude Code │
│ └── QueryEngine │
│ └── tools.ts │
│ └── MCPTool ────────────────┐ │
└──────────────────────────────────────┼─────────────────────────────┘
│
┌─────────────▼─────────────┐
│ MCP Client │
│ └── mcp1.ts │
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ MCP Server │
│ (例如:GitHub, Slack) │
└─────────────────────────┘
源码位置:`src/tools/MCPTool/MCPTool.ts`
export const MCPTool = buildTool({
isMcp: true,
name: 'mcp',
maxResultSizeChars: 100_000,
async description() {
return DESCRIPTION;
},
async prompt() {
return PROMPT;
},
get inputSchema(): InputSchema {
return inputSchema(); // 动态 schema
},
async call() {
return { data: '' };
},
async checkPermissions(): Promise<PermissionResult> {
return {
behavior: 'passthrough',
message: 'MCPTool requires permission.',
};
},
renderToolUseMessage,
renderToolResultMessage,
});
问 1:MCP 工具的输入模式为什么用 lazySchema?
// MCP 工具的输入模式是动态的,由 MCP Server 提供
// 使用 lazySchema 延迟解析
export const inputSchema = lazySchema(() =>
z.object({}).passthrough() // 允许任何输入
);
// 实际模式由 MCP Server 在运行时提供
// Claude Code 不知道也不需要知道具体的 schema
问 2:MCP 工具如何调用外部服务?
// MCPTool.call() 被 MCP Client 覆盖
// 实际的调用逻辑在 mcpClient.ts 中
class MCPTool {
async call(args: unknown, context: ToolContext) {
// 1. 从 MCP Client 获取工具
const mcpClient = getMCPClient(context);
// 2. 调用 MCP Server 的工具
const result = await mcpClient.callTool({
name: this.mcpToolName, // MCP Server 提供的工具名
args: args,
});
// 3. 返回结果
return { data: result };
}
}
问 3:MCP 工具如何注册?
// MCP 工具在启动时动态注册
async function registerMCPtools(mcpClients: MCPClient[]) {
const tools: Tool[] = [];
for (const client of mcpClients) {
// 获取 MCP Server 提供的工具列表
const mcpTools = await client.listTools();
for (const mcpTool of mcpTools) {
// 为每个 MCP 工具创建一个 MCPTool 实例
tools.push(createMCPTool(client, mcpTool));
}
}
return tools;
}
// 插件可以注册自定义工具
interface Plugin {
name: string;
tools?: Tool[]; // 插件提供的工具
// ...
}
// 假设开发一个天气插件
const weatherPlugin: Plugin = {
name: 'weather',
tools: [
buildTool({
name: 'get_weather',
description: 'Get the current weather for a location',
inputSchema: z.object({
city: z.string().describe('City name'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
async execute(args) {
const { city, unit } = args;
// 调用天气 API
const weather = await fetchWeather(city, unit);
return {
content: `The weather in ${city} is ${weather.temp}°${unit === 'celsius' ? 'C' : 'F'}`,
};
},
}),
],
};
问 1:插件工具和内置工具有什么区别?
| 方面 | 内置工具 | 插件工具 |
| 注册方式 | 静态定义在 tools.ts | 动态注册 |
| 加载时机 | 程序启动时 | 可延迟加载 |
| 隔离性 | 与主程序共享 | 可隔离 |
| 分发方式 | 随 Claude Code 发布 | 独立发布 |
问 2:插件工具如何访问 Claude Code 的上下文?
// 通过 ToolContext 传递上下文
interface ToolContext {
toolUseId: string;
toolUseContext: ToolUseContext;
messages: Message[]; // 对话历史
cwd: string; // 当前工作目录
// ...
}
async execute(args: unknown, context: ToolContext) {
// 访问对话历史
const recentMessages = context.messages.slice(-5);
// 访问当前目录
const currentDir = context.cwd;
// 使用工具上下文
const { signal } = context.toolUseContext.abortController;
}
问 3:插件工具的权限如何处理?
// 插件工具使用 checkPermissions 实现权限检查
const myTool = buildTool({
name: 'myTool',
checkPermissions: async (args, context) => {
// 自定义权限逻辑
if (args.sensitive && !context.user.isAdmin) {
return {
behavior: 'deny',
message: 'Admin permission required',
};
}
return { behavior: 'allow' };
},
});
// src/tools/MyCustomTool/MyCustomTool.ts
import { buildTool } from '../../Tool.js';
import { z } from 'zod/v4';
export const MyCustomTool = buildTool({
name: 'my_custom_tool',
description: 'Description of what this tool does',
inputSchema: z.object({
param1: z.string().describe('First parameter'),
param2: z.number().optional().describe('Optional second parameter'),
}),
// 工具执行逻辑
async execute(args, context) {
// 1. 验证参数(Zod 会自动验证)
// 2. 执行业务逻辑
const result = await doSomething(args.param1, args.param2);
// 3. 返回结果
return {
content: result,
};
},
// 可选:自定义权限检查
checkPermissions: async (args) => {
if (args.param1.startsWith('restricted:')) {
return { behavior: 'deny', message: 'Access denied' };
}
return { behavior: 'allow' };
},
// 可选:标记为只读(影响并发执行策略)
isReadOnly: (args) => args.param1.startsWith('read:'),
});
// 在 tools.ts 中注册
import { MyCustomTool } from './tools/MyCustomTool/MyCustomTool.ts';
export const getTools = (permissionContext): Tools => {
const baseTools = [
// ... 内置工具
MyCustomTool,
];
return filterToolsByDenyRules(baseTools, permissionContext);
};
// 自定义工具在 UI 中的显示方式
import { renderToolUseMessage, renderToolResultMessage } from './UI.js';
export const MyCustomTool = buildTool({
name: 'my_custom_tool',
// 自定义工具调用显示
renderToolUseMessage: (toolUse, children) => {
return (
<div class="tool-use my-custom-tool">
<span class="tool-name">my_custom_tool</span>
{children}
</div>
);
},
// 自定义结果显示
renderToolResultMessage: (result) => {
return <div class="tool-result">{result.content}</div>;
},
});
问题:什么时候使用 MCP,什么时候使用插件系统?
答案:
| 场景 | 推荐方案 | 原因 |
| 外部服务(GitHub, Slack) | MCP | 标准化协议,动态发现 |
| 本地工具(数据库、文件系统) | 插件 | 直接访问本地资源 |
| 需要编译打包的工具 | 插件 | 可优化性能 |
| 临时/实验性工具 | MCP | 无需重新编译 Claude Code |
问题:当自定义工具不工作时,如何调试?
答案:
1. 使用 CLI 的 --debug 模式:
claude --debug "your prompt"
2. 检查工具注册:
// 在工具中添加日志
async execute(args, context) {
console.log('[MyTool] Called with:', args);
try {
const result = await doSomething(args);
console.log('[MyTool] Result:', result);
return result;
} catch (error) {
console.error('[MyTool] Error:', error);
throw error;
}
}
3. 使用 test 模式:
// 创建测试
const testResult = await MyTool.execute({ param1: 'test' });
console.log(testResult);
问题:如何将自定义工具分享给其他人?
答案:
方案 1:作为插件发布
# 创建插件包
npm init @my-org/claude-code-plugin-weather
# 实现插件接口
export const plugin = {
name: 'weather',
tools: [getWeatherTool],
};
# 发布
npm publish
方案 2:作为 MCP Server 发布
# 创建 MCP Server
npm init @my-org/mcp-server-weather
# 实现 MCP 协议
const server = new MCPServer({
name: 'weather',
tools: [{
name: 'get_weather',
description: 'Get weather',
inputSchema: { ... },
}],
});
# 启动 MCP Server
npx @my-org/mcp-server-weather
| 文件 | 核心内容 |
| `src/Tool.ts` | buildTool 工厂函数 |
| `src/tools/MCPTool/MCPTool.ts` | MCP 工具实现 |
| `src/utils/plugins/` | 插件系统 |
下一节我们将深入 命令系统:
- 斜杠命令(/help, /compact)的实现
- 命令解析器
- 内置命令列表
*- 第一轮:□ 事实准确性*
*- 第二轮:□ 深度与洞见*
*- 第三轮:□ 可读性与价值*