
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 文件添加独立主机名(临时工作区)
- 在
/etc/hosts(MacOS/Linux)或 Hosts File Editor(Windows)中添加两个主机名指向127.0.0.1,例如backend和mcp。 - 在 Starlette 应用中使用
Host路由将不同主机指向不同应用:MCP 服务挂载在mcp主机下,不涉及多级路径前缀。 - 启动服务后,MCP 客户端连接
http://mcp:8000/sse,由于挂载在根路径,SSE 握手返回正确 endpoint。
方法二:自定义路由注册(可优先尝试,推荐解决方案)
- 创建辅助函数
register_mcp_router(),接受 Starlette/FastAPI 应用、MCP 服务器实例和base_path参数。 - 在函数内部手动创建
SseServerTransport实例,传入正确 base_path(例如f"{base_path}/messages/")。 - 手动注册 SSE 路由和消息处理路由:调用
starlette_app.add_route(f"{base_path}/sse", handle_sse)和starlette_app.mount(f"{base_path}/messages/", sse.handle_post_message)。 - 删除原先的
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 服务挂载在根路径(最简单的限制条件)
- 修改
Mount("/", app=mcp.sse_app()),保证 SSE endpoint 返回不带前缀的相对路径。 - 注意:此方法无法同时满足自定义路由需求,如需要额外路由需配合方法二。
方法四:等待官方修复(参见 Pull Requests #524、#540)
- 关注
modelcontextprotocol/python-sdk仓库的sse_app()支持base_path参数或 auto-detect 的合并情况。 - 更新 SDK 到最新版本,检查是否已部分修复。
验证方法
启动服务后,在浏览器访问 http://127.0.0.1:8000/mcp/sse(或对应挂载前缀路径),观察 SSE 事件:
event: endpoint data: /mcp/messages/?session_id=...
如果 endpoint 路径包含正确前缀(如 /mcp/messages/),且客户端 POST 到该地址能收到 200 响应,则问题已解决。



