fix: serve MCP over Streamable HTTP instead of legacy SSE #58

Merged
jcabillot merged 1 commits from fix/streamable-http-transport into main 2026-10-17 17:42:36 -04:00
Owner

Why

Current MCP clients (OpenCode, Claude, ...) only speak MCP Streamable HTTP: they POST JSON-RPC to the configured URL. Serving only the legacy SSE transport meant every connection failed with:

POST /sse -> 405 Method Not Allowed

No tool could ever be reached, even though the server itself was healthy (/health answering 200).

What

  • build_app() now returns FastMCP's http_app(path="/mcp"), run under uvicorn instead of mcp.run(transport="sse"): the MCP endpoint is http://<host>:8000/mcp.
  • The custom GET /health route is preserved for the Kubernetes liveness/readiness/startup probes.
  • MCP_HOST / MCP_PORT / MCP_HTTP_PATH constants make the endpoint explicit.

Tests

Transport regression tests added:

  • /mcp is exposed (and equals MCP_HTTP_PATH)
  • /sse is gone
  • GET /health still answers {"status": "ok"}

Verified locally

With fastmcp 4.1.0 (same major as production):

Request Result
POST /mcp (initialize, Accept: application/json, text/event-stream) 200 + mcp-session-id + serverInfo
GET /health 200 {"status": "ok"}
GET /sse, POST /sse 404

Full unit suite passes. The client side (opencode.jsonc) is updated in the same move: /sse -> /mcp.

## Why Current MCP clients (OpenCode, Claude, ...) only speak MCP **Streamable HTTP**: they POST JSON-RPC to the configured URL. Serving only the legacy SSE transport meant every connection failed with: ``` POST /sse -> 405 Method Not Allowed ``` No tool could ever be reached, even though the server itself was healthy (`/health` answering 200). ## What - `build_app()` now returns FastMCP's `http_app(path="/mcp")`, run under uvicorn instead of `mcp.run(transport="sse")`: the MCP endpoint is `http://<host>:8000/mcp`. - The custom `GET /health` route is preserved for the Kubernetes liveness/readiness/startup probes. - `MCP_HOST` / `MCP_PORT` / `MCP_HTTP_PATH` constants make the endpoint explicit. ## Tests Transport regression tests added: - `/mcp` is exposed (and equals `MCP_HTTP_PATH`) - `/sse` is gone - `GET /health` still answers `{"status": "ok"}` ## Verified locally With fastmcp 4.1.0 (same major as production): | Request | Result | | --- | --- | | `POST /mcp` (initialize, `Accept: application/json, text/event-stream`) | 200 + `mcp-session-id` + `serverInfo` | | `GET /health` | 200 `{"status": "ok"}` | | `GET /sse`, `POST /sse` | 404 | Full unit suite passes. The client side (`opencode.jsonc`) is updated in the same move: `/sse` -> `/mcp`.
opencodecabilloteu added 1 commit 2026-10-17 16:45:45 -04:00
fix: serve MCP over Streamable HTTP instead of legacy SSE
PR Checks / hadolint (pull_request) Successful in 40s
PR Checks / build-test (pull_request) Successful in 3m19s
PR Checks / pytest (pull_request) Successful in 2m47s
Docker Build and Push / test (pull_request) Successful in 3m1s
Docker Build and Push / integration-test (pull_request) Successful in 3m24s
Docker Build and Push / build (pull_request) Successful in 3m4s
bc3deb8676
Current MCP clients (OpenCode, Claude, ...) only speak Streamable HTTP: they
POST JSON-RPC to the configured URL, so a legacy SSE endpoint answers
405 Method Not Allowed and not a single tool can be reached.

Run FastMCP's http_app (build_app()) under uvicorn instead of mcp.run(sse):
the MCP endpoint is now /mcp, and the custom GET /health route keeps serving
the Kubernetes probes.

Add transport tests asserting /mcp and /health are exposed and /sse is gone.
opencodecabilloteu requested review from jcabillot 2026-10-17 16:45:46 -04:00
jcabillot merged commit dd41603a0b into main 2026-10-17 17:42:36 -04:00
jcabillot deleted branch fix/streamable-http-transport 2026-10-17 17:42:42 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: perso/mcp-maildir#58