大模型应用:MCP 协议
营长 Lv3

封面图

背景

大模型应用需要面对训练数据或是模型本身的局限性。

一个直观的例子是:用户想要应用根据最近的天气预报来去制定旅行计划。但天气属于实时数据,通常不包含在模型的训练数据中。此时,大模型本身无法给出可靠答案,应用需要依赖某个天气预报服务来完成任务。

另外一个更典型的例子是计数问题。大模型本身不擅长计数,这是由于其工作机制决定的。模型在底层会将文本拆分为词元(token)进行概率预测,而不是逐字符处理,因此在涉及字符频率、精确数量等问题时,输出往往不稳定:

图一:llama3.1 无法稳定解答字符出现频率

但与此同时,大模型很擅长生成解决计数问题的代码:

图二:llama3.1 正确输出频率计算代码

在这种情况下,应用只需要调用本地的 Python 解释器执行这段代码,就可以得到准确结果。如今市面上的大多数 AI 对话产品,在处理复杂计算、数据分析等问题时,都会在幕后调用计算器、代码解释器或其他工具。大模型本身只负责理解问题并给出“该做什么”,而不真正参与计算过程。

按照传统方式开发大模型应用,开发者需要为每一个工具(远端服务或是本地能力)单独编写一套交互逻辑,包括但不限于:接口协议、身份认证、参数校验、错误处理以及权限控制。这种模式在系统规模较小时尚可接受,但随着工具数量增加,其缺陷会迅速放大——

  • 新增一个工具,就要改一轮应用代码
  • 工具接口一旦变更,应用需要同步调整
  • 不同应用之间几乎无法复用同一套集成逻辑
    这种高度耦合、缺乏标准化的方式,并不具备良好的拓展性,也逐渐成为限制大模型应用进一步演进的瓶颈。

正是在这样的背景下,Model Context Protocol(MCP)被提出,用来系统性地解决“大模型应用如何与外部系统沟通”的问题。

MCP 概述

MCP 遵循客户端–服务端架构,其核心目标是为大模型使用外部工具提供一套统一、标准化的交互框架。在这一架构中,大模型应用并不直接与具体工具或服务交互,而是通过 MCP 这一协议层完成能力发现与调用。

  • MCP 客户端(Client)
    MCP 客户端通常嵌入在大模型应用内部,负责将模型产生的工具调用意图转化为符合协议规范的请求。同时,它会接收并解析来自 MCP 服务端的响应结果,再将结果注入回模型上下文或应用逻辑中。

  • MCP 服务端(Server)
    MCP 服务端通常由具体工具或能力的提供方实现,用于对外暴露其可用能力。这些能力以标准化的形式描述,包括工具列表、调用参数、返回结构以及必要的说明信息,而不关心客户端或模型的具体实现。

客户端与服务端之间通过标准化的 JSON-RPC 消息格式进行交互。这种设计使协议本身不依赖于具体的底层传输方式,既可以运行在 HTTP、TCP、WebSocket 之上,也可以通过本地 pipe 等方式进行通信。

实例搭建

我们可以用 Cloudflare 提供的 MCP 服务模板迅速搭建一个服务实例。打开终端,执行以下命令:

1
2
3
npm create cloudflare@latest -- my-mcp-server --template=cloudflare/ai/demos/remote-mcp-authless
cd my-mcp-server
npm start

程序成功启动后,会在终端中输出服务监听的端口号:

图三:启动 MCP 服务

接下来启动 MCP Inspector。该工具可以作为一个临时的 MCP 客户端,用于测试和调试 MCP 服务端提供的能力。打开一个新的终端窗口并执行:

1
npx @modelcontextprotocol/inspector@latest
图四:启动 MCP Inspector

启动成功后,会进入 MCP Inspector 的控制台界面。此时需要在左侧配置连接参数,将端口号替换为前面 MCP 服务启动时打印的端口号即可完成连接:

图五:MCP Inspector 控制台

至此,实例搭建完成。

流量分析

我们可以使用 Chrome 浏览器的开发者工具(F12,切换到 Network 标签页)对 MCP 的通信过程进行流量分析。过滤条件选择 Fetch/XHR,以便只关注由 JavaScript 发起的网络请求。

在当前示例中,MCP 采用的是 HTTP 请求 + SSE(Server-Sent Events)推流 的传输模式。因此,在任何 MCP 消息发送之前,客户端会首先与服务端建立一条 SSE 长连接

1
2
3
4
5
6
7
8
9
>>
GET /sse HTTP/1.1
Accept: text/event-stream
...

<<
HTTP/1.1 200 OK
Content-Type: text/event-stream
...

该请求的响应不会立即结束,而是保持打开状态,用于后续持续接收服务端推送的事件。

