本篇是《MCP 开发实战教程》专栏的第 3 篇。上一篇我们用 FastMCP 搭了一个能跑的 MCP Server,但你可能对底层发生了什么还是一头雾水------Client 和 Server 之间到底传了什么消息?握手是怎么完成的?协议经历了哪些版本变化?本篇将拆开 MCP 协议的"黑盒",让你不只会用,还懂原理。
引言
你可能有过这种体验:用 FastMCP 写了一个 Server,接入 Claude Desktop,一切正常。但有一天出了问题------Claude 说"无法连接到服务器"。你打开日志,看到一堆 JSON 消息在屏幕上滚动,完全看不懂。这时候你就会想:如果我知道这些消息是什么意思,是不是就能自己排查问题了?
上一篇我们写了十几行 Python 代码就搭了一个 MCP Server。FastMCP 帮我们屏蔽了所有协议细节------JSON-RPC 消息的构造、握手流程、能力协商,全部由框架自动处理。这在开发阶段很方便,但到了生产环境,你需要理解底层协议才能做高级的事情:调试连接问题、优化性能、理解安全边界、或者自己实现一个 Client。
本篇会带你搞清楚四件事:MCP 的消息格式长什么样、规范经历了哪些重要版本变化、两种传输方式各自的适用场景,以及即将到来的无状态核心设计意味着什么。
读完本篇,你将获得:
- 理解 MCP 消息的三种类型和各自的用途
- 掌握 MCP 规范从 2024-11 到 2026-07 RC 的完整演进脉络
- 知道 STDIO 和 Streamable HTTP 的技术细节和选型依据
- 理解无状态核心设计对 Server 架构的深远影响
1. MCP 的通信基础:JSON-RPC 2.0
1.1 为什么选 JSON-RPC
MCP 选择 JSON-RPC 2.0 作为消息格式,而不是更常见的 REST API 或 gRPC。这个选择背后有明确的技术理由。
| 维度 | REST API | gRPC | JSON-RPC 2.0 |
|---|---|---|---|
| 消息格式 | HTTP 方法 + 路径 | Protobuf 二进制 | 纯 JSON |
| 通信模式 | 请求-响应 | 请求-响应 + 流 | 请求-响应 + 通知(单向) |
| 能力协商 | 需要 OpenAPI 等额外机制 | Proto 定义 | 协议内置 |
| 可读性 | 高 | 低(二进制) | 高(纯文本) |
| 连接管理 | 无状态 | 长连接 | 有状态会话 |
MCP 的核心需求是:有状态会话 (Client 连接 Server 后要维持上下文)、双向通知 (Server 可以主动通知 Client 能力变化)、内置能力协商(握手阶段互相声明支持的功能)。JSON-RPC 2.0 天然支持这些特性------它定义了请求、响应、通知三种消息类型,其中通知是单向消息不需要对方回复,非常适合 Server 主动推送场景。
另外,JSON 是人类可读的。这意味着你在调试时可以直接阅读消息内容,不需要专门的解码工具。对于一个开发者协议来说,可调试性很重要。
1.2 三种消息类型
JSON-RPC 2.0 定义了三种消息类型,MCP 全部使用:
请求(Request)------需要对方回复的消息:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
jsonrpc:固定值 "2.0"id:请求的唯一标识,用于匹配响应method:要调用的方法名params:方法参数(可选)
响应(Response)------对请求的回复:
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "add_task",
"description": "添加一个新任务",
"inputSchema": {
"type": "object",
"properties": {
"title": { "type": "string" },
"priority": { "type": "string", "default": "medium" }
},
"required": ["title"]
}
}
]
}
}
id:与请求的id对应result:成功时的返回数据error:失败时的错误信息(与result互斥)
通知(Notification)------不需要回复的单向消息:
json
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed",
"params": {}
}
注意没有 id 字段------因为通知不需要回复。Server 可以通过通知告诉 Client:"我的工具列表变了,请重新获取。"
1.3 MCP 特有的方法
MCP 在 JSON-RPC 2.0 的基础上定义了一套标准方法。这些方法按功能分为几类:
生命周期管理:
| 方法 | 方向 | 说明 |
|---|---|---|
initialize |
Client → Server | 握手,协商协议版本和能力 |
initialized |
Client → Server | 通知,表示初始化完成 |
ping |
双向 | 心跳检测 |
工具相关:
| 方法 | 方向 | 说明 |
|---|---|---|
tools/list |
Client → Server | 获取工具列表 |
tools/call |
Client → Server | 调用工具 |
资源相关:
| 方法 | 方向 | 说明 |
|---|---|---|
resources/list |
Client → Server | 获取资源列表 |
resources/read |
Client → Server | 读取资源内容 |
resources/subscribe |
Client → Server | 订阅资源变更 |
notifications/resources/updated |
Server → Client | 通知资源已更新 |
提示模板相关:
| 方法 | 方向 | 说明 |
|---|---|---|
prompts/list |
Client → Server | 获取提示模板列表 |
prompts/get |
Client → Server | 获取提示模板内容 |
这些方法构成了 MCP 协议的"词汇表"。Client 和 Server 通过这些标准化的方法名进行通信,就像两个人说同一种语言。

