基于 MCP 2.0 SDK 的 无状态 Streamable HTTP 最小可运行示例,并落地了 2026-07-28 协议大版本 的关键新特性。
约束遵循
项目开发文档.md(SDD):无 stdio、无 SSE、无 session-id、无 LLM。
MCP 协议在 2026-07-28 迎来了一次大版本升级(协议 era 从 2024/2025 演进到 2026-07-28)。本项目是这次升级的最小落地示范,用一套可运行的 Server + Client 验证"现代协议时代"的完整交互流程,重点回答三件事:
- 协议怎么变 —— 移除
initialize握手、改走server/discover,请求自动携带协议版本头; - 架构怎么变 —— 从"有状态会话"变为"无状态请求自包含",服务端零会话存储;
- 生产能力补了什么 —— 工具列表缓存、W3C Trace Context 分布式追踪、单行请求/响应日志、MCP Apps 交互式 UI。
| 能力 | 解决的问题 | 以前 MCP(传统模式) | 新方向(Server/Capability 模式) | 对 Agent / Planner 的影响 | ChatBI 示例 |
|---|---|---|---|---|---|
| 1. 动态发现(Dynamic Discovery) | 工具和服务越来越多,不想全部写死 | Client 先知道有哪些 Server,然后调用 tools/list 获取工具 |
Client 可以先发现业务能力 / Server,再发现 Server 下的 Tool | Planner 不需要维护所有工具清单,可以根据任务找到对应能力 | 用户问“分析销售下降原因”,先发现 Sales Analytics Server,再选择 analyze_growth |
| 核心变化 | 发现范围扩大 | Server → Tools |
Capability → Server → Tools |
从“找工具”变成“找解决问题的能力” | 从找 query_sales,变成找“销售分析能力” |
| 2. Server 自描述能力增强(Server Self Description) | Agent 不知道一个 Server 能干什么 | Tool 有 name、description、inputSchema |
Server 自己描述领域、业务对象、能力范围、权限、工具集合 | Planner 可以理解业务边界,不只是看到工具名字 | Sales MCP Server 描述:负责销售领域,支持收入、订单、客户分析 |
| 核心变化 | 描述粒度提升 | Tool 级描述 | Server / Domain 级描述 | LLM 可以先理解“这个服务适合解决什么问题” | 不只是看到 query_sales,而知道“这是销售分析领域能力” |
| 3. 对 Agent Planner 更友好(Planner-oriented) | Tool 数量爆炸,Prompt 塞不下 | Planner Prompt 中维护大量工具列表 | Planner 根据当前任务加载相关能力和工具 | Planner 从“工具选择”升级为“能力选择 + 工具选择” | 先选择 Sales Analytics 能力,再选择 query_sales、analyze_growth |
一句话总结新版本的价值:
旧版本演示"MCP 怎么跑起来";新版本演示"MCP 在 2026-07-28 时代该怎么设计"——无状态、可观测、可缓存、可交互。
| 能力 | 说明 | 意义 |
|---|---|---|
| MCP Server / Client | 分别位于 server/ 与 client/ |
一个仓库同时示范两端,闭环验证协议 |
| Streamable HTTP | http://localhost:8000/mcp,仅 POST |
单一 HTTP 端点,对网关/防火墙友好 |
| Stateless(无状态) | 服务端不保存 session、不产生 mcp-session-id,每个请求自包含 |
无共享状态 → 天然支持水平扩展、容器化/边缘部署,故障恢复简单 |
| 现代协议 era(2026-07-28) | 客户端用 server/discover 握手,移除 initialize,自动发送 Mcp-Method / Mcp-Name / Mcp-Protocol-Version 头 |
单请求完成握手、请求自描述,连接更快、更贴近 REST 语义 |
| Tools | add multiply query_order list_orders get_user semantic_query |
覆盖算术、业务查询、语义查询(§13 Phase 2 预览) |
| Resource | resource://company/profile(静态)、resource://orders/{order_id}(模板) |
数据资源化访问,支持动态参数 |
| Prompt | welcome、order_analysis(order_id) |
提示词模板化,便于复用 LLM 引导 |
| MCP Apps(2026-07-28) | orders_table 工具绑定 ui://orders/table HTML 资源 |
工具输出从"纯文本"升级为"可交互界面" |
| tools/list 缓存(2026-07-28) | 响应带 _meta.ttlMs / cacheScope |
减少重复拉取,降低 LLM 应用的延迟与带宽 |
| W3C Trace Context(2026-07-28) | OpenTelemetry 分布式追踪,客户端 span 跨进程级联到服务端 | 端到端链路可观测,生产排障必备 |
| 请求/响应日志 | 每个 JSON-RPC 请求打印 [REQ] / [RESP](单行、可 grep) |
审计友好,无需额外中间件即可排查问题 |
旧时代 = 2024/2025 协议(基于
initialize+ 会话);现代时代 = 2026-07-28 协议。
| 维度 | 旧时代(2024/2025) | 现代时代(2026-07-28) | 新版本优势 |
|---|---|---|---|
| 握手方式 | initialize 协商,需维护握手状态 |
server/discover 单请求完成 |
少一轮往返、更简单,接近普通 HTTP 调用 |
| 会话模型 | 有状态,携带 mcp-session-id |
无状态,每个请求自包含 | 可水平扩展、部署/运维简化、无粘性会话需求 |
| 传输格式 | 默认 SSE 流式响应 | Streamable HTTP,支持 json_response=True 返回纯 JSON |
客户端实现简单,任何 HTTP 栈都能对接 |
| 方法路由 | 靠请求体 method 字段识别 |
自动带 Mcp-Method / Mcp-Name / Mcp-Protocol-Version 头 |
网关/代理可直接按头路由,无需解析 body |
| 工具列表 | 每次全量拉取 | ttlMs / cacheScope 缓存提示 |
减少重复请求,LLM 应用首屏响应更快 |
| UI 能力 | 仅文本/结构化输出 | MCP Apps(ui:// 交互式 HTML) |
工具结果可渲染成可操作界面,而非静态文本 |
| 可观测性 | 无标准追踪 | W3C Trace Context(OTel) | 跨进程分布式追踪,一个 trace_id 贯穿两端 |
| 请求审计 | 无 | [REQ] / [RESP] 单行中间件 |
可直接 grep、可审计,无需额外部署 |
核心结论:新版本不是在旧模型上打补丁,而是把 MCP 的传输层、握手层、能力层全部升级——从"会话式 RPC"变成"无状态可观测的 HTTP 风格协议",这让 MCP Server 可以像普通 Web 服务一样部署、扩容、排障。
.
├── 项目开发文档.md # SDD 规范(§5/6/7/8 为实现依据)
├── pyproject.toml # 依赖(mcp[cli]>=2.0.0、fastapi、uvicorn、opentelemetry-sdk)
├── README.md
├── server/ # MCP Server
│ ├── main.py # 入口 + McpServerLauncher 启动类
│ ├── tools.py # 工具 / 资源 / 提示词 / MCP Apps 定义
│ ├── logging_middleware.py # 请求/响应日志中间件(ServerMiddleware)
│ └── tracing.py # OpenTelemetry 追踪配置(Console/OTLP 导出)
└── client/ # MCP Client
├── main.py # discover → list → call 流程演示
└── tracing.py # 客户端追踪 + W3C traceparent 注入
环境:Python 3.12+,包管理用 uv。
# 1. 安装依赖
uv sync
# 2. 启动服务端(终端 1)
.venv/bin/python server/main.py
# -> Uvicorn running on http://127.0.0.1:8000
# 或使用启动类自定义端口:
# .venv/bin/python -c "from server.main import McpServerLauncher; McpServerLauncher(port=9000).run()"
# 3. 运行客户端(终端 2)
.venv/bin/python client/main.py客户端预期输出:
[Connected] modern protocol, supported=2026-07-28
client root span: 337fa235c2870945c7d857160f4dd58b
[Available Tools] orders_table, add, multiply, query_order, list_orders, get_user, semantic_query
[cache] tools/list ttlMs=300000 cacheScope=public
[Available Resources] ui://orders/table, resource://company/profile
[Available Prompts] welcome, order_analysis
[call] add(10, 20) -> 30
[call] query_order('10001') -> {...}
[resource] company/profile -> ...
[mcp-app] ui://orders/table -> <!DOCTYPE html>...
[prompt] order_analysis('10003') -> ...
[error] unknown tool -> is_error=True: Unknown tool: unknown
注意输出中的 [cache] tools/list ttlMs=300000 cacheScope=public 和 [mcp-app] ui://orders/table——这两行就是 2026-07-28 新特性(缓存提示、MCP Apps)的实际效果。
MCP Client(modern era)
│ discover(server/discover) ── Mcp-Method / Mcp-Protocol-Version 头,无会话
│ list_tools / call_tool / read_resource / get_prompt
▼ (每个请求自包含:capabilities/clientInfo 放 _meta,带 traceparent)
MCP Server(stateless_http=True, json_response=True)
│ OpenTelemetryMiddleware 提取 traceparent 建 span
│ RequestResponseLoggingMiddleware 打印 [REQ]/[RESP]
▼
Tools / Resources / Prompts / MCP Apps(MCPServer 注册)
协议时代说明:SDK 2.0 支持"双时代"——首个请求是 initialize 走旧协议(2024/2025 版本),
首个请求是 server/discover(或带 2026 封装的请求)则进入现代 era(2026-07-28)。
本 demo 客户端显式走现代 era。
- 服务端:
streamable_http_app(stateless_http=True)—— 每次请求新建 transport,零会话存储。 - 客户端:
session.discover()替代session.initialize()—— 请求自动携带Mcp-Protocol-Version/Mcp-Method/Mcp-Name头。 - 验证:裸
curl -D无mcp-session-id响应头;连续请求各自独立。
server/tools.py 中:
cache_hints={
"tools/list": CacheHint(ttl_ms=300_000, scope="public"),
"server/discover": CacheHint(ttl_ms=60_000, scope="public"),
}响应会带上 cacheScope/ttlMs;客户端可据此缓存 tools/list 结果 5 分钟。
apps = Apps()
@apps.tool(resource_uri="ui://orders/table")
async def orders_table() -> list[dict]: ...
apps.add_html_resource("ui://orders/table", html, ...)
mcp = MCPServer(..., extensions=[apps])宿主(如 Claude Desktop)在沙箱 iframe 渲染 ui:// HTML,经 postMessage + JSON-RPC
双向通信,把工具返回的数据渲染成可交互表格。本 demo 是文本客户端,只能读取 HTML 源文件,
不渲染界面。
- 服务端:SDK 内置
OpenTelemetryMiddleware为每个请求建 span (mcp.method.name/mcp.protocol.version/gen_ai.tool.name)。 - 客户端:
trace_meta()用opentelemetry.propagate.inject把traceparent写进每个请求的_meta,服务端extract出上下文 → 服务端 span 挂到客户端 span 下。 - 观察:客户端与服务端日志中 span 共享同一
trace_id,且parent_id指向客户端对应请求 span。 - 导出器:默认
ConsoleSpanExporter(打印 span);设置环境变量OTEL_EXPORTER_OTLP_ENDPOINT可切换为 OTLP(需另装opentelemetry-exporter-otlp-proto-http)。
server/logging_middleware.py 基于 SDK 的 ServerMiddleware 协议实现,
在每个 JSON-RPC 请求前后记录方法与参数/结果(单行、长内容截断到 500 字符):
08/01 15:54:43 INFO [REQ ] id=5 method=tools/call params={"name": "add", "arguments": {"a": 10, "b": 20}, "_meta": {...}}
08/01 15:54:43 INFO [RESP] id=5 method=tools/call result={"content": [{"text": "30", "type": "text"}], "isError": false, ...}
处理失败时记 [RESP] ... error=... 后原样抛出。
- AC-001 服务端启动:
python server/main.py→Uvicorn running on http://127.0.0.1:8000 - AC-002 客户端连接:
[Connected] ... - AC-003 工具发现:
add, multiply, query_order, list_orders, get_user, semantic_query(+orders_table) - AC-004 工具调用:
add(10, 20) -> 30 - AC-005 无状态验证:无
mcp-session-id,连续请求独立 - §10 错误场景:未知工具/非法参数以
is_error=True结果返回 - 2026 新特性:现代协议 era、tools/list 缓存、MCP Apps、W3C Trace Context
- 文本客户端无法渲染 MCP Apps 界面;想看交互式 UI 需使用支持 MCP Apps 的宿主。
- OTel span 由
BatchSpanProcessor异步导出,打印可能晚于请求执行。 - 客户端 span 输出会混在演示输出里(结尾统一 flush),属正常现象。
_ORDERS/_USERS为只读演示数据(相当于 mock 数据库),不违反无状态要求。