news 2026/10/4 17:29:59

MCP协议开发实战:用TypeScript从零搭建AI Agent工具链并接入TaoToken

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MCP协议开发实战:用TypeScript从零搭建AI Agent工具链并接入TaoToken

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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 看到它的能力说明。它的 API 入口是 https://taotoken.net/api,不额外加 UTM 参数。对于 MCP 开发来说,最实用的点是:你只需要一个 Key,就能在客户端侧调用不同模型来驱动工具选择逻辑,不用为每个模型单独配一套鉴权。

先拿 Key。打开 https://taotoken.net/api-keys ,登录后创建一个 API Key,复制出来存到环境变量里。我习惯用.env文件管理,但注意不要提交到 Git。

# .env TAOTOKEN_API_KEY=sk-你的实际key TAOTOKEN_BASE_URL=https://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 SDK,zod用来做参数校验。装完之后配置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 本身不需要 Key,Key 是给客户端侧调模型用的,这个区分要清楚,不然容易把鉴权逻辑写错地方。

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/api,Key 是你从 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.ts:

import { 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 Key,Model 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。修复方法是确认配置里用的是command+args的 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 ID;MCP 配置里填 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 工具链底座。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 17:27:08

context-mode:Shell环境上下文切换工具的设计与实践

最近我遇到一个特别折磨人的场景:在同一个项目里,要维护老后端服务,又要切到前端联调,还得不时去改一下配置中心。每次切换,我都得手动改一串环境变量、跳目录、装载不同的本地工具链和别名。哪怕写一个小脚本&#xf…

作者头像 李华
网站建设 2026/10/4 17:26:05

神经编码不是AI调参数:端到端可微压缩如何重构视频编码

“神经编码不是‘AI 调参数’这句话,是我在跟不少做视频云、转码引擎、编解码研究的团队聊完一圈后,最想放到台面上掰扯清楚的一个观点。过去几年,AI 在视频编码里的主流存在感,确实容易让人产生“AI 就是给编码器加个滤镜、调几个…

作者头像 李华
网站建设 2026/10/4 17:25:43

Origin科学计数法零点显示为0.0的修复方案

1. 这个“0.0→0”问题,本质是Origin对科学计数法刻度标签的格式化逻辑缺陷Origin2018汉化版里,当你把坐标轴设置成科学计数法(比如10^3、10^6这种形式)后,零点位置的刻度标签常常顽固地显示为“0.0”,而不…

作者头像 李华
网站建设 2026/10/4 17:20:20

SSE知识梳理(1)

作者:没有四次元口袋的蓝胖 日期:2026-10-03 标签:SSE, 流式响应SSE知识梳理(1) 你有没有注意过 ChatGPT 的回答是一个字一个字"打"出来的?这不是前端特效,而是后端真的在一边生成一边发送数据——这就是流式…

作者头像 李华
网站建设 2026/10/4 17:19:54

中断里调用malloc导致偶发死机?嵌入式RTOS故障排查实录

凌晨三点半,产线上的一台采集设备死机了。面板无响应,串口不再输出任何日志,看门狗也没能把它拉回来。重启后一切正常,可再过几小时或者一两天,同样的偶发死机又随机出现。这不是第一次了——三周内同一批设备已经报了…

作者头像 李华