
4.3.6 Streamable HTTP传输
Streamable HTTP传输是一种网络传输协议,允许服务器和客户端之间进行双向、持久的通信。与传统的HTTP"请求-响应"模式不同,Streamable HTTP传输支持服务器推送(Server-Sent Events,SSE)和流式数据传输,使得服务器可以在客户端请求之后继续发送数据,而无需客户端发起新的请求。这种传输方式特别适用于需要实时更新或持续数据流的应用场景,如在线协作工具、实时通知系统和动态用户界面。在本项目的MCP服务器中,Streamable HTTP传输通过streamable_http.py、streamable_http_manager.py和streaming_asgi_transport.py等文件实现,提供了灵活的会话管理和事件处理机制,以支持复杂的交互式工作流。
(1)文件src/mcp/server/streamable_http.py定义了类StreamableHTTPServerTransport,该类实现了一个支持事件流(SSE)的HTTP服务器传输层,用于处理MCP协议的HTTP请求。它管理了内存中的读写流,支持POST请求的JSON-RPC消息处理,以及通过SSE流发送通知。此外,还实现了会话管理、事件存储和重新连接时的事件重放功能。这个类是FastMCP服务器与客户端进行交互的核心组件,用于处理来自客户端的请求,并将响应发送回客户端。
python
class StreamableHTTPServerTransport:
"""
支持事件流的HTTP服务器传输,用于MCP。
处理带有SSE流的HTTP POST 请求中的JSON-RPC消息。
支持可选的JSON响应和会话管理。
"""
# 服务器通知流,用于POST请求以及独立的SSE流
_read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] | None = None
_read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] | None = None
_write_stream: MemoryObjectSendStream[SessionMessage] | None = None
_write_stream_reader: MemoryObjectReceiveStream[SessionMessage] | None = None
_security: TransportSecurityMiddleware
def __init__(
self,
mcp_session_id: str | None,
is_json_response_enabled: bool = False,
event_store: EventStore | None = None,
security_settings: TransportSecuritySettings | None = None,
) -> None:
"""
初始化一个新的可流式HTTP服务器传输。
参数:
mcp_session_id:此连接的可选会话标识符。
必须只包含可见的ASCII字符(0x21-0x7E)。
is_json_response_enabled:如果为True,则返回JSON响应而不是SSE流。默认为False。
event_store:用于恢复支持的事件存储。如果提供,
恢复功能将被启用,允许客户端重新连接并恢复消息。
security_settings:用于DNS重绑定保护的可选安全设置。
引发:
ValueError:如果会话ID包含无效字符。
"""
# ...(此处省略了部分代码)
async def handle_request(self, scope: Scope, receive: Receive, send: Send) -> None:
"""处理所有HTTP请求的应用程序入口点"""
# ...(此处省略了部分代码)
async def _handle_post_request(self, scope: Scope, request: Request, receive: Receive, send: Send) -> None:
"""处理包含JSON-RPC消息的POST请求。"""
# ...(此处省略了部分代码)
async def _handle_get_request(self, request: Request, send: Send) -> None:
"""处理建立SSE的GET请求。
这允许服务器在客户端首先通过HTTP POST发送数据之前与客户端通信。服务器可以通过此流发送JSON-RPC请求和通知。
"""
# ...(此处省略了部分代码)
async def terminate(self) -> None:
""终止当前会话,关闭所有流。
一旦终止,所有带有此会话ID的请求都将收到404未找到。
"""
# ...(此处省略了部分代码)
(2)文件src/mcp/server/streamable_http_manager.py定义了类StreamableHTTPSessionManager,该类负责管理MCP服务器中的StreamableHTTP会话。StreamableHTTPSessionManager能够处理客户端的会话跟踪、可选的事件存储以支持恢复能力、连接管理和请求处理。这个类抽象了会话管理的复杂性,为StreamableHTTP传输提供了一个简化的接口。它支持通过事件存储实现会话的可选恢复能力,允许客户端在断开连接后重新连接并接收错过的事件。此外,它还支持无状态模式,为每个请求创建一个全新的传输,不跟踪会话或在请求之间保持状态。
python
class StreamableHTTPSessionManager:
"""
通过事件存储可选的恢复能力管理StreamableHTTP会话。
此类抽象了会话管理、事件存储和请求处理的复杂性,用于StreamableHTTP传输。它处理:
1. 客户端的会话跟踪
2. 通过可选的事件存储实现恢复能力
3. 连接管理和生命周期
4. 请求处理和传输设置
重要:每个应用程序应该只创建一个StreamableHTTPSessionManager实例。
实例的run()上下文完成后,不能重复使用该实例。如果需要重新启动管理器,请创建新实例。
参数:
app:MCP服务器实例
event_store:可选的事件存储,用于支持恢复。
如果提供,启用恢复连接,允许客户端重新连接并接收错过的事件。
如果为None,会话仍然被跟踪,但不支持恢复。
json_response:是否使用JSON响应代替SSE流
stateless:如果为True,则为每个请求创建一个全新的传输
不跟踪会话或在请求之间保持状态。
"""
def __init__(
self,
app: MCPServer[Any, Any],
event_store: EventStore | None = None,
json_response: bool = False,
stateless: bool = False,
security_settings: TransportSecuritySettings | None = None,
):
self.app = app
self.event_store = event_store
self.json_response = json_response
self.stateless = stateless
self.security_settings = security_settings
# 会话跟踪(仅在非无状态模式下使用)
self._session_creation_lock = anyio.Lock()
self._server_instances: dict[str, StreamableHTTPServerTransport] = {}
# 任务组将在生命周期中设置
self._task_group = None
# 线程安全的run()调用跟踪
self._run_lock = anyio.Lock()
self._has_started = False
@contextlib.asynccontextmanager
async def run(self) -> AsyncIterator[None]:
"""
运行会话管理器,进行适当的生命周期管理。
这将创建并管理所有会话操作的任务组。
重要:每个实例只能调用一次此方法。同一个
StreamableHTTPSessionManager实例不能在此上下文管理器退出后重复使用。
如果需要重新启动,请创建新实例。
在Starlette应用程序的生命周期上下文管理器中使用此方法:
@contextlib.asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
async with session_manager.run():
yield
"""
# 线程安全检查以确保run()只调用一次
async with self._run_lock:
if self._has_started:
raise RuntimeError(
"StreamableHTTPSessionManager .run() 每个实例只能调用一次。如果需要再次运行,请创建新实例。"
)
self._has_started = True
async with anyio.create_task_group() as tg:
# 存储任务组以供以后使用
self._task_group = tg
logger.info("StreamableHTTP会话管理器启动")
try:
yield # 让应用程序运行
finally:
logger.info("StreamableHTTP会话管理器关闭")
# 取消任务组以停止所有生成的任务
tg.cancel_scope.cancel()
self._task_group = None
# 清除任何剩余的服务器实例
self._server_instances.clear()
(3)文件src/mcp/server/streaming_asgi_transport.py定义了一个修改版的ASGI传输,名为StreamingASGITransport,它支持流式响应,例如服务器发送事件(SSE)。这个传输层将ASGI应用程序作为独立的anyio任务运行,使其能够处理那些应用程序不会立即终止的响应(比如SSE端点)。它主要用于为SSE传输编写测试。StreamingASGITransport类扩展了标准的ASGI传输,以处理应用程序生成初始响应后继续生成响应体的情况。
python
class StreamingASGITransport(AsyncBaseTransport):
"""
自定义AsyncTransport,直接将请求发送到ASGI应用程序并支持流式响应,如SSE。
与标准ASGITransport不同,这个传输层在单独的anyio任务中运行ASGI应用程序,允许它处理来自不会立即终止的应用程序的响应(如SSE端点)。
参数:
* `app` - ASGI应用程序。
* `raise_app_exceptions` - 布尔值,指示是否应该引发应用程序中的异常。默认为 `True`。可以设置为 `False` 用于测试客户端500响应的内容等情况。
* `root_path` - ASGI应用程序应挂载的根路径。
* `client` - 表示传入请求的客户端IP和端口的元组。
* `response_timeout` - 等待初始响应的超时时间(秒)。默认为10秒。
TODO: https://github.com/encode/httpx/pull/3059 添加了类似的东西
上游httpx。当它合并时,我们应该删除这个并切换回上游实现。
"""
def __init__(
self,
app: ASGIApp,
task_group: anyio.abc.TaskGroup,
raise_app_exceptions: bool = True,
root_path: str = "",
client: tuple[str, int] = ("127.0.0.1", 123),
) -> None:
self.app = app
self.raise_app_exceptions = raise_app_exceptions
self.root_path = root_path
self.client = client
self.task_group = task_group
async def handle_async_request(
self,
request: Request,
) -> Response:
assert isinstance(request.stream, AsyncByteStream)
# ASGI scope。
scope = {
"type": "http",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"method": request.method,
"headers": [(k.lower(), v) for (k, v) in request.headers.raw],
"scheme": request.url.scheme,
"path": request.url.path,
"raw_path": request.url.raw_path.split(b"?")[0],
"query_string": request.url.query,
"server": (request.url.host, request.url.port),
"client": self.client,
"root_path": self.root_path,
}
# 请求体
request_body_chunks = request.stream.__aiter__()
request_complete = False
# 响应状态
status_code = 499
response_headers = None
response_started = False
response_complete = anyio.Event()
initial_response_ready = anyio.Event()
# 流式响应同步
asgi_send_channel, asgi_receive_channel = anyio.create_memory_object_stream[dict[str, Any]](100)
content_send_channel, content_receive_channel = anyio.create_memory_object_stream[bytes](100)
# ASGI 可调用函数。
async def receive() -> dict[str, Any]:
nonlocal request_complete
if request_complete:
await response_complete.wait()
return {"type": "http.disconnect"}
try:
body = await request_body_chunks.__anext__()
except StopAsyncIteration:
request_complete = True
return {"type": "http.request", "body": b"", "more_body": False}
return {"type": "http.request", "body": body, "more_body": True}
async def send(message: dict[str, Any]) -> None:
nonlocal status_code, response_headers, response_started
await asgi_send_channel.send(message)
# 在单独的任务中启动ASGI应用程序
async def run_app() -> None:
try:
# 将接收和发送函数转换为ASGI类型
await self.app(cast(Scope, scope), cast(Receive, receive), cast(Send, send))
except Exception:
if self.raise_app_exceptions:
raise
if not response_started:
await asgi_send_channel.send({"type": "http.response.start", "status": 500, "headers": []})
await asgi_send_channel.send({"type": "http.response.body", "body": b"", "more_body": False})
finally:
await asgi_send_channel.aclose()
# 处理来自ASGI应用程序的消息
async def process_messages() -> None:
nonlocal status_code, response_headers, response_started
try:
async with asgi_receive_channel:
async for message in asgi_receive_channel:
if message["type"] == "http.response.start":
assert not response_started
status_code = message["status"]
response_headers = message.get("headers", [])
response_started = True
# 一旦我们有了头信息,我们就可以返回响应
initial_response_ready.set()
elif message["type"] == "http.response.body":
body = message.get("body", b"")
more_body = message.get("more_body", False)
if body and request.method != "HEAD":
await content_send_channel.send(body)
if not more_body:
response_complete.set()
await content_send_channel.aclose()
break
finally:
# 即使发生错误也要确保事件被设置
initial_response_ready.set()
response_complete.set()
await content_send_channel.aclose()
# 创建运行应用程序和处理消息的任务
self.task_group.start_soon(run_app)
self.task_group.start_soon(process_messages)
# 等待初始响应或超时
await initial_response_ready.wait()
# 创建流式响应
return Response(
status_code,
headers=response_headers,
stream=StreamingASGIResponseStream(content_receive_channel),
content=b"",
)
class StreamingASGIResponseStream(AsyncByteStream):
"""
修改版的ASGIResponseStream,支持流式响应。
这个类扩展了标准的ASGIResponseStream以处理在初始响应返回后响应体继续生成的情况。
"""
def __init__(
self,
receive_channel: anyio.streams.memory.MemoryObjectReceiveStream[bytes],
) -> None:
self.receive_channel = receive_channel
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
try:
async for chunk in self.receive_channel:
yield chunk
finally:
await self.receive_channel.aclose()
4.3.7 传输安全设置
文件src/mcp/server/transport_security.py定义了MCP服务器传输层的安全设置和中间件,用于实现DNS重绑定保护。DNS重绑定保护是一种安全措施,用于防止恶意客户端通过伪造的HTTP头信息来访问服务器上的资源。TransportSecuritySettings类提供了配置选项,用于启用或禁用DNS重绑定保护,以及设置允许的主机和来源列表。TransportSecurityMiddleware类实现了一个中间件,用于验证传入请求的HTTP头信息,确保它们符合安全。
python
import logging
from pydantic import BaseModel, Field
from starlette.requests import Request
from starlette.responses import Response
logger = logging.getLogger(__name__)
class TransportSecuritySettings(BaseModel):
"""MCP传输安全功能的设置。
这些设置有助于通过验证传入请求头信息来防止DNS重绑定攻击。
"""
enable_dns_rebinding_protection: bool = Field(
default=True,
description="启用DNS重绑定保护(推荐用于生产环境)",
)
allowed_hosts: list[str] = Field(
default=[],
description="允许的Host头值列表。仅当enable_dns_rebinding_protection为True时适用。"
)
allowed_origins: list[str] = Field(
default=[],
description="允许的Origin头值列表。仅当enable_dns_rebinding_protection为True时适用。"
)
class TransportSecurityMiddleware:
"""用于MCP传输端点的DNS重绑定保护的中间件。"""
def __init__(self, settings: TransportSecuritySettings | None = None):
# 如果未指定,则默认禁用DNS重绑定保护
# 为了向后兼容
self.settings = settings or TransportSecuritySettings(enable_dns_rebinding_protection=False)
def _validate_host(self, host: str | None) -> bool:
"""验证Host头是否符合允许的值。"""
if not host:
logger.warning("请求中缺少Host头")
return False
# 首先检查完全匹配
if host in self.settings.allowed_hosts:
return True
# 检查通配符端口模式
for allowed in self.settings.allowed_hosts:
if allowed.endswith(":*"):
# 从模式中提取基础主机
base_host = allowed[:-2]
# 检查实际主机是否以基础主机开头并带有端口
if host.startswith(base_host + ":"):
return True
logger.warning(f"无效的Host头:{host}")
return False
def _validate_origin(self, origin: str | None) -> bool:
"""验证Origin头是否符合允许的值。"""
# 对于同源请求,Origin可以缺失
if not origin:
return True
# 首先检查完全匹配
if origin in self.settings.allowed_origins:
return True
# 检查通配符端口模式
for allowed in self.settings.allowed_origins:
if allowed.endswith(":*"):
# 从模式中提取基础来源
base_origin = allowed[:-2]
# 检查实际来源是否以基础来源开头并带有端口
if origin.startswith(base_origin + ":"):
return True
logger.warning(f"无效的Origin头:{origin}")
return False
def _validate_content_type(self, content_type: str | None) -> bool:
"""验证POST请求的Content-Type头。"""
if not content_type:
logger.warning("POST请求中缺少Content-Type头")
return False
# Content-Type必须以application/json开头
if not content_type.lower().startswith("application/json"):
logger.warning(f"无效的Content-Type头:{content_type}")
return False
return True
async def validate_request(self, request: Request, is_post: bool = False) -> Response | None:
"""验证请求头信息以进行DNS重绑定保护。
如果验证通过,则返回None,或者在验证失败时返回错误响应。
"""
# 始终验证POST请求的Content-Type
if is_post:
content_type = request.headers.get("content-type")
if not self._validate_content_type(content_type):
return Response("无效的Content-Type头", status_code=400)
# 如果禁用了DNS重绑定保护,则跳过其余验证
if not self.settings.enable_dns_rebinding_protection:
return None
# 验证Host头
host = request.headers.get("host")
if not self._validate_host(host):
return Response("无效的Host头", status_code=421)
# 验证Origin头
origin = request.headers.get("origin")
if not self._validate_origin(origin):
return Response("无效的Origin头", status_code=400)
return None