Skip to content

Support stateless MCP Streamable HTTP - #509

Merged
imran-siddique merged 1 commit into
mainfrom
agent/streamable-http-2026
Aug 14, 2026
Merged

Support stateless MCP Streamable HTTP#509
imran-siddique merged 1 commit into
mainfrom
agent/streamable-http-2026

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

Closes #496.

Implements the MCP 2026-07-28 stateless Streamable HTTP request contract for network upstreams: per-request protocol metadata, mirrored headers, JSON and request-scoped SSE responses, and x-mcp-header parameter mirroring. Invalid streams, content types, annotations, and unimplemented WebSocket catalogs fail closed.

The issue comment proposed initialize and Mcp-Session-Id, but those belong to 2025-era compatibility. The 2026-07-28 protocol explicitly removes both; this PR follows the current official contract and documents that decision.

Evidence:

  • focused transport, forwarding, catalog, and conformance tests passed
  • full suite: 1167 passed, 24 skipped
  • Ruff and mypy passed
  • Bandit passed on the new transport module
  • git diff check passed

@imran-siddique
imran-siddique marked this pull request as ready for review August 14, 2026 22:31
@imran-siddique
imran-siddique merged commit 6d2ab32 into main Aug 14, 2026
13 checks passed
@imran-siddique
imran-siddique deleted the agent/streamable-http-2026 branch August 14, 2026 23:32
imran-siddique pushed a commit that referenced this pull request Aug 17, 2026
* fix(mcp): negotiate a handshake-era revision at initialize

#509 replaced the hardcoded `2024-11-05` in the `initialize` result with
`PROTOCOL_VERSION`, addressing "stop hardcoding the downstream protocol
version" from #496. That constant is `2026-07-28`, the revision that removed
`initialize` altogether, so the gateway answered every handshake by naming a
protocol in which the request just made does not exist, and in which each
later request must carry `_meta` plus the mirrored MCP-Protocol-Version /
Mcp-Method headers a handshake-era client has no way to know it should send.

Confirmed against every revision a real client offers: asked 2025-06-18,
2025-03-26 or 2024-11-05, the gateway answered 2026-07-28 in all three cases.

The direction of the swap is the defect. PROTOCOL_VERSION is correct on the
outbound leg, where the gateway is the client and #509 got it right, and wrong
on the inbound one, where reaching `initialize` is itself proof the caller is
handshake-era. Negotiate over _LEGACY_PROTOCOL_VERSIONS instead, echoing the
client's request when the gateway speaks it. A client asking for 2026-07-28 at
a handshake is deliberately not humoured.

server.py no longer imports PROTOCOL_VERSION; #509 added that import solely
for this misuse.

Verified by mutation: reverting only the `initialize` line while keeping the
new constant fails 9 of the 10 new tests. Full unit suite 1076 passed, with
the 8 pre-existing agent_manifest SDK failures unchanged from main.

Signed-off-by: Mohammed Zoheb Shaik <zoheb.shaik7@gmail.com>

* fix(mcp): support 2025-11-25 and reject non-object initialize params

Review feedback on #513.

1. _LEGACY_PROTOCOL_VERSIONS omitted 2025-11-25, the newest revision that still
defines `initialize`. A client offering the latest handshake revision was
therefore answered 2025-06-18. The lifecycle spec requires a server to echo a
requested version it supports, and says a client that does not support the
server's answer SHOULD disconnect, so a needless downgrade is the same class of
defect this branch already fixes, one revision over. 2025-11-25 now heads the
tuple.

Echo and fallback are covered twice over. The parametrized echo test walks
_LEGACY_PROTOCOL_VERSIONS, and two further tests name 2025-11-25 literally:
one that it is echoed rather than downgraded, one that an unknown version falls
back to it. The literal pair matters because a test parametrized over the
constant under test loses its own case when that constant is wrong, which is
exactly the regression being fixed.

2. test_unnegotiable_params_fall_back_to_the_newest_legacy_revision asserted
that array-shaped `initialize` params negotiate successfully, turning an
existing validation gap into an asserted contract. InitializeRequestParams is
an object with required protocolVersion, capabilities and clientInfo, so a
non-object is now rejected with -32600, matching how #500 already rejects
non-object tools/call params. Absent params stays legal and negotiates the
newest revision. The array case moves to a negative test alongside a string
and an integer.

Verified by mutation, each fix independently: dropping 2025-11-25 fails three
tests, re-blessing non-object params fails three more. Full unit suite 1082
passed, with the 8 pre-existing agent_manifest SDK failures unchanged from
main. Ruff and mypy clean.

Signed-off-by: Mohammed Zoheb Shaik <zoheb.shaik7@gmail.com>

---------

Signed-off-by: Mohammed Zoheb Shaik <zoheb.shaik7@gmail.com>
Co-authored-by: Mohammed Zoheb Shaik <zoheb.shaik7@gmail.com>
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.

Check the gateway against the stateless MCP revision (2026-07-28)

1 participant