Skip to content

[SEP-2575][SEP-2322][SEP-2567] 2026-06 stateless support over StreamableHTTP#2131

Draft
felixweinberger wants to merge 10 commits into
fweinberger/v2-dispatcherfrom
fweinberger/v2-http-stateless
Draft

[SEP-2575][SEP-2322][SEP-2567] 2026-06 stateless support over StreamableHTTP#2131
felixweinberger wants to merge 10 commits into
fweinberger/v2-dispatcherfrom
fweinberger/v2-http-stateless

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor

@felixweinberger felixweinberger commented May 20, 2026

2026-06 stateless stack (v2-stateless label):

# PR
1 #2128 tasks-delete (mechanical)
2 #2129 schema-sync (mechanical)
3 #2130 Dispatcher extraction (zero-Δ refactor)
4 #2131 HTTP-stateless (the substantive review)
5 #2132 stdio/InMemory transports (additive)
6 #2133 docs + changeset
7 #2134 LegacyServer/LegacyClient extraction

Implements SEP-2575 (stateless connection model), SEP-2322 (MRTR), and SEP-2567 (per-message routing) over StreamableHTTP.

Server/Client remain the same classes; each gains a // 2026 stateless section. New: stateless.ts, subscriptions.ts, statelessHttp.ts, handleHttp.ts, asyncQueue.ts. Transport interface gains optional setStatelessHandlers/sendAndReceive.

Motivation and Context

2026-06 spec release.

How Has This Been Tested?

pnpm test:all (1367). Conformance vs modelcontextprotocol/conformance@main: 32 scenarios / 60 checks / 0 failed; server-stateless 17/17.

Breaking Changes

None to existing API; additive. @deprecated JSDoc added to session-dependent top-level methods (still work; ctx.mcpReq.* is the both-protocols path).

Types of changes

  • New feature

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 20, 2026

⚠️ No Changeset found

Latest commit: af76de4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 20, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2131

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2131

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2131

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2131

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2131

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2131

commit: af76de4

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2-stateless 2026-06 SDK: Protocol decomposition + SEP alignment (request-first / stateless)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant