
1. 为什么你的 Agent 总是接不上工具MCP 要解决的真实问题如果你正在做 Agent 开发大概率遇到过这种场景模型本身很聪明但一到调用外部工具这一步就开始掉链子。你想让它查一下数据库、读一个本地文件、调一个内部 HTTP 接口结果发现每个平台都有自己的 function call 格式OpenAI 一套、Claude 一套、国内模型又一套工具描述写三遍参数 schema 改到怀疑人生。MCPModel Context Protocol模型上下文协议就是冲着这个痛点来的。它做的事情说白了只有一件把Agent 怎么发现工具、怎么调用工具、怎么拿回结果这件事标准化。你可以把它理解成 AI 世界的 Type-C 接口——以前每个设备一根专用线现在统一成一个口谁都能插。它适合谁三类人最该学一是正在写 Agent 编排逻辑的后端开发二是想把内部系统暴露给大模型的产品团队三是做智能硬件/边缘设备、需要让本地模型调用本地能力的工程师。这篇不讲空泛概念直接给你一套能跑起来的 MCP Server 骨架 客户端接入示例围绕 SSE 和 JSON-RPC 2.0 这两条通信主线展开最后带你把消息链路验证一遍。我试过从零手撸一遍之后最大的感受是MCP 的复杂度不在协议本身而在两通道 四步骤这个生命周期你得记牢。下面按这个顺序拆。2. 动手前先把 TaoToken 的接入信息准备好MCP Server 本身不依赖任何模型服务它只负责暴露工具。但你要验证整条链路——尤其是让模型真的去选工具、拼参数、调接口——就需要一个能跑 Agent 的模型入口。这里我用 TaoToken 来做模型侧接入因为它同时提供对话、Coding Plan 和标准 API验证阶段切换很方便。先把几个地址记下来后面配置会反复用到官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基址https://taotoken.net/api模型对话验证工具调用最直观https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Plan长期跑 Agent/编码任务https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite注意MCP Server 和模型服务是两件事。Server 负责我有哪些工具模型负责我决定调哪个工具。别把两者混在一个进程里否则排障时你会分不清是协议问题还是模型问题。拿到 API Key 之后先别急着写 MCP先用模型对话页面确认你的 Key 能正常返回。这一步是基线基线不通后面全是白搭。Key 的创建在 API Keys 页面完成建议单独建一个用于 MCP 联调的 Key方便随时吊销。3. 可复制的 MCP Server 配置骨架SSE JSON-RPC 2.0MCP 的通信机制可以浓缩成一句话客户端先拉一条 SSE 长连接收通知再用普通 HTTP POST 发请求到服务端两端必须先 initialize 打招呼才能调工具最后客户端关 SSE 就结束。3.1 两通道SSE 端点和 POST 端点服务端启动时必须同时暴露两个端点缺一不可端点方法作用关键点/sseGET建立 SSE 长连接服务端→客户端单向推送响应头Content-Type: text/event-stream/message/{sessionId}POST客户端→服务端发送 JSON-RPC 请求消费application/jsonSSE 的报文格式只有 4 个固定字段记牢就行data实际负载、id事件序号断线续传用、event事件类型、retry重连间隔毫秒。每条消息以两个换行结束。服务端在客户端连上 SSE 后第一件事是推一个event: endpoint把后续 POST 的 URI 告诉客户端。这一步很多人会漏导致客户端不知道往哪发请求。3.2 四步骤MCP 的完整生命周期连客户端 GET/sse建立长连接。取服务端回event: endpoint给出 POST 地址。握客户端 POSTinitialize请求 → 服务端返回 capabilities客户端再发notifications/initialized通知 → 握手完成。用正常会话tools/list拉工具列表tools/call调具体工具服务端随时在 SSE 流里推状态更新。3.3 JSON-RPC 2.0 消息骨架请求和响应都是四字段结构这是 MCP 的说什么{ jsonrpc: 2.0, method: tools/call, params: { name: get_time, arguments: {} }, id: 1 }{ jsonrpc: 2.0, id: 1, result: { content: [{ type: text, text: 2025-01-01 12:00:00 }] } }失败时把result换成error结构是{code, message, data?}。注意成功响应里不能同时出现result和error这是 JSON-RPC 2.0 的硬规定写错客户端会直接解析失败。3.4 一个最小可跑的 Server 骨架Node.js 版下面这段是能直接跑的骨架用 Express 起两个端点工具注册用一个 Map 管理const express require(express); const app express(); app.use(express.json()); const sessions new Map(); // sessionId - resSSE 响应对象 const tools new Map(); // 工具注册表 // 注册一个示例工具 tools.set(get_time, { description: Returns current server time, inputSchema: { type: object, properties: {} }, handler: async () ({ content: [{ type: text, text: new Date().toISOString() }] }) }); // 通道一SSE 长连接 app.get(/sse, (req, res) { const sessionId sess- Date.now(); res.set({ Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive }); res.flushHeaders(); sessions.set(sessionId, res); // 第一步推送 endpoint 事件 res.write(event: endpoint\ndata: /message/${sessionId}\n\n); req.on(close, () sessions.delete(sessionId)); }); // 通道二POST 接收 JSON-RPC 请求 app.post(/message/:sessionId, async (req, res) { const { sessionId } req.params; const sse sessions.get(sessionId); const msg req.body; let response null; if (msg.method initialize) { response { jsonrpc: 2.0, id: msg.id, result: { protocolVersion: 2024-11-05, capabilities: { tools: { listChanged: true } }, serverInfo: { name: demo-mcp-server, version: 1.0.0 } } }; } else if (msg.method tools/list) { response { jsonrpc: 2.0, id: msg.id, result: { tools: [...tools.entries()].map(([name, t]) ({ name, description: t.description, inputSchema: t.inputSchema })) } }; } else if (msg.method tools/call) { const tool tools.get(msg.params.name); const result tool ? await tool.handler(msg.params.arguments) : { content: [{ type: text, text: tool not found }], isError: true }; response { jsonrpc: 2.0, id: msg.id, result }; } else if (msg.method notifications/initialized) { // 通知类消息不需要响应 return res.status(202).end(); } if (response sse) sse.write(event: message\ndata: ${JSON.stringify(response)}\n\n); res.status(202).end(); }); app.listen(8080, () console.log(MCP server on :8080));跑起来之后/sse是长连接入口/message/{sessionId}是请求入口工具通过tools这个 Map 注册。你要加新工具只需要往 Map 里塞一条tools/list会自动带上。4. 客户端接入示例与消息链路验证Server 有了接下来验证客户端能不能正常握手、拉工具、调工具。这里给一个 Node 客户端示例用EventSource收 SSE用fetch发 POST。4.1 客户端接入代码const EventSource require(eventsource); const BASE http://localhost:8080; let postUri null; let msgId 0; const pending new Map(); const es new EventSource(${BASE}/sse); es.addEventListener(endpoint, (e) { postUri BASE e.data; console.log(拿到 POST 端点:, postUri); sendInitialize(); }); es.addEventListener(message, (e) { const msg JSON.parse(e.data); if (msg.id ! undefined pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id); } }); function rpc(method, params) { const id msgId; return new Promise((resolve) { pending.set(id, resolve); fetch(postUri, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ jsonrpc: 2.0, method, params, id }) }); }); } async function sendInitialize() { const initRes await rpc(initialize, { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: { name: demo-client, version: 1.0.0 } }); console.log(initialize 返回:, JSON.stringify(initRes.result)); // 握手第二步发 initialized 通知 await fetch(postUri, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ jsonrpc: 2.0, method: notifications/initialized }) }); const list await rpc(tools/list, {}); console.log(可用工具:, list.result.tools.map(t t.name)); const call await rpc(tools/call, { name: get_time, arguments: {} }); console.log(调用结果:, JSON.stringify(call.result)); }4.2 预期成功结果正常跑通后控制台会依次打印拿到 POST 端点: http://localhost:8080/message/sess-1735... initialize 返回: {protocolVersion:2024-11-05,capabilities:{tools:{listChanged:true}},serverInfo:{name:demo-mcp-server,version:1.0.0}} 可用工具: [ get_time ] 调用结果: {content:[{type:text,text:2025-01-01T12:00:00.000Z}]}看到这四行说明 SSE 通道、POST 通道、JSON-RPC 编解码、工具注册与调用全部打通。这时候你再去模型对话页面把工具 schema 喂给模型让它自己决定调get_time整条 Agent 链路就闭环了。4.3 用模型侧验证工具选择把tools/list返回的 schema 转成模型能识别的 function 定义发给模型观察它是否在合适的问题下选择get_time。这一步是验证模型能不能正确选工具、拼参数的关键。如果模型选错工具问题通常在工具描述写得太模糊而不是协议本身。5. 本篇常见错误排查清单排障时按先通道、后协议、再业务的顺序查能省一半时间。SSE 连不上或立刻断开先看响应头有没有Content-Type: text/event-stream缺了这个浏览器/客户端会当普通响应处理。再看有没有被中间层缓冲SSE 必须禁用缓冲否则消息会攒着一起发。客户端收不到 endpoint 事件检查服务端是不是在res.flushHeaders()之后立刻写了 endpoint 事件。如果先写业务数据再写 endpoint客户端可能已经错过。POST 请求 404sessionId对不上。SSE 连接建立时生成的 sessionId 必须和 POST 路径里的一致多实例部署时要注意 session 不能跨进程。JSON-RPC 解析报错最常见的是成功响应里同时带了result和error或者id类型不一致请求是数字、响应是字符串。JSON-RPC 2.0 要求id原样回传。tools/call 返回 tool not found工具名大小写或拼写不一致。建议在服务端注册时统一小写客户端调用前先tools/list确认。模型不调工具先确认工具描述是否清晰再确认 schema 是否符合模型要求。协议层没问题的话问题基本在提示词和 schema 描述上。提示联调阶段建议开两个终端一个跑 Server 看日志一个跑 Client 看输出消息一来一回对得上问题定位会快很多。如果 POST 通了但 SSE 没收到响应重点查sessions这个 Map 里 sessionId 是否还在。6. 下一步把 MCP 接进你的 Agent 工作流跑通最小闭环之后你可以往三个方向扩展。一是加更多工具把内部 HTTP 接口、数据库查询、文件操作都包成 MCP 工具让模型按需调用。二是把 SSE 换成流式传输处理长耗时工具时体验更好。三是把 MCP Server 部署到内网配合 Coding Plan 跑长期的编码/Agent 任务让模型稳定地调用你的私有能力。如果你在接入阶段卡在 Key 或鉴权上直接去 API Keys 页面重新建一个如果是协议细节对不上接入文档里有完整的消息字段说明。验证模型选工具的能力用模型对话最快要长期跑 Agent 任务Coding Plan 更合适。先把这篇的骨架跑通再往上叠业务比一上来就啃完整规范要快得多。