Skip to content

Refactor MCP SSE response boundaries#59

Merged
hunner merged 1 commit into
mainfrom
codex/mcp-transport-rpc-boundaries
Jun 12, 2026
Merged

Refactor MCP SSE response boundaries#59
hunner merged 1 commit into
mainfrom
codex/mcp-transport-rpc-boundaries

Conversation

@hunner

@hunner hunner commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

This refactors Atryum's upstream MCP HTTP handling so raw transport, JSON-RPC message extraction, and downstream response negotiation are separate responsibilities.

The change keeps the narrow SSE decoding fix, but addresses the architectural issue behind the review feedback: preserving upstream SSE is correct at the transport layer, but Atryum still needs to parse MCP messages correctly and choose a downstream response shape that the calling harness can understand.

Previous Shape: Conflated Responsibilities

Previously doHTTPEnvelope mixed transport and JSON-RPC parsing behavior:

Upstream MCP server
    |
    | HTTP response
    | application/json OR text/event-stream
    v
Atryum doHTTPEnvelope
    |
    | if SSE:
    |   read first data: line
    |   rewrite body as JSON
    |   rewrite Content-Type as application/json
    v
Callers
    |
    | initialize / tools/list / tools/call unmarshal result.Body directly
    | generic proxy forwards Content-Type/body directly
    v
Downstream harness

That made some current paths work, but hid two contracts inside one helper:

Transport contract:
  what did the upstream HTTP server actually send?

JSON-RPC contract:
  which JSON-RPC response message corresponds to my request?

It also meant a future change that preserved raw SSE at transport level could accidentally leak raw SSE to downstream clients that only asked for JSON.

New Shape: Explicit Boundaries

This PR splits those responsibilities:

                  ┌────────────────────────────┐
                  │ Raw upstream transport     │
                  │ status/body/content-type   │
                  └─────────────┬──────────────┘
                                │
              ┌─────────────────┴─────────────────┐
              │                                   │
              v                                   v
┌─────────────────────────────┐     ┌─────────────────────────────┐
│ JSON-RPC response decoder   │     │ Downstream proxy bridge     │
│ - handles application/json  │     │ - honors downstream Accept  │
│ - handles SSE events        │     │ - passes SSE only when ok   │
│ - joins multiline data      │     │ - otherwise collapses JSON  │
│ - skips notifications       │     └─────────────────────────────┘
│ - matches response id       │
└─────────────────────────────┘
              │
              v
┌─────────────────────────────┐
│ Atryum internal MCP logic   │
│ initialize/tools/list/call  │
└─────────────────────────────┘

Behavior Changes

  • ForwardResult now preserves upstream SSE body and content type as raw transport data.
  • initialize, tools/list, and tools/call decode JSON-RPC through one MCP response decoder instead of directly unmarshalling result.Body.
  • The SSE decoder no longer assumes the first data: line is the response:
    • it skips notification events
    • it joins multi-line data: fields
    • it prefers the response with the expected JSON-RPC request id
  • Upstream initialize HTTP errors with SSE JSON-RPC bodies now surface the underlying error message instead of SSE framing text.
  • Generic proxy forwarding now negotiates the downstream shape:
    • downstream Accept: text/event-stream gets upstream SSE preserved
    • downstream clients that do not explicitly accept SSE get the matching JSON-RPC response collapsed to application/json

What This Is Not

This is not full upstream streaming support. Atryum still consumes one matching JSON-RPC response for current request/response operations. The new structure is meant to make full streaming a cleaner follow-up by avoiding transport-layer lies and giving JSON-RPC extraction a real boundary.

Validation

  • go test ./...
  • ./integrations/scripts/agent_harness_integration_tests.sh run --harness fake-agent --auth no-auth --target calculator
  • ./integrations/scripts/agent_harness_integration_tests.sh run --harness fake-agent --auth no-auth --target calc-mcp

@hunner hunner force-pushed the codex/mcp-transport-rpc-boundaries branch from 897371d to f8e7f23 Compare June 12, 2026 17:38
@hunner hunner marked this pull request as ready for review June 12, 2026 17:39
@hunner hunner merged commit 51ea629 into main Jun 12, 2026
3 checks passed
@hunner hunner deleted the codex/mcp-transport-rpc-boundaries branch June 12, 2026 18:19
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