KBCodeKB
Unverified

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)

risk: lowgithubpublished

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.

  1. Pass the mount prefix explicitly: `mcp.sse_app("/mcp")`
  2. Ensure the mount_path matches the Mount prefix exactly
  3. 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
0 verified0 failed

Mount MCP Server at Root / (Quick Workaround)

risk: lowgithubpublished

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.

  1. Change your Starlette/FastAPI route to use `Mount("/", app=mcp.sse_app())` instead of a sub-path mount
  2. Ensure no other routes conflict with the MCP server's `/sse` and `/messages/` paths
  3. 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
0 verified0 failed

Upgrade to mcp >= 1.9.0 (Official Fix)

risk: lowofficialpublished

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.

  1. Check current mcp version: `pip show mcp | grep Version`
  2. Upgrade the MCP Python SDK: `pip install --upgrade 'mcp>=1.9.0'`
  3. Restart your MCP server application
  4. 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
0 verified0 failed

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"
}
← Back to all issuesPowered by CodeKB