(4-2-05)Python SDK仓库:MCP服务器端(5)Streamable HTTP传输+Streamable HTTP传输

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
相关推荐
UP_Continue6 小时前
C++11--引言折叠与完美转发
开发语言·c++
守城小轩6 小时前
轻量级HTTP&Socks代理GOST: 搭建 HTTP(S)和Socks代理
网络·网络协议·http·浏览器网路
十铭忘6 小时前
Vue3实现Pixso中的钢笔工具
开发语言·javascript·vue
鸽鸽程序猿6 小时前
【JavaEE】SpringMVC获取HTTP中的元素
http·java-ee
鱼腩同学6 小时前
使用 curl 进行 HTTP 请求:详尽指南
网络·网络协议·http
IT枫斗者6 小时前
Spring Boot 4.0 正式发布:新一代起点到底“新”在哪?(Spring Framework 7 / Java 25 / JSpecify / API 版本管理 / HTTP Service
java·开发语言·spring boot·后端·python·spring·http
William_cl6 小时前
ASP.NET入门必吃透:HTTP 协议从流程到状态码,代码 + 避坑指南
后端·http·asp.net
龙茶清欢6 小时前
WebClient:Spring WebFlux 响应式 HTTP 客户端权威说明文档
java·spring·http
AI大佬的小弟6 小时前
Python基础(10):Python函数基础详解
开发语言·python·函数·ai大模型基础·嵌套函数·变量的作用域·全局变量和局部变量