ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

大模型开发实战:从零手写一个MCP Server,彻底搞懂JSON-RPC与Agent协作原理

大模型开发实战:从零手写一个MCP Server,彻底搞懂JSON-RPC与Agent协作原理 1. 为什么我要手写一个 MCP ServerMCP Server 是大模型应用里把「模型能调用的工具」标准化暴露出来的服务端程序它让 Agent 不用为每个平台单独适配接口只要按 Model Context Protocol 说话就能被支持 MCP 的客户端发现并调用工具。适合谁适合已经会写一点后端、想真正搞懂 Agent 工具调用链路的大模型开发者。我见过太多人背得出「MCP 是模型上下文协议」但一被问「initialize 之后客户端怎么拿到 POST 地址」「tools/call 的响应为什么必须走 SSE 回推」就卡住。原因很简单官方白皮书讲的是规范不是运行轨迹。你只看文档会觉得 SSE、JSON-RPC、工具注册是三块孤立知识只有自己起一个 Server用 curl 或 Inspector 打一遍才会发现它们是同一条时间线上的三个动作——先拉长连接拿端点再用 JSON-RPC 打招呼最后才轮到工具上场。这篇就按这个顺序来先讲清楚 MCP 的三层结构再给一份能跑的最小骨架然后逐条拆 JSON-RPC 报文最后用本地请求验证成功结果并把最容易踩的坑列出来。全程不依赖任何特殊网络环境本地 localhost 就能完成。2. TaoToken 前置统一 Key 与 API 通道在动手写 Server 之前先把「模型侧」的通道准备好。因为 MCP Server 本身只负责暴露工具真正决定 Agent 怎么理解工具描述、怎么生成调用参数的是背后的大模型。如果你每个模型都单独配一套 Key、一套 Base URL调试阶段会非常痛苦。我习惯用 TaoToken 做统一入口一个 Key 覆盖多家模型API 地址固定为https://taotoken.net/api兼容 OpenAI 风格的调用方式。这样我的 MCP Server 只管工具逻辑模型切换只改一个 model 字段不用动业务代码。具体操作分三步。第一步打开官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册并登录。第二步进入控制台创建 API Key地址是https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite创建后立刻复制保存页面刷新后不再完整显示。第三步如果你要长期跑编码类 Agent建议直接看 Coding Plan 页面https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite它针对长会话和工具调用场景做了额度优化。注意API Key 只放在服务端环境变量里不要写进前端代码或提交到 Git。MCP Server 如果对外暴露务必加 Origin 校验后面排障章节会讲。配好之后你可以先用模型对话页面https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite验证 Key 是否可用确认通道没问题再进入编码环节避免把「Key 错」和「Server 写错」混在一起排查。3. 可复制配置MCP Server 最小骨架MCP 的通信结构可以拆成三层传输层用 SSE 做服务端到客户端的单向推送用 HTTP POST 做客户端到服务端的请求消息层用 JSON-RPC 2.0 定义请求和响应格式协议层规定 initialize、tools/list、tools/call 这些方法的调用顺序。任何语言实现 MCP Server本质都是把这三层拼起来。下面用 Node.js 写一个最小可运行版本不引第三方 MCP SDK纯手写方便你看清每一行在干什么。先建目录并初始化mkdir mcp-demo cd mcp-demo npm init -y npm install express然后创建server.js核心是三个路由GET /sse建立长连接并下发 endpoint 事件POST /message接收 JSON-RPC 请求以及一个工具注册表。const express require(express); const app express(); app.use(express.json()); // 会话表sessionId - res 对象 const sessions new Map(); // 工具注册表name - { description, inputSchema, handler } const tools { get_time: { description: 返回服务器当前时间, inputSchema: { type: object, properties: {} }, handler: () new Date().toISOString(), }, echo: { description: 原样返回传入的 message, inputSchema: { type: object, properties: { message: { type: string, description: 要回显的内容 } }, required: [message], }, handler: (args) echo: ${args.message}, }, }; // 1. SSE 长连接下发 endpoint 事件 app.get(/sse, (req, res) { res.setHeader(Content-Type, text/event-stream); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); const sessionId sess- Date.now(); sessions.set(sessionId, res); // 关键告诉客户端后续 POST 到哪个地址 res.write(event: endpoint\ndata: /message?sessionId${sessionId}\n\n); req.on(close, () sessions.delete(sessionId)); }); // 2. 接收 JSON-RPC 请求 app.post(/message, (req, res) { const sessionId req.query.sessionId; const sse sessions.get(sessionId); const { jsonrpc, id, method, params } req.body; let result null; let error null; if (method initialize) { result { protocolVersion: 2024-11-05, capabilities: { tools: { listChanged: false } }, serverInfo: { name: demo-mcp-server, version: 1.0.0 }, }; } else if (method tools/list) { result { tools: Object.entries(tools).map(([name, t]) ({ name, description: t.description, inputSchema: t.inputSchema, })), }; } else if (method tools/call) { const tool tools[params.name]; if (!tool) { error { code: -32601, message: Unknown tool: ${params.name} }; } else { const output tool.handler(params.arguments || {}); result { content: [{ type: text, text: String(output) }] }; } } else if (method notifications/initialized) { res.status(202).end(); return; } else { error { code: -32601, message: Method not found: ${method} }; } // 3. 响应通过 SSE 回推而不是直接返回 const payload error ? { jsonrpc: 2.0, id, error } : { jsonrpc: 2.0, id, result }; if (sse) sse.write(event: message\ndata: ${JSON.stringify(payload)}\n\n); res.status(202).end(); }); app.listen(3000, () console.log(MCP Server on http://localhost:3000));启动node server.js到这里一个具备 SSE 通道、JSON-RPC 解析、工具注册能力的 MCP Server 骨架就完成了。它没有依赖任何 MCP 专用库所有行为都对应规范里的固定动作。4. 验证请求与成功结果先验证 SSE 通道。开一个终端执行curl -N http://localhost:3000/sse你会看到服务端立刻推来一行event: endpoint data: /message?sessionIdsess-1750000000000这行就是 MCP 的「两通道」握手SSE 负责回推endpoint 事件告诉客户端 POST 往哪打。记下这个 sessionId另开终端发 initializecurl -X POST http://localhost:3000/message?sessionIdsess-1750000000000 \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2024-11-05,capabilities:{},clientInfo:{name:curl,version:1.0}}}POST 接口返回 202真正的响应会从刚才那个 SSE 终端里冒出来{jsonrpc:2.0,id:1,result:{protocolVersion:2024-11-05,capabilities:{tools:{listChanged:false}},serverInfo:{name:demo-mcp-server,version:1.0.0}}}接着拉工具列表curl -X POST http://localhost:3000/message?sessionIdsess-1750000000000 \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:2,method:tools/list,params:{}}SSE 终端会收到包含get_time和echo的 tools 数组。最后调用工具curl -X POST http://localhost:3000/message?sessionIdsess-1750000000000 \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:3,method:tools/call,params:{name:echo,arguments:{message:hello mcp}}}SSE 回推{jsonrpc:2.0,id:3,result:{content:[{type:text,text:echo: hello mcp}]}}看到这个结果说明整条链路通了SSE 建连、endpoint 下发、initialize 协商、tools/list 发现、tools/call 执行五步全部命中。你也可以用 MCP Inspector 图形化验证执行npx modelcontextprotocol/inspector选择 SSE Transport填入http://localhost:3000/sse点 Connect 后依次点 Initialize、List Tools、Call Tool效果和 curl 完全一致。5. 本篇常见错排查第一个高频错误是 POST 响应直接返回 JSON。很多人习惯让/message接口res.json(result)结果客户端收不到。MCP 规定响应必须走 SSE 回推POST 只返回 202 表示已接收。如果你发现 Inspector 一直转圈先检查这里。第二个是 endpoint 事件格式写错。规范要求data是纯 URI 字符串不要包成 JSON 对象。写成data: {uri:/message}客户端会解析失败表现为连上了但发不出请求。第三个是 sessionId 对不上。SSE 建连时生成的 sessionId 必须和 POST 查询参数一致否则服务端找不到对应的 SSE 通道响应无处可推。多客户端场景下建议用 Map 严格隔离。第四个是 initialize 没返回 protocolVersion。部分客户端会校验版本号缺失或格式不对会直接断开。固定写2024-11-05即可。第五个是工具 inputSchema 写成非标准 JSON Schema。type、properties、required三个字段要齐全否则模型侧生成参数时容易出错。如果你用 TaoToken 接入模型做联调可以在接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite里对照请求格式确认工具描述被正确传递。第六个是 Origin 校验缺失导致的安全问题。生产环境务必校验 Origin 头只允许可信来源避免被恶意页面利用。本地调试可以暂时放开上线前补上。6. 继续深入的方向把上面这套跑通之后你对 MCP 的理解就不再停留在概念层了。下一步可以做的把工具 handler 换成真实业务逻辑比如查数据库、调内部 API给 tools/list 加上动态刷新配合listChanged: true让客户端感知工具变更或者把 SSE 换成 Streamable HTTP 传输适配更新的客户端实现。如果你打算把这套 Server 接到真实 Agent 里跑长会话建议用 TaoToken 的 Coding Plan 通道Key 和 API 地址在 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite管理模型对话调试用https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite。工具注册和 JSON-RPC 这两块吃透之后剩下的就是业务问题了。
返回列表