sse_app() ignores mount prefix, resulting in 404 from client

用户在 FastAPI 或 Starlette 应用中通过 Mount() 将 FastMCP 的 sse_app() 挂载到非根路径下(例如 Mount("/mcp", app=mcp.sse_app()) ),启动后 SSE 握手返回的 endpoint 路径缺失前缀,导致 MCP 客户端发起

sse_app() ignores mount prefix, resulting in 404 from client

sse_app() ignores mount prefix, resulting in 404 from client

快速结论:当使用 Starlette 或 FastAPI 通过 Mount() 将 MCP SSE 服务挂载在非根路径(如 /mcp)时,sse_app() 返回的 SSE 握手事件中 /messages/ 路径缺少挂载前缀,导致客户端解析到错误 URL 而返回 404。优先排查是否使用了非根路径挂载,并检查 SSE 握手返回的 endpoint 路径。

问题场景

用户在 FastAPI 或 Starlette 应用中通过 Mount() 将 FastMCP 的 sse_app() 挂载到非根路径下(例如 Mount("/mcp", app=mcp.sse_app())),启动后 SSE 握手返回的 endpoint 路径缺失前缀,导致 MCP 客户端发起 POST 请求时访问了错误地址,收到 404 响应。

报错原文

event: endpoint
data: /messages/?session_id=...

# 客户端执行 urljoin 后实际请求地址
# http://127.0.0.1:8000/messages/  (错误)
# 正确应为:
# http://127.0.0.1:8000/mcp/messages/  (预期)

# 客户端报错示例
Error in post_writer: Client error '404 Not Found' for url 'http://127.0.0.1:8080/messages/?session_id=7054b5ed39574cb28470f6308868c6a8'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404

原因分析

根本原因在于 sse_app() 内部生成的 SSE endpoint 路径是硬编码的默认值(/messages/),没有考虑 Mount 前缀。当客户端收到 SSE 握手事件后,使用 urljoin() 将 SSE URL 与 endpoint 路径拼接时,由于前缀被忽略,导致地址错误。该问题在 sse_app() 的 endpoint 生成逻辑(src/mcp/server/sse.py#L98)中起源。

环境排查

  • Python SDK 版本: modelcontextprotocol/python-sdk(具体版本未指定,问题出现在当前主分支)
  • Web 框架: Starlette 或 FastAPI(后者是 Starlette 子类)
  • MCP 服务器: FastMCP
  • 操作系统: macOS(用户环境注明),理论上跨平台问题
  • 依赖版本: 未指定 Python、CUDA、PyTorch、显卡版本,非依赖驱动问题

解决步骤

方法一:使用 /etc/hosts 或 Hosts 文件添加独立主机名(临时工作区)

  1. /etc/hosts(MacOS/Linux)或 Hosts File Editor(Windows)中添加两个主机名指向 127.0.0.1,例如 backendmcp
  2. 在 Starlette 应用中使用 Host 路由将不同主机指向不同应用:MCP 服务挂载在 mcp 主机下,不涉及多级路径前缀。
  3. 启动服务后,MCP 客户端连接 http://mcp:8000/sse,由于挂载在根路径,SSE 握手返回正确 endpoint。

方法二:自定义路由注册(可优先尝试,推荐解决方案)

  1. 创建辅助函数 register_mcp_router(),接受 Starlette/FastAPI 应用、MCP 服务器实例和 base_path 参数。
  2. 在函数内部手动创建 SseServerTransport 实例,传入正确 base_path(例如 f"{base_path}/messages/")。
  3. 手动注册 SSE 路由和消息处理路由:调用 starlette_app.add_route(f"{base_path}/sse", handle_sse)starlette_app.mount(f"{base_path}/messages/", sse.handle_post_message)
  4. 删除原先的 Mount() 调用,改用此函数注册路由。
def register_mcp_router(
    starlette_app: Starlette,
    mcp_server: FastMCP,
    base_path: str,
):
    sse = SseServerTransport(f"{base_path}/messages/")

    async def handle_sse(request: Request) -> None:
        async with sse.connect_sse(
            request.scope,
            request.receive,
            request._send,
        ) as (read_stream, write_stream):
            await mcp_server._mcp_server.run(
                read_stream,
                write_stream,
                mcp_server._mcp_server.create_initialization_options(),
            )

    starlette_app.add_route(f"{base_path}/sse", handle_sse)
    starlette_app.mount(f"{base_path}/messages/", sse.handle_post_message)

方法三:将 MCP 服务挂载在根路径(最简单的限制条件)

  1. 修改 Mount("/", app=mcp.sse_app()),保证 SSE endpoint 返回不带前缀的相对路径。
  2. 注意:此方法无法同时满足自定义路由需求,如需要额外路由需配合方法二。

方法四:等待官方修复(参见 Pull Requests #524、#540)

  1. 关注 modelcontextprotocol/python-sdk 仓库的 sse_app() 支持 base_path 参数或 auto-detect 的合并情况。
  2. 更新 SDK 到最新版本,检查是否已部分修复。

验证方法

启动服务后,在浏览器访问 http://127.0.0.1:8000/mcp/sse(或对应挂载前缀路径),观察 SSE 事件:

event: endpoint
data: /mcp/messages/?session_id=...

如果 endpoint 路径包含正确前缀(如 /mcp/messages/),且客户端 POST 到该地址能收到 200 响应,则问题已解决。

参考来源

modelcontextprotocol/python-sdk #412

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

celebrityanime
celebrityanime
文章: 9040

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注