SEP-2575: Make MCP Stateless#2575
Conversation
| server **MUST** return a `Method not found` JSON-RPC error (`-32601`). For HTTP, | ||
| the response status code MUST be `404 Not Found`. | ||
|
|
||
| #### `server/discover` RPC |
There was a problem hiding this comment.
Should we add the ttl to this that @CaitieM20 is proposing in the other SEP?
One thought on my mind is that for older server/newer client compatibility, realistically the client will actually need to remember the server's older protocol version if it wants to avoid an error on every request trying to figure out the compatible version, so we need a way to cache that.
There was a problem hiding this comment.
Agree it should be added somewhere. I wasn't sure which would get merged first.
There was a problem hiding this comment.
FWIW, I was going to suggest the exact same thing. I'd prefer to err on the side of putting it in both places, to make sure it doesn't get missed. :)
43950d9 to
0cc1245
Compare
| export interface RequestMetaObject extends MetaObject { | ||
| progressToken?: ProgressToken; | ||
| + /** | ||
| + * The MCP Protocol Version being used for this request. | ||
| + */ | ||
| + "io.modelcontextprotocol/protocolVersion": string; | ||
| } |
There was a problem hiding this comment.
I wonder if we are overloading _meta here over time. Would we ever add any top level field anymore or shove everything into meta? For me the distinction might be more along the lines of what is a required field by the protocol vs what is an optional field or something provided by an extension. I can't fully point my finger at it, but it feels more right to me, to have a top protocolVersion field instead.
There was a problem hiding this comment.
I think we started with a top-level field, and there was some feedback that we should use _meta instead because of this line in the spec:
Additionally, definitions in the schema may reserve particular names for purpose-specific metadata, as declared in those definitions.
Is protocolVersion the only field you think makes sense at the top level? Or should the others be as well?
This comment was marked as spam.
This comment was marked as spam.
|
I'm sorry, but if I read the So yes, I do understand that you don't need a session, but merely an HTTP connection (for Streamable HTTP transport), but what's the point? I mean, you won't be able to scale an application anyway because the subscription is bound to a specific server instance. Or am I missing something obvious? |
Support [SEP-2575](modelcontextprotocol/modelcontextprotocol#2575) This PR also updates our custom auth error code since it contradicts with some new errors added in this SEP.: ``` // Custom auth error codes UNAUTHORIZED = -401 FORBIDDEN = -403 ``` For stdio transport: * If `_meta.io.modelcontextprotocol/protocolVersion` was provided, it will use the protocol version specified (take precedence over existing version). * Else, it will use the version during initialization method. * If no initialization method was called, it will default to `v2024-11-05` similar to Toolbox's HTTP transport. Toolbox had a custom transport on v2024-11-05 --> "regular" HTTP POST (without SSE), that does not enforce the initialize method For HTTP transport: * Use the version that was specified in header as protocol version. * If it specified the DRAFT version, it will check against `_meta.io.modelcontextprotocol/protocolVersion` to validate.
Support [SEP-2575](modelcontextprotocol/modelcontextprotocol#2575) This PR also updates our custom auth error code since it contradicts with some new errors added in this SEP.: ``` // Custom auth error codes UNAUTHORIZED = -401 FORBIDDEN = -403 ``` For stdio transport: * If `_meta.io.modelcontextprotocol/protocolVersion` was provided, it will use the protocol version specified (take precedence over existing version). * Else, it will use the version during initialization method. * If no initialization method was called, it will default to `v2024-11-05` similar to Toolbox's HTTP transport. Toolbox had a custom transport on v2024-11-05 --> "regular" HTTP POST (without SSE), that does not enforce the initialize method For HTTP transport: * Use the version that was specified in header as protocol version. * If it specified the DRAFT version, it will check against `_meta.io.modelcontextprotocol/protocolVersion` to validate.
## Motivation and Context SEP-2575 (modelcontextprotocol/modelcontextprotocol#2575, merged for the 2026-07-28 spec release) makes MCP stateless, removing the `initialize`/`notifications/initialized` handshake in favor of per-request `_meta`-carried negotiation. The full lifecycle rewrite is unmerged in both the TypeScript SDK (the closed #2063/#2064/#2251 prototype stack, on hold since 2026-06-08) and the Python SDK, but two pieces of its wire contract stayed stable across every prototype iteration and are purely additive: - The `server/discover` method, which servers MUST implement: sessionless capability discovery returning `supportedVersions`, `capabilities`, `serverInfo`, and `instructions`. The new `Server#discover` handler reuses the same data `init` already builds, but is state-free and idempotent: it stores no client info, never marks a session initialized, and responds regardless of declared capabilities or initialization state (added to the no-capability group in `Methods.ensure_capability!`). `serverInfo` is returned unfiltered because discovery precedes version negotiation; the draft's `ttlMs`/`cacheScope` fields on `DiscoverResult` are left to the SEP-2549 work. On `StreamableHTTPTransport`, a `server/discover` POST is exempt from the `Mcp-Session-Id` requirement and the `MCP-Protocol-Version` header check, like `initialize`, so clients can probe a server with a bare POST in both stateful and stateless modes. - The error code vocabulary: the new `MCP::ErrorCodes` module exports `MISSING_REQUIRED_CLIENT_CAPABILITY = -32003` and `UNSUPPORTED_PROTOCOL_VERSION = -32004`. Constants only; nothing raises them yet (that belongs to the 2026-07-28 lifecycle work), and the existing inline `-32042` of `URLElicitationRequiredError` is intentionally left untouched. The removal of `initialize`, `ping`, and `logging/setLevel` that SEP-2575 also specifies is deliberately NOT implemented: the SDK targets protocol versions up to 2025-11-25, where those methods are required. Part of #389. ## How Has This Been Tested? - `test/mcp/server_test.rb`: `server/discover` returns the supported versions, the server's capabilities, server info, and instructions; it responds before `initialize` and with empty capabilities; it does not mark the session initialized (a subsequent `initialize` still succeeds); `instructions` is omitted when the server has none; and `define_custom_method(method_name: "server/discover")` now raises `MethodAlreadyDefinedError`. - `test/mcp/methods_test.rb`: `ensure_capability!` does not raise for `SERVER_DISCOVER` with empty capabilities. - `test/mcp/server/transports/streamable_http_transport_test.rb`: a `server/discover` POST without a session ID returns 200 with the discover result in both stateful and stateless modes, without issuing an `Mcp-Session-Id`. - New `test/mcp/error_codes_test.rb` pins the -32003/-32004 values. `bundle exec rake` (tests, RuboCop, and conformance baseline) passes. ## Breaking Changes None at the wire level for existing clients. One narrow API consequence: servers that previously registered their own `"server/discover"` handler via `define_custom_method` now get `MethodAlreadyDefinedError` because the method is built in.
Summary
This PR introduces a SEP for making MCP stateless-by-default, migrated from the original issue discussion at #1442 into the PR-based SEP format per SEP-1850.
Motivation
The current MCP specification requires a mandatory initialization handshake that establishes persistent session state. This creates significant challenges for scalability (load balancing requires sticky sessions), resilience (server failure loses session state), and implementation complexity (both client and server must manage session lifecycles).
What This SEP Proposes
This SEP removes the initialization handshake and replaces it with discrete, stateless alternatives following a "pay as you go" model:
MCP-Protocol-VersionHTTP header and_metafield, with version negotiation throughUnsupportedVersionErrorresponsesserver/discoverRPC — optional discovery endpoint for server capabilities, supported versions, and metadata, replacing the capability exchange from initialization_metaper-request instead of negotiating once at connection timemessages/listenRPC — dedicated endpoint for client-initiated streaming, replacing the existing GET endpoint behavior for Streamable HTTPRelationship to Other SEPs
This SEP focuses on removing the initialization handshake and providing stateless alternatives. Session-related concerns are handled separately:
The content of this SEP will be updated in subsequent commits to reflect these decisions and remove session-related material that is now covered by those SEPs.
Fixes #1442