3. MCP消息结构:JSON-RPC 2.0规范、请求/响应/通知格式、消息字段详解

聊到MCP的消息结构,咱们得先搞清楚它的底子——JSON-RPC 2.0。说白了,MCP就是站在这个协议的肩膀上,定义了一套AI工具调用的“通用语言”。我刚开始接触时也觉得,不就是个RPC嘛,有啥特别的?后来挖了几个漏洞才发现,细节全藏在消息结构里。

3.1 JSON-RPC 2.0 规范概览

JSON-RPC 2.0是一种轻量级的远程过程调用协议。它用JSON格式传输数据,核心就三个概念:请求响应通知。MCP完全遵循这套规范,但加了一些自己的“私货”。

我个人习惯把JSON-RPC 2.0看作一个“快递系统”:

  • 请求:你寄出一个包裹,上面贴了唯一单号(id)
  • 响应:快递员回执,告诉你包裹送到了(对应同一个id)
  • 通知:你扔了个没贴单号的包裹,对方收不收都行,你也不等回执

嗯,这里要注意:MCP里通知用得特别多,尤其是服务端主动推送事件时。我曾在项目中遇到过客户端没正确处理通知,结果内存泄漏的案例——因为通知没有id,客户端却傻傻地等响应,最后堆了一堆未完成的Promise。

3.2 请求格式详解

一个标准的MCP请求长这样:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "city": "北京"
    }
  }
}

字段拆开来看:

字段 类型 说明
jsonrpc string 固定为"2.0",版本标识
id number/string/null 请求唯一标识,响应时会原样返回
method string 要调用的方法名,MCP里通常是"tools/xxx"、"resources/xxx"等
params object/array 方法参数,MCP里几乎都用object
注意:id字段不能重复!如果你发了两个相同id的请求,服务端返回响应时客户端就懵了——到底哪个请求成功了?我曾经挖过一个MCP服务端的bug,就是因为id冲突导致响应错乱,最终可以越权调用其他用户的工具。

3.3 响应格式详解

有请求就有响应。MCP的响应分两种:成功和错误。

成功响应:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "北京今天晴,25°C"
      }
    ]
  }
}

错误响应:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32601,
    "message": "Method not found",
    "data": null
  }
}

你想想看,为什么MCP要定义这么严格的响应格式?因为AI模型是异步调用的,它发一个请求出去,可能过几秒甚至几分钟才收到响应。如果响应格式不统一,模型根本没法解析。

关键点:result和error不能同时存在。这是JSON-RPC 2.0的硬性规定。我在逆向分析时发现,有些MCP服务端实现不严谨,既返回了result又返回了error,结果客户端只取了result,错误信息被静默丢弃了——这其实是个安全漏洞,攻击者可以利用这个“静默吞错”来掩盖攻击行为。

3.4 通知格式详解

通知和请求长得几乎一样,唯一的区别是——没有id字段

{
  "jsonrpc": "2.0",
  "method": "notifications/initialized",
  "params": {}
}

通知的典型场景:

  • 客户端通知服务端“我已初始化完成”
  • 服务端主动推送“资源列表已更新”
  • 心跳检测(虽然MCP没强制,但很多实现会加)

为什么会这样设计?说白了,通知就是“说了你就当听到了,我不等你回复”。这在AI场景下特别有用——模型可以一边处理任务,一边接收服务端推送的进度更新,不用阻塞等待。

我的经验:挖MCP漏洞时,通知是个“富矿”。因为通知没有id,很多服务端实现会忽略对通知的校验。我曾经发现一个服务端,它不验证通知的来源,导致攻击者可以伪造“资源已更新”通知,诱骗客户端重新拉取恶意资源。嗯,这个坑后来被列入了CVE。

3.5 消息字段深度解析

除了上面说的基础字段,MCP还扩展了一些“私货”。咱们挑几个重点的:

3.5.1 method 命名规范

MCP的method命名很有规律:

  • tools/xxx:工具调用相关
  • resources/xxx:资源操作相关
  • prompts/xxx:提示模板相关
  • notifications/xxx:通知事件
  • initialize:初始化(特殊,没有前缀)

我个人习惯把这个命名空间看作“权限边界”。比如,一个服务端如果只暴露了resources/read,却偷偷实现了resources/write,那就有问题了。逆向分析时,我通常会先枚举所有method,看看有没有“隐藏接口”。

3.5.2 params 的玄机

params字段在MCP里几乎都是object,但内部结构因method而异。举个例子:

// tools/call 的 params
{
  "name": "get_weather",
  "arguments": {
    "city": "北京"
  }
}

// resources/read 的 params
{
  "uri": "file:///etc/passwd"
}

看到了吗?resources/read的params里直接传了一个文件路径。如果服务端没做路径校验,这就是个路径穿越漏洞。我挖过好几个这样的案例——攻击者通过构造uri: "file:///../../etc/shadow"来读取敏感文件。

避坑指南:我曾经在审计一个MCP服务端时,发现它对params中的字符串字段几乎不做任何校验。攻击者可以传入超长字符串导致服务端OOM,也可以传入特殊字符触发SQL注入(如果服务端把params存到了数据库里)。所以,永远不要信任params里的任何字段

3.5.3 id 的陷阱

id字段看似简单,其实坑不少:

  • id为null:有些实现把id为null的请求当作通知处理,但规范里id为null是合法的请求id
  • id重复:前面说过了,会导致响应错乱
  • id类型混淆:有些服务端只支持number,你传个string它就报错

我建议在实现MCP客户端时,id用递增的整数,并且做好去重检查。服务端则应该对id做严格校验,防止注入攻击。

3.6 消息结构全景图

下面这张图展示了MCP消息结构的全貌,以及请求、响应、通知之间的关系:

MCP消息结构全景图 请求 (Request) 响应 (Response) 通知 (Notification) 有id 请求字段 • jsonrpc: "2.0" • id: number/string • method: string • params: object 响应字段 • jsonrpc: "2.0" • id: 与请求一致 • result: object • error: object 通知字段 • jsonrpc: "2.0" • 无 id 字段 • method: string • params: object MCP 扩展字段与安全关注点 • method 命名空间:tools/、resources/、prompts/、notifications/ • params 注入风险:路径穿越、命令注入、SQL注入 • id 冲突导致响应错乱,可被利用进行越权 • 通知无校验,可伪造事件触发客户端异常行为

3.7 实战:如何解析MCP消息

讲完理论,咱们来点实际的。假设你正在逆向一个MCP服务端,第一步就是抓包看消息。我一般用Wireshark或者mitmproxy,过滤出JSON-RPC流量。

拿到消息后,按以下步骤解析:

  1. 检查jsonrpc版本:不是"2.0"的直接丢弃
  2. 判断是否有id:有id是请求或响应,无id是通知
  3. 检查method:是否在MCP规范定义的范围内
  4. 校验params:类型是否正确,有没有可疑字段
  5. 检查result/error:不能同时存在

小技巧:我写了个Python脚本,用jsonschema库自动校验MCP消息结构。每次抓到新消息,跑一遍脚本就能快速发现异常。比如,某个字段类型不对,或者多了不该有的字段。这招在挖0day时特别管用。

好了,MCP的消息结构就聊到这儿。记住一句话:消息结构是MCP的“语法”,而安全漏洞往往藏在“语法”的边界处。下一节咱们会深入method的调用流程,看看请求是怎么被路由到具体处理函数的——那里才是漏洞的高发区。


公众号:蓝海资料掘金营,微信deep3321