Skip to content

fix(mcp): SSE JSON-RPC decoding#58

Closed
hunner wants to merge 1 commit into
mainfrom
upstream-mcp-session-bugfixes
Closed

fix(mcp): SSE JSON-RPC decoding#58
hunner wants to merge 1 commit into
mainfrom
upstream-mcp-session-bugfixes

Conversation

@hunner

@hunner hunner commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

This fixes how Atryum handles JSON-RPC responses from upstream MCP servers when those responses are delivered as text/event-stream instead of plain application/json.

  • Preserve upstream SSE response bodies at the transport layer instead of rewriting them as JSON.
  • Decode JSON-RPC payloads from either plain JSON or SSE at the MCP client parsing sites.
  • Cover SSE initialize and missing-session retry responses with focused MCP client tests.

MCP Control Flow

There are two separate MCP legs involved:

Agent / harness
    |
    | MCP request to Atryum
    | initialize / tools/list / tools/call
    v
Atryum MCP server endpoint
    |
    | policy / approval / audit layer
    v
Atryum upstream MCP client     <-- this PR changes this side
    |
    | MCP request to upstream server
    | initialize / tools/list / tools/call
    v
Upstream MCP server

The relevant response path is:

Upstream MCP server
    |
    | response can be:
    |   application/json
    |   text/event-stream with JSON-RPC in data: frames
    v
Atryum upstream MCP client
    |
    | JSON-RPC parsing for initialize / tools/list / tools/call
    v
Atryum result handling

What Was Broken

Before this change, the upstream HTTP transport layer saw Content-Type: text/event-stream, extracted the first data: frame immediately, changed the response internally to look like application/json, and returned only that extracted JSON payload.

That made some current request/response paths work, but it mixed transport handling with JSON-RPC parsing:

Before:
Upstream SSE response
    -> transport layer extracts first data: frame
    -> transport layer pretends response is application/json
    -> JSON-RPC caller unmarshals result.Body directly

That is fragile because generic forwarding and future streaming/task work need to know what the upstream actually returned. It also meant retry and initialize paths could drift from normal response parsing.

How This PR Fixes It

This PR keeps transport and parsing responsibilities separate:

After:
Upstream SSE response
    -> transport layer preserves raw body + content type
    -> JSON-RPC parsing helper unwraps SSE only when a caller needs JSON-RPC
    -> initialize / tools/list / tools/call all use the same decoding path

The new helper decodes JSON-RPC like this:

if Content-Type contains text/event-stream:
    extract the first data: frame and parse that as JSON-RPC
else:
    parse the body directly as JSON-RPC

This keeps the existing one-response semantics for initialize, tools/list, and tools/call, while making the lower-level transport representation honest. Full upstream streaming support is still a larger follow-up; this PR is the narrow correctness fix that makes the current behavior safer.

Tests

  • go test ./...

Added focused MCP client coverage for:

  • SSE JSON-RPC returned from upstream initialize.
  • SSE JSON-RPC returned after a missing-session reinitialize/retry flow.

@hunner hunner marked this pull request as draft June 11, 2026 19:27
@hunner

hunner commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

Partial application of #5. Closing in favor of #59

@hunner hunner closed this Jun 12, 2026
@hunner hunner deleted the upstream-mcp-session-bugfixes branch June 12, 2026 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant