Skip to main content

What happened

PEBBLE MCP gateway integrated Traceo (26-tool requirements management server) via HTTP transport protocol instead of subprocess invocation. This required understanding and fixing three critical issues that aren’t documented in FastMCP’s public HTTP documentation.

The three issues

1. Accept Header Incompleteness

Problem: HTTP requests to FastMCP servers fail silently if the Accept header includes only application/json. Root cause: FastMCP uses Server-Sent Events (SSE) for streaming responses. The server checks that both application/json AND text/event-stream are acceptable before responding. Fix: Change the Accept header from:
Accept: application/json
To:
Accept: application/json, text/event-stream
Impact: Without this, the server returns HTTP 415 (Unsupported Media Type) or hangs indefinitely.

2. SSE Response Wrapping

Problem: FastMCP HTTP responses are wrapped in Server-Sent Event format, not raw JSON. Expected response: {"jsonrpc": "2.0", "result": {...}, "id": 1} Actual response:
event: message
data: {"jsonrpc": "2.0", "result": {...}, "id": 1}
Root cause: FastMCP’s HTTP transport uses SSE as its wire protocol for streaming and keepalive. This is architectural, not a bug. Fix: Parse the SSE wrapper. Implementation:
def _parse_sse_response(response_text):
    """Extract JSON from SSE 'data:' line."""
    for line in response_text.strip().split('\n'):
        if line.startswith('data: '):
            return json.loads(line[6:])
    return None
Impact: If not parsed, the client receives garbage JSON and raises json.JSONDecodeError.

3. Session ID Requirement

Problem: After the initial handshake, FastMCP HTTP servers require a mcp-session-id header in every subsequent request. Why it’s needed: HTTP is stateless, but the MCP protocol expects session context. The server assigns a session ID during initialization and validates it on every call. Discovery: This isn’t mentioned in FastMCP docs. Found by examining response headers:
mcp-session-id: 01ARZ3NDEKTSV4RRFFQ69G5FAV
Fix: Capture the session ID from the first response and store it:
self._session_id = response.headers.get('mcp-session-id')
Then include it in all subsequent requests:
headers = {
    'mcp-session-id': self._session_id,
    'Accept': 'application/json, text/event-stream',
    'Content-Type': 'application/json'
}
Impact: Without this header, subsequent requests fail with HTTP 401 or session-not-found errors.

Why this matters

These three issues are not surfaced in error messages. The stack looks like:
  1. Accept header incomplete → HTTP 415 (but some clients retry)
  2. SSE wrapper present → JSON parsing fails silently
  3. Session ID missing → 401 Unauthorized (looks like auth failure, not session failure)
When combining issues, debugging becomes a three-layer onion: the client looks broken, the server looks broken, and the protocol looks broken. None of them are — they’re just undocumented interactions.

Organizational takeaway

When integrating HTTP MCP servers into any platform:
  1. Assume SSE wrapping. Don’t assume raw JSON.
  2. Always include both content types in Accept. Make it a constant.
  3. Capture and validate session headers. Treat HTTP MCP like stateful protocol despite HTTP being stateless.
  4. Log the raw response before parsing. When things fail, the SSE wrapper often reveals the real error nested in the data: line.

Current state

  • PEBBLE: HttpTransport class in src/pebble/infrastructure/mcp/transports/http.py implements all three fixes
  • Traceo: Running on Railway at https://mcp.traceo.cat, health endpoint verified
  • Integration: 16 core tools discovered and tested via HTTP MCP
  • Testing: 11 integration tests passing, all scenarios covered
This learning will prevent 2-3 hours of debugging for the next team integrating HTTP MCP servers into PEBBLE or similar platforms.