首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >MCP 进阶实战:不用 SDK,60 行 Node.js 看穿协议本质

MCP 进阶实战:不用 SDK,60 行 Node.js 看穿协议本质

原创
作者头像
用户11136834
发布于 2026-10-04 01:50:29
发布于 2026-10-04 01:50:29
270
举报

前言

上一篇 MCP 入门里,我们用官方 SDK 十几行就跑起来一个待办清单 Server。方便是方便,但 SDK 就像自动挡——你会开,却未必知道离合器在哪。面试官或同事一句"MCP 底下到底是什么",很多人就卡壳了。

这篇文章反其道而行:不用任何 SDK,用 Node.js 原生能力手写一个能被真实客户端连接的 MCP Server。目标只有一个——把 SDK 藏起来的协议层亲手摸一遍。读完你会明确知道:MCP 的传输是什么、消息长什么样、客户端和服务端各自怎么对话。

一、先掀开天花板:MCP 的协议骨架

把 SDK 拿掉之后,MCP 只剩三句话:

  1. 传输层是 stdio:客户端把服务端当子进程拉起,双方通过标准输入/输出交换字节;
  2. 消息是 JSON-RPC 2.0:每条消息是一个 JSON 对象,用换行符分隔(一行一条,不搞多行拼接);
  3. 交互就是三种角色:请求(带 id,要回应答)、通知(无 id,发了就算)、响应(请求的应答,result 或 error 二选一)。

没有魔法。所谓"能力协商",就是客户端先发一个 initialize 请求报上自己支持什么,服务端回应自己支持什么;所谓"工具调用",就是客户端发 tools/call 请求带上工具名和参数,服务端执行后回 result。全部是普通 JSON 文本。

二、60 行最小实现

不装任何依赖,新建 mini-mcp.mjs:

代码语言:javascript
复制
import { createInterface } from 'node:readline';

const TOOLS = [
  {
    name: 'echo',
    description: '原样返回输入文本,用于连通性测试',
    inputSchema: {
      type: 'object',
      properties: { text: { type: 'string', describe: '要返回的文本' } },
      required: ['text'],
    },
  },
  {
    name: 'now',
    description: '返回当前时间的 ISO 8601 字符串',
    inputSchema: { type: 'object', properties: {} },
  },
];

const reply = (id, result) =>
  process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');

const replyError = (id, code, message) =>
  process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n');

const handlers = {
  initialize: (id) =>
    reply(id, {
      protocolVersion: '2024-11-05',
      capabilities: { tools: { listChanged: false } },
      serverInfo: { name: 'mini-mcp', version: '0.1.0' },
    }),
  'tools/list': (id) => reply(id, { tools: TOOLS }),
  'tools/call': (id, params) => {
    const tool = TOOLS.find((t) => t.name === params.name);
    if (!tool) {
      replyError(id, -32602, 'unknown tool: ' + params.name);
      return;
    }
    const text = params.name === 'echo'
      ? String(params.arguments.text)
      : new Date().toISOString();
    reply(id, { content: [{ type: 'text', text }] });
  },
};

const rl = createInterface({ input: process.stdin });
rl.on('line', (line) => {
  if (!line.trim()) return;
  let msg;
  try {
    msg = JSON.parse(line);
  } catch {
    return;
  }
  if (msg.id === undefined) return; // 通知(如 initialized):无需回应
  const handler = handlers[msg.method];
  if (handler) handler(msg.id, msg.params || {});
  else replyError(msg.id, -32601, 'method not found: ' + msg.method);
});

不到 60 行,一个合法的 MCP Server 就写完了。逐段看它处理了什么。

三、逐段拆解:SDK 替你做了什么

3.1 换行分隔的读写循环

readline 逐行读 stdin,每行 parse 成一个 JSON-RPC 消息——这就是 stdio 传输的全部实现。SDK 里的 StdioServerTransport 干的就是这件事,外加一些缓冲和背压处理。

注意响应必须单行写出:JSON.stringify 不会产生裸换行,所以末尾补一个 \n 就是完整的帧。反过来,任何多行 JSON 都会把协议流弄脏。

3.2 请求与通知的判别:看有没有 id

