MCP 协议开发实战:用 TypeScript 从零搭建可扩展的 AI Agent 工具服务器并接入 TaoToken
发布时间:2026/9/27 6:57:56来源:尧图网络
1. 为什么我要自己写一个 MCP Server大模型能推理、能写代码但它不知道你公司内部的工单系统长什么样也读不到你本地那台跑着测试数据的 SQLite。MCPModel Context Protocol要解决的就是这件事给模型和外部世界之间定一套统一的接口让 AI Agent 能发现工具、调用工具、拿到结构化结果。你可以把它理解成「AI 世界的 USB-C」——Host 是电脑Client 是接口协议Server 就是那根插上去就能用的线。我这次的目标很具体用 TypeScript 从零搭一个可扩展的 MCP 工具服务器先实现一个只读的工单查询工具跑通 JSON-RPC 通信再通过 TaoToken 的统一 Key/API 通道把它接进真实的模型调用链路里。适合谁看写过一点 Node.js、想让自己的 Agent 真正「动手干活」而不是只会聊天的开发者。整篇会给出可运行的项目骨架、配置文件片段以及一次端到端的调用验证你照着敲就能跑起来。需要先澄清一个容易传错的点MCP 规范有多个日期版本目前可确认的正式版本之一是 2025-06-18它包含初始化、能力协商和可选会话「无状态优先」是后来被接受的演进方向。工程实现时一定要明确目标版本别把不同版本的字段混着写否则客户端握手阶段就会失败。2. 前置准备TaoToken 通道与项目初始化在写协议代码之前先把「模型侧」的通道准备好。MCP Server 本身只负责暴露工具真正发起调用的 Agent 需要一个能访问大模型的入口。我这边用的是 TaoToken 的统一 Key/API 通道好处是模型对话、编码计划、控制台和 API Key 管理都在一个后台里不用为每个模型单独配一套凭证。第一步去控制台创建一个 API Key。地址是 https://taotoken.net/api-keys 登录后点新建把 Key 复制出来存到环境变量里别硬编码进代码。这个 Key 后面既用于模型对话也用于验证 MCP 工具调用链路。第二步初始化 TypeScript 项目。我习惯用 pnpmnpm 也一样mkdir mcp-ticket-server cd mcp-ticket-server pnpm init pnpm add modelcontextprotocol/sdk zod pnpm add -D typescript tsx types/node npx tsc --inittsconfig.json里把模块系统调成 NodeNext否则 SDK 的 ESM 导出会报错{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, outDir: dist, strict: true, esModuleInterop: true, skipLibCheck: true }, include: [src] }第三步把 Key 写进.env并在package.json里加两个脚本{ scripts: { dev: tsx src/server.ts, build: tsc node dist/server.js } }# .env TAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api到这里前置就绪。注意TAOTOKEN_BASE_URL用不带查询参数的裸 API 地址业务代码里再按需拼路径避免把 UTM 参数混进请求。3. 可复制的 Server 骨架与工具注册模板MCP 的消息层是 JSON-RPC 2.0SDK 已经帮你封装了握手、能力协商和消息路由你只需要关心「注册什么工具」和「工具怎么执行」。下面是我实际在用的骨架拆成三个文件方便以后加工具。先看工具定义层src/tools/getTicket.ts。核心思路是工具名单一职责、输入用 zod 校验、输出只给模型真正需要的字段。import { z } from zod; export const TOOL_NAME get_ticket; export const TICKET_ID_FIELD ticketId; export const inputSchema { [TICKET_ID_FIELD]: z .string() .regex(/^T-\d$/, 工单编号必须形如 T-1001), }; export interface TicketResult { id: string; title: string; status: open | pending | closed; } // 模拟仓储层真实项目替换为数据库或内部 API 调用 export async function findTicketById( ticketId: string ): PromiseTicketResult | null { const fakeDb: Recordstring, TicketResult { T-1001: { id: T-1001, title: 登录页偶发白屏, status: open }, T-1002: { id: T-1002, title: 导出报表超时, status: pending }, }; return fakeDb[ticketId] ?? null; }再看服务器入口src/server.ts。这里注册工具并把错误分成「不存在」和「系统失败」两类方便 Agent 判断下一步。import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { TOOL_NAME, inputSchema, findTicketById, } from ./tools/getTicket.js; const server new McpServer({ name: ticket-server, version: 1.0.0, }); server.tool( TOOL_NAME, 按编号查询当前用户有权访问的单个工单只返回脱敏后的必要信息。仅接受 T-数字 格式不会修改工单。若工单不存在返回 NOT_FOUND。, inputSchema, async ({ ticketId }) { const ticket await findTicketById(ticketId); if (!ticket) { return { isError: true, content: [{ type: text, text: NOT_FOUND }], }; } return { content: [ { type: text, text: JSON.stringify({ id: ticket.id, title: ticket.title, status: ticket.status, }), }, ], }; } ); const transport new StdioServerTransport(); await server.connect(transport); console.error(ticket-server 已启动等待 JSON-RPC 消息);工具描述那段文字很关键它不是注释而是给模型看的「使用说明」什么时候调用、参数格式、有没有副作用、失败返回什么。描述越含糊模型误调用概率越高。我踩过的坑就是把描述写成「查询工单」结果模型在用户说「猜一下 T-1001 状态」时也去调虽然这次调对了但边界不清迟早出事。最后是客户端配置。本地桌面 Agent 用 stdio 最省事在它的 MCP 配置里加一段{ mcpServers: { ticket-server: { command: node, args: [/绝对路径/mcp-ticket-server/dist/server.js], env: { TAOTOKEN_API_KEY: sk-你的key } } } }如果你要部署成远程服务把StdioServerTransport换成 Streamable HTTP 传输并额外考虑鉴权、跨域、超时和限流。业务处理器要做到每个请求自带身份和版本信息不依赖单机内存保存上下文这样实例才能水平扩容。4. 端到端验证从握手到一次真实调用代码写完先本地跑一遍协议层确认握手和工具发现没问题。MCP 用 JSON-RPC 2.0你可以直接往 stdio 里喂消息。启动服务pnpm build node dist/server.js然后手动发一条初始化请求实际调试时我用的是 SDK 自带的客户端这里为了看清协议手写{jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2025-06-18,capabilities:{},clientInfo:{name:test,version:1.0.0}}}正常会返回服务端的能力声明和serverInfo。接着发工具发现请求{jsonrpc:2.0,id:2,method:tools/list,params:{}}你应该能看到get_ticket及其输入 schema。再发一次真实调用{jsonrpc:2.0,id:3,method:tools/call,params:{name:get_ticket,arguments:{ticketId:T-1001}}}返回内容里content[0].text是{id:T-1001,title:登录页偶发白屏,status:open}说明工具链路通了。接下来把它接进模型侧。用 TaoToken 的模型对话入口做一次 Agent 级验证地址是 https://taotoken.net/models 在对话里挂上这个 MCP Server然后输入「帮我查一下 T-1001 的状态」。预期行为是模型识别出需要调用工具发起tools/call拿到结果后用自然语言回复。再试两条边界用例——「猜一下 T-1001 状态」也必须走工具而不是瞎编「把所有工单导出」应该被拒绝因为工具只支持单个查询。这两条过了说明工具描述和 schema 约束是有效的。如果你在做长期编码或 Agent 项目建议把模型调用统一走 Coding Plan地址是 https://taotoken.net/coding-plan 这样工具服务器和模型通道的凭证管理能收敛到一处换模型时不用改业务代码。5. 本篇常见错误排查握手失败报 protocolVersion 不匹配。最常见的原因是客户端和服务端对齐了不同版本的规范。检查两边声明的protocolVersion是否一致别把 2025-06-18 的字段和更早版本的写法混用。工具注册了但tools/list里看不到。多半是server.tool()的调用发生在server.connect()之后或者工具名重复。注册必须在连接传输层之前完成。调用返回 schema 校验错误。zod 的 regex 太严或太松都会出问题。T-\d只接受T-1001这种格式用户输入t1001或T 1001都会被拒。要么在描述里写清楚格式要么在工具内部做归一化别让模型去猜。stdio 模式下日志污染了协议流。这是新手最容易踩的坑用console.log打调试信息结果这些文本混进了 JSON-RPC 消息流客户端直接解析失败。所有日志走console.errorstdout 只留给协议消息。远程部署后调用超时。Streamable HTTP 传输下检查反向代理的超时设置和 CORS 配置。另外确认业务处理器没有依赖单机内存保存会话状态否则多实例部署时请求打到不同实例就会丢上下文。模型不调用工具直接编答案。回到工具描述上找原因。描述里要明确「什么时候调用」和「哪些情况不要调用」并说明失败返回什么。描述含糊时模型倾向于用自己的知识回答而不是调工具。6. 把工具服务器接进你的工作流搭完这个骨架你会发现 MCP Server 的扩展成本很低新增一个工具就是在src/tools/下加一个文件定义 schema 和执行逻辑然后在server.ts里注册一行。真正需要花心思的不是代码量而是边界设计——工具职责要小、权限要窄、返回数据要脱敏、写操作要有幂等和审计。工具层只做协议适配业务规则留在领域服务里这样将来换 SDK 或换传输方式核心逻辑照样能测、能复用。接入侧我建议把凭证和模型通道统一管理。API Key 在 https://taotoken.net/api-keys 创建接入文档在 https://taotoken.net/doc 可以查到具体的请求格式和参数说明模型对话验证走 https://taotoken.net/models 长期编码项目用 https://taotoken.net/coding-plan 。这样一套 Key 覆盖对话、编码和工具调用省得在多个后台之间来回切。最后一个实用建议先写测试再写工具。至少覆盖初始化、版本不匹配、schema 校验失败、越权、下游超时和超大输出截断这几种情况。协议层的 bug 往往在 Agent 真实调用时才暴露提前用 JSON-RPC 消息把边界跑一遍比事后 debug 省事得多。
网站建设高端定制企业官网