
1. 为什么你的工具代码总在重复适配不同 AI 客户端如果你写过 Function Calling大概率经历过这种循环给 Claude 写一套工具描述给 GPT 写一套给本地模型再写一套。参数格式不一样调用协议不一样连工具发现这件事都得手动硬编码。我试过在一个项目里同时对接三个模型平台光是维护工具定义就占掉了将近一半的开发时间。MCPModel Context Protocol想解决的就是这个问题。它把工具调用抽象成一套基于 JSON-RPC 2.0 的标准协议Server 端只负责声明我有哪些工具、参数是什么、怎么执行Client 端负责把这些工具翻译成各家模型能理解的格式。同一套 MCP ServerClaude Desktop 能调Cline 能调你自己写的 Agent 也能调。这篇文章面向的是需要让同一套工具服务被多个 AI 客户端复用的开发者。我会从协议结构讲到可运行的 Server 和 Client 代码再演示怎么通过 TaoToken 的统一 Key 通道完成一次开发、多处调用的验证。全程 TypeScript代码可直接跑。核心检索词先明确MCP 是一套让 AI 应用以统一方式调用外部工具的开放协议适合需要跨客户端复用工具能力的 Agent 开发者。读完你能自己写一个 MCP Server并让它在不同客户端里被发现和调用。2. TaoToken 统一通道MCP Client 接入前的准备MCP 本身只管工具协议不管模型调用。但一个完整的 Agent 循环里Client 拿到工具列表后最终还是要发给某个大模型去决策该调哪个工具。这时候模型 API 的接入方式就成了变量——不同平台的 Base URL、Key、模型 ID 都不一样切换一次就要改一轮配置。TaoToken 在这里的角色是统一模型调用通道。它提供兼容主流格式的 API 入口你用一个 Key 就能访问多个模型Base URL 固定模型 ID 按需切换。对 MCP Client 来说这意味着工具协议不变模型侧只改三个字段。先把接入信息固定下来后面代码里直接引用配置项值Base URLhttps://taotoken.net/apiAPI Key在控制台创建形如sk-...Model ID按需选择例如claude-3-5-sonnet系列API Key 的创建入口在控制台的 API Keys 页面登录后新建即可。文档页有各语言的调用示例接入前建议扫一眼确认参数格式。注意Base URL 用https://taotoken.net/api不要带末尾斜杠也不要拼其他路径。Key 只放在环境变量里别写进代码提交到仓库。为什么要在 MCP 教程里先讲模型通道因为 MCP Client 的核心工作有两件一是通过 JSON-RPC 跟 Server 通信拿工具二是把工具转成模型能理解的 Function Calling 格式发出去。第二件事依赖模型 API如果这里配置混乱后面调试工具调用时会分不清是协议问题还是模型接入问题。先把模型通道固定排障时变量就少一个。环境变量这样设export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api后面 Client 初始化模型时直接读这两个变量。这样同一套 MCP 代码换模型只改 Model ID不用动工具层。3. 可复制的 MCP Server 配置与 Tools 注册片段这一节给出能直接落地的配置和代码。先看 MCP Server 在客户端里的配置格式这是一次开发多处调用的关键——不同客户端读的是同一份 Server 声明。以常见的 MCP 客户端配置为例Server 通过 stdio 启动配置片段如下JSON 格式路径按你本机实际调整{ mcpServers: { dev-tools: { command: node, args: [/absolute/path/to/mcp-agent-demo/dist/index.js], env: { MCP_WORKSPACE: /absolute/path/to/workspace, TAOTOKEN_API_KEY: sk-你的key, TAOTOKEN_BASE_URL: https://taotoken.net/api } } } }这份配置的核心是三件套启动命令、工作目录、环境变量。任何支持 MCP 的客户端都认这个结构区别只是配置文件放的位置不同。Claude Desktop 放在claude_desktop_config.jsonCline 在设置里填Codex 走auth.json加 MCP 段。同一份 Server配置复制过去就能用。接下来是 Tools 注册。MCP 的工具声明用 JSON Schema 描述参数Server 端注册时把 name、description、inputSchema 三样给全。下面是一个精简但完整的注册片段import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { ListToolsRequestSchema, CallToolRequestSchema, } from modelcontextprotocol/sdk/types.js; const server new Server( { name: dev-tools, version: 1.0.0 }, { capabilities: { tools: {} } } ); // 工具清单name description inputSchema const TOOLS [ { name: read_file, description: 读取工作目录下的文本文件内容, inputSchema: { type: object, properties: { path: { type: string, description: 相对工作目录的文件路径 }, }, required: [path], }, }, { name: http_request, description: 发送 HTTP 请求并返回响应体, inputSchema: { type: object, properties: { url: { type: string, description: 请求 URL }, method: { type: string, enum: [GET, POST], default: GET }, }, required: [url], }, }, ]; server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: TOOLS }; }); server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; if (name read_file) { const fs await import(fs/promises); const content await fs.readFile(String(args?.path), utf-8); return { content: [{ type: text, text: content }] }; } if (name http_request) { const res await fetch(String(args?.url), { method: String(args?.method ?? GET), }); const text await res.text(); return { content: [{ type: text, text }] }; } return { content: [{ type: text, text: Unknown tool: ${name} }], isError: true, }; }); const transport new StdioServerTransport(); await server.connect(transport);这段代码里ListToolsRequestSchema处理tools/list请求CallToolRequestSchema处理tools/call请求正好对应 JSON-RPC 的两个核心方法。工具声明和实现分离加新工具只需往TOOLS数组里加一项、在 handler 里加一个分支。编译运行npm install modelcontextprotocol/sdk npx tsc node dist/index.jsServer 启动后会阻塞在 stdin 上等待 JSON-RPC 消息。你可以手动发一条测试echo {jsonrpc:2.0,id:1,method:tools/list,params:{}} | node dist/index.js正常会返回包含两个工具的 JSON。这一步通了说明 Server 侧的协议层没问题接下来接 Client。4. 验证请求从 tools/list 到模型决策的完整链路Server 能响应tools/list只是第一步。真正的验证是让 Client 连上 Server、拿到工具、发给模型、模型决定调用、结果回传。这条链路走通才算一次开发多处调用成立。先写 Client 连接部分。MCP SDK 的 Client 通过 stdio transport 启动 Server 子进程import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; const client new Client( { name: mcp-client, version: 1.0.0 }, { capabilities: {} } ); const transport new StdioClientTransport({ command: node, args: [/absolute/path/to/dist/index.js], }); await client.connect(transport); // 发现工具 const toolsResult await client.request( { method: tools/list, params: {} }, {} as any ); console.log(Discovered tools:, JSON.stringify(toolsResult, null, 2));跑通后你会看到 Server 声明的工具列表。这一步验证的是 JSON-RPC 通信和工具发现。接下来把工具转成模型能用的格式发给 TaoToken 通道。这里用 OpenAI 兼容的 chat completions 格式举例工具转成tools字段const toolDefs (toolsResult as any).tools.map((t: any) ({ type: function, function: { name: t.name, description: t.description, parameters: t.inputSchema, }, })); const response await fetch(${process.env.TAOTOKEN_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY}, }, body: JSON.stringify({ model: claude-3-5-sonnet, messages: [ { role: user, content: 帮我读取 README.md 的内容 }, ], tools: toolDefs, tool_choice: auto, }), }); const data await response.json(); console.log(JSON.stringify(data.choices[0].message, null, 2));如果模型决定调用工具返回的 message 里会带tool_calls字段里面是工具名和参数。你把这个参数原样传给 MCP Server 的tools/callconst toolCall data.choices[0].message.tool_calls[0]; const callResult await client.request( { method: tools/call, params: { name: toolCall.function.name, arguments: JSON.parse(toolCall.function.arguments), }, }, {} as any ); console.log(Tool result:, callResult);到这里完整链路是Client 发现工具 → 转成模型格式 → 模型决策 → 回传工具调用 → Client 执行 → 结果返回。整个过程里MCP 协议负责工具侧TaoToken 通道负责模型侧两边解耦。实测下来同一套 Server 代码换成 Claude Desktop 的配置、换成 Cline 的 MCP 设置、换成自己写的 Agent工具层一行不用改。这就是协议标准化的价值——你开发一次工具服务多个客户端复用。验证成功的标志有三个tools/list返回完整工具清单、模型返回带tool_calls的响应、tools/call返回预期结果。三个都过链路就通了。5. 常见报错排查401、local proxy failed 与 reading choices接入过程里踩的坑基本集中在几个固定报错上。这一节按真实错误信息对照排查。401 Unauthorized。这个最常见出现在模型调用那一步。原因通常是 Key 没设对或没传。检查三处环境变量TAOTOKEN_API_KEY是否导出、请求头Authorization是否是Bearer sk-...格式、Key 是否在控制台被禁用。如果 Key 正确但仍 401确认 Base URL 是https://taotoken.net/api路径拼成/v1/chat/completions不要多拼或少拼。local proxy failed / connection refused。这个报错通常出现在 Client 启动 Server 子进程时。MCP 的 stdio transport 要求command和args指向真实可执行文件。排查args里的路径必须是绝对路径相对路径在不同客户端的工作目录下会失效node命令是否在 PATH 里某些客户端环境变量精简需要写 node 的绝对路径Server 编译产物dist/index.js是否存在npx tsc有没有报错。reading choices of undefined。这个报错说明模型响应结构和你预期的不一样。常见原因是请求体格式不对比如model字段写错、messages为空、或者返回的是错误对象而不是正常响应。排查时先把response.json()的完整结果打印出来看是error字段还是choices字段。如果是error里面通常有具体原因如果是choices为空检查messages是否至少有一条 user 消息。OAuth / authentication failed。某些客户端比如 Claude Code 类工具在接入时会走 OAuth 流程。如果你用的是 API Key 模式需要在配置里明确指定认证方式别让它默认走 OAuth。检查客户端的 MCP 配置段确认env里传的是 API Key 而不是 OAuth token。工具被发现但调用返回 Unknown tool。这是 Server 端 handler 的 name 匹配问题。tools/list里声明的 name 必须和tools/call里 handler 判断的字符串完全一致大小写、下划线都不能差。建议把工具名抽成常量声明和判断都引用同一个常量。Codex auth.json 配置。如果你用 Codex 类客户端MCP 配置写在auth.json里结构是mcpServers对象。三件套要写全command启动命令、args脚本路径、env含TAOTOKEN_API_KEY和TAOTOKEN_BASE_URL。缺任何一个都会导致 Server 起不来或模型调不通。排障的通用思路是分层验证先单独跑 Server 确认tools/list能返回再单独调模型确认 API 通最后合起来跑完整链路。哪一层断了一眼就能定位。6. 把工具服务跑起来从本地验证到多客户端复用代码和排障都过了最后说怎么真正把工具服务用起来。本地验证阶段建议先写一个最小 Server只放一个echo工具参数就一个字符串。跑通tools/list和tools/call后再逐步加真实工具。这样出问题时变量最少。多客户端复用的关键是配置分离。Server 代码和工具实现放在一个仓库里编译产物路径固定。每个客户端只维护自己的配置文件里面引用同一个dist/index.js。新增客户端时复制配置片段、改一下路径就行工具层零改动。模型侧通过 TaoToken 统一通道接入Base URL 和 Key 固定换模型只改 Model ID。这样工具协议和模型调用两条线各自独立任何一边调整都不影响另一边。如果你要长期跑 Agent 类任务建议把模型调用走 Coding Plan 这类长期通道避免频繁换 Key。工具服务本身部署在本地或内网通过 stdio 或 SSE 暴露安全边界清晰。最后给一个实用技巧在 Server 启动时把工具清单打到 stderr客户端日志里能看到实际注册了哪些工具。调试时比翻代码快得多。console.error(Registered tools:, TOOLS.map((t) t.name).join(, ));工具服务跑起来后你会发现同一套代码在不同客户端里的行为是一致的——因为协议层统一了。剩下的差异只在模型决策风格上那是模型侧的事跟工具无关。