[SEP-2575][SEP-2322][SEP-2567] 2026-06 stateless support over StreamableHTTP#2131
Draft
felixweinberger wants to merge 10 commits into
Draft
[SEP-2575][SEP-2322][SEP-2567] 2026-06 stateless support over StreamableHTTP#2131felixweinberger wants to merge 10 commits into
felixweinberger wants to merge 10 commits into
Conversation
|
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
This was referenced May 20, 2026
NEW core/shared/stateless.ts: parseClientMeta, ClientMeta, META_KEYS, STATEFUL_PROTOCOL_VERSIONS, isStatelessProtocolVersion, isStatelessRequest, STATELESS_REMOVED_METHODS, InputRequiredError/isInputRequiredError, DispatchContext, StatelessHandlers, ListenContext, ListenStream. Transport interface gains optional setStatelessHandlers (server-side) and sendAndReceive (client-side). Both unimplemented at this commit; transport routers and Client wiring come in P3. Exported from core internal barrel and core/public. Satisfies: 2575-R1 (per-request _meta keys), 2575-R3 (version classification)
`subscriptions/listen` is the one 2026-06 method that is request->stream rather than request->response, so it lives outside the dispatcher. InMemorySubscriptions: in-memory backend keyed by server-minted UUID (clients on a shared instance cannot collide); wire `_meta.subscriptionId` is `String(request.id)`. Queue cap 256 (evict slow consumers). `resourceSubscriptions` capped at 256 + Set lookup; fail-closed without `onAuthorizeResourceSubscription`. Satisfies: 2575-R6 (subscriptions/listen + ack)
…iptions + statelessHandlers (sectioned)
Server (existing class, extends Protocol) gains a clearly-divided
`// 2026 stateless` section with:
- `subscriptions: SubscriptionBackend` (defaults to InMemorySubscriptions)
- `statelessHandlers(): {dispatch, listen}`
- `_dispatchStateless(req, dctx)` — version check, removed-methods reject,
build ctx, dispatcher.dispatch, default resultType (not for discover)
- `_buildDispatchServerContext` — MRTR throw-then-cache for elicit/sampling/
listRoots, notify stamps subscriptionId (server wins), log severity-gated,
send throws
- `_ondiscover()` — returns supportedVersions/capabilities/serverInfo
- `inputRequiredMiddleware` (module-level) — InputRequiredError → cap-gate
→ InputRequiredResult
`// dual-mode` section: `connect()` override wires setStatelessHandlers;
`send*ListChanged` get `subscriptions.notify(...)` prepended.
`ServerContext` gains `clientCapabilities?` + `mcpReq.listRoots`. Existing
`buildContext` populates them from session state.
Existing session-dependent methods (`createMessage`, `elicitInput`,
`listRoots`, `sendLoggingMessage`, `_oninitialize`, `ping`, etc.) are
unchanged; comment divider documents the per-method 2026 equivalents.
`ProtocolErrorCode.MissingRequiredClientCapability = -32003` added (spec
`MISSING_REQUIRED_CLIENT_CAPABILITY`).
Satisfies: 2575-R2 (discover), 2575-R4 (removed methods), 2575-R5 (per-request
ctx), 2322-R1 (InputRequiredError), 2322-R2 (cap-gate -32003)
Tool wrapper catch checks isInputRequiredError(e) and re-throws so inputRequiredMiddleware translates to InputRequiredResult (otherwise the error would be swallowed into an isError:true CallToolResult and MRTR would not work for McpServer-registered tools). McpServer.sendLoggingMessage JSDoc points to ctx.mcpReq.log() for the both-protocols path. Satisfies: 2322-R2
`statelessHttpHandler(handlers, req, opts)`: POST-only, CT exact-match → 415, bounded streaming reader (never trust Content-Length), batch cap 64, per-request `_meta` validation (presence, stateless-ness, header agreement), `subscriptions/listen` → SSE, dispatch → JSON or SSE per Accept. Explicit 400 for non-request/non-notification messages. `sseResponse` releases listener registration in `finally` (not only via abort). `handleHttp(server, opts)`: host/origin allowlist BEFORE auth callback, IPv6-safe `stripPort` (brackets removed), then `statelessHttpHandler`. `SUPPORTED_PROTOCOL_VERSIONS` gains `DRAFT_PROTOCOL_VERSION` (at the end so `[0]` stays latest-released; 2026 is opted into via discover auto-probe). `ProtocolErrorCode.HeaderMismatch = -32001`. Satisfies: 2575-R7 (HTTP entry), 2575-R8 (per-request _meta validation), 2567-R1 (header/meta agreement)
StreamableHTTPClientTransport.sendAndReceive: async generator over fetch (SSE-parse or JSON body). Auth via _commonHeaders; 401/403 retry left to caller. Self-contained; does not go through Protocol.request(). Satisfies: 2575-R12 (client sendAndReceive contract, HTTP)
streamableHttp server: handleRequest routes by MCP-Protocol-Version header (falls back to body _meta) to statelessHttpHandler; pre-2026 or absent header falls through to handleStatefulRequest (body unchanged, GHSA-345p guard stays inside). Node middleware: setStatelessHandlers forwards to wrapped web-standard transport. Server.connect() already calls transport.setStatelessHandlers?.() (C7). StreamableHTTPClientTransport.sendAndReceive gains opts?.signal (AbortSignal.any with transport-wide controller). Satisfies: 2567-R1 (HTTP), 2575-R7
…ss/subscribe; typed methods route via _send (sectioned) Client (existing class, extends Protocol) gains a `// 2026 stateless` section: - `_isStateless`, `_logLevel` - `_buildMeta()` / `_withMeta()` — namespaced `_meta` from client identity - `_collect(it, opts)` — drain sendAndReceive: progress→onprogress by token, return raw result, throw on JSON-RPC error - `_send(req, schema, opts)` — route via sendAndReceive when stateless, else fall back to `Protocol.request()`. MRTR loop ≤16: on input_required, dispatch each input request via `this.dispatcher.dispatch` (so `_validationMiddleware` runs), accumulate inputResponses + thread requestState, propagate signal - `_negotiate(transport)` — probe server/discover, set `_isStateless` on success, fall through on isFallbackable error (wired to connect() in C13) - `subscribe(filter)` — async generator over subscriptions/listen - `_listChangedLoop` — stateless backing for options.listChanged with debounce `// dual-mode`: typed methods (callTool/listTools/getPrompt/listPrompts/ readResource/listResources/listResourceTemplates/complete) route via `_send`. `setLoggingLevel` stores level for `_buildMeta`, sends legacy RPC when not stateless. `// session-dependent` divider above existing `connect()`/`ping()`/ `subscribeResource()`/`_setupListChangedHandler*` (bodies unchanged). `applyElicitationDefaults` unchanged. Satisfies: 2575-R10 (per-request _meta), 2575-R11 (discover probe), 2575-R12 (sendAndReceive routing), 2322-R3 (MRTR resume loop), 2322-R4 (requestState round-trip)
connect() now probes server/discover via transport.sendAndReceive before the legacy initialize handshake. On success the client enters stateless mode (server identity/capabilities from DiscoverResult, initialize skipped). On MethodNotFound / HTTP 4xx / parse failure it falls through to the legacy initialize (extracted verbatim into _initialize()). _setupListChanged() routes options.listChanged to _listChangedLoop (subscriptions/listen) when stateless, else to the existing notification-handler path. Existing tests that exercise pre-2026 connection-model behavior now need LegacyTestClient (C14).
…atchV2 target NEW test/integration/__fixtures__/testClient.ts: LegacyTestClient — advertises only pre-2026 versions so connect() skips discover probe. NEW statelessAcceptance.test.ts (HTTP scenarios): Server stateless dispatch, SubscriptionBackend, handleHttp, StreamableHTTP zero-change consumer, audit invariants. conformance: extract everythingServerSetup.ts; add everythingServerDispatchV2.ts target wired to run-server-conformance.sh.
dd9e762 to
af76de4
Compare
5b5c538 to
09fc142
Compare
dd9e762 to
af76de4
Compare
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements SEP-2575 (stateless connection model), SEP-2322 (MRTR), and SEP-2567 (per-message routing) over StreamableHTTP.
Server/Clientremain the same classes; each gains a// 2026 statelesssection. New:stateless.ts,subscriptions.ts,statelessHttp.ts,handleHttp.ts,asyncQueue.ts. Transport interface gains optionalsetStatelessHandlers/sendAndReceive.Motivation and Context
2026-06 spec release.
How Has This Been Tested?
pnpm test:all(1367). Conformance vsmodelcontextprotocol/conformance@main: 32 scenarios / 60 checks / 0 failed;server-stateless17/17.Breaking Changes
None to existing API; additive.
@deprecatedJSDoc added to session-dependent top-level methods (still work;ctx.mcpReq.*is the both-protocols path).Types of changes
Checklist