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`.
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
No tool could ever be reached, even though the server itself was healthy (
/healthanswering 200).What
build_app()now returns FastMCP'shttp_app(path="/mcp"), run under uvicorn instead ofmcp.run(transport="sse"): the MCP endpoint ishttp://<host>:8000/mcp.GET /healthroute is preserved for the Kubernetes liveness/readiness/startup probes.MCP_HOST/MCP_PORT/MCP_HTTP_PATHconstants make the endpoint explicit.Tests
Transport regression tests added:
/mcpis exposed (and equalsMCP_HTTP_PATH)/sseis goneGET /healthstill answers{"status": "ok"}Verified locally
With fastmcp 4.1.0 (same major as production):
POST /mcp(initialize,Accept: application/json, text/event-stream)mcp-session-id+serverInfoGET /health{"status": "ok"}GET /sse,POST /sseFull unit suite passes. The client side (
opencode.jsonc) is updated in the same move:/sse->/mcp.