在第 4 章中,我们讨论了使用 服务器发送事件(Server-Sent Events,SSE) 作为传输方式来构建 MCP 服务器。在那一章里你了解到:如果希望用户在 Web 上访问你的 MCP 服务器,就不能使用 STDIO,而需要使用一种传输方式,比如 SSE,或者像本章要介绍的 Streamable HTTP。
因此,在本章中,你将学习以下内容:
- Streamable HTTP 传输
- 为什么应优先使用它而不是 SSE
- 如何处理 通知(notifications) 和 可恢复性(resumability) 等概念
本章涵盖以下主题:
- Streamable HTTP 与 SSE 的比较,以及为何它是新的标准
- MCP 中的 Streamable HTTP
- 可恢复性(Resumability)
- 通知(Notifications)
- 使用 Streamable HTTP 创建并测试服务器
- 测试服务器
- 测试可恢复性
Streamable HTTP 与 SSE 的比较,以及为何它是新的标准
在为你的应用选择合适技术时,理解 SSE 与 Streamable HTTP 的差异非常重要。
有几个关键点。首先,在 MCP 中 SSE 已被视为弃用(deprecated) ;你应当改用 Streamable HTTP。
那为什么本书还单独写了一章讲 SSE?原因在于本书既面向 MCP 服务器的开发者,也面向服务器的使用者;你可能需要为一个现有、仍在使用 SSE 的服务器编写客户端。简而言之,出于遗留代码的现实,你需要同时懂得处理这两种传输方式。事实上,文章 github.com/modelcontex… 指出:在宣布弃用时,有 20 个参考服务器、50+ 个官方集成、以及 186 个社区开发的服务器与客户端 使用了 SSE。这意味着在进行 MCP 开发时,即便你用 Streamable HTTP 开发新服务器,也要考虑同时处理 SSE 与 Streamable HTTP。
那么,为什么要弃用 SSE?因为 Streamable HTTP 有以下优势:
- 单一端点的简洁性:客户端与服务器通过单个端点(如
/mcp)通信,同时支持 POST 与 GET。这简化了实现并减少连接开销。 - 可恢复性支持:Streamable HTTP 通过
Last-Event-ID与Mcp-Session-ID等请求头支持可恢复会话,使客户端能可靠地重连与恢复流。当客户端掉线后,可从断开前的位置继续接收数据,而非从头开始。 - 更好的兼容性:它能与现代 HTTP 基础设施(负载均衡、代理、API 网关)无缝协作;SSE 往往在这些环境中失效或需要权宜之计。
- 双向通信能力:SSE 是单向的;而 Streamable HTTP 可以升级以支持双向流,更适合代理-到-代理或客户端-服务器交互。
- 面向未来:Streamable HTTP 与演进中的 MCP 标准与社区最佳实践保持一致。它模块化、可扩展,既支持无状态模型也支持基于会话的模型。无状态服务器更轻量、更易构建,而能为不同场景选择合适模型也极具吸引力。
MCP 中的 Streamable HTTP
提到“流式”,你可能会想到把文件分块,或让 AI 模型分片返回结果。但在 MCP 语境下,“流式”更多指遵循 streamable 标准的 HTTP 传输方式:使用 Streamable HTTP 的客户端通常会发送如下 Accept 请求头:
Accept: application/json, text/event-stream
这告诉服务器:客户端既能处理批量 JSON 响应,也能处理通过 SSE 发送的流式事件。服务器可根据请求类型与上下文选择合适的响应模式。
仅此而已吗?不止如此,关键还在于可恢复性(resumability) 。
可恢复性(Resumability)
可恢复性指:当客户端在数据传输过程中与服务器失去连接时,重连后可以从断点继续数据交换,而不是从头开始。对于长耗时操作,这将极大改善体验。从技术上说,SSE 与 Streamable HTTP 都能实现可恢复性,但在 MCP 协议的上下文中,只有 Streamable HTTP 支持它。
如下图示意:
图 5.1——可恢复性
如图所示,客户端无需从头开始,而是可以继续进行。这是因为客户端在重连时会发送 mcp-session-id 与 last-event-id 请求头。需要注意:在断开连接时,客户端应当优雅地断开并保存这两个头,以便后续使用。
服务器端要支持可恢复性,需要做什么?需要:
- 创建会话存储(session store) ,用于放置已生成的消息。
- 设置中间件(middleware) 来处理入站请求与出站响应,确保消息被正确存储并能在会话恢复时取回。具体做法因框架而异:
# 1. 创建内存存储;生产环境应使用持久化存储
event_store = InMemoryEventStore()
# 2. 使用我们的 app 与事件存储创建会话管理器
session_manager = StreamableHTTPSessionManager(
app=app,
event_store=event_store, # 启用可恢复性
json_response=json_response,
)
# 3. 启动会话管理器
@contextlib.asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
"""管理会话管理器生命周期。"""
async with session_manager.run():
logger.info("Application started with StreamableHTTP session manager!")
try:
yield
finally:
logger.info("Application shutting down…")
# 4. 创建 Web 服务器并指定 lifespan 处理器
starlette_app = Starlette(
debug=True,
routes=[
Mount("/mcp", app=handle_streamable_http),
],
lifespan=lifespan,
)
让我们拆解一下上述代码:
- 先创建一个会话存储,用于保存与检索消息。
- 然后创建一个
StreamableHTTPSessionManager类型的会话管理器来控制对存储的访问。 - 启动会话管理器,并创建带有合适 lifespan 处理器 的 Web 服务器。
- 最后,创建 Web 服务器并绑定该 lifespan 处理器。
至此一切就绪,任何客户端都可以连接到服务器并启动会话。
既然我们已经了解了可恢复性如何显著改善用户体验——客户端在断线处继续接收消息——接下来谈另一个概念:通知(notifications) 。
通知(Notifications)
通知并非 Streamable HTTP 独有的概念,在 SSE 中也可使用。但当通知与**可恢复性(resumability)**结合时,会变得尤为强大。先说明什么是通知,再谈它与可恢复性的关系。
通知有多种形式,用于告知“发生了重要事件”。它们是实时更新,在 SDK 层面会被单独处理。这意味着 SDK 为监听通知提供了专门的处理器(handler),稍后你会看到示例。
一些适合用通知表达的场景包括:
- 状态更新(Status updates)
- 进度通知(Progress notifications)
- 错误消息(Error messages)
- 信息性消息(Informational messages)
例如,客户端发送给服务器的最后一条消息通常是名为 notifications/initialized 的通知,表示客户端与服务器已完成握手,接下来可以进行常规操作(如列出工具、读取资源等)。其 JSON-RPC 结构如下:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
发送(Producing)通知
如何产生通知?本质上,通知就是一条 JSON-RPC 消息;各 SDK 通常会提供专门的方法来便捷发送不同类型的通知。因为通知类型不同,所以关键在于用对方法并传入合适参数。
要发送通知,我们需要拿到**上下文对象(context)**的引用(例如把它作为工具的入参)。有了引用后,就可以调用特定的通知方法,如 debug、info、warning、error,分别用于发送调试信息、一般信息、警告与错误:
from mcp.server.session import ServerSession
# 1. 获取上下文对象
@mcp.tool(description="A simple tool returning file content")
async def echo(message: str, ctx: Context[ServerSession, None]) -> str:
# 2. 选择合适的方法发送对应类型的通知
await ctx.debug(f"Debug: Processing '{data}'")
await ctx.info("Info: Starting processing")
await ctx.warning("Warning: This is experimental")
await ctx.error("Error: (This is just a demo)")
return "Final result"
在上面的代码中,工具 echo 会产生四条不同类型的通知,并返回最终结果。
注:原文示例中
ctx: ctx: Context[...]疑为排版错误,这里按ctx: Context[...]书写。
处理(Handling)通知
作为客户端消费通知时,它们出现的位置与常规消息不同:通知不走常规消息流,而是通过独立的回调处理:
# 1. 定义消息处理器
def message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult]
| types.ServerNotification
| Exception,
) -> None:
print("Received message:", message)
if isinstance(message, Exception):
raise message
else:
if isinstance(message, types.ServerNotification):
print("NOTIFICATION:", message)
elif isinstance(message, RequestResponder):
print("REQUEST_RESPONDER:", message)
else:
print("SERVER_REQUEST:", message)
# 2. 创建客户端会话,并把处理器赋给 message_handler 属性
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
await session.initialize()
print("Session initialized, ready to call tools.")
# 调用工具
tool_result = await session.call_tool("echo", {"message": "hello"})
在该客户端中我们:
- 定义了消息处理器,能够识别并分别处理不同类型的消息;
- 创建客户端会话,并把处理器设置到
message_handler属性上。
可以看到,调用工具、读取资源等常规功能响应仍按正常通道处理;而通知则通过消息处理器 message_handler 专门分流处理。
Inspector 工具中的通知
了解如何发送与接收通知后,再看看它们在 Inspector 可视化工具中的呈现方式,方便你在正确的位置查看到它们。
当以可视化模式启动 Inspector,你会看到类似下图的界面(图 5.2)。
运行某个工具后,会看到该工具的结果;在其下方有一个区域显示通知(图 5.3)。
带可恢复性的通知
正如前文所述,SSE 与 Streamable HTTP 都能发送通知,但在 Streamable HTTP 中通知尤为关键。想象客户端因为网络不稳多次断开连接;得益于客户端的自动重连逻辑与服务器对可恢复性的支持,终端用户依然能获得良好体验:他们不会错过任何通知或常规消息(例如某个工具的响应)。
现在,让我们继续进入创建服务器的内容。
使用 Streamable HTTP 创建并测试服务器
让我们创建一个服务器,并在此过程中集成通知(notifications) 。在完成服务器构建后,下一节将使用多种工具来测试它:自己编写客户端、使用 Inspector 工具,以及用 cURL 进行测试。
要编写服务器代码,我们需要完成几件事:
- 将传输方式设置为 Streamable HTTP
- 添加功能(features)
- 在调用工具时发送通知的代码
很好,有了计划就开始实现。下面是对前两点的实现尝试:
# server.py
from mcp.server.fastmcp import FastMCP, Context
from typing import Optional, Dict, Any, List, AsyncGenerator
from mcp.types import (
LoggingMessageNotificationParams,
TextContent
)
# Create an MCP server
mcp = FastMCP("Streamable DEMO")
# 2. Adding features
@mcp.tool(description="A simple tool returning file content")
async def echo(message: str, ctx: Context) -> str:
return f"Here's the file content: {message}"
# 1. Set up the transport as streamable HTTP.
mcp.run(transport="streamable-http")
要添加发送通知的能力,需要把 Context 对象加入方法签名,例如:
@mcp.tool(description="A simple tool returning file content")
async def echo(message: str, ctx: Context) -> str:
最后,在工具中通过 Context 对象发送通知:
@mcp.tool(description="A simple tool returning file content")
async def echo(message: str, ctx: Context) -> str:
# 3. Send a notification
await ctx.info(f"Processing file 1/3:")
await ctx.info(f"Processing file 2/3:")
await ctx.info(f"Processing file 3/3:")
return f"Here's the file content: {message}"
完整代码如下:
from mcp.server.fastmcp import FastMCP, Context
from typing import Optional, Dict, Any, List, AsyncGenerator
from mcp.types import (
LoggingMessageNotificationParams,
TextContent
)
# Create an MCP server
mcp = FastMCP("Streamable DEMO")
# 2. Adding features
@mcp.tool(description="A simple tool returning file content")
async def echo(message: str, ctx: Context) -> str:
# 3. Send a notification
await ctx.info(f"Processing file 1/3:")
await ctx.info(f"Processing file 2/3:")
await ctx.info(f"Processing file 3/3:")
return f"Here's the file content: {message}"
# 1. Set up the transport as streamable HTTP.
mcp.run(transport="streamable-http")
接下来看看如何测试服务器。
测试服务器
一如既往,我们可以用多种方式验证服务器功能:
- 编写客户端
- 使用 Inspector 工具
- 使用 cURL 测试
下面逐一尝试。
使用 Inspector 工具
本章前面已经介绍过 Inspector,这里快速回顾如何与我们新建的服务器配合使用。我们可以这样启动 Inspector 的网页界面,并在界面中选择如下字段:
- Transport Type:HTTP
- Server URL:
http://localhost:8000/mcp(如有需要调整端口)
点击 Connect 连接服务器后即可使用其功能。启动命令为:
npx @modelcontextprotocol/inspector
可以看到,与测试 SSE 服务器很相似,只需更改传输类型,并确保 URL 以 /mcp 结尾(而不是 /sse)。
使用 cURL 测试
上一章介绍了 cURL,这里同样适用。要获取会话 ID,你需要先发送一条 initialize 消息;该消息应包含你支持的能力(例如 tools)。
发送如下消息:
curl -X POST "http://127.0.0.1:8000/mcp" \
-H "Accept: text/event-stream, application/json" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {} }, "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } }
}'
注意要同时发送 Accept 与 Content-Type 头。记录返回的会话 ID,后续调用都要用它。
打开第二个终端窗口,执行下列命令(把 mcp-session-id 的值替换为上一步获取的会话 ID):
curl -X POST "http://127.0.0.1:8000/mcp" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream, application/json" \
-H "mcp-session-id: 39a0b504364140ce97d8eded79b1c244" \
-d '{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}'
请注意,会话 ID 不再是查询参数 session_id,而是请求头 mcp-session-id。这条消息是 notifications/initialized 类型的通知,表示握手流程的最后一步。之后即可进行常规操作(列出工具、调用工具等)。
继续在第二个终端中(替换 mcp-session-id 值)调用工具:
curl -X POST "http://127.0.0.1:8000/mcp" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream, application/json" \
-H "mcp-session-id: 39a0b504364140ce97d8eded79b1c244" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "echo",
"arguments": { "message": "chris" }
}
}'
在这条 tools/call 消息中,我们调用名为 echo 的工具,传入参数 chris,应能看到类似如下的工具响应(会在第二个终端中显示):
event: message
data: {"method":"notifications/message","params":{"level":"info","data":"Processing file 1/3:"},"jsonrpc":"2.0"}
event: message
data: {"method":"notifications/message","params":{"level":"info","data":"Processing file 2/3:"},"jsonrpc":"2.0"}
event: message
data: {"method":"notifications/message","params":{"level":"info","data":"Processing file 3/3:"},"jsonrpc":"2.0"}
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Here's the file content: chris"}],"structuredContent":{"result":"Here's the file content: chris"},"isError":false}}
这表明我们收到了三条通知以及最终的工具返回,说明 MCP 服务器按预期工作。
可见 Inspector 与 cURL 都是测试服务器的好工具。不过在实际方案中,我们更可能通过自定义客户端来集成 MCP 服务器,下一节继续。
编写能处理通知的客户端
谈谈客户端。客户端通常需要额外配置来处理通知,这属于常规功能之外的补充。先制定实现计划:
- 创建 Streamable HTTP 传输与客户端
- 设置通知处理器(notification handler)
- 调用一个工具
针对第一点,代码如下:
# 1. Create a streamable HTTP transport and client
async with streamablehttp_client(f"http://localhost:{port}/mcp") as (
read_stream,
write_stream,
session_callback,
):
# Create a session using the client streams
async with ClientSession(
read_stream,
write_stream
) as session:
这里使用 streamablehttp_client 创建 Streamable HTTP 客户端,然后通过 ClientSession 基于其读写流创建会话。
为了支持接收通知,需要给客户端会话配置一个 message_handler 回调函数:
# 2. Set up a notification handler
async def message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult]
| types.ServerNotification
| Exception,
) -> None:
print("Received message:", message)
if isinstance(message, Exception):
raise message
else:
if isinstance(message, types.ServerNotification):
print("NOTIFICATION:", message)
elif isinstance(message, RequestResponder):
print("REQUEST_RESPONDER:", message)
else:
print("SERVER_REQUEST:", message)
# omitted code for brevity
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
可以看到,我们把 message_handler 函数赋给 ClientSession 的同名参数,用来处理所有入站消息并打印输出。
就这样,我们就完成了一个 Streamable HTTP 客户端,并具备接收通知的能力。完整代码如下:
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
import asyncio
from typing import Optional, Dict, Any, List
import mcp.types as types
from mcp.types import (
LoggingMessageNotificationParams,
TextContent,
)
from mcp.shared.session import RequestResponder
port = 8000
# I get normal messages, notifications, and exceptions
# 2. Set up a notification handler
async def message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult]
| types.ServerNotification
| Exception,
) -> None:
print("Received message:", message)
if isinstance(message, Exception):
raise message
async def main():
print("Starting client...")
# 1. Create a streamable HTTP transport and client
async with streamablehttp_client(f"http://localhost:{port}/mcp") as (
read_stream,
write_stream,
session_callback,
):
# 2. Set up a notification handler
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
# Initialize the connection
await session.initialize()
# 3. Call a tool
results = []
tool_result = await session.call_tool("echo",
{"message": "hello"})
print("Tool result:", tool_result)
asyncio.run(main())
现在运行该客户端,你会看到类似如下输出,表明这些确实是通知:
NOTIFICATION: root=LoggingMessageNotification(method='notifications/message', params=LoggingMessageNotificationParams(meta=None, level='info', logger=None, data='Processing file 3/3:'), jsonrpc='2.0')
结论是:通过 message_handler,我们能够捕获服务器发送的所有消息、通知与异常,从而进行恰当处理并向用户提供反馈。
使用可恢复性进行测试
SDK 中其实提供了一个可恢复性的示例实现。我们来试试那段代码,看看有何不同。你可以在此处找到它的简化版本:github.com/PacktPublis…。
这段代码做的事是定义了一个只有一个工具 process-files 的服务器。调用该工具时,你会收到三条通知以及一条最终响应。测试该服务器最简单的方法是使用 cURL。借助 cURL,我们可以完成握手流程、调用工具,甚至发送定制请求来**回放(replay)**事件。让我们一步步来:
-
先启动服务器。
-
开启第二个终端,用下面的 cURL 负载发起请求,以交换客户端与服务器双方支持的特性:
curl -X POST "http://127.0.0.1:8000/mcp" -H "Accept: text/event-stream, application/json" -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": { "tools": {}, "logging": {} }, "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } }'去第一个终端窗口查看响应。你应能看到服务器显示的会话 ID;把它复制下来备用。
-
发送
initialized通知以结束服务端—客户端的握手;把mcp-session-id的值替换为上一步复制的会话 ID:curl -X POST "http://127.0.0.1:8000/mcp" -H "Content-Type: application/json" -H "Accept: text/event-stream, application/json" -H "mcp-session-id: 957f11af-4766-4c1c-a1f2-5bd6776cca6a" -d '{ "jsonrpc": "2.0", "method": "notifications/initialized" }' -
调用工具:在第二个终端窗口中粘贴以下命令(先替换
"mcp-session-id"的值):curl -X POST "http://127.0.0.1:8000/mcp" -H "Content-Type: application/json" -H "Accept: text/event-stream, application/json" -H "mcp-session-id: 957f11af-4766-4c1c-a1f2-5bd6776cca6a" -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "process-files", "arguments": { "message": "chris" } } }'此时你应能在第二个终端窗口中看到一串通知及最终结果,如下所示:
event: message id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976937_z6m5xbyc data: {"method":"notifications/message","params":{"level":"info","data":"sales1.csv processed"},"jsonrpc":"2.0"} event: message id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976940_meh2n52f data: {"method":"notifications/message","params":{"level":"info","data":"sales2.csv processed"},"jsonrpc":"2.0"} event: message id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976943_e3v55tmn data: {"method":"notifications/message","params":{"level":"info","data":"sales3.csv processed"},"jsonrpc":"2.0"} event: message id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976946_sgpvardt data: {"result":{"content":[{"type":"text","text":"Files processed: 3"}]},"jsonrpc":"2.0","id":1}我们重点关注下面这条消息:它是一条通知,表示我们正在处理 第 2/3 个文件。假设我们此时进了隧道、网络断了。记下该条消息的 ID,因为这是断线前你看到的最后一条:
event: message id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976940_meh2n52f data: {"method":"notifications/message","params":{"level":"info","data":"sales2.csv processed"},"jsonrpc":"2.0"} -
回放缺失事件:最后一步,我们需要向
/mcp端点发送一个 GET 请求,并在请求头里带上 会话 ID 和 最后事件 ID。服务器随后会回放我们缺失的消息——此处应是sales3.csv的通知与最终工具结果。粘贴下面命令前,请把mcp-session-id与last-event-id都替换为你的值:curl "http://127.0.0.1:8000/mcp" -H "Content-Type: application/json" -H "Accept: text/event-stream, application/json" -H "mcp-session-id: 957f11af-4766-4c1c-a1f2-5bd6776cca6a" -H "last-event-id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976940_meh2n52f"运行后,你应在第二个终端窗口看到如下输出:
event: message id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976943_e3v55tmn data: {"method":"notifications/message","params":{"level":"info","data":"sales3.csv processed"},"jsonrpc":"2.0"} event: message id: 3a9d76c3-36d8-45f3-bd6e-8b9c82826de8_1757284976946_sgpvardt data: {"result":{"content":[{"type":"text","text":"Files processed: 3"}]},"jsonrpc":"2.0","id":1}我们缺失的一条通知和工具结果都回来了!太棒了——没有丢任何消息。
补充说明:如果你要编写能利用重放能力的代码,那么在失去网络连接时应监听浏览器事件,从而有机会保存 会话 ID 与 最后事件 ID,并记得使用 GET /mcp(而不是 POST /mcp)进行重连;后者会开启一个新会话。
另外,如果你使用的是文中的事件存储(event store)实现,请注意它不适用于生产环境;要达到生产级别,需要把消息持久化到数据库或类似存储中。
总结
本章我们探讨了 Streamable HTTP 的概念及其与 SSE 的差异。我们了解到,流式传输可以实现实时数据下发,这对需要即时数据访问的应用(如实时事件或大文件)非常有益。
此外,我们实现了一个支持 Streamable HTTP 的 MCP 服务器,并示范了如何使用 MCP SDK 来消费流式数据。我们还讨论了**通知(notifications)**在向客户端实时推送更新方面的重要性,以及如何有效处理这些通知。
在下一章中,我们将说明如何使用低层级的服务器 API,因为在某些用例中你可能希望直接使用这些接口。