第四节:自定义工具开发
工具注册与调用

作者:小学子 📚 | 日期:2026年4月2日 | 第二阶段 · 模块二


# 第二阶段 · 模块二 · 第四节:自定义工具开发

核心问题

如何开发自定义工具?buildTool 工厂函数如何使用?MCP 工具的接口是什么?插件系统如何扩展工具?


◇ 本节位置


        Claude Code 全局架构
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │  工具/服务层                                                        │
        │                                                                      │
        │  tools.ts                                                          │
        │  ├── 内置工具(Read, Write, Bash...)                              │
        │  ├── MCP 工具 ← 本节                                               │
        │  └── 插件工具                                                      │
        └─────────────────────────────────────────────────────────────────────┘
        


一、buildTool 工厂函数

1.1 源码位置

源码位置:`src/Tool.ts` 第 783 行


        export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
          return {
            ...TOOL_DEFAULTS,
            userFacingName: () => def.name,
            ...def,
          } as BuiltTool<D>;
        }
        

1.2 TOOL_DEFAULTS

源码位置:`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.3 五问分析

问 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 工具

2.1 什么是 MCP?

MCP(Model Context Protocol)是一个允许外部服务为 Claude 提供工具的协议。


        ┌─────────────────────────────────────────────────────────────────────┐
        │  Claude Code                                                      │
        │  └── QueryEngine                                                  │
        │      └── tools.ts                                                 │
        │          └── MCPTool ────────────────┐                             │
        └──────────────────────────────────────┼─────────────────────────────┘
                                               │
                                 ┌─────────────▼─────────────┐
                                 │  MCP Client              │
                                 │  └── mcp1.ts            │
                                 └─────────────┬─────────────┘
                                               │
                                 ┌─────────────▼─────────────┐
                                 │  MCP Server             │
                                 │  (例如:GitHub, Slack)  │
                                 └─────────────────────────┘
        

2.2 MCPTool 源码

源码位置:`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,
        });
        

2.3 五问分析

问 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;
        }
        


三、插件系统

3.1 插件工具接口


        // 插件可以注册自定义工具
        interface Plugin {
          name: string;
          tools?: Tool[];  // 插件提供的工具
          // ...
        }
        

3.2 插件工具示例


        // 假设开发一个天气插件
        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'}`,
                };
              },
            }),
          ],
        };
        

3.3 五问分析

问 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' };
          },
        });
        


四、自定义工具开发步骤

4.1 创建工具文件


        // 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:'),
        });
        

4.2 注册工具


        // 在 tools.ts 中注册
        
        import { MyCustomTool } from './tools/MyCustomTool/MyCustomTool.ts';
        
        export const getTools = (permissionContext): Tools => {
          const baseTools = [
            // ... 内置工具
            MyCustomTool,
          ];
        
          return filterToolsByDenyRules(baseTools, permissionContext);
        };
        

4.3 实现渲染(可选)


        // 自定义工具在 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>;
          },
        });
        


五、思考题

思考题 1:MCP 和插件系统的选择?

问题:什么时候使用 MCP,什么时候使用插件系统?

答案

场景推荐方案原因
外部服务(GitHub, Slack)MCP标准化协议,动态发现
本地工具(数据库、文件系统)插件直接访问本地资源
需要编译打包的工具插件可优化性能
临时/实验性工具MCP无需重新编译 Claude Code


思考题 2:如何调试自定义工具?

问题:当自定义工具不工作时,如何调试?

答案

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);
        


思考题 3:如何发布自定义工具?

问题:如何将自定义工具分享给其他人?

答案

方案 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)的实现

- 命令解析器

- 内置命令列表


*- 第一轮:□ 事实准确性*

*- 第二轮:□ 深度与洞见*

*- 第三轮:□ 可读性与价值*