Skip to content

Align Streamable HTTP transport with the 2025-06-18/2025-11-25 spec - #34

Merged
dbernheisel merged 1 commit into
dbernheisel:mainfrom
satoren:fix/streamable-http-compliance
Oct 5, 2026
Merged

dbernheisel merged 1 commit into
dbernheisel:mainfrom
satoren:fix/streamable-http-compliance

Conversation

@satoren

@satoren satoren commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Rebased onto 0.6.0. Fixes places where Phantom.Plug deviates from the Streamable HTTP transport for the protocol versions before 2026-07-28. 2026-07-28 requests keep their own validation and are not affected.

Change Spec
New Phantom.Plug option validate_session: true: answer 404 to a mcp-session-id the server did not issue in an initialize response, or that was terminated with DELETE The server MUST respond 404 to a session ID it does not know, so the client initializes again.
Reject an unsupported MCP-Protocol-Version header with 400. The error lists the supported and requested versions. The server MUST respond 400 to an invalid or unsupported version. A missing header is still accepted, since clients before 2025-06-18 don't send it.
Respond 200 instead of 202 when a GET opens an SSE stream 202 Accepted is for notifications and responses with no body.
Respond 406 to a GET whose Accept excludes text/event-stream. A missing Accept, text/*, or */* is still allowed. The client MUST list text/event-stream in Accept.
Accept a client's JSON-RPC error response (e.g. to elicitation/create) with 202; the waiting Session.elicit/3 returns :error. Also over stdio. Responses are accepted with 202. It used to be answered with -32600 and the elicitation waited until it timed out.
Respond 400 to an invalid JSON-RPC response instead of 202 The server MUST return an HTTP error status if it cannot accept the input.
Add DELETE to access-control-allow-methods Browser clients terminate sessions with DELETE.
Stop using the JSON-RPC id as the SSE id: field Event IDs MUST be globally unique within a session; JSON-RPC ids are chosen by the client and can repeat. Streams aren't resumable, so the field is omitted (2026-07-28 already omitted it).

Why validate_session is opt-in

Phantom only records sessions in memory (Phantom.SessionMeta), so a restart forgets them. The test app's Test.SessionStore shows apps persisting sessions and answering {:not_found, _} from connect/2; checking by default would break that. With the option on:

  • initialize and requests without a session id are not checked.
  • With a pubsub, a miss is retried briefly (5 × 20ms) so a session initialized on another node isn't rejected before replication.
  • Without the SessionMeta table (an escript), sessions are not checked.

Behavior changes

These used to succeed and now return errors (conforming clients are not affected):

  • requests with an unsupported MCP-Protocol-Version
  • GET requests that only accept application/json
  • malformed JSON-RPC responses

Not addressed here

  • A missing mcp-session-id on a non-initialize request is still accepted (the spec's 400 is a SHOULD for servers that require sessions).
  • Protocol version negotiation in initialize, logging/setLevel validation, listChanged, and tool input validation errors are outside the transport and left for separate PRs.

Testing

  • New tests in test/phantom/plug_test.exs, test/phantom/sse_headers_test.exs, and test/phantom/stdio_test.exs, written first and confirmed failing before the fix. The GET status and SSE body are checked over a real HTTP connection.
  • mix test: 495 passed. mix test --only conformance: 10 passed (2025-11-25 and 2026-07-28, single and distributed).
  • mix format --check-formatted and mix compile --warnings-as-errors are clean.

🤖 Generated with Claude Code

@satoren satoren closed this Oct 4, 2026
@satoren satoren reopened this Oct 5, 2026
For the protocol versions before 2026-07-28:

- Add `validate_session: true` to answer 404 to session ids the server
  did not issue or that were terminated. Off by default, since apps that
  persist sessions answer 404 from connect/2.
- Reject an unsupported MCP-Protocol-Version header with 400.
- Open GET SSE streams with 200 instead of 202.
- Answer 406 to a GET whose Accept header excludes text/event-stream.
- Accept a client's JSON-RPC error response with 202 and fail the
  waiting elicitation with :error (HTTP and stdio).
- Answer 400 to an invalid JSON-RPC response instead of 202.
- Allow DELETE in CORS allow-methods.
- Stop using JSON-RPC ids as SSE event ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@satoren
satoren force-pushed the fix/streamable-http-compliance branch from f9d3fc3 to 81ee0a3 Compare October 5, 2026 00:36
@dbernheisel
dbernheisel merged commit a0b7438 into dbernheisel:main Oct 5, 2026
1 of 2 checks passed
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.

2 participants