All checks were successful
Build and Push Monarch MCP Docker Image / build (push) Successful in 8s
The previous implementation added health route directly to mcp_app and returned it, which broke MCP's internal /mcp endpoint routing. Now matches the working pattern from komodo-mcp-custom: - Mount mcp_app at / inside a parent Starlette app - Pass lifespan=mcp_app.lifespan for proper task group init - Health check is a separate route in the parent app
3.2 KiB
3.2 KiB
PROBLEM: Monarch MCP Server SSE Routing and Handshake Failures
Issue Description
The Monarch MCP server, implemented using FastMCP and Starlette, is experiencing persistent 404 Not Found, TypeError, or 400 Bad Request errors when AI agent clients attempt to establish an SSE connection at http://docker.local.ben.io:8070/mcp. While the /health endpoint is functioning correctly, the MCP handshake fails to complete or routes to non-existent endpoints.
Root Cause Analysis (Suspected)
The primary issue stems from the complexity of mounting a FastMCP HTTP application within a Starlette wrapper.
- Endpoint Overlap:
FastMCP.http_app()generates its own internal routing (e.g.,/mcpor/sse). When this app is mounted at/mcpin a parentStarletteapp, the resulting path becomes/mcp/mcp, leading to404errors on the expected/mcppath. - ASGI Signature Mismatches: Manual attempts to bridge the
SseServerTransportwith Starlette routes led toTypeError: 'NoneType' object is not callableor missing positional arguments (receive,send), indicating that the custom endpoint wrappers were not correctly following the ASGI/Starlette interface. - Transport Defaults:
FastMCPdefault transport ("streamable-http") expects specific headers and session IDs that differ from standard MCP SSE implementations, causing400 Bad Request: Missing session IDwhen using generic tools likecurl.
Troubleshooting Steps Taken
- Refactored Auth: Removed
keyringfrom the Docker runtime to prevent headless environment crashes; moved toMONARCH_TOKENenvironment variable. - Infrastructure Validation: Verified port
8070is unique on thelocal.ben.ioserver. - Internal Logic Debugging:
- Attempted mounting
FastMCPat root (/) and sub-paths (/mcp). - Inspected
FastMCPsource code to identify internal attributes (_mcp_server,_lifespan). - Switched from manual
SseServerTransporthandling tomcp.http_app().
- Attempted mounting
- Endpoint Verification:
- Confirmed
/healthreturns200 OK. - Tested
/mcpwithAccept: text/event-stream, which yielded400 Bad Requestor406 Not Acceptabledepending on the mount point.
- Confirmed
- Session Exploration: Investigated OpenCode session storage (
~/.local/share/opencode/storage/session/) to understand how clients identify and connect to projects.
Current State
The server is running in a Komodo stack, healthy on port 8070, and visible at https://monarch-mcp.ext.ben.io. However, the handshake remains broken.
Resolution (Applied)
The fix was to match the working pattern from komodo-mcp-custom:
def create_app():
mcp_app = mcp.http_app()
routes = [
Route("/health", health, methods=["GET"]),
Mount("/", app=mcp_app), # MCP handles /mcp endpoint internally
]
return Starlette(routes=routes, lifespan=mcp_app.lifespan)
app = create_app()
Key changes:
- Mount at root:
Mount("/", app=mcp_app)lets FastMCP handle/mcpinternally - Pass lifespan:
lifespan=mcp_app.lifespanensures proper task group initialization - Separate health route:
/healthis a standalone route in the parent Starlette app