msg.id === undefined 的就是通知。客户端完成 initialize 握手后会发一条 notifications/initialized 通知,正确行为是什么都不回——对通知回响应反而是协议错误。SDK 里这些分支都被封装掉了,手写一次就忘不掉。

3.3 initialize:能力协商

客户端:你好,我说 2024-11-05 版协议,我会这些能力。服务端:收到,我支持 tools(不支持 resources/prompts 就不写进 capabilities)。之后客户端才会开始调 tools/list。这段对话决定了后面哪些方法会被调用——协商过什么,才能用什么。

3.4 错误也是协议的一部分

工具不存在返回 -32602(无效参数),未知方法返回 -32601(方法不存在)——这些码来自 JSON-RPC 2.0 规范。SDK 会把异常自动包装成错误响应;手写时你要自己保证:任何分支都必须回消息,否则客户端会一直等到超时,表现就是"卡死"。

四、命令行实测:三条消息走完全流程

不用客户端,直接用管道喂数据:

代码语言:javascript
复制
printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hello mcp"}}}' \
  | node mini-mcp.mjs

预期输出两行(initialize 的应答 + tools/call 的应答,中间的通知没有回应):

代码语言:javascript
复制
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":false}},"serverInfo":{"name":"mini-mcp","version":"0.1.0"}}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"hello mcp"}]}}

id=1 对第一行、id=2 对第二行——JSON-RPC 靠 id 关联请求与响应,异步乱序也不怕。

五、接入真实客户端

把它写进 Claude Desktop 的配置(macOS 路径 ~/Library/Application Support/Claude/claude_desktop_config.json),和上一篇的待办 Server 并排:

代码语言:javascript
复制
{
  "mcpServers": {
    "mini": {
      "command": "node",
      "args": ["/绝对路径/mini-mcp.mjs"]
    }
  }
}

重启客户端,对话里说"用 echo 工具返回 MCP",模型就会发起完整的握手和调用链。你的 60 行代码扛住了真实客户端。

六、协议在往哪走:一段演进注记

以上是当前客户端生态的事实标准路径(initialize 握手)。值得知道的是,最新版规范(2026-07-28 起)把协议推向无状态化:每个请求在 _meta 里自带协议版本与客户端能力,服务端能力改由 server/discover 上报,transport 层也全面转向 Streamable HTTP。方向是让服务端更容易做水平扩展。

但演进不改变本文的核心事实:消息格式仍是 JSON-RPC 2.0,语义仍是请求/通知/响应三件套。手写过这一层的人,读任何新版本的 spec 都只是在看"方法名变了没"。

七、什么时候不该手写

手写��为了理解,不是为了生产。真实项目请回到 SDK,理由很实际:

  1. 输入校验:SDK 用 zod 生成并校验参数 schema,手写版裸奔;
  2. 传输抽象:同一套工具逻辑要同时支持 stdio 和 Streamable HTTP 时,SDK 一行切换,手写要自己实现 HTTP 层;
  3. 细节长尾:分页、取消、进度通知、工具列表变更通知这些,SDK 都处理了。

一句话:手写一次懂原理,生产永远用 SDK。

小结

MCP 被 SDK 包装得像黑盒,拆开看只是"换行分隔的 JSON-RPC":请求带 id 要回应答、通知不回、错误有码。60 行代码实现 initialize 协商、tools/list、tools/call 之后,协议在你眼里就再无秘密——下次调试 MCP 连接问题,你直接看消息流就知道哪一步断了。

参考

  1. MCP 官方文档:Architecture(传输层与数据层)
  2. JSON-RPC 2.0 规范(jsonrpc.org)
  3. 本系列上篇:《给大模型装上「USB-C」口:MCP 协议入门与实战》

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 前言
  • 一、先掀开天花板:MCP 的协议骨架
  • 二、60 行最小实现
  • 三、逐段拆解:SDK 替你做了什么
    • 3.1 换行分隔的读写循环
    • 3.2 请求与通知的判别:看有没有 id
    • 3.3 initialize:能力协商
    • 3.4 错误也是协议的一部分
  • 四、命令行实测:三条消息走完全流程
  • 五、接入真实客户端
  • 六、协议在往哪走:一段演进注记
  • 七、什么时候不该手写
  • 小结
  • 参考
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档