從抓包看 MCP:AI 工具調(diào)用背后的通信機制
TL;DR
通過抓包分析,我們清晰地了解了 MCP 通信的全過程:從建立 SSE 連接、三步初始化、工具調(diào)用操作到最終的連接終止??梢钥闯?,MCP 基于簡單的 SSE 協(xié)議搭建了一個功能強大的工具調(diào)用框架,使 AI 代理能夠便捷地調(diào)用外部工具完成復(fù)雜任務(wù)。
相比傳統(tǒng)的接口調(diào)用方式,MCP 更加靈活,能夠自動適應(yīng)不同的工具集,讓 AI 代理 " 即插即用 " 地使用各種服務(wù)能力,這也是其設(shè)計的精妙之處。
當(dāng)然,MCP 也并不是完美的,作為一個新興的協(xié)議,它仍然在不斷發(fā)展中。未來可能會有更多的功能和特性被添加進(jìn)來,以滿足更復(fù)雜的需求。
背景
MCP 支持兩種標(biāo)準(zhǔn)的傳輸實現(xiàn):標(biāo)準(zhǔn)輸入/輸出(stdio)和 Server-Sent Event[1](下稱 SSE)。stdio 基于命令行工具,多用于本地集成,通過進(jìn)程通信來實現(xiàn);SSE 基于客戶端和服務(wù)器的網(wǎng)絡(luò)通信,用于跨設(shè)備網(wǎng)絡(luò)的通信場景。
既然是用抓包來分析,我們就要選擇使用 SSE 傳輸 MCP server,然后通過工具進(jìn)行網(wǎng)絡(luò)抓包分析。在抓包分析之前,我們必要對 SSE 協(xié)議進(jìn)行簡單的了解。
SSE 協(xié)議
SSE 協(xié)議 是一種服務(wù)器推送技術(shù),使客戶端能夠通過 HTTP 連接從服務(wù)器自動接受更新,通常用于服務(wù)器向客戶端發(fā)送消息更新或者連續(xù)的數(shù)據(jù)流(流信息 streaming)。
本質(zhì)上,HTTP 協(xié)議是無法實現(xiàn)主動推送消息的,除非服務(wù)端“通知”客戶端接下來發(fā)送的是流信息。因此客戶端便不會斷開該連接,并持續(xù)從該連接上接收數(shù)據(jù)流。
看到這里你是否想到了 WebSocket 協(xié)議,二者看起來都是客戶端與服務(wù)端建立連接,然后服務(wù)端向客戶端推送數(shù)據(jù)??此葡嗤瑢嶋H差別還挺大:
- SSE 是基于 HTTP 的輕量級協(xié)議;WebSocket 是獨立的協(xié)議。
- SSE 是基于 HTTP 請求
Accept: text/event-stream
;WebSocket 借助 HTTP 升級協(xié)議Upgrade: websocket
,之后使用獨立協(xié)議。 - SSE 是偽雙工,只支持服務(wù)端到客戶端的單向通信,客戶端到服務(wù)端的通信還需要另外發(fā)送 HTTP 請求進(jìn)行;WebSocket 是全雙工的雙向通信。
- SSE 簡單、輕量,適合單向低頻推送;WebSocket 復(fù)雜度高、實時性強,適合雙向高頻交互。
從上面的對比不難看出 MCP 選擇 SSE 作為網(wǎng)絡(luò)傳輸協(xié)議的原因了。
了解了 SSE 協(xié)議之后,我們就可以開始了。
環(huán)境
- 抓包工具:Proxyman ,并安裝 CA 證書,方便處理 HTTPS 的請求。
- AI 應(yīng)用:VSCode Insiders,安裝 Github Copilot 插件并開啟 Agent 模式。
- MCP Server:使用 上一篇文章[2]。
圖片
配置 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 通信
下面我們將通過抓包分析,詳細(xì)了解 MCP 通信的完整生命周期,包括建立連接、初始化、操作和終止四個階段。
當(dāng)我們的 VSCode 成功連接到 MCP Server,此時從 Proxyman 已經(jīng)可以看到多條通信了。
圖片
建立連接
由于不確定 Server 支持哪種方法,MCP Client 會同時發(fā)送 GET 和 POST 請求到我們配置的 Server 地址,嘗試建立連接。請求中的 Accept 是 text/event-stream,說明是與 Server 嘗試進(jìn)行 SSE 通信。
這里配置的 Server 僅支持通過 GET 方式建立 SSE 通信,POST 請求收到 404 響應(yīng)。而 GET請求的響應(yīng)中,Server 端回傳了如下信息:
- 會話 id:3e19fbcd-51f4-4784-9f63-538c9a203859
- 事件 event :endpoint
- 數(shù)據(jù) data:*/mcp/messages?sessinotallow=3e19fbcd-51f4-4784-9f63-538c9a203859*,其中 /mcp/messages 是由服務(wù)側(cè)配置的
spring.ai.mcp.server.sse-message-endpoint: /mcp/messages
。
id:3e19fbcd-51f4-4784-9f63-538c9a203859
event:endpoint
data:/mcp/messages?sessinotallow=3e19fbcd-51f4-4784-9f63-538c9a203859
這個 HTTP 連接作為后續(xù) Server 向 Client 推送流信息的通道,所以在截圖中我們看到了其他的流信息。此時 MCP Client 與 Server 連接的聲明周期就開始了:
- 初始化
- 操作
- 終止
圖片
初始化
初始化階段必須是客戶端與服務(wù)器之間的首次交互,這個過程有點類似 TCP 的三次握手。
Client 發(fā)起初始化請求
從 Server 接收到后續(xù)的通信端點后,Client 會發(fā)送 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 響應(yīng)初始化請求
同樣 Server 也回傳了流信息
- 相同的會話 id
- 事件類型 message
- 事件數(shù)據(jù)
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"}}}
在事件的數(shù)據(jù)部分,Server 也提供了與請求類似的內(nèi)容(在下文中將直接展示流信息中的數(shù)據(jù)部分):
{
"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 端的信息交換,并協(xié)商(如版本兼容、功能支持)成功后,Client 發(fā)送請求完成初始化。
{
"method": "notifications/initialized",
"jsonrpc": "2.0"
}
這一次 Server 并不會有任何響應(yīng),像是 TCP 握手時客戶端發(fā)送了 ACK
操作
獲取 tool 列表
完成初始化后,Client 發(fā)送請求獲取 Server 支持的 tool 列表。
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
服務(wù)端通過 SSE 連接回傳 tool 列表,我們使用的示例 Server 中包含了 4 個 tool。在響應(yīng)內(nèi)容包含了如 tool 名字、輸入 schema 參數(shù)說明等信息??蛻舳耸盏竭@個響應(yīng)后,會在本地緩存 tool 列表避免頻繁的請求。只有當(dāng) Server 端更新了列表并通知 Client 后才會更新緩存內(nèi)容。
篇幅原因,沒有全部展示列表內(nèi)容。
{
"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 為了執(zhí)行任務(wù)了。在 Copilot Agent 模式下輸入和上次一樣的任務(wù):
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[3]; then check the list again to see if the new user was added successfully. Finally, say hello to Carson.
先來看執(zhí)行結(jié)果。
在我發(fā)出任務(wù)請求后,VSCode 經(jīng)過一通分析決定一次執(zhí)行幾個 tool 來完成任務(wù)。這里我使用的是 GPT-4o。
圖片
如果切換到 Claude 3.7 Sonnet。
圖片
執(zhí)行
回到 Proxyman 查看抓取的請求。
1.VScode 先請求 Copilot Server 時傳輸?shù)恼埱髢?nèi)容比較長。以 GPT-4o 模型為例,請求大小為 49.7 KB,響應(yīng) 1.34 KB。
請求中包含了:在響應(yīng)中包含了經(jīng)過分析任務(wù)后決定要調(diào)用的 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"
}
- 一段非常長的系統(tǒng) Prompt,有興趣的可以參考開發(fā)者整理的 GitHub Copilot Agent 官方 Prompt[4]
- 可用的 tool 列表,包括 VSCode 官方提供的系統(tǒng) tool 以及配置的 MCP Server 提供的 tool
2.VSCode 根據(jù)響應(yīng)的內(nèi)容,調(diào)用 MCP Tool。
//http://nio.local:8080/mcp/messages?sessinotallow=3e19fbcd-51f4-4784-9f63-538c9a203859
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "getUsers",
"arguments": {}
}
}
MCP Server 在 SSE 連接中回傳 tool 的調(diào)用結(jié)果。
{
"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 將調(diào)用結(jié)果發(fā)送給 Copilot Server 進(jìn)行處理,然后又得到一個要調(diào)用的 tool,以及需要提供的參數(shù)。
3.如此往復(fù),直到最終完成任務(wù)的執(zhí)行。在最右一個發(fā)送給 Copilot Server 的請求中,可以看到這個任務(wù)執(zhí)行過程中所有調(diào)用的 tool 請求和響應(yīng)的列表。也就是說,每次調(diào)用模型時,都會帶上此前調(diào)用的所有 tool 請求和響應(yīng),因此請求的 size 也是逐漸變大的。
終止
終止操作就簡單了,對于 SSE 傳輸類型的 MCP 交互來說,就是斷開相關(guān)的 HTTP 連接。
參考資料
[1]Server-Sent Event: https://en.wikipedia.org/wiki/Server-sent_events
[2]上一篇文章: https://mp.weixin.qq.com/s/jhXpxkMSyts_O5OuDsE9XA
[3]carson@gmail.com: vscode-file://vscode-app/Applications/Visual%20Studio%20Code%20-%20Insiders.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html
[4]GitHub Copilot Agent 官方 Prompt: https://github.com/LouisShark/chatgpt_system_prompt/blob/main/prompts/official-product/github/github_copilot_agent.md