2. 会话生命周期:从握手到断开
2.1 初始化握手
一个 MCP 会话的开始是初始化握手 。Client 连接 Server 后,第一件事就是发送 initialize 请求:
json
{
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"roots": { "listChanged": true },
"sampling": {}
},
"clientInfo": {
"name": "Claude Desktop",
"version": "1.2.3"
}
}
}
这里 Client 告诉 Server:
- 我支持哪个版本的协议 (
protocolVersion) - 我有哪些能力 (
capabilities)------比如支持 Roots 列表变更通知、支持 Sampling - 我是谁 (
clientInfo)------客户端名称和版本
Server 收到后回复自己的能力:
json
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true },
"prompts": {}
},
"serverInfo": {
"name": "任务管理器",
"version": "1.0.0"
}
}
}
Server 告诉 Client:
- 我也支持这个版本的协议
- 我有哪些能力------支持工具列表变更通知、支持资源订阅、支持提示模板
- 我是谁------服务器名称和版本
握手完成后,Client 发送 initialized 通知,表示"我知道你的能力了,可以开始工作了"。
这个握手过程的关键是版本协商。如果 Client 和 Server 支持的协议版本不兼容,连接会被拒绝。这就是为什么协议版本演进很重要------每次升级都可能改变消息格式或新增能力。
2.2 能力协商
能力协商(Capability Negotiation)是 MCP 的一个重要设计。它让 Client 和 Server 在开始工作前就知道对方支持什么功能,避免运行时出错。
常见的能力包括:
| 能力 | 含义 |
|---|---|
tools |
Server 支持工具调用 |
resources |
Server 支持资源读取 |
prompts |
Server 支持提示模板 |
sampling |
Client 支持 Server 请求 LLM 推理 |
roots |
Client 支持文件系统根目录信息 |
logging |
Server 支持日志输出 |
如果 Client 声明了 sampling 能力,Server 就可以在处理过程中请求 Client 调用 LLM。如果 Client 没声明这个能力,Server 就不能使用它。
这就像两个人合作前先交换简历------"我会 Python,你会 Java,那我们用 Python 来写这个项目"。
2.3 典型交互流程
把上面的内容串起来,一个完整的 MCP 会话是这样的:
lua
Client Server
| |
|--- initialize (握手) -------->|
|<-- initialize result ---------|
|--- initialized (通知) ------->|
| |
|--- tools/list (获取工具) ---->|
|<-- tools/list result ---------|
| |
|--- resources/list (获取资源)->|
|<-- resources/list result -----|
| |
| [LLM 决定调用工具] |
| |
|--- tools/call (调用工具) ---->|
|<-- tools/call result ---------|
| |
| [Server 工具列表变化] |
|<-- notifications/tools/ ---- |
| list_changed (通知) |
| |
|--- tools/list (重新获取) ---->|
|<-- tools/list result ---------|
| |
|--- ping (心跳) -------------->|
|<-- pong (心跳回复) -----------|
整个过程中,Client 和 Server 通过标准化的 JSON-RPC 消息进行通信。每条消息都有明确的语义------是请求、响应还是通知,是查询工具还是调用工具。
3. 规范版本演进:从 0.1 到 2026-07 RC
MCP 规范从 2024 年 11 月首次发布到现在,经历了五个重要版本。每个版本都引入了重大变化。了解这些变化不仅帮助你理解协议的设计思路,更重要的是------你在网上看到的很多教程可能基于旧版本,如果不了解版本差异,你可能会被过时的信息误导。
3.1 版本时间线总览
| 版本 | 发布日期 | 一句话概括 |
|---|---|---|
| 2024-11-05 | 2024 年 11 月 | 首次发布,定义了基本框架 |
| 2025-03-26 | 2025 年 3 月 | OAuth 授权、Streamable HTTP、工具注解 |
| 2025-06-18 | 2025 年 6 月 | 结构化工具输出、Elicitation、Resource Indicators |
| 2025-11-25 | 2025 年 11 月 | Tasks、Sampling 增强、Extensions 系统 |
| 2026-07-28 | 2026 年 7 月(RC) | 无状态核心、缓存策略、废弃策略 |
下面逐一拆解每个版本的关键变化。
3.2 2024-11-05:首次发布
这是 MCP 的"出生证明"。Anthropic 在 2024 年 11 月发布 MCP 时,定义了协议的基本框架:
- 三种核心原语:Tools、Resources、Prompts
- 传输方式:仅支持 STDIO(本地进程通信)
- 消息格式:基于 JSON-RPC 2.0
- 能力协商:基础的握手和能力声明
这个版本的设计比较保守------只支持本地传输,没有远程通信能力,没有授权机制。但它奠定了 MCP 的核心架构:Host/Client/Server 三层模型、三种原语、基于 JSON-RPC 的通信。
3.3 2025-03-26:走向生产级
这是 MCP 的第一次重大升级,引入了大量生产级特性:
Streamable HTTP 传输------这是最重要的变化。在 STDIO 之外新增了 HTTP 传输方式,让 MCP Server 可以部署在远程服务器上。Client 通过 HTTP POST 发送请求,Server 通过 SSE(Server-Sent Events)推送流式响应。这打开了 MCP 的远程使用场景。
OAuth 2.1 授权------远程传输带来了认证需求。这个版本引入了基于 OAuth 2.1 的授权框架,Server 可以要求 Client 提供访问令牌。
Tool Annotations(工具注解)------Server 可以给工具添加元数据标注,比如"这个工具是只读的"、"这个工具可能有破坏性"。Host 可以根据这些标注决定是否需要用户确认。
其他变化:
- JSON-RPC 批处理支持
- 音频内容类型
- 进度消息(长时间运行的工具可以报告进度)
3.4 2025-06-18:双向通信
这个版本的核心主题是增强双向通信能力:
Elicitation(引出)------Server 可以在执行过程中向用户提问。比如一个支付 Server 在扣款前弹出确认框。这是 MCP 从"单向调用"走向"双向协作"的关键一步。
结构化工具输出------之前工具只能返回纯文本,现在可以返回结构化数据(JSON)。这让 Client 可以更精确地解析工具结果。
MCP Server 作为 OAuth Resource Server------明确了 Server 在 OAuth 架构中的角色:它是资源服务器,不是授权服务器。认证职责由专门的授权服务器承担。
Resource Indicators(RFC 8707)------支持在 OAuth 令牌中指定目标资源,让一个令牌只能访问特定的 Server。
其他变化:
- 移除了 JSON-RPC 批处理支持(在 2025-03 中引入,但发现实现复杂度高收益低)
- 新增
MCP-Protocol-Version头,用于版本路由 - Resource Links(工具结果可以引用资源)
3.5 2025-11-25:异步与扩展
这个版本引入了三个重大特性:
Tasks(任务) ------支持"先调用、后取结果"的异步模式。对于耗时操作(如大文件处理、ETL 流程),Client 可以先发起调用获得一个任务 ID,之后再轮询获取结果。任务有完整的生命周期:working → completed / failed / cancelled。
Sampling 增强------Server 请求 Client 调用 LLM 的能力得到了增强,支持在 Sampling 请求中附加工具定义。这意味着 Server 可以让 LLM 在推理时使用特定的工具。
Extensions 系统------引入了官方的扩展机制,允许社区在不修改核心协议的情况下添加新功能。第一个官方扩展是 CIMD(Common Interactive Message Data),用于标准化消息格式。
其他变化:
- Elicitation 增强,支持 URL 模式(用于 OAuth 流程中的重定向)
- 图标支持(Server 和工具可以声明图标)
- OAuth 改进(动态客户端注册等)
3.6 2026-07-28 RC:无状态革命
这是即将到来的最大版本更新,目前处于 Release Candidate 阶段。核心变化是无状态核心设计:
移除有状态会话------这是最激进的变化。之前的 MCP 依赖有状态会话(Client 和 Server 维护一个长连接),这导致 MCP Server 无法水平扩展------因为请求必须路由到同一个 Server 实例(sticky session)。新版本移除了会话概念,每个请求都是独立的,Server 不需要维护客户端状态。
移除 initialize 握手------既然没有会话,就不需要握手。Client 在每个请求中通过 HTTP 头声明自己的协议版本和能力,Server 根据头信息决定如何处理。
新增 HTTP 头 ------引入 Mcp-Method 和 Mcp-Name 头,用于在无会话的情况下标识请求类型和目标。
缓存策略 ------新增 ttlMs 和 cacheScope 头,让 Client 可以缓存 Server 的响应,减少重复请求。
废弃策略 ------这是协议成熟度的标志。正式引入了 API 废弃流程:先标记为 deprecated,给社区迁移时间,然后在后续版本中移除。
MCP Apps 和 Tasks 扩展------将 2025-11-25 中引入的 Tasks 和 MCP Apps 进一步规范化,作为官方扩展。
这个版本的影响是深远的:它让 MCP Server 可以像普通的 HTTP API 一样部署和扩展,不再需要特殊的基础设施支持。

