
从抓包看 MCP:AI 工具调用背后的通信机制
TL;DR
通过抓包分析,我们清晰地了解了 MCP 通信的全过程:从建立 SSE 连接、三步初始化、工具调用操作到最终的连接终止。可以看出,MCP 基于简单的 SSE 协议搭建了一个功能强大的工具调用框架,使 AI 代理能够便捷地调用外部工具完成复杂任务。
相比传统的接口调用方式,MCP 更加灵活,能够自动适应不同的工具集,让 AI 代理 " 即插即用 " 地使用各种服务能力,这也是其设计的精妙之处。
当然,MCP 也并不是完美的,作为一个新兴的协议,它仍然在不断发展中。未来可能会有更多的功能和特性被添加进来,以满足更复杂的需求。
背景
MCP 支持两种标准的传输实现:标准输入/输出(stdio)和 Server-Sent Event(下称 SSE)。stdio 基于命令行工具,多用于本地集成,通过进程通信来实现;SSE 基于客户端和服务器的网络通信,用于跨设备网络的通信场景。
既然是用抓包来分析,我们就要选择使用 SSE 传输 MCP server,然后通过工具进行网络抓包分析。在抓包分析之前,我们必要对 SSE 协议进行简单的了解。
SSE 协议
SSE 协议 是一种服务器推送技术,使客户端能够通过 HTTP 连接从服务器自动接受更新,通常用于服务器向客户端发送消息更新或者连续的数据流(流信息 streaming)。
本质上,HTTP 协议是无法实现主动推送消息的,除非服务端“通知”客户端接下来发送的是流信息。因此客户端便不会断开该连接,并持续从该连接上接收数据流。
看到这里你是否想到了 WebSocket 协议,二者看起来都是客户端与服务端建立连接,然后服务端向客户端推送数据。看似相同,实际差别还挺大:
- SSE 是基于 HTTP 的轻量级协议;WebSocket 是独立的协议。
- SSE 是基于 HTTP 请求
Accept: text/event-stream
;WebSocket 借助 HTTP 升级协议Upgrade: websocket
,之后使用独立协议。 - SSE 是伪双工,只支持服务端到客户端的单向通信,客户端到服务端的通信还需要另外发送 HTTP 请求进行;WebSocket 是全双工的双向通信。
- SSE 简单、轻量,适合单向低频推送;WebSocket 复杂度高、实时性强,适合双向高频交互。
从上面的对比不难看出 MCP 选择 SSE 作为网络传输协议的原因了。
了解了 SSE 协议之后,我们就可以开始了。
环境
- 抓包工具:Proxyman ,并安装 CA 证书,方便处理 HTTPS 的请求。
- AI 应用:VSCode Insiders,安装 Github Copilot 插件并开启 Agent 模式。
- MCP Server:使用 上一篇文章 中的 Spring REST API 示例。
配置 MCP Server
在 settings.json 中添加 MCP Server 配置,为了能够使用 Proxyman 的 HTTP Proxy 在 /etc/hosts 中添加 127.0.0.1 nio.local
。
{
"mcp": {
"servers": {
"spring-ai-mcp-sample": {
"type": "sse",
"url": "http://nio.local:8080/sse"
}
}
}
}
添加好之后就可以启动 MCP Client 连接 Server 了。
MCP 通信
下面我们将通过抓包分析,详细了解 MCP 通信的完整生命周期,包括建立连接、初始化、操作和终止四个阶段。"
当我们的 VSCode 成功连接到 MCP Server,此时从 Proxyman 已经可以看到多条通信了。
建立连接
由于不确定 Server 支持哪种方法,MCP Client 会同时发送 GET 和 POST 请求到我们配置的 Server 地址,尝试建立连接。请求中的 Accept 是 text/event-stream,说明是与 Server 尝试进行 SSE 通信。
这里配置的 Server 仅支持通过 GET 方式建立 SSE 通信,POST 请求收到 404 响应。而 GET请求的响应中,Server 端回传了如下信息:
- 会话 id:3e19fbcd-51f4-4784-9f63-538c9a203859
- 事件 event :endpoint 表示数据的内容,也就是后续客户端 Client 与 Server 单向通信的端点。
- 数据 data:/mcp/messages?sessionId=3e19fbcd-51f4-4784-9f63-538c9a203859,其中 /mcp/messages 是由服务侧配置的
spring.ai.mcp.server.sse-message-endpoint: /mcp/messages
。
id:3e19fbcd-51f4-4784-9f63-538c9a203859
event:endpoint
data:/mcp/messages?sessionId=3e19fbcd-51f4-4784-9f63-538c9a203859
这个 HTTP 连接作为后续 Server 向 Client 推送流信息的通道,所以在截图中我们看到了其他的流信息。此时 MCP Client 与 Server 连接的声明周期就开始了:
- 初始化
- 操作
- 终止
初始化
初始化阶段必须是客户端与服务器之间的首次交互,这个过程有点类似 TCP 的三次握手。
Client 发起初始化请求
从 Server 接收到后续的通信端点后,Client 会发送 initialize 请求进行初始化,上报信息和功能协商。
- protocolVersion 协议的版本
- capabilities 功能支持:listChanged 表示支持列表变更通知
- clientInfo 客户端信息
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {
"roots": {
"listChanged": true
}
},
"clientInfo": {
"name": "Visual Studio Code - Insiders",
"version": "1.100.0-insider"
}
}
}
Server 响应初始化请求
同样 Server 也回传了流信息
- 相同的会话 id
- 事件类型 message
- 事件数据
id:3e19fbcd-51f4-4784-9f63-538c9a203859
event:message
data:{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"logging":{},"tools":{"listChanged":true}},"serverInfo":{"name":"webmvc-mcp-server","version":"1.0.0"}}}
在事件的数据部分,Server 也提供了与请求类似的内容(在下文中将直接展示流信息中的数据部分):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"logging": {},
"tools": {
"listChanged": true
}
},
"serverInfo": {
"name": "webmvc-mcp-server",
"version": "1.0.0"
}
}
}
初始化完成
在完成与 Server 端的信息交换,并协商(如版本兼容、功能支持)成功后,Client 发送请求完成初始化。
{
"method": "notifications/initialized",
"jsonrpc": "2.0"
}
这一次 Server 并不会有任何响应,像是 TCP 握手时客户端发送了 ACK 后,服务端不会进行任何处理一样。
操作
获取 tool 列表
完成初始化后,Client 发送请求获取 Server 支持的 tool 列表。
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
服务端通过 SSE 连接回传 tool 列表,我们使用的示例 Server 中包含了 4 个 tool。在响应内容包含了如 tool 名字、输入 schema 参数说明等信息。客户端收到这个响应后,会在本地缓存 tool 列表避免频繁的请求。只有当 Server 端更新了列表并通知 Client 后才会更新缓存内容。
篇幅原因,没有全部展示列表内容。
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "addUser",
"description": "Add a new user",
"inputSchema": {
"type": "object",
"properties": {
"arg0": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"email",
"name"
],
"description": "user to add"
}
},
"required": [
"arg0"
],
"additionalProperties": false
}
},
//...
]
}
}
有了 tool 列表之后,我们便可以尝试让 Copilot 为了执行任务了。在 Copilot Agent 模式下输入和上次一样的任务:
First, help me check the user list to see if there is a user named Carson. If not, add a new user: Carson carson@gmail.com; then check the list again to see if the new user was added successfully. Finally, say hello to Carson.
先来看执行结果。
在我发出任务请求后,VSCode 经过一通分析决定一次执行几个 tool 来完成任务。这里我使用的是 GPT-4o 的模型,没有任何推理过程的展示。如果不是展开了工具的执行结果,最终能看到的只有最后的一句话。
如果切换到 Claude 3.7 Sonnet 模型,执行时会加入推理,整个流程会清晰很多。
执行
回到 Proxyman 查看抓取的请求。
-
VScode 先请求 Copilot Server 时传输的请求内容比较长。以 GPT-4o 模型为例,请求大小为 49.7 KB,响应 1.34 KB。
请求中包含了:
- 一段非常长的系统 Prompt,有兴趣的可以参考开发者整理的 GitHub Copilot Agent 官方 Prompt
- 可用的 tool 列表,包括 VSCode 官方提供的系统 tool 以及配置的 MCP Server 提供的 tool
在响应中包含了经过分析任务后决定要调用的 tool:
{ "choices": [ { "index": 0, "delta": { "content": null, "role": "assistant", "tool_calls": [ { "function": { "arguments": "", "name": "bb7_getUsers" }, "id": "call_nL7ToTNvrfLwUPYoqtUH8Yx3", "index": 0, "type": "function" } ] } } ], "created": 1745649196, "id": "chatcmpl-BQTO863fJsOBHD4tU1LN3AEk5Uuo2", "model": "gpt-4o-2024-11-20", "system_fingerprint": "fp_ee1d74bde0" }
-
VSCode 根据响应的内容,调用 MCP Tool。
//http://nio.local:8080/mcp/messages?sessionId=3e19fbcd-51f4-4784-9f63-538c9a203859 { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "getUsers", "arguments": {} } }
MCP Server 在 SSE 连接中回传 tool 的调用结果。
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "[{\"name\":\"John\",\"email\":\"john@example.com\"},{\"name\":\"Jane\",\"email\":\"jane@example.com\"}]" } ], "isError": false } }
紧接着 VSCode 将调用结果发送给 Copilot Server 进行处理,然后又得到一个要调用的 tool,以及需要提供的参数。
-
如此往复,直到最终完成任务的执行。在最右一个发送给 Copilot Server 的请求中,可以看到这个任务执行过程中所有调用的 tool 请求和响应的列表。也就是说,每次调用模型时,都会带上此前调用的所有 tool 请求和响应,因此请求的 size 也是逐渐变大的。
终止
终止操作就简单了,对于 SSE 传输类型的 MCP 交互来说,就是断开相关的 HTTP 连接。