ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Agent Harness Engineering 的“手”不够用?用 MCP 协议统一工具调用标准的 TypeScript 配置骨架

AI Agent Harness Engineering 的“手”不够用?用 MCP 协议统一工具调用标准的 TypeScript 配置骨架 1. 当 Agent 的“手”开始打架工具调用碎片化的真实困境如果你正在做 AI Agent 的 Harness Engineering大概率遇到过这种场面代码助手 Agent 已经接好了本地文件读写、Git 操作、终端命令执行跑得挺顺。结果产品说“再加个 Jira 查询吧”你打开项目一看工具注册表里已经躺着十几个函数每个函数的参数 Schema 写法都不一样有的用zod有的手写 JSON Schema有的干脆把参数拼成字符串让模型自己解析。更头疼的是换一个 Harness 框架——比如从自研的调度循环切到某个开源 Agent 框架——这些工具定义几乎要全部重写一遍。这就是 AI Agent 工具调用碎片化的典型症状。Agent 的“大脑”LLM越来越强但它的“手”工具调用层却因为缺乏统一标准而各自为政。每个 Harness 有自己的工具注册方式每个工具提供方有自己的接口约定结果就是工具复用率极低接入成本极高维护起来像在打地鼠。MCP 协议Model Context Protocol试图解决的就是这个问题。它把工具调用从“每个 Harness 自己定一套”变成“大家共用一套描述和调用标准”。你可以把它理解成 Agent 工具世界的 USB-C不管你是笔记本、手机还是平板接口形状统一了插上就能用。本文聚焦 TypeScript 项目场景给出一套可复制的 MCP 工具注册配置骨架和类型定义并带你走通本地验证工具调用链路的完整步骤。适合正在做 Agent 工程化、被工具接入折磨过的开发者。2. 前置准备TaoToken 接入与 TypeScript 项目环境在开始写 MCP 工具注册骨架之前需要先把模型调用通道准备好。我试过用 TaoToken 作为统一接入层它兼容 OpenAI 风格的接口同时支持 Claude Code、Coding Plan 等场景对于需要频繁切换模型做工具调用验证的 Harness Engineering 来说比较省事。2.1 获取 API Key 与配置环境变量首先到 TaoToken 控制台创建一个 API Key。地址是https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_harness_ts。创建完成后把 Key 写入项目根目录的.env文件# .env TAOTOKEN_API_KEYsk-你的实际key TAOTOKEN_BASE_URLhttps://taotoken.net/api注意API 地址不要加 UTM 参数直接用https://taotoken.net/api即可。如果你用的是 Claude Code 或 Anthropic 风格的调用可以参考接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_harness_ts。2.2 初始化 TypeScript 项目创建一个新的 TypeScript 项目安装 MCP 官方 SDK 和必要的类型依赖mkdir mcp-harness-skeleton cd mcp-harness-skeleton npm init -y npm install modelcontextprotocol/sdk zod npm install -D typescript types/node tsx然后在tsconfig.json中开启严格模式和 ESM 支持{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, strict: true, esModuleInterop: true, outDir: dist, rootDir: src }, include: [src/**/*] }在package.json中加上type: module和运行脚本{ type: module, scripts: { dev: tsx src/server.ts, build: tsc } }到这里项目骨架就准备好了。接下来进入核心部分用 TypeScript 定义 MCP 工具注册的标准结构。3. 可复制配置MCP 工具注册骨架与 TypeScript 类型定义MCP 协议的核心思路是工具提供方MCP Server用标准格式描述自己有哪些工具、每个工具接受什么参数、返回什么结果工具使用方MCP Client / Harness按照同样的标准去发现和调用。下面这套骨架可以直接复制到你的项目里。3.1 定义工具描述的类型结构先创建一个src/types.ts把工具注册需要的类型集中管理// src/types.ts import { z } from zod; /** MCP 工具的标准描述结构 */ export interface McpToolDefinitionT extends z.ZodTypeAny z.ZodTypeAny { /** 工具名称全局唯一建议用 snake_case */ name: string; /** 工具用途描述会直接进入模型上下文写清楚“什么时候用” */ description: string; /** 参数 Schema用 zod 定义运行时校验 生成 JSON Schema */ inputSchema: T; /** 工具执行函数入参类型由 inputSchema 推导 */ handler: (args: z.inferT) PromiseMcpToolResult; } /** 工具调用返回结构 */ export interface McpToolResult { content: Array{ type: text | resource; text?: string; resource?: { uri: string; mimeType: string; text: string }; }; isError?: boolean; } /** 工具注册表Harness 通过它统一发现和调用 */ export class McpToolRegistry { private tools new Mapstring, McpToolDefinition(); registerT extends z.ZodTypeAny(tool: McpToolDefinitionT): void { if (this.tools.has(tool.name)) { throw new Error(工具名称冲突: ${tool.name}); } this.tools.set(tool.name, tool as McpToolDefinition); } list(): McpToolDefinition[] { return Array.from(this.tools.values()); } get(name: string): McpToolDefinition | undefined { return this.tools.get(name); } /** 生成 MCP 标准的 tools/list 响应 */ toMcpToolList() { return this.list().map((tool) ({ name: tool.name, description: tool.description, inputSchema: zodToJsonSchema(tool.inputSchema), })); } }这里的关键点是用 zod 作为单一事实来源。参数校验、类型推导、JSON Schema 生成都从同一个定义出发避免手写 Schema 和实际校验逻辑不一致。3.2 实现 zod 到 JSON Schema 的转换MCP 协议要求工具的inputSchema是 JSON Schema 格式。写一个轻量转换函数覆盖常用类型即可// src/schema.ts import { z } from zod; export function zodToJsonSchema(schema: z.ZodTypeAny): Recordstring, unknown { if (schema instanceof z.ZodObject) { const shape schema.shape; const properties: Recordstring, unknown {}; const required: string[] []; for (const [key, value] of Object.entries(shape)) { properties[key] zodToJsonSchema(value as z.ZodTypeAny); if (!(value instanceof z.ZodOptional)) { required.push(key); } } return { type: object, properties, required }; } if (schema instanceof z.ZodString) { return { type: string, description: schema.description }; } if (schema instanceof z.ZodNumber) { return { type: number, description: schema.description }; } if (schema instanceof z.ZodBoolean) { return { type: boolean, description: schema.description }; } if (schema instanceof z.ZodOptional) { return zodToJsonSchema(schema.unwrap() as z.ZodTypeAny); } if (schema instanceof z.ZodEnum) { return { type: string, enum: schema.options }; } return { type: object }; }这段代码不追求覆盖所有 zod 特性但足够支撑大部分工具定义场景。如果你的工具参数更复杂可以按需扩展。3.3 注册两个示例工具创建src/tools.ts注册两个典型工具一个读文件一个查 Git 状态。这两个工具在 Harness Engineering 里出现频率很高。// src/tools.ts import { z } from zod; import { readFile } from node:fs/promises; import { exec } from node:child_process; import { promisify } from node:util; import { McpToolRegistry } from ./types.js; const execAsync promisify(exec); export function createToolRegistry(): McpToolRegistry { const registry new McpToolRegistry(); registry.register({ name: read_local_file, description: 读取本地文件内容。当需要查看项目中的源码、配置或文档时使用。, inputSchema: z.object({ path: z.string().describe(文件的绝对路径或相对于项目根目录的路径), encoding: z.enum([utf-8, ascii]).optional().describe(文件编码默认 utf-8), }), handler: async ({ path, encoding }) { try { const content await readFile(path, { encoding: encoding ?? utf-8 }); return { content: [{ type: text, text: content }], }; } catch (err) { return { content: [{ type: text, text: 读取失败: ${(err as Error).message} }], isError: true, }; } }, }); registry.register({ name: git_status, description: 查看当前 Git 仓库的状态包括修改、暂存和未跟踪文件。, inputSchema: z.object({ cwd: z.string().describe(Git 仓库根目录路径), short: z.boolean().optional().describe(是否使用简短输出格式), }), handler: async ({ cwd, short }) { try { const { stdout } await execAsync(git status${short ? --short : }, { cwd }); return { content: [{ type: text, text: stdout || 工作区干净 }], }; } catch (err) { return { content: [{ type: text, text: Git 命令执行失败: ${(err as Error).message} }], isError: true, }; } }, }); return registry; }注意description的写法不要只写“读取文件”而要写清楚“什么时候用”。模型在选择工具时描述的质量直接影响调用准确率。3.4 接入 MCP Server 标准协议最后创建src/server.ts把注册表挂到 MCP Server 上// src/server.ts import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema, } from modelcontextprotocol/sdk/types.js; import { createToolRegistry } from ./tools.js; const registry createToolRegistry(); const server new Server( { name: harness-tool-server, version: 1.0.0 }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () ({ tools: registry.toMcpToolList(), })); server.setRequestHandler(CallToolRequestSchema, async (request) { const tool registry.get(request.params.name); if (!tool) { return { content: [{ type: text, text: 未知工具: ${request.params.name} }], isError: true, }; } const parsed tool.inputSchema.safeParse(request.params.arguments); if (!parsed.success) { return { content: [{ type: text, text: 参数校验失败: ${parsed.error.message} }], isError: true, }; } return tool.handler(parsed.data); }); const transport new StdioServerTransport(); await server.connect(transport);这套骨架的核心价值在于工具定义、参数校验、协议响应三者统一。新增工具只需要在createToolRegistry里加一段注册代码不需要改 Server 逻辑也不需要手写 JSON Schema。4. 验证请求本地跑通工具调用链路配置写完了接下来验证它是否真的能跑通。分两步先用 MCP Inspector 做协议层验证再用 TaoToken 的模型对话做端到端验证。4.1 用 MCP Inspector 检查工具列表MCP 官方提供了一个调试工具modelcontextprotocol/inspector可以直接查看 Server 暴露的工具npx modelcontextprotocol/inspector npx tsx src/server.ts启动后浏览器会自动打开一个界面左侧能看到read_local_file和git_status两个工具点击每个工具可以查看它的 JSON Schema 和描述。如果这里能看到正确的工具列表说明协议层配置没问题。4.2 用模型对话验证工具调用协议层通了之后需要验证模型能否正确选择工具并生成合法参数。到 TaoToken 的模型对话页面创建一个测试会话https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_harness_ts。把上面toMcpToolList()生成的工具列表作为tools参数传给模型然后发一条测试消息{ model: claude-sonnet-4-20250514, messages: [ { role: user, content: 帮我看看 /Users/demo/project 这个仓库当前的 Git 状态用简短格式 } ], tools: [ { name: git_status, description: 查看当前 Git 仓库的状态包括修改、暂存和未跟踪文件。, inputSchema: { type: object, properties: { cwd: { type: string, description: Git 仓库根目录路径 }, short: { type: boolean, description: 是否使用简短输出格式 } }, required: [cwd] } } ] }如果模型返回的tool_calls里包含git_status并且arguments是{cwd: /Users/demo/project, short: true}说明工具描述和 Schema 的质量足够让模型正确理解。实测下来描述写得越具体模型选错工具的概率越低。4.3 完整链路从模型输出到工具执行把模型返回的tool_calls参数传给registry.get(name).handler()执行结果再作为tool角色的消息回传给模型。这一步在 Harness 里通常是一个循环async function runToolLoop(userMessage: string) { const messages [{ role: user, content: userMessage }]; const tools registry.toMcpToolList(); while (true) { const response await callModel({ messages, tools }); const choice response.choices[0]; if (choice.finish_reason tool_calls) { for (const call of choice.message.tool_calls) { const tool registry.get(call.function.name); const args JSON.parse(call.function.arguments); const result await tool!.handler(args); messages.push({ role: tool, tool_call_id: call.id, content: result.content.map((c) c.text).join(\n), }); } continue; } return choice.message.content; } }这段循环就是 Harness Engineering 里最核心的“手脑协同”逻辑。MCP 的价值在于tools的格式是标准的tool_calls的解析是标准的工具执行结果的回传格式也是标准的。换一个模型、换一个 Harness这套循环几乎不用改。5. 本篇常见错排查工具调用链路跑不通时问题通常集中在几个地方。下面按出现频率从高到低排列。5.1 工具列表为空或 Schema 格式错误现象MCP Inspector 里看不到工具或者模型返回“没有可用工具”。排查方向检查ListToolsRequestSchema的 handler 是否真的返回了tools数组检查zodToJsonSchema对z.ZodOptional的处理是否正确——如果 optional 字段被错误地放进了required部分模型会拒绝调用。另外确认inputSchema的顶层type是objectMCP 协议要求工具参数必须是对象类型。5.2 参数校验失败但模型生成的参数看起来没问题现象safeParse返回失败但打印出来的arguments肉眼看着是对的。常见原因是类型不匹配。比如模型把short传成了字符串true而不是布尔值true或者把数字传成了字符串。zod 的z.boolean()不会自动转换字符串。解决办法是在 Schema 里用z.coerce.boolean()做强制转换或者在 handler 里做一次规范化。另一个原因是模型把可选字段传成了null而 zod 的.optional()只接受undefined这时需要用.nullable().optional()。5.3 工具名称冲突导致注册失败现象启动时报工具名称冲突。MCP 协议要求工具名称在单个 Server 内唯一。如果多个模块各自注册了同名工具McpToolRegistry.register会直接抛错。建议在工具命名时加前缀比如file_read、git_status、jira_query避免不同模块之间撞名。如果确实需要覆盖可以在注册表里加一个override选项但更推荐从命名规范上解决。5.4 模型选错工具或反复调用同一个工具现象模型明明该调read_local_file却调了git_status或者调用失败后不换工具反复重试同一个。这通常不是代码问题而是工具描述的问题。description里要写清楚“什么时候用这个工具”和“什么时候不要用”。比如read_local_file的描述可以加上“仅用于读取文件内容不用于查看目录结构”。另外工具执行失败时返回的isError: true和错误信息要足够具体模型会根据错误信息决定下一步。如果错误信息只写“失败”模型很难做出正确判断。5.5 Stdio 传输下日志输出干扰协议通信现象MCP Inspector 能连上但工具调用没有响应或者连接随机断开。Stdio 传输模式下Server 的stdout被协议占用任何console.log都会污染 JSON-RPC 消息流。解决办法是把所有调试日志写到stderr用console.error而不是console.log。如果用了第三方库确认它没有往stdout打印内容。这个问题在本地开发时特别隐蔽因为单独跑 Server 看起来一切正常一接入 Harness 就出问题。6. 把工具调用标准落到你的 Harness 里回到最开始的问题Agent 的“手”不够用本质不是工具太少而是工具的接入和复用成本太高。MCP 协议提供的统一描述和调用标准让工具从“每个 Harness 自己适配”变成“一次定义、多处使用”。上面这套 TypeScript 骨架可以直接作为你项目里的工具注册层新增工具只需要加一段registry.register协议响应、参数校验、类型推导都自动完成。如果你正在做长期编码类 Agent 或需要频繁切换模型的场景可以了解一下 TaoToken 的 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_harness_ts。它把模型调用和工具调用链路的验证放在同一个环境里省去来回切换配置的麻烦。API Key 管理入口在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_harness_ts接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_harness_ts。最后留一个实用建议工具注册表里的description值得反复打磨。我踩过的坑是一开始把描述写得太简略模型经常在read_local_file和git_status之间犹豫。后来把每个工具的“适用场景”和“不适用场景”都写进描述调用准确率明显提升。工具调用的稳定性一半靠协议标准一半靠描述质量。
返回列表