MCP 是如何工作的?逐步拆解协议流程(2026)
MCP 是如何工作的?逐步拆解 2026-07-28 版协议:discover、tools/list、tools/call、elicitation、通知、传输与鉴权,附真实 JSON 报文。
MCP 的工作方式是:AI 应用(Host)为每个连接的服务器创建一个客户端,由客户端与服务器交换 JSON-RPC 2.0 消息——列出服务器的工具,让模型挑选一个,再发送 tools/call,把结果交回给模型。从当前规范版本 2026-07-28 起,MCP 是无状态的:没有握手,每个请求都在 _meta 中携带自己的协议版本和客户端能力,因此任何一个服务器实例都能处理任何一个请求。
本文跟随一次工具调用,逐条消息看它在线路上发生了什么。如果你想先了解全貌,请阅读 什么是 MCP?;服务器那一侧的介绍见 什么是 MCP 服务器?。
三个角色:Host、Client、Server
- Host:用户直接使用的应用——Claude Code、Cursor、VS Code、ChatGPT。它掌管对话、模型和授权确认弹窗。
- Client:Host 为某一个服务器创建的连接器。根据 架构规范,每个客户端只与一个服务器通信;接了五个服务器的 Host 就运行五个客户端。(详见我们的 MCP 客户端指南。)
- Server:暴露工具、资源和提示词的程序——可以是本地进程,比如 Filesystem 服务器,也可以是远程服务,比如托管的 Sentry MCP 服务器。
规范中写明了一条关键设计原则:服务器看不到完整对话,也不能"窥视"其他服务器。每个服务器能收到什么,由 Host 决定。
第 0 步:建立传输通道
在任何 MCP 消息流动之前,客户端需要一条通道。规范定义了 两种标准传输方式:
| stdio | Streamable HTTP | |
|---|---|---|
| 服务器运行在哪里 | 客户端启动的子进程 | 任意位置,通过一个 URL 访问,如 https://example.com/mcp |
| 消息分帧 | stdin/stdout 上按换行分隔的 JSON-RPC | 每条消息一个 HTTP POST;回复为 JSON 或仅属于该请求的 SSE 流 |
| 凭证 | 来自环境变量 | OAuth 2.1 Bearer Token(见第 7 步) |
| 取消请求 | 发送 notifications/cancelled | 关闭该请求的响应流 |
旧的 HTTP+SSE 传输(2024-11-05)已被正式标记为弃用——迁移方法见 SSE 与 Streamable HTTP。
第 1 步(可选):server/discover
客户端可以先问服务器支持什么。服务器必须实现 server/discover,但客户端不一定要调用。下面这组来自官方 架构指南:
{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "example-client",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": { "elicitation": {} }
}
}
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": { "tools": { "listChanged": true }, "resources": {} },
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "example-server",
"version": "1.0.0"
}
},
"ttlMs": 3600000,
"cacheScope": "public"
}
}
这个 _meta 块就是无状态设计的核心。每个请求都包含:
io.modelcontextprotocol/protocolVersion(必填)——本次请求使用的协议版本。io.modelcontextprotocol/clientCapabilities(必填)——客户端在本次请求中能做什么,例如elicitation。io.modelcontextprotocol/clientInfo(建议填写)——客户端名称和版本。
如果服务器不支持请求的版本,会返回 UnsupportedProtocolVersionError(错误码 -32022)并附上 supported 列表,客户端从中选一个版本重试。缺少必填字段的请求会收到 -32602(Invalid params)。
第 2 步:列出工具
接着客户端请求服务器的工具清单:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": { "_meta": { "...": "same three fields as above" } }
}
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"tools": [
{
"name": "weather_current",
"title": "Weather Information",
"description": "Get current weather information for any location worldwide",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name, address, or coordinates"
},
"units": {
"type": "string",
"enum": ["metric", "imperial", "kelvin"],
"default": "metric"
}
},
"required": ["location"]
}
}
],
"ttlMs": 300000,
"cacheScope": "public"
}
}
2026-07-28 版本带来了三点变化:
- 列表结果现在必须带
ttlMs和cacheScope——这里表示客户端可以缓存这份列表五分钟。 - 列表不得随连接而变化。它可以随调用者的授权而变化(例如按 OAuth scope 过滤工具)。
- 服务器应以确定的顺序返回工具,这样模型的 prompt 缓存更容易命中。
大型列表用 cursor 分页。聚合了很多服务器的 Host 往往会按需加载工具——见 MCP 工具发现。
第 3 步:模型选择工具
这一步其实不属于 MCP。Host 把每个工具的 name、description 和 inputSchema 转换成自家模型的 function calling 格式,连同用户消息一起发给模型。当模型返回一个工具调用(weather_current,参数 location: "San Francisco")时,Host 应该先让用户看到即将执行什么——规范规定应当始终有一个能拒绝工具调用的人在环中。
第 4 步:tools/call
客户端发出调用。在 Streamable HTTP 上,传输层还会把关键字段镜像到请求头里,方便网关在不解析 JSON 的情况下完成路由:Mcp-Method 在所有请求上都是必需的,Mcp-Name 在 tools/call、resources/read 和 prompts/get 上必需。下面是 Streamable HTTP 规范 中的示例:
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Seattle, WA" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
如果 MCP-Protocol-Version 请求头与 body 中 _meta 的值不一致,服务器会以 400 和 HeaderMismatch 错误(-32020)拒绝。成功的回复如下:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "Current weather in Seattle, WA: 54°F, light rain..."
}
]
}
}
现在每个结果都带有 resultType。"complete" 表示这就是最终答案。工具在业务层面失败时仍然返回一个结果,只是带 isError: true,这样模型可以读取错误信息并换个办法再试;只有协议层面的失败(例如工具不存在)才会以 JSON-RPC 错误返回。Host 把 content(如果工具声明了 outputSchema,还有 structuredContent)放进对话,模型据此写出回答。
第 5 步:服务器需要更多输入时(MRTR)
有时工具离不开用户才能完成——确认一笔购买、选择一个账号。在 2026-07-28 之前,服务器会反过来给客户端发一个 JSON-RPC 请求。现在服务器从不主动发起请求,而是使用 多轮往返请求(MRTR):
- 客户端发送
tools/call(id 1)。 - 服务器回复
resultType: "input_required",其中带有inputRequests(比如一个elicitation/create表单)和一个不透明的requestState。 - 客户端把表单展示给用户。
- 客户端以新请求(id 2)重试原来的调用,传入
params.inputResponses(用户的回答,使用相同的 key),并原样回传params.requestState。 - 服务器根据这次重试恢复状态,返回
resultType: "complete"。
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"github_login": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please provide your GitHub username",
"requestedSchema": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
}
}
},
"requestState": "AEAD-protected blob"
}
}
由于两次调用之间服务器不保存任何状态,重试落到负载均衡后面的哪个实例都没关系。规范要求服务器把 requestState 视为攻击者可控的输入,并保护其完整性(HMAC 或 AEAD)。Elicitation 有两种模式:form 用于普通数据,URL 用于任何敏感信息(如凭证),这类信息绝不能经过客户端。
另外两项客户端功能——sampling 和 roots——同样可以借助 MRTR 传递,但它们(连同协议日志功能)在 2026-07-28 中已被弃用。新服务器应直接调用 LLM API,并通过工具参数接收路径。
第 6 步:通知
MCP 有两类通知,走的是不同的路径:
- **请求级通知。**在请求的
_meta中加上progressToken,服务器就可以在最终结果之前,在该请求自己的响应流上推送notifications/progress(progress、total、message)。 - **变更通知。**要获知工具、提示词或资源的变化,客户端需要发起一个长连接的
subscriptions/listen请求,并选择要订阅的类型。它取代了resources/subscribe和旧的 HTTP GET 流。
{
"jsonrpc": "2.0",
"id": 1,
"method": "subscriptions/listen",
"params": {
"_meta": { "...": "standard fields" },
"notifications": {
"toolsListChanged": true,
"resourceSubscriptions": ["file:///project/config.json"]
}
}
}
服务器首先发送 notifications/subscriptions/acknowledged,列出它同意推送的内容;之后 notifications/tools/list_changed 或 notifications/resources/updated 陆续到达,每条都带有 io.modelcontextprotocol/subscriptionId 标记。收到 list_changed 后,客户端重新调用 tools/list。
要取消一个请求:stdio 客户端发送带请求 ID 的 notifications/cancelled;Streamable HTTP 客户端直接关闭该请求的流。实现方应为每个请求设置超时。
第 7 步:鉴权
鉴权位于传输层,在 JSON-RPC 消息之外。授权规范 规定:
- stdio 服务器不应使用 OAuth 流程,而是从环境中读取凭证(通常是客户端配置里的
env块)。 - HTTP 服务器作为 OAuth 2.1 资源服务器。未认证的请求会收到
401,其WWW-Authenticate头指向服务器的受保护资源元数据。客户端由此发现授权服务器、完成注册(现在优先使用 Client ID Metadata Documents,Dynamic Client Registration 已弃用)、执行 PKCE,之后在每个请求上携带Authorization: Bearer。
完整流程及可运行示例见我们的 MCP 鉴权指南。
那 initialize 呢?
网上大多数讲解——以及今天仍在运行的许多服务器——描述的都是 2025-11-25 及更早版本的旧版流程:客户端先发 initialize 请求(带 protocolVersion、capabilities、clientInfo),服务器回复自己的能力,客户端再发 notifications/initialized;此后连接就是一个会话(在 HTTP 上,服务器下发 Mcp-Session-Id 请求头,后续请求必须回传)。2026-07-28 版本移除了握手、会话 ID、ping 以及流的断点续传。
两个时代通过 版本规范 中的回退规则实现互通。在 stdio 上,同时支持两代的客户端先用 server/discover 探测,若收到的不是新版响应就回退到 initialize;在 HTTP 上,它先发送新版请求,如果收到的 400 不含新版错误体,就回退。
SDK 会帮你处理这些,但要留意默认值:在 TypeScript SDK v2 中,new Client(...) 默认仍执行旧版握手,除非你传入 versionNegotiation: { mode: 'auto' }——这样它会用 server/discover 探测,并自动回退。服务端的 serveStdio 辅助函数则两代都能应答(协议版本指南)。
亲眼看看
读 JSON 是一回事,实时观察更直观。用 MCP Inspector 连接任意服务器(例如 npx @modelcontextprotocol/inspector 搭配 GitHub MCP 服务器),每一次 tools/list 和 tools/call 交换都能看得清清楚楚。想自己写一个服务器,可以从 构建 MCP 服务器 开始。
下一步
现在你已经了解完整的往返流程:discover、list、call、input-required 重试、通知、鉴权。想看它实际运行,可以浏览 MCP 服务器目录,把一个服务器接入你的客户端。
常见问题
用简单的话说,MCP 是怎么工作的?
- AI 应用为每个服务器运行一个 MCP 客户端。客户端询问服务器有哪些工具,模型选择其中一个,客户端发送 tools/call 请求,再把结果交回模型。所有消息都使用 JSON-RPC 2.0。
MCP 还有 initialize 握手吗?
- 当前的 2026-07-28 版本已经没有了。每个请求都在 _meta 中携带协议版本和客户端能力,客户端也可以先调用 server/discover。2025-11-25 及更早版本的服务器仍使用 initialize,所以 SDK 会在需要时回退。
MCP 使用什么传输方式?
- 有两种标准传输。本地服务器使用 stdio,即在子进程的 stdin 和 stdout 上传输按换行分隔的 JSON-RPC。远程服务器使用 Streamable HTTP:每条消息一个 POST,回复为 JSON 或仅属于该请求的 SSE 流。旧的 HTTP+SSE 传输已弃用。
MCP 服务器如何向用户索取输入?
- 通过 elicitation,以多轮往返请求(MRTR)的方式传递。服务器对 tools/call 返回一个 input_required 结果,说明它需要什么。客户端收集回答后,带上 inputResponses 和原样的 requestState 重试原请求。