Skip to content

streamable-http proxy omits the SSE event name, breaking spec-lenient MCP clients #5655

Description

@danbarr

Bug description

The streamable-http proxy writes JSON-RPC responses as SSE frames containing only a data: line, with no event: line:

pkg/transport/proxy/streamable/streamable_proxy.go:549 (success path):

if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil {

and :533 (error path), same pattern.

Per the WHATWG SSE spec this is legal (a frame with no event: field dispatches as a message event), so strictly compliant clients cope. But it diverges from every reference MCP server transport, which always writes an explicit event: message:

  • @modelcontextprotocol/sdk server/sse.js: event: message\ndata: ...
  • @modelcontextprotocol/sdk server/webStandardStreamableHttp.js: event: message\n

ToolHive's own SSE transport (pkg/transport/ssecommon/sse_common.go:53) already emits event: %s, and native streamable-http servers proxied through ToolHive carry the upstream event: message and work everywhere. Only the streamable proxy's own response writer omits it.

This breaks clients that assume the common event: message convention. Concretely, the @ai-sdk/mcp HTTP transport only dispatches frames where event === "message" and silently drops event-less frames, so its initialize call hangs forever against any stdio server fronted by this proxy. Downstream report: stacklok/toolhive-studio#2333.

Steps to reproduce

  1. Run any stdio MCP server under ToolHive, e.g. thv run fetch (or context7, github, etc.).
  2. Find its proxied URL (thv list), e.g. http://127.0.0.1:PORT/mcp.
  3. POST an initialize and inspect the raw SSE bytes:
curl -s -X POST http://127.0.0.1:PORT/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"t","version":"1.0"}}}' \
  | od -c | head

Observed: the stream starts with d a t a : (no event: line).

Expected behavior

Each JSON-RPC SSE frame is emitted with an explicit event name, matching the reference MCP server transports:

event: message
data: {"jsonrpc":"2.0",...}

i.e. fmt.Fprintf(w, "event: message\ndata: %s\n\n", data) in both the success and error write paths.

Actual behavior

The proxy emits data:-only frames. Spec-compliant clients (curl, thv mcp list tools, @modelcontextprotocol/sdk) cope, but spec-lenient clients such as @ai-sdk/mcp drop the responses and hang.

Environment (if relevant)

  • OS/version: macOS (darwin/arm64)
  • ToolHive version: v0.31.0

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds-triageIssue needs initial triage by a maintainer

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions