Files
monarch-mcp-custom/PROBLEM.md
Ben 1210cbf6d2
All checks were successful
Build and Push Monarch MCP Docker Image / build (push) Successful in 8s
fix: use correct Starlette mount pattern for MCP SSE routing
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
2025-12-24 05:10:32 +00:00

46 lines
3.2 KiB
Markdown

# 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.
1. **Endpoint Overlap:** `FastMCP.http_app()` generates its own internal routing (e.g., `/mcp` or `/sse`). When this app is mounted at `/mcp` in a parent `Starlette` app, the resulting path becomes `/mcp/mcp`, leading to `404` errors on the expected `/mcp` path.
2. **ASGI Signature Mismatches:** Manual attempts to bridge the `SseServerTransport` with Starlette routes led to `TypeError: 'NoneType' object is not callable` or missing positional arguments (`receive`, `send`), indicating that the custom endpoint wrappers were not correctly following the ASGI/Starlette interface.
3. **Transport Defaults:** `FastMCP` default transport ("streamable-http") expects specific headers and session IDs that differ from standard MCP SSE implementations, causing `400 Bad Request: Missing session ID` when using generic tools like `curl`.
## Troubleshooting Steps Taken
1. **Refactored Auth:** Removed `keyring` from the Docker runtime to prevent headless environment crashes; moved to `MONARCH_TOKEN` environment variable.
2. **Infrastructure Validation:** Verified port `8070` is unique on the `local.ben.io` server.
3. **Internal Logic Debugging:**
- Attempted mounting `FastMCP` at root (`/`) and sub-paths (`/mcp`).
- Inspected `FastMCP` source code to identify internal attributes (`_mcp_server`, `_lifespan`).
- Switched from manual `SseServerTransport` handling to `mcp.http_app()`.
4. **Endpoint Verification:**
- Confirmed `/health` returns `200 OK`.
- Tested `/mcp` with `Accept: text/event-stream`, which yielded `400 Bad Request` or `406 Not Acceptable` depending on the mount point.
5. **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`:
```python
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:
1. **Mount at root:** `Mount("/", app=mcp_app)` lets FastMCP handle `/mcp` internally
2. **Pass lifespan:** `lifespan=mcp_app.lifespan` ensures proper task group initialization
3. **Separate health route:** `/health` is a standalone route in the parent Starlette app