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 |
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"来读取敏感文件。
3.5.3 id 的陷阱
id字段看似简单,其实坑不少:
- id为null:有些实现把id为null的请求当作通知处理,但规范里id为null是合法的请求id
- id重复:前面说过了,会导致响应错乱
- id类型混淆:有些服务端只支持number,你传个string它就报错
我建议在实现MCP客户端时,id用递增的整数,并且做好去重检查。服务端则应该对id做严格校验,防止注入攻击。
3.6 消息结构全景图
下面这张图展示了MCP消息结构的全貌,以及请求、响应、通知之间的关系:
3.7 实战:如何解析MCP消息
讲完理论,咱们来点实际的。假设你正在逆向一个MCP服务端,第一步就是抓包看消息。我一般用Wireshark或者mitmproxy,过滤出JSON-RPC流量。
拿到消息后,按以下步骤解析:
- 检查jsonrpc版本:不是"2.0"的直接丢弃
- 判断是否有id:有id是请求或响应,无id是通知
- 检查method:是否在MCP规范定义的范围内
- 校验params:类型是否正确,有没有可疑字段
- 检查result/error:不能同时存在
小技巧:我写了个Python脚本,用jsonschema库自动校验MCP消息结构。每次抓到新消息,跑一遍脚本就能快速发现异常。比如,某个字段类型不对,或者多了不该有的字段。这招在挖0day时特别管用。
好了,MCP的消息结构就聊到这儿。记住一句话:消息结构是MCP的“语法”,而安全漏洞往往藏在“语法”的边界处。下一节咱们会深入method的调用流程,看看请求是怎么被路由到具体处理函数的——那里才是漏洞的高发区。
公众号:蓝海资料掘金营,微信deep3321