第 03 篇:协议详解 —— 拆开 MCP 的"黑盒"

本篇是《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,之后再轮询获取结果。任务有完整的生命周期:workingcompleted / 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-MethodMcp-Name 头,用于在无会话的情况下标识请求类型和目标。

缓存策略 ------新增 ttlMscacheScope 头,让 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 最早支持的传输方式,也是目前最常用的方式。它的工作原理很简单:

  1. Host 以子进程的方式启动 MCP Server
  2. Client 通过 Server 进程的**标准输入(stdin)**发送 JSON-RPC 消息
  3. Server 通过**标准输出(stdout)**返回 JSON-RPC 消息
  4. 标准错误(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 中执行以下操作,观察产生的消息:

  1. 连接时 ------ 发生 initialize 握手
  2. 查看 Tools ------ 发生 tools/list 请求
  3. 调用 echo ------ 发生 tools/call 请求
  4. 查看 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 协议的"裸"消息格式。每个请求都有 jsonrpcidmethod 字段;每个响应都有对应的 idresult。工具的定义通过 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: initializeinitialized 有什么区别?

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 阶段,正式日期可能调整。但核心设计(无状态核心、缓存策略)已经比较稳定,可以开始了解和准备。生产环境建议等正式发布后再迁移。


总结

  1. MCP 基于 JSON-RPC 2.0 :三种消息类型(请求、响应、通知)构成了协议的通信基础。tools/listtools/callresources/read 等标准方法构成了协议的"词汇表"。理解这些消息格式是调试和优化的基础。

  2. 规范经历了五个版本:从 2024-11 的基础框架,到 2025-03 的 OAuth 和 HTTP 传输,到 2025-06 的 Elicitation,到 2025-11 的 Tasks 和 Extensions,再到 2026-07 的无状态核心。每个版本都在解决真实问题。了解版本差异能帮你避免被过时教程误导。

  3. STDIO 适合本地,Streamable HTTP 适合远程:STDIO 零配置、低延迟、天然安全,是开发者的默认选择。Streamable HTTP 支持远程部署和多客户端,是企业级场景的基础。2026-07 的无状态核心将让 Streamable HTTP 的扩展能力大幅提升。

  4. 能力协商让协议灵活演进:Client 和 Server 在握手时交换能力声明,只有双方都支持的功能才会被使用。这让新版本可以安全地添加新能力,而不会破坏旧的实现。

  5. 无状态核心是 MCP 的未来:移除有状态会话让 MCP Server 可以像普通 HTTP API 一样部署和扩展。这对 Serverless 部署、水平扩展、高可用架构都有深远影响。

下篇预告

协议讲清楚了,下一篇我们回到开发。《SDK 对比与选型》将对比 FastMCP(Python)、TypeScript SDK、以及其他社区 SDK 的功能差异、性能表现和适用场景------帮你做出正确的技术选型。


附录:参考资料

相关推荐
MateCloud微服务7 小时前
从源码设计看 MateClaw v1.5.0:Goal Checklist、LLM Wiki 自维护与 Memory 隔离
java agent·spring ai·mcp·agent runtime·llm wiki·goal checklist
星马梦缘20 小时前
提示词工程 与 实践 合集
人工智能·rag·提示词工程·mcp
yyk的萌1 天前
创建属于自己的mysql的mcp
mysql·adb·ai·mcp
winlife_1 天前
全程用 AI 做一款商业级手游 · EP0 立项:能做到吗、怎么做、边界在哪
人工智能·unity·ai编程·游戏开发·商业化·mcp·funplay
星马梦缘1 天前
MCP 模型上下文协议、Agent Skills 智能体技能、Harness操作系统 课程内容
人工智能·大模型·llm·agent·智能体·mcp·skills
winlife_2 天前
全程用 AI 做一款商业级手游 · EP1 地基:先搭框架层,不急着写玩法
unity·ai编程·游戏架构·mcp·框架设计·funplay
Super Scraper2 天前
如何使用 cURL 发送 JSON:-d、--json 及常见错误的完整指南
人工智能·爬虫·python·自动化·json·mcp
Bolt2 天前
Kimi code 用不了 Figma?看这里解决
shell·mcp