4. 传输层:STDIO vs Streamable HTTP
4.1 STDIO:本地集成的首选
STDIO 是 MCP 最早支持的传输方式,也是目前最常用的方式。它的工作原理很简单:
- Host 以子进程的方式启动 MCP Server
- Client 通过 Server 进程的**标准输入(stdin)**发送 JSON-RPC 消息
- Server 通过**标准输出(stdout)**返回 JSON-RPC 消息
- 标准错误(stderr)用于日志输出,不参与协议通信
lua
Host (Claude Desktop)
|
|-- 启动子进程: python task_manager.py
| |
| |-- stdin --> JSON-RPC 消息 --> Server
| |-- stdout <-- JSON-RPC 消息 <-- Server
| |-- stderr <-- 日志输出 <-- Server
|
STDIO 的优势:
| 优势 | 说明 |
|---|---|
| 零网络开销 | 数据通过管道传递,不经过网络栈 |
| 天然安全 | 不暴露任何网络端口,不存在被远程攻击的风险 |
| 简单 | 不需要处理 HTTP 协议、认证、CORS 等复杂问题 |
| 低延迟 | 进程间通信比网络通信快几个数量级 |
STDIO 的局限:
| 局限 | 说明 |
|---|---|
| 单客户端 | 一个 Server 进程只能服务一个 Client |
| 本地 only | Server 必须运行在同一台机器上 |
| 无法水平扩展 | 每个连接是一个独立进程,无法共享状态 |
STDIO 非常适合开发者本地使用------你在自己的电脑上跑一个 MCP Server,连接 Claude Desktop 或 Cursor,不需要任何网络配置。这就是为什么前两篇的例子都是用 STDIO 模式。
4.2 Streamable HTTP:远程部署的基础
Streamable HTTP 是 2025-03-26 版本引入的传输方式,为远程场景设计。它的核心思路是:
- Client 通过 HTTP POST 发送请求
- Server 通过 SSE(Server-Sent Events)推送流式响应
一个典型的 Streamable HTTP 交互:
css
Client Server (HTTP)
| |
|--- POST /mcp -------------------->|
| Content-Type: application/json |
| Body: {"jsonrpc":"2.0",...} |
| |
|<-- 200 OK ------------------------|
| Content-Type: text/event-stream |
| data: {"jsonrpc":"2.0",...} |
| data: {"jsonrpc":"2.0",...} |
| |
为什么用 SSE 而不是普通的 HTTP 响应?因为 MCP 的某些操作可能产生多个消息------比如一个长时间运行的工具可能先发进度通知,再发最终结果。SSE 让 Server 可以逐步推送这些消息,而不是等到所有消息都准备好再一次性返回。
Streamable HTTP 的优势:
| 优势 | 说明 |
|---|---|
| 远程部署 | Server 可以部署在任何可访问的网络位置 |
| 多客户端 | 一个 Server 可以同时服务多个 Client |
| 标准基础设施 | 可以使用现有的 HTTP 负载均衡、CDN、WAF 等 |
| OAuth 集成 | 天然支持 HTTP 标准的认证机制 |
Streamable HTTP 的局限:
| 局限 | 说明 |
|---|---|
| 有状态会话(当前版本) | 需要 sticky session,限制了水平扩展 |
| 更高延迟 | 网络通信比进程间通信慢 |
| 更复杂 | 需要处理 HTTP 协议细节、认证、安全等 |
4.3 选型决策
| 场景 | 推荐传输方式 | 原因 |
|---|---|---|
| 本地开发调试 | STDIO | 零配置、低延迟 |
| Claude Desktop 本地使用 | STDIO | 最常见、最简单 |
| 团队内部共享 Server | Streamable HTTP | 需要远程访问 |
| 企业级部署 | Streamable HTTP | 需要负载均衡、监控 |
| Serverless 部署 | Streamable HTTP(2026-07+) | 无状态核心支持 |
大多数开发者在日常开发中使用 STDIO,只有在需要远程部署时才切换到 Streamable HTTP。FastMCP 让这个切换非常简单------只需要在启动时指定不同的传输参数:
python
# STDIO 模式(默认)
if __name__ == "__main__":
mcp.run()
# Streamable HTTP 模式
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
4.4 早期传输方式的淘汰
在 2025-03-26 之前,MCP 使用 HTTP+SSE 作为远程传输方式。这种方式有一个 SSE 端点用于 Server → Client 推送,另一个普通 HTTP 端点用于 Client → Server 请求。
2025-03-26 版本用 Streamable HTTP 取代了它。新方式统一了双向通信------同一个 HTTP 端点既可以接收请求,也可以通过 SSE 推送响应。这简化了实现,也更容易与现有的 HTTP 基础设施集成。
如果你在网上看到教程还在讲 "HTTP+SSE 传输",那它基于的是 2025-03 之前的旧版本规范。
5. 实战:用代码观察协议消息
5.1 用 FastMCP 查看消息
FastMCP 支持在开发模式下输出协议消息,让你直观地看到 Client 和 Server 之间传了什么。
创建文件 protocol_demo.py:
python
"""协议消息演示 MCP Server"""
from fastmcp import FastMCP
mcp = FastMCP("协议演示")
@mcp.tool()
def echo(message: str) -> str:
"""回显输入的消息。
Args:
message: 要回显的消息
"""
return f"你说的是:{message}"
@mcp.tool()
def add(a: int, b: int) -> str:
"""计算两个整数的和。
Args:
a: 第一个数
b: 第二个数
"""
return f"{a} + {b} = {a + b}"
@mcp.resource("info://protocol")
def protocol_info() -> str:
"""返回协议演示服务器的信息"""
return "这是一个用于观察 MCP 协议消息的演示服务器。"
if __name__ == "__main__":
mcp.run()
启动 Inspector 测试:
bash
fastmcp dev inspector protocol_demo.py
在 Inspector 中执行以下操作,观察产生的消息:
- 连接时 ------ 发生
initialize握手 - 查看 Tools ------ 发生
tools/list请求 - 调用
echo------ 发生tools/call请求 - 查看 Resources ------ 发生
resources/list请求
5.2 手动发送 JSON-RPC 消息
如果你想更直接地观察协议,可以用 Python 手动构造 JSON-RPC 消息并通过 STDIO 发送给 Server。这比 Inspector 更底层,能看到原始消息格式。
创建文件 manual_client.py:
python
"""手动发送 JSON-RPC 消息给 MCP Server"""
import json
import subprocess
import sys
def send_message(process, message: dict) -> dict | None:
"""通过 stdin 发送消息,通过 stdout 读取响应"""
msg_str = json.dumps(message)
process.stdin.write(msg_str + "\n")
process.stdin.flush()
# 读取响应(如果是通知则没有响应)
if "id" in message:
response_line = process.stdout.readline()
if response_line:
return json.loads(response_line)
return None
def main():
# 启动 MCP Server 子进程
process = subprocess.Popen(
[sys.executable, "protocol_demo.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
try:
# 1. 发送 initialize 请求
print("=== 发送 initialize 请求 ===")
init_request = {
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {"name": "Manual Client", "version": "0.1.0"},
},
}
response = send_message(process, init_request)
print(f"Server 响应:{json.dumps(response, indent=2, ensure_ascii=False)}")
# 2. 发送 initialized 通知
print("\n=== 发送 initialized 通知 ===")
initialized_notification = {
"jsonrpc": "2.0",
"method": "notifications/initialized",
}
send_message(process, initialized_notification)
print("通知已发送(无需响应)")
# 3. 获取工具列表
print("\n=== 发送 tools/list 请求 ===")
list_request = {"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
response = send_message(process, list_request)
print(f"工具列表:{json.dumps(response, indent=2, ensure_ascii=False)}")
# 4. 调用 echo 工具
print("\n=== 发送 tools/call 请求(echo)===")
call_request = {
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {"name": "echo", "arguments": {"message": "Hello MCP!"}},
}
response = send_message(process, call_request)
print(f"调用结果:{json.dumps(response, indent=2, ensure_ascii=False)}")
# 5. 获取资源列表
print("\n=== 发送 resources/list 请求 ===")
res_request = {
"jsonrpc": "2.0",
"id": 3,
"method": "resources/list",
"params": {},
}
response = send_message(process, res_request)
print(f"资源列表:{json.dumps(response, indent=2, ensure_ascii=False)}")
finally:
process.terminate()
process.wait()
if __name__ == "__main__":
main()
运行这个脚本,你会看到完整的 JSON-RPC 消息交互:
bash
python manual_client.py
预期输出:
bash
=== 发送 initialize 请求 ===
Server 响应:{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"tools": {},
"resources": {}
},
"serverInfo": {
"name": "协议演示",
"version": "0.1.0"
}
}
}
=== 发送 initialized 通知 ===
通知已发送(无需响应)
=== 发送 tools/list 请求 ===
工具列表:{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "echo",
"description": "回显输入的消息。",
"inputSchema": {
"type": "object",
"properties": {
"message": {"type": "string", "description": "要回显的消息"}
},
"required": ["message"]
}
},
{
"name": "add",
"description": "计算两个整数的和。",
"inputSchema": {
"type": "object",
"properties": {
"a": {"type": "integer", "description": "第一个数"},
"b": {"type": "integer", "description": "第二个数"}
},
"required": ["a", "b"]
}
}
]
}
}
=== 发送 tools/call 请求(echo)===
调用结果:{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "你说的是:Hello MCP!"
}
]
}
}
=== 发送 resources/list 请求 ===
资源列表:{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resources": [
{
"uri": "info://protocol",
"name": "protocol_info",
"description": "返回协议演示服务器的信息"
}
]
}
}
这个例子让你看到了 MCP 协议的"裸"消息格式。每个请求都有 jsonrpc、id、method 字段;每个响应都有对应的 id 和 result。工具的定义通过 inputSchema 描述,这是标准的 JSON Schema 格式。
5.3 从代码中看 Schema 定义
你可能注意到了工具定义中的 inputSchema。这是 MCP 用来描述工具参数的标准方式------基于 JSON Schema。FastMCP 从 Python 的类型注解自动生成这些 Schema:
| Python 代码 | 生成的 JSON Schema |
|---|---|
title: str |
{"type": "string"} |
count: int |
{"type": "integer"} |
price: float |
{"type": "number"} |
active: bool |
{"type": "boolean"} |
tags: list[str] |
{"type": "array", "items": {"type": "string"}} |
priority: str = "medium" |
{"type": "string", "default": "medium"} |
这意味着你在 Python 代码中写的类型注解,直接就是协议定义的一部分。LLM 看到的是 JSON Schema,用户写的是 Python 代码------FastMCP 自动完成翻译。
6. 深入理解:协议设计的 trade-off
6.1 为什么版本号是日期格式?
MCP 的版本号不是传统的语义化版本(如 1.0.0、2.1.3),而是发布日期(如 2025-11-25)。这在协议设计中比较少见,但有其合理性:
- 明确时间点:每个版本对应一个具体的规范发布日期,没有"这个功能是 1.2 还是 1.3"的歧义
- 反映迭代速度:MCP 规范在快速演进,日期格式比递增版本号更能体现这种节奏
- 便于兼容性判断:Client 和 Server 可以直接比较日期,判断是否兼容
缺点是不够直观------"2025-11-25"和"2025-06-18"哪个更新?你需要比较日期才能知道。但考虑到 MCP 的使用者主要是开发者,这个代价可以接受。
6.2 为什么 2025-06 移除了批处理?
JSON-RPC 2.0 规范支持批处理(Batching)------可以在一个请求中发送多个消息。MCP 在 2025-03-26 版本中引入了批处理支持,但在 2025-06-18 中又移除了。
原因是:实现复杂度高,收益低。
批处理需要 Client 和 Server 都支持消息的拆分和合并,增加了实现难度。而 MCP 的实际使用场景中,批量请求的需求并不强烈------大多数时候 Client 是逐个调用工具的。更重要的是,批处理与流式响应(SSE)的结合存在设计冲突------Server 怎么在 SSE 流中表示一批响应?
这是一个典型的"先引入、后发现不实用、再移除"的设计迭代。MCP 团队没有因为"已经发布了"就保留一个不实用的特性,而是果断移除。这也是为什么了解版本演进很重要------你在网上看到的某些"高级技巧"可能基于已经移除的特性。
6.3 无状态核心的深远影响
2026-07-28 RC 引入的无状态核心设计,影响远不止"移除了会话"这么简单。它改变了 MCP Server 的部署和扩展方式:
之前(有状态会话):
- 每个 Client 连接对应一个 Server 会话
- 请求必须路由到同一个 Server 实例(sticky session)
- Server 崩溃 = 会话丢失 = 需要重新握手
- 水平扩展困难------需要会话同步或会话亲和
之后(无状态核心):
- 没有会话概念,每个请求独立
- 请求可以路由到任意 Server 实例
- Server 崩溃不影响其他请求
- 天然支持水平扩展------加机器就能提升容量
这意味着 MCP Server 可以像普通的 REST API 一样部署在 Serverless 平台(如 AWS Lambda、Cloudflare Workers)上,按需扩缩容。这对企业级部署来说是一个巨大的简化。
但无状态也有代价:Server 不能在内存中维护客户端状态。如果你的 Server 需要跨请求的状态(比如对话上下文),你需要自己用外部存储(Redis、数据库)来管理。这就是为什么第 2 篇的任务管理器示例使用内存列表------在有状态会话下这没问题,但在无状态下你需要改用持久化存储。
7. 常见问题与踩坑记录
Q1: 网上看到的教程说 MCP 用 HTTP+SSE,跟你说的不一样?
HTTP+SSE 是 2025-03-26 之前的旧传输方式,已被 Streamable HTTP 取代。如果你看到教程还在讲 HTTP+SSE,它基于的是旧版规范。Streamable HTTP 统一了双向通信,实现更简单,功能更强。
Q2: initialize 和 initialized 有什么区别?
initialize 是请求(有 id,需要回复),Client 发送给 Server 进行握手。initialized 是通知(没有 id,不需要回复),Client 告诉 Server "我准备好了"。先发 initialize,收到回复后再发 initialized。
Q3: 我需要手动构造 JSON-RPC 消息吗?
不需要。FastMCP 等 SDK 帮你处理了所有协议细节。本篇展示手动构造消息是为了帮你理解底层发生了什么,实际开发中你只需要写 Python 函数,SDK 自动完成消息的序列化和反序列化。
Q4: 协议版本不兼容会怎样?
Client 和 Server 在 initialize 握手时会交换 protocolVersion。如果版本不兼容,连接会被拒绝。FastMCP 会自动处理版本协商,你不需要手动管理。
Q5: 2026-07-28 RC 什么时候正式发布?
目前还是 Release Candidate 阶段,正式日期可能调整。但核心设计(无状态核心、缓存策略)已经比较稳定,可以开始了解和准备。生产环境建议等正式发布后再迁移。
总结
-
MCP 基于 JSON-RPC 2.0 :三种消息类型(请求、响应、通知)构成了协议的通信基础。
tools/list、tools/call、resources/read等标准方法构成了协议的"词汇表"。理解这些消息格式是调试和优化的基础。 -
规范经历了五个版本:从 2024-11 的基础框架,到 2025-03 的 OAuth 和 HTTP 传输,到 2025-06 的 Elicitation,到 2025-11 的 Tasks 和 Extensions,再到 2026-07 的无状态核心。每个版本都在解决真实问题。了解版本差异能帮你避免被过时教程误导。
-
STDIO 适合本地,Streamable HTTP 适合远程:STDIO 零配置、低延迟、天然安全,是开发者的默认选择。Streamable HTTP 支持远程部署和多客户端,是企业级场景的基础。2026-07 的无状态核心将让 Streamable HTTP 的扩展能力大幅提升。
-
能力协商让协议灵活演进:Client 和 Server 在握手时交换能力声明,只有双方都支持的功能才会被使用。这让新版本可以安全地添加新能力,而不会破坏旧的实现。
-
无状态核心是 MCP 的未来:移除有状态会话让 MCP Server 可以像普通 HTTP API 一样部署和扩展。这对 Serverless 部署、水平扩展、高可用架构都有深远影响。
下篇预告
协议讲清楚了,下一篇我们回到开发。《SDK 对比与选型》将对比 FastMCP(Python)、TypeScript SDK、以及其他社区 SDK 的功能差异、性能表现和适用场景------帮你做出正确的技术选型。