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
- Run any stdio MCP server under ToolHive, e.g.
thv run fetch (or context7, github, etc.).
- Find its proxied URL (
thv list), e.g. http://127.0.0.1:PORT/mcp.
- 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
Bug description
The streamable-http proxy writes JSON-RPC responses as SSE frames containing only a
data:line, with noevent:line:pkg/transport/proxy/streamable/streamable_proxy.go:549(success path):and
:533(error path), same pattern.Per the WHATWG SSE spec this is legal (a frame with no
event:field dispatches as amessageevent), so strictly compliant clients cope. But it diverges from every reference MCP server transport, which always writes an explicitevent: message:@modelcontextprotocol/sdkserver/sse.js:event: message\ndata: ...@modelcontextprotocol/sdkserver/webStandardStreamableHttp.js:event: message\nToolHive's own SSE transport (
pkg/transport/ssecommon/sse_common.go:53) already emitsevent: %s, and native streamable-http servers proxied through ToolHive carry the upstreamevent: messageand work everywhere. Only the streamable proxy's own response writer omits it.This breaks clients that assume the common
event: messageconvention. Concretely, the@ai-sdk/mcpHTTP transport only dispatches frames whereevent === "message"and silently drops event-less frames, so itsinitializecall hangs forever against any stdio server fronted by this proxy. Downstream report: stacklok/toolhive-studio#2333.Steps to reproduce
thv run fetch(or context7, github, etc.).thv list), e.g.http://127.0.0.1:PORT/mcp.initializeand inspect the raw SSE bytes:Observed: the stream starts with
d a t a :(noevent:line).Expected behavior
Each JSON-RPC SSE frame is emitted with an explicit event name, matching the reference MCP server transports:
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/mcpdrop the responses and hang.Environment (if relevant)
Additional context
client/streamableHttp.js:if (!event.event || event.event === 'message')), which is why most clients work;@ai-sdk/mcponly handles the explicit form.