查看 SSE 响应流,可以看到服务端向客户端推送的第一条事件是 endpoint

1
2
event: endpoint
data: /sse/message?sessionId=d281d68617db3d18923a008d7a19737a3ee81f2ce2b2cbf426861b30db4cd6d3

这是一条传输层控制事件,用于告知客户端后续上行请求应当访问的路径,并同时分配本次会话所使用的 sessionId。需要注意的是,这一过程并不属于 MCP 协议本身,而是 MCP over SSE 的实现约定。

在此之后,客户端发送的所有 MCP 请求,其基本形式均为:

1
2
3
4
POST /sse/message?sessionId=d281d68617db3d18923a008d7a19737a3ee81f2ce2b2cbf426861b30db4cd6d3 HTTP/1.1
Accept: text/event-stream
Content-Type: application/json
...

在请求被成功接收的情况下,服务端会立即返回:

1
2
3
HTTP/1.1 202 Accepted
Content-Length: 8
Content-Type: text/event-stream

该响应仅表示请求已被接收并进入处理流程,而不包含任何业务结果。当服务端完成实际处理后,会将结果通过之前建立的 SSE 通道推送给客户端:

1
2
event: message
data: {事件载荷}

这种“请求与响应分离”的通信模式与传统 HTTP 接口有明显不同,但它带来的好处也很直观:客户端可以在同一时间并发发起多个工具调用请求,而无需等待某个请求返回后才能继续发送下一个。

MCP 握手

在 MCP 会话建立阶段,客户端与服务端会完成一次明确的初始化流程。该流程由两条 JSON-RPC 消息构成,通常被称为“两阶段握手”。

第一阶段,客户端发送 initialize 请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"sampling": {},
"elicitation": {},
"roots": { "listChanged": true }
},
"clientInfo": {
"name": "inspector-client",
"version": "0.18.0"
}
}
}

该请求主要完成三项工作:

  1. MCP 协议版本声明:通过 protocolVersion 指明客户端期望使用的协议版本
  2. 客户端能力声明:通过 capabilities 告知服务端客户端能够理解和处理的协议特性
  3. 客户端身份信息:通过 clientInfo 提供用于调试和日志的客户端信息

服务端在收到该请求后,会通过 SSE 推送一条 message 事件作为响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
event: message
data: {
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true }
},
"serverInfo": {
"name": "Authless Calculator",
"version": "1.0.0"
}
}
}

该响应同样完成了三件事:

  1. 确定最终使用的协议版本:若双方版本不同,以服务端返回的版本为准
  2. 服务端能力声明tools.listChanged 表示工具列表在会话期间可能发生变化
  3. 服务端身份信息:用于标识当前 MCP 服务

在客户端成功处理完上述响应后,会发送第二阶段的通知:

1
2
3
4
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}

该消息用于显式告知服务端:客户端已完成初始化,可以进入正常通信阶段。该通知不要求服务端返回任何内容。

工具发现

在 MCP Inspector 中点击 List Tools 后,客户端会发起如下请求以获取可用工具列表:

1
2
3
4
5
6
7
8
9
10
{
"id": 1,
"jsonrpc": "2.0",
"method": "tools/list",
"params": {
"_meta": {
"progressToken": 1
}
}
}

服务端随后通过 SSE 推送工具清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"tools": [
{
"name": "add",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"]
}
},
{
"name": "calculate",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"]
},
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["operation", "a", "b"]
}
}
]
}
}

其中 result.tools 描述了每个工具的名称及其参数结构规范(JSON Schema)。客户端或模型会依据这些信息决定调用哪个工具,并生成合法的调用参数。

工具调用

在控制台中选择 add 工具,填写参数并点击运行后,客户端会发送如下请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"id": 2,
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"_meta": {
"progressToken": 2
},
"name": "add",
"arguments": {
"a": 1,
"b": 1
}
}
}

params 字段中包含了工具调用所需的全部信息,包括工具名称和调用参数。服务端在完成计算后,会将结果通过 SSE 推送回客户端:

1
2
3
4
5
6
7
8
9
10
11
12
{
"id": 2,
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "2"
}
]
}
}

需要注意的是,MCP 并不强制规定工具返回结果的结构。上述 content 形式是一种常见的实现约定,用于表示可直接展示给用户或模型的文本内容。MCP Inspector 会对该结构进行 UI 层解包,仅展示其中的文本部分。

结语

MCP 的价值并不在于引入了新的能力,而在于把大模型与外部能力之间的交互方式标准化。通过清晰的分层设计,模型只需要表达“要做什么”,而具体的执行、传输和会话管理都被放在了协议和实现层中完成。