跳到主要内容

01 · 协议地基:JSON-RPC 与消息形态

这一章讲 MCP 最底层的“话术”——所有交互都是 JSON-RPC 2.0 消息。搞懂请求/响应/通知三种形态、draft 新增的 resultType、错误码规则、以及每请求自带的 _meta,后面所有机制都是在这之上搭的。

1.1 为什么是 JSON-RPC

MCP 不发明自己的线协议,直接用 JSON-RPC 2.0:一个极简、跨语言、无关传输的“远程调用”约定。客户端和服务器之间的每一条消息都必须符合 JSON-RPC(docs/specification/draft/basic/index.mdx:27)。

好处很实在:任何语言都有现成 JSON 解析;同一套消息格式能跑在 stdio、HTTP、甚至 TCP 上(传输层只负责搬字节)。

1.2 三种消息形态

MCP 只有三种消息(docs/specification/draft/basic/index.mdx:31 起)。先看一张对照表:

形态id?有回复?用途
Request(请求)发起一次操作(如 tools/call
Response(响应)是(与请求同 id)—(它本身就是回复)返回结果或错误
Notification(通知)单向告知,接收方不得回复

请求的形状(docs/specification/draft/basic/index.mdx:35):

{
jsonrpc: "2.0";
id: string | number; // 不得为 null;不得和未完成的其它请求重号
method: string; // 如 "tools/call"
params?: { [key: string]: unknown };
}

两条铁律值得记:id 不得为 null(这点比原版 JSON-RPC 更严),且不得复用一个尚未收到响应的 id(docs/specification/draft/basic/index.mdx:46)。

通知没有 id,发出去就不管了——比如 notifications/tools/list_changed(工具清单变了)。

1.3 draft 的新意:每个 result 都带 resultType

这是 draft 相对旧版最显眼的一处改动。成功响应长这样(docs/specification/draft/basic/index.mdx:59):

{
jsonrpc: "2.0";
id: string | number;
result: {
resultType: string; // ← draft 新增的必填字段
[key: string]: unknown;
};
}

resultType 是一个多态结果的判别标签(docs/specification/draft/basic/index.mdx:77),客户端靠它决定怎么解析 result:

  • "complete" — 请求完成了result 里是最终内容。
  • "input_required" — 请求还没完,服务器需要更多输入;result 是一个 InputRequiredResult(这正是第 04 章 MRTR 的入口)。
  • 扩展可以新增取值;客户端遇到不认识resultType 必须当作非法。

向后兼容的关键一招(docs/specification/draft/basic/index.mdx:85):旧版服务器的响应里没有 resultType,所以客户端必须把“缺失”当成 "complete"。这样新客户端能无痛对接老服务器。

1.4 错误响应与错误码分区

失败时回 error(docs/specification/draft/basic/index.mdx:91):

{
jsonrpc: "2.0";
id?: string | number; // 一般同请求 id;请求烂到读不出 id 时可省
error: { code: number; message: string; data?: unknown; }
}

MCP 沿用 JSON-RPC 标准码(-32700-32600-32603),并把 JSON-RPC 给“实现自定义”预留的 -32000-32099 重新划了界docs/specification/draft/basic/index.mdx:113):

子区间归谁规则
-32000 ~ -32019历史遗留新实现不应再用;除 -32002 外不得假设含义
-32020 ~ -32099MCP 规范专属只有规范能定义;实现不得乱发

draft 在专属区里定义了三个新码(docs/specification/draft/basic/index.mdx:129,常量见 schema/draft/schema.ts:426):

Code名字含义
-32020HeaderMismatchHTTP 头与请求体不一致(见 05 章)
-32021MissingRequiredClientCapability处理请求需要某个客户端没声明的能力
-32022UnsupportedProtocolVersion服务器不支持请求声明的协议版本(见 02 章)

注意 draft 把“资源不存在”的码从旧版的 -32002 改成了标准的 -32602(Invalid Params),但客户端仍应接受老服务器发来的 -32002docs/specification/draft/server/resources.mdx:406、changelog :37)。

1.5 工具错误的两层模型(一个易踩的坑)

一个很重要的区分:MCP 里“出错”有两种完全不同的表达方式(docs/specification/draft/server/tools.mdx:730):

请求结构本身有问题?
┌───────────────┴───────────────┐
是(协议错误) 否,但工具执行失败
│ │
JSON-RPC error 字段 正常 result 里 isError: true
code: -32602 等 content 里放给模型看的错误文字
例:未知工具、参数不合 schema 例:API 失败、日期格式错、业务校验
→ 模型一般无力自救 → 模型能据此改参数重试

这个设计很巧:把“模型修不好的协议错误”和“模型能据此自我纠正的执行错误”分开。后者放进正常结果的 isError: true + 文字描述里,让 LLM 看到、自己改 arguments 重试(docs/specification/draft/server/tools.mdx:752:776)。

1.6 _meta:每请求自带的元数据袋

_meta 是 MCP 预留给“附加元数据”的字段,命名有严格规则(docs/specification/draft/basic/index.mdx:320)。draft 版让它扛起了无状态协议的关键职责——每个客户端请求都必须_meta 里带这几样(docs/specification/draft/basic/index.mdx:347 的表):

_meta类型必填作用
io.modelcontextprotocol/protocolVersionstring本请求用的协议版本
io.modelcontextprotocol/clientInfoImplementation客户端名字与版本
io.modelcontextprotocol/clientCapabilitiesClientCapabilities本请求相关的客户端能力
io.modelcontextprotocol/logLevelLoggingLevel本请求希望服务器输出的最低日志级别(取代了已移除的 logging/setLevel 方法,changelog :20

少了任一必填字段 = 请求畸形,服务器必须-32602 拒绝(HTTP 上是 400)。若请求需要某个客户端没声明的能力,服务器必须MissingRequiredClientCapabilityError-32021),并在 data.requiredCapabilities 里列出缺的能力(docs/specification/draft/basic/index.mdx:365)。

命名规则要点(docs/specification/draft/basic/index.mdx:335):一般前缀用 reverse-DNS 写法(如 com.example/,而非 example.com/)。其中有个保留前缀红线:前缀第二段是 modelcontextprotocolmcp 的,全归 MCP 保留(io.modelcontextprotocol/dev.mcp/ 都保留;但 com.example.mcp/ 不保留,因为第二段是 example)(docs/specification/draft/basic/index.mdx:338)。另有一个例外:traceparent / tracestate / baggage 三个键直接复用,承载 OpenTelemetry 追踪上下文(docs/specification/draft/basic/index.mdx:379)。

1.7 无状态:整份规范的灵魂

draft 把 MCP 明确定义为无状态协议——处理一条请求所需的全部信息,都在这条请求里(docs/specification/draft/basic/index.mdx:182)。具体几条硬性约束:

  • 服务器不得靠“同一连接上的前序请求”来推断上下文(能力、版本、身份都从 _meta 现取)。
  • 一个开着的连接(如一个 stdio 子进程)不等于一次会话:客户端可以在同一根管子上交错发不相关的请求,服务器不得把“连接/进程身份”当成对话连续性的代理(docs/specification/draft/basic/index.mdx:202)。
  • 需要跨多请求的状态(长任务、应用级句柄)必须用一个显式标识符,由客户端每次请求都带上。

这条原则会在后面反复回响:工具要维持状态得返回显式 handle(03 章)、服务器反向请求客户端得把上下文编码进 requestState(04 章)、stdio 进程崩了直接重启重试(05 章)。

代码地图

主题文件路径符号 / 锚点
请求/响应/通知类型schema/draft/schema.tsJSONRPCRequestJSONRPCResultResponseJSONRPCNotification
版本/JSON-RPC 常量schema/draft/schema.tsLATEST_PROTOCOL_VERSIONJSONRPC_VERSION
错误码常量schema/draft/schema.tsUNSUPPORTED_PROTOCOL_VERSIONHeaderMismatchErrorMissingRequiredClientCapabilityError
input_required 结果schema/draft/schema.tsInputRequiredResult
消息形态散文docs/specification/draft/basic/index.mdxMessages / Statelessness / _meta