
1. 为什么我要自己写一个 MCP Server从工具散落到统一协议MCP 协议Model Context Protocol说白了就是给 AI Agent 和外部工具之间定一套“普通话”。以前我们做 Agent每接一个工具就要写一套适配代码查天气写一个函数、读文件写一个函数、查数据库再写一个函数最后这些函数还得各自处理参数校验、错误返回、鉴权。工具一多代码就像一团乱麻改一个接口能牵动三四个文件。我最早做 Agent 工具链的时候用的是最土的办法——把所有工具塞进一个大文件里用 switch-case 分发。结果就是每次加工具都要动主流程测试也难写。后来接触到 MCP 协议发现它把“工具发现”和“工具调用”拆成了标准化的 JSON-RPC 方法客户端只需要知道tools/list和tools/call两个入口剩下的交给服务端自己管。这个设计思路一下子把扩展成本降下来了。MCP 协议能做什么简单说它让 AI 模型客户端可以动态发现服务端注册了哪些工具、每个工具需要什么参数、返回什么结构然后按需调用。适合谁适合正在做 AI Agent 工具链的开发者尤其是用 TypeScript 和 Node.js 技术栈的团队。你不需要改模型本身只需要按协议把工具暴露出去Agent 运行时就能自动识别。这篇文章我会带你从零搭一个 TypeScript 版的 MCP Server把工具注册、调用、鉴权三个环节串起来最后接入 TaoToken 的统一 Key 做模型侧验证。整个过程我会给出可复制的配置和命令你跟着敲就能跑通。2. 前置准备TaoToken 统一 Key 与 MCP 开发环境在开始写 MCP Server 之前先把两件事准备好一个是模型侧的访问凭证一个是本地开发环境。MCP Server 本身不依赖模型但你要验证工具调用链路就需要一个能发起tools/call的客户端而客户端背后通常要连模型。这里我用 TaoToken 的统一 Key 来简化模型接入避免在多个平台之间来回切换。TaoToken 的定位是统一模型接入层你可以在官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 看到它的能力说明。它的 API 入口是 https://taotoken.net/api不额外加 UTM 参数。对于 MCP 开发来说最实用的点是你只需要一个 Key就能在客户端侧调用不同模型来驱动工具选择逻辑不用为每个模型单独配一套鉴权。先拿 Key。打开 https://taotoken.net/api-keys 登录后创建一个 API Key复制出来存到环境变量里。我习惯用.env文件管理但注意不要提交到 Git。# .env TAOTOKEN_API_KEYsk-你的实际key TAOTOKEN_BASE_URLhttps://taotoken.net/api然后是 Node.js 环境。我实测下来Node 18 以上都能跑推荐用 20 LTS。检查一下版本node -v npm -v接着初始化项目。我习惯把 MCP Server 和客户端验证分成两个目录但为了演示方便先在一个项目里做。mkdir mcp-agent-toolchain cd mcp-agent-toolchain npm init -y npm install typescript types/node tsx --save-dev npm install modelcontextprotocol/sdk zod这里modelcontextprotocol/sdk是官方 TypeScript SDKzod用来做参数校验。装完之后配置tsconfig.json{ compilerOptions: { target: ES2022, module: commonjs, lib: [ES2022], outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true }, include: [src/**/*], exclude: [node_modules] }在package.json里加脚本{ scripts: { build: tsc, start: node dist/server.js, dev: tsx src/server.ts } }到这里环境就绪。如果你还没拿 Key先去 https://taotoken.net/api-keys 创建后面验证工具调用链路时会用到。注意MCP Server 本身不需要 KeyKey 是给客户端侧调模型用的这个区分要清楚不然容易把鉴权逻辑写错地方。3. 可复制配置用 TypeScript 注册 MCP 工具与鉴权中间件现在进入核心部分写一个 MCP Server注册两个工具一个查天气模拟外部 API一个读本地文件模拟资源访问并在调用链路上加一层鉴权中间件。这样你能看到工具注册、参数校验、鉴权拦截的完整流程。先建src/server.ts。MCP SDK 的服务端用法是创建一个Server实例然后通过setRequestHandler注册tools/list和tools/call的处理逻辑。我先把工具定义写出来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 { z } from zod; const server new Server( { name: agent-toolchain-server, version: 1.0.0, }, { capabilities: { tools: {}, }, } ); // 工具参数 schema const WeatherArgsSchema z.object({ city: z.string().min(1).describe(城市名称例如北京), }); const ReadFileArgsSchema z.object({ path: z.string().min(1).describe(要读取的文件绝对路径), }); // 工具注册表 const tools [ { name: get_weather, description: 查询指定城市的天气信息, inputSchema: { type: object, properties: { city: { type: string, description: 城市名称 }, }, required: [city], }, }, { name: read_file, description: 读取指定路径的文本文件内容, inputSchema: { type: object, properties: { path: { type: string, description: 文件绝对路径 }, }, required: [path], }, }, ];上面这段是工具声明。注意inputSchema用的是 JSON Schema 格式客户端会根据这个生成调用参数。接下来注册tools/list处理器server.setRequestHandler(ListToolsRequestSchema, async () { return { tools }; });然后是tools/call处理器这里加鉴权中间件。鉴权逻辑我设计成从环境变量读一个MCP_AUTH_TOKEN调用时检查请求上下文里是否带了匹配的 token。实际生产中你可以换成 JWT 或数据库校验这里用简单 token 演示链路。const AUTH_TOKEN process.env.MCP_AUTH_TOKEN || dev-token-123; function checkAuth(request: any): boolean { const token request?.params?._meta?.authToken; return token AUTH_TOKEN; } server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; // 鉴权拦截 if (!checkAuth(request)) { return { content: [ { type: text, text: 鉴权失败缺少或错误的 authToken, }, ], isError: true, }; } try { if (name get_weather) { const parsed WeatherArgsSchema.parse(args); // 模拟外部 API 调用 const weatherData { city: parsed.city, temperature: 22°C, condition: 晴, humidity: 45%, }; return { content: [ { type: text, text: JSON.stringify(weatherData, null, 2), }, ], }; } if (name read_file) { const parsed ReadFileArgsSchema.parse(args); const fs await import(fs/promises); const content await fs.readFile(parsed.path, utf-8); return { content: [ { type: text, text: content.slice(0, 2000), }, ], }; } return { content: [{ type: text, text: 未知工具: ${name} }], isError: true, }; } catch (error) { return { content: [ { type: text, text: 工具执行出错: ${error instanceof Error ? error.message : 未知错误}, }, ], isError: true, }; } });最后启动 stdio 传输async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP Server 已启动等待客户端连接...); } main().catch((error) { console.error(启动失败:, error); process.exit(1); });这段代码里鉴权 token 通过_meta.authToken传递这是 MCP 协议允许的扩展字段。客户端调用时需要在请求里带上。如果你用的是 Claude Code 或 Cline 这类客户端它们通常有配置文件来注入环境变量和参数。为了让客户端能连上这个 Server你需要一个 MCP 配置文件。以 Claude Code 为例配置文件路径是~/.claude/claude_desktop_config.json不同版本可能略有差异内容如下{ mcpServers: { agent-toolchain: { command: node, args: [/绝对路径/mcp-agent-toolchain/dist/server.js], env: { MCP_AUTH_TOKEN: dev-token-123, TAOTOKEN_API_KEY: sk-你的实际key, TAOTOKEN_BASE_URL: https://taotoken.net/api } } } }注意这里三件套要写全Base URL 是https://taotoken.net/apiKey 是你从 https://taotoken.net/api-keys 拿到的Model ID 在客户端侧指定比如claude-3-5-sonnet或你实际使用的模型标识。MCP Server 本身不关心 Model ID但客户端调模型时会用到。如果你用的是 Cline 或 CC Switch配置结构类似核心是command、args、env三个字段。Cline 的 MCP 配置在设置界面里填CC Switch 则是通过settings.json管理。不管哪个客户端只要支持 stdio 传输就能连上这个 Server。4. 验证请求本地启动并跑通工具调用链路配置写完后先编译再启动。我习惯用npm run build确认 TypeScript 没报错然后用npm run dev直接跑 tsx 版本省去编译步骤。npm run build npm run dev如果看到MCP Server 已启动等待客户端连接...说明 stdio 传输正常。接下来验证工具调用链路。有两种方式一种是用 MCP 客户端比如 Claude Code直接对话触发另一种是写一个简单的测试脚本模拟客户端请求。我先说测试脚本的方式这样你能看到原始请求和响应。建一个src/test-client.tsimport { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; async function main() { const transport new StdioClientTransport({ command: node, args: [dist/server.js], env: { ...process.env, MCP_AUTH_TOKEN: dev-token-123, }, }); const client new Client( { name: test-client, version: 1.0.0 }, { capabilities: {} } ); await client.connect(transport); // 1. 列出工具 const toolsResult await client.listTools(); console.log(可用工具:, JSON.stringify(toolsResult.tools, null, 2)); // 2. 调用天气工具带鉴权 const weatherResult await client.callTool({ name: get_weather, arguments: { city: 北京 }, _meta: { authToken: dev-token-123 }, }); console.log(天气结果:, JSON.stringify(weatherResult, null, 2)); // 3. 调用文件工具带鉴权 const fileResult await client.callTool({ name: read_file, arguments: { path: process.cwd() /package.json }, _meta: { authToken: dev-token-123 }, }); console.log(文件结果:, JSON.stringify(fileResult, null, 2)); // 4. 故意用错 token 验证鉴权 const failResult await client.callTool({ name: get_weather, arguments: { city: 上海 }, _meta: { authToken: wrong-token }, }); console.log(鉴权失败结果:, JSON.stringify(failResult, null, 2)); await client.close(); } main().catch(console.error);跑这个脚本npx tsx src/test-client.ts预期输出里可用工具会列出get_weather和read_file两个工具定义天气结果会返回北京的温度、天气、湿度 JSON文件结果会返回 package.json 的前 2000 字符鉴权失败结果会返回isError: true和鉴权失败提示。如果这四步都符合预期说明工具注册、调用、鉴权链路全部跑通。接下来在真实客户端里验证。以 Claude Code 为例把前面的claude_desktop_config.json配好重启客户端。然后在对话里输入“帮我查一下北京的天气”客户端会先调tools/list发现工具再调tools/call执行get_weather。你可以在客户端日志里看到完整的 JSON-RPC 请求和响应。如果你用的是 Cline在 MCP 设置里添加 Server 后对话时它会自动把工具列表注入到模型上下文里。模型决定调用哪个工具后Cline 会发起tools/call请求。这里模型侧的调用就走 TaoToken 的统一 Key你只需要在 Cline 的模型配置里填 Base URLhttps://taotoken.net/api和 API KeyModel ID 按你实际用的填。这样工具链和模型链就串起来了。验证模型侧的时候你可以打开 https://taotoken.net/console 看调用记录确认请求确实到了。如果只是想快速试模型对话可以用 https://taotoken.net/model-chat 直接测。长期做编码和 Agent 的话https://taotoken.net/coding-plan 里有更完整的方案说明。5. 常见报错排查401、local proxy failed 与 reading choices这一节我整理几个实际开发中高频出现的报错每个都给出原因和修复动作。你遇到问题时可以对照着查。401 Unauthorized。这个最常见通常出现在两个位置一是 MCP Server 的鉴权中间件返回的二是模型侧调用 TaoToken API 时返回的。如果是 MCP Server 返回的检查客户端请求里的_meta.authToken是否和环境变量MCP_AUTH_TOKEN一致。我踩过的坑是客户端配置里 env 没传进去导致 Server 读到的 token 是默认值。修复方法是确认配置文件里env字段写全并且重启客户端让配置生效。如果是模型侧 401检查TAOTOKEN_API_KEY是否复制完整有没有多余空格。可以去 https://taotoken.net/api-keys 重新生成一个再试。local proxy failed。这个报错通常出现在客户端尝试连接 MCP Server 时stdio 传输启动失败。原因可能是command路径不对或者args里的脚本路径是相对路径。MCP 客户端启动 Server 时工作目录可能不是你项目的根目录所以args里最好用绝对路径。另外确认node在系统 PATH 里如果你用 nvm 管理 Node 版本客户端可能读不到 nvm 的环境。修复方法是把command改成node的绝对路径比如/Users/你的用户名/.nvm/versions/node/v20.x.x/bin/node。reading choices 报错。这个通常出现在模型侧返回结构解析时客户端期望拿到choices字段但实际响应结构不匹配。原因可能是 Base URL 配错了比如漏了/api或者多加了/v1。TaoToken 的 API 入口是https://taotoken.net/api不要自己拼/v1/chat/completions之类的路径客户端 SDK 会处理。修复方法是检查客户端配置里的 Base URL确保和文档一致。如果用的是 OpenAI 兼容模式Base URL 填https://taotoken.net/api即可。OAuth 相关报错。有些客户端在连接远程 MCP Server 时会走 OAuth 流程如果你用的是 stdio 本地 Server一般不会触发。但如果报错里出现 OAuth token 无效检查是不是客户端把本地 Server 当成了远程 SSE Server。修复方法是确认配置里用的是commandargs的 stdio 模式而不是url字段。工具调用返回 isError 但没详细信息。这个多半是参数校验失败zod 的parse抛错后被 catch 住了但错误信息没透传。修复方法是在 catch 里把error.message完整返回我上面的代码已经这么做了。另外确认客户端传的参数类型和inputSchema一致比如city必须是字符串传数字会校验失败。Server 启动后客户端看不到工具。检查tools/list处理器是否注册成功以及客户端是否在连接后主动调用了tools/list。有些客户端需要手动刷新工具列表。另外确认capabilities里声明了tools: {}否则客户端可能不认为 Server 支持工具。排查的时候我建议先单独跑test-client.ts确认 Server 本身没问题再排查客户端配置。这样能把问题范围缩小到客户端侧省得两头猜。6. 从工具链到生产把 MCP Server 接入 TaoToken 的完整动作工具链跑通之后下一步是把它接入真实的模型调用流程。这里的关键是让客户端侧用 TaoToken 的统一 Key 驱动模型模型决定调用哪个 MCP 工具客户端执行调用并把结果回传给模型。整个链路是用户提问 → 客户端调模型TaoToken→ 模型返回工具调用意图 → 客户端调 MCP Server → 结果回传模型 → 模型生成最终回复。要让这个链路稳定跑有几个动作要做。第一把 MCP Server 的鉴权 token 和 TaoToken 的 API Key 分开管理不要混在一个环境变量里。我习惯用MCP_AUTH_TOKEN管工具侧TAOTOKEN_API_KEY管模型侧这样排查问题时能快速定位是哪一层出的错。第二在客户端配置里把三件套写全。以 Cline 为例模型配置里填 Base URLhttps://taotoken.net/api、API Key、Model IDMCP 配置里填 Server 的command、args、env。两边都配好之后对话时模型会自动发现工具并调用。第三加日志。MCP Server 侧我在tools/call里加了console.error输出工具名和参数客户端侧可以在 TaoToken 的 console 里看调用记录。这样出问题时能对照两边日志快速定位是工具没被调用还是模型没返回工具意图。第四处理超时和重试。MCP 工具调用如果涉及外部 API可能超时。我在tools/call里包了一层 try-catch返回isError: true让客户端知道调用失败。客户端侧可以根据isError决定是否重试。模型侧如果返回的工具调用格式不对客户端通常会忽略并重新生成这个行为因客户端而异。第五权限控制。生产环境不要把read_file这种工具暴露给所有调用方最好在鉴权中间件里根据 token 区分权限。比如dev-token只能调天气工具admin-token才能调文件工具。这个逻辑可以在checkAuth里扩展根据 token 返回不同的权限集合。如果你要做更复杂的 Agent 工具链可以考虑把多个 MCP Server 组合起来每个 Server 负责一类工具。客户端侧配置多个 Server 条目模型会发现所有工具并统一调度。这种架构下TaoToken 的统一 Key 优势更明显因为你不需要为每个 Server 单独配模型鉴权。最后验证模型侧调用是否走通可以打开 https://taotoken.net/console 看请求记录确认模型调用和工具调用都正常。如果只是想快速验证模型对话用 https://taotoken.net/model-chat 就行。长期做编码和 Agent 开发的话https://taotoken.net/coding-plan 里有更系统的接入方案。接入文档在 https://taotoken.net/doc API Keys 在 https://taotoken.net/api-keys 需要的时候直接去对应页面操作。整个流程跑下来你会发现 MCP 协议的价值在于把工具链的扩展成本从“改主流程”降到了“加一个工具定义”。TypeScript 的类型系统加上 zod 的参数校验让工具注册和调用都很踏实。TaoToken 的统一 Key 则把模型侧的鉴权简化成一处配置不用在多个平台之间同步凭证。这两者结合基本就是一套可维护的 AI Agent 工具链底座。