Fix MCP Python SDK sse_app() Mount Prefix 404 Error in Starlette, FastAPI, and Custom MCP Servers
The MCP Python SDK's `sse_app()` method ignores URL mount prefixes when generating SSE message endpoint URIs. When an MCP server is mounted under a prefix via Starlette's `Mount()` or FastAPI's `app.mount()`, the SSE handshake returns `/messages/` instead of `/<prefix>/messages/`. MCP clients then POST to the wrong URL, receiving HTTP 404. This affects all environments using non-root-mounted MCP servers — including Claude Desktop, Cursor, VS Code, and custom MCP clients. Fixed in mcp >= 1.9.0 via PR #659, which reads the ASGI `root_path` scope attribute for proper prefix inference.
Symptoms
- MCP client returns HTTP 404 when connecting to an SSE server mounted under a URL prefix
- SSE endpoint event returns incorrect path: `data: /messages/?session_id=...` instead of `data: /mcp/messages/?session_id=...`
- MCP server started successfully but client cannot establish message channel
- Affects Starlette `Mount()` and FastAPI `app.mount()` with any non-root path prefix
- Also affects reverse proxy deployments where the proxy strips a URL prefix before forwarding
Error signatures
HTTP 404 Not Found on POST /messages/
SSE event: event: endpoint\ndata: /messages/?session_id=... (missing mount prefix)
Expected SSE event: event: endpoint\ndata: /mcp/messages/?session_id=... (with mount prefix)
Client-side: 'Failed to connect to MCP server: 404' or 'MCP connection error -32000'
Possible causes
- The core issue is that `SseServerTransport` in the MCP Python SDK hardcoded the message endpoint path as `/messages/` without reading the ASGI `scope['root_path']` attribute. Starlette's `Mount()` strips the prefix from `scope['path']` but stores it in `scope['root_path']`. The SDK ignored this field, causing the SSE handshake to return a root-relative `/messages/` path regardless of how the app was mounted. When the MCP client performs URL resolution between the SSE URL and the endpoint path, it produces an incorrect URL that lacks the mount prefix, resulting in a 404.
- This affects all ASGI frameworks that set `root_path` — including Starlette, FastAPI, and any deployment behind a reverse proxy that uses ASGI path rewriting.
Solutions
Use the sse_app() mount_path Parameter (mcp 1.8.x)
Community PR #540 added a `mount_path` parameter to `sse_app()` in some mcp 1.8.x versions, allowing explicit prefix specification as a temporary workaround before the automatic fix in 1.9.0.
- Pass the mount prefix explicitly: `mcp.sse_app("/mcp")`
- Ensure the mount_path matches the Mount prefix exactly
- Restart the application
Commands
curl -s -N http://localhost:8000/mcp/sse 2>&1 | head -5
Config examples
# Explicit mount_path parameter (mcp 1.8.x)
from starlette.applications import Starlette
from starlette.routing import Mount
from mcp.server.fastmcp import FastMCP
mcp = FastMCP()
app = Starlette(routes=[
Mount("/mcp", app=mcp.sse_app("/mcp")),
])Risks
- Requires mcp 1.8.x with the mount_path patch — not all 1.8.x versions have it
- Doubles the mount path specification (both Mount and sse_app), making code fragile to refactoring
Verification
- Step 1: Add mount_path parameter matching Mount prefix → expect: server starts without errors
- Step 2: Test SSE endpoint with `curl -s -N http://localhost:8000/mcp/sse 2>&1 | head -5` → expect: `event: endpoint` followed by `data: /mcp/messages/?session_id=...` (with prefix)
- Step 3: Connect MCP client → expect: successful connection, no 404
Mount MCP Server at Root / (Quick Workaround)
If upgrading is not immediately possible, mounting the MCP server at the root path avoids the prefix issue entirely since root-mounted apps do not need prefix inference.
- Change your Starlette/FastAPI route to use `Mount("/", app=mcp.sse_app())` instead of a sub-path mount
- Ensure no other routes conflict with the MCP server's `/sse` and `/messages/` paths
- Restart the application
Commands
curl -s -N http://localhost:8000/sse 2>&1 | head -5
Config examples
# Quick fix: mount at root
from starlette.applications import Starlette
from starlette.routing import Mount
from mcp.server.fastmcp import FastMCP
mcp = FastMCP()
app = Starlette(routes=[
Mount("/", app=mcp.sse_app()),
])Risks
- Only works if you can dedicate the root path to the MCP server
- Limits ability to serve multiple MCP servers or other routes on the same host
Verification
- Step 1: Change Mount path to '/' and restart server → expect: server starts without errors
- Step 2: Test SSE endpoint with `curl -s -N http://localhost:8000/sse 2>&1 | head -5` → expect: `event: endpoint` followed by `data: /messages/?session_id=...` (resolves correctly at root)
- Step 3: Connect MCP client to root URL → expect: connection succeeds with no 404
Upgrade to mcp >= 1.9.0 (Official Fix)
PR #659 merged into mcp 1.9.0 makes `SseServerTransport` read the ASGI `scope['root_path']` attribute, so mounted prefixes are automatically respected without any code changes.
- Check current mcp version: `pip show mcp | grep Version`
- Upgrade the MCP Python SDK: `pip install --upgrade 'mcp>=1.9.0'`
- Restart your MCP server application
- Verify the SSE endpoint event now returns the correct prefixed path using curl
Commands
pip show mcp | grep Version
pip install --upgrade 'mcp>=1.9.0'
curl -s -N http://localhost:8000/mcp/sse 2>&1 | head -5
Config examples
# After upgrade, this works out of the box:
from starlette.applications import Starlette
from starlette.routing import Mount
from mcp.server.fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool()
async def hello() -> str:
return "Hello, world!"
app = Starlette(routes=[
Mount("/mcp", app=mcp.sse_app()),
])
# SSE endpoint now correctly returns /mcp/messages/ instead of /messages/Risks
- Requires mcp >= 1.9.0 — may introduce other breaking changes if upgrading from a very old version
- External reverse proxy deployments where the SDK cannot see the prefix are tracked separately in issue #795
Verification
- Step 1: Check current version with `pip show mcp | grep Version` → expect: Version below 1.9.0 (e.g., 1.8.1) if bug is present
- Step 2: Upgrade with `pip install --upgrade 'mcp>=1.9.0'` → expect: `Successfully installed mcp-1.X.Y` where X.Y >= 9.0
- Step 3: Start server and test SSE endpoint with `curl -s -N http://localhost:8000/mcp/sse 2>&1 | head -5` → expect: output contains `event: endpoint` followed by `data: /mcp/messages/?session_id=...` (notice /mcp/ prefix, not bare /messages/)
- Step 4: Connect an MCP client (Claude Desktop, Cursor, VS Code) to the prefixed URL → expect: successful connection, no 404 error
Agent JSON
Canonical machine-readable representation of this issue:
{
"issue_id": "874e2d5f-2468-4b7e-9b05-0dcfcba00508",
"slug": "fix-mcp-python-sdk-sse-app-mount-prefix-404-error-in-starlette-fastapi-and-custom-mcp-servers-y2yvoy",
"verification_status": "unverified",
"canonical_json": "https://codekb.dev/v1/issues/fix-mcp-python-sdk-sse-app-mount-prefix-404-error-in-starlette-fastapi-and-custom-mcp-servers-y2yvoy"
}