From 572113b457c27d1bd34e0f50d5f9eaea909e79ef Mon Sep 17 00:00:00 2001 From: Maciek Kisiel Date: Thu, 30 Apr 2026 15:14:48 +0000 Subject: [PATCH 1/5] Design stateless protocol connection init --- design/stateless.md | 248 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 design/stateless.md diff --git a/design/stateless.md b/design/stateless.md new file mode 100644 index 000000000..e16d98d21 --- /dev/null +++ b/design/stateless.md @@ -0,0 +1,248 @@ +# Stateless MCP for 2026-06-30 protocol version + +This document discusses the changes needed to fully support protocol changes introduced +in `2026-06-30` version. It focuses on the following SEPs: + +* [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322) +* [SEP-2567: Sessionless MCP via Explicit State Handles](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567) +* [SEP-2575: Make MCP Stateless](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575) + +These proposals together define a stateless and sessionless protocol that is largely +incompatible with its previous version. This poses challenges given our commitment to +not introducing backwards incompatible changes to the SDK. + +## Design decisions + +### Stateless mode reuse + +On the server side, there already exists a `Stateless` mode that works similarly to +what the proposals define. We will make it a dependency of the new protocol version +support - only servers that will enable it will be able to support the new version. + +This will simplify the implementation and provide opt-in mechanism for the new behavior. +Such opt-in is needed, because it's not possible to automatically enable the new protocol +version for all SDK users, especially if they are using functionality that is removed +in the new version. `Stateless` mode already disallows many features that were removed. +Any backwards incompatible behavior changes will be guarded by `MCPGODEBUG` flags +to give developers more time to adapt. + +Note: For v2, it would make sense to rename `ClientSession` to avoid confusion. +`ServerSession` should be removed. + +### Keeping the session-based APIs + +Session objects (`ServerSession`/`ClientSession`) are the core part of the Go MCP SDK API. +They are returned from `Connect` functions and they are used to interact with the peer. +Even though SEP-2567 removes the concept of sessions from the protocol, we cannot simply remove +these objects. Instead, we will adjust their lifetime of the server side to be per-request +and attempt to fill the session state on both sides with the most appropriate data available. + +## Connection establishment + +There are several changes affecting how the connection from the client to the server is established. + +### Initialization handshake removal + +> [!WARNING] +> Deprecated APIs: +> - `ServerOptions.InitializedHandler` + +SEP-2575 removes the initialization handshake from the protocol. This means that +we need adjust the way we establish the protocol version and the capabilities of the peer. + +#### Protocol version + +For the server side, the protocol version will come in each request. It will be used to populate +`ServerSession.state.InitializeParams` and used for the call duration. Initialize request with +`2026-06-30` protocol version specified will be rejected. + +For client side, the protocol version must be provided in each request. We cannot depend +on the initialization for provide a negotiated version. Instead, the server returns +`UnsupportedProtocolVersionError` if the version specified by the client is not supported. +The error also contains a list of `supported` versions. If any of them is supported by the client, +then one of two things will happen: + +* the version will be saved to `ClientSession.state.InitializeResult` and used in subsequent requests if it is `>= 2026-06-30` +* normal initialization will be performed if it is `< 2026-06-30` + +New types in `protocol.go`: + +```go +const ErrCodeUnsupportedProtocolVersion = -32602 // INVALID_PARAMS per SEP-2575 + +type UnsupportedProtocolVersionData struct { + Supported []string `json:"supported"` + Requested string `json:"requested"` +} +``` + +The protocol version may also be determined by calling `ServerDiscover` with sufficiently modern +servers. See [Server Capabilities](#server-capabilities) for details. + +#### Server Capabilities + +SEP-2575 introduces `server/discover` as a replacement for capability exchange during initialization. +It returns `supportedVersions`, `capabilities`, `serverInfo`, +and `instructions` — effectively the same information that was in `InitializeResult`, plus +the list of supported protocol versions. + +It will also be available to old clients — it is harmless and +does not require initialization. + +New types in `protocol.go`: + +```go +type DiscoverParams struct { + Meta `json:"_meta,omitempty"` +} + +type DiscoverResult struct { + SupportedVersions []string `json:"supportedVersions"` + Capabilities *ServerCapabilities `json:"capabilities"` + ServerInfo *Implementation `json:"serverInfo"` + Instructions string `json:"instructions,omitempty"` +} +``` + +The handler will be registered with the `missingParamsOK` flag and will be allowed +pre-initialization. +It will be implemented on `Server` (not `ServerSession`) since it is not session-scoped. +The handler will use `Server.capabilities()` and `Server.impl` to construct the response. + +On the client side, `server/discover` will be called during connection establishment to determine the protocol version to be used. +If the server returns `Method not found`, the client knows the server is pre-`2026-06-30` and should +fall back to the initialization handshake. +The capabilities and protocol version from the `server/discover` result will be saved tp `ClientSession.state.InitializeResult`. +This ensures that existing client code that reads `ClientSession.InitializeResult` will continue to work without changes. + +> [!WARNING] +> Deprecated APIs: +> - `ClientSession.InitializeResult` + +#### Client Capabilities + +SEP-2575 moves client capabilities from session-level negotiation to per-request `_meta` fields: + +* `io.modelcontextprotocol/protocolVersion` (string, required) +* `io.modelcontextprotocol/clientInfo` (`Implementation`, required) +* `io.modelcontextprotocol/clientCapabilities` (`ClientCapabilities`, required) +* `io.modelcontextprotocol/logLevel` (`LoggingLevel`, optional) + +On the server side, the stateless session handler will extract these fields from each incoming +request's `_meta` and populate `ServerSession.state.InitializeParams` with the values for the +duration of the call. This ensures that existing server code that reads `ServerSession.InitializeParams()` +continues to work without changes. The extraction will happen in the `ServerSession.handle()` method, +before dispatching to the registered handler. +If a server receives a request with missing `_meta` fields, it will return `INVALID_PARAMS` +(-32602) when operating with `>= 2026-06-30`. + +> [!WARNING] +> Deprecated APIs: +> - `ServerSession.InitializeParams` +> - `ServerOptions.InitializedHandler` + +On the client side, `ClientSession` will automatically populate these `_meta` fields on every +outgoing request when operating with `>= 2026-06-30` protocol version. The values will come from +`ClientOptions` (for `clientInfo` and `clientCapabilities`) and from the negotiated +protocol version. + +### SSE standalone stream removal + +SEP-2575 removes the HTTP GET endpoint used for standalone SSE streaming. In the new protocol +version, GET requests MUST return `405 Method Not Allowed`. This is already the behavior in +`Stateless` mode (`streamable.go:336-342`), so no new server-side changes are needed for +stateless servers. + +The standalone SSE stream was used for two purposes: + +1. Server-to-client notifications outside the context of a request (e.g., `notifications/tools/list_changed`) +2. Server-to-client requests (e.g., `ping`, sampling, elicitation) + +Purpose (1) is replaced by a new `subscriptions/listen` RPC method. +Purpose (2) is replaced by MRTR (see [Changed paradigm for server to client communication](#changed-paradigm-for-server-to-client-communication)). + +On the client side, the streamable client transport currently opens a GET SSE stream after +initialization. For `>= 2026-06-30` protocol versions, this step will be skipped. The client +will instead use `subscriptions/listen` if it needs to receive server-initiated notifications. + +### Ping + +SEP-2575 removes `ping` in both directions. Server-to-client ping is removed because servers +can no longer independently send requests. Client-to-server ping is removed because any normal +RPC call proves server liveness, and transport-layer mechanisms (HTTP keep-alives, SSE comments) +handle connection-health checks. + +Currently, `ping` is used in two ways: + +1. Explicit calls via `ServerSession.Ping()` / `ClientSession.Ping()` (`server.go:1167`, `client.go:966`) +2. Keepalive mechanism via `startKeepalive()` (`shared.go:577-622`) + +For `>= 2026-06-30`: + +* Sending a `ping` request will return `Method not found` (-32601). +* `ServerSession.Ping()` will return an error for stateless sessions (already the case: + `streamable.go:1367-1369`). +* `ClientSession.Ping()` will continue to exist for backwards compatibility but will return + an error if the server doesn't support it (`ErrMethodNotFound` from keepalive is already + handled gracefully — `shared.go:607-610`). +* The keepalive mechanism will be disabled for stateless sessions. Currently, keepalive starts + even for stateless sessions and races with `defer session.Close()` (`streamable_test.go:366-374`). + For the new protocol version, `Server.Connect()` should skip `startKeepalive` when operating + in stateless mode. + +Removal will be gated by MCPGODEBUG flag `mcpping` defaulting to `mcpping=enabled` (keep the +method) initially, switching to `mcpping=disabled` to remove it. The keepalive graceful +degradation (`ErrMethodNotFound` → silently stop) means that even without the flag, clients +connecting to new servers will work correctly. + +> [!WARNING] +> Deprecated APIs: +> - `ClientSession.Ping` +> - `ServerSession.Ping` + + +### SDK connection establishment flow changes + +#### Original flow + +```mermaid +sequenceDiagram + participant c as Client SDK + participant s as Server SDK + + Note over c: Client.Connect() + c->>s: initialize {"protocolVersion": "2025-11-25"} + s->>c: initialized + c->>s: notifications/initialized + c->>s: hanging standalone SSE stream + note over c: Client.Connect() finished + note over c,s: Operation +``` + +#### Updated flow + +```mermaid +sequenceDiagram + participant c as Client SDK + participant s as Server SDK + + Note over c: Client.Connect() + c->>s: server/discover + + alt 2026-06-30 version supported + s->>c: {supported: ["2026-06-30"]} + opt if notification handlers configured + c->>s: hanging subscribe/listen + end + note over c: Client.Connect() finished + note over c,s: Operation using 2026-06-30 version + else 2026-06-30 version not supported + s->>c: failed or "2026-06-30" not in supported list + note over c,s: fall back to legacy flow + c->>s: initialize {"protocolVersion": "2025-11-25"} + s->>c: initialized + c->>s: notifications/initialized + c->>s: hanging standalone SSE stream + note over c: Client.Connect() finished + note over c,s: Operation using 2025-11-25 version + end From f497c241faf1276892458951d6007293f1ccabd5 Mon Sep 17 00:00:00 2001 From: Maciek Kisiel Date: Mon, 4 May 2026 14:19:31 +0000 Subject: [PATCH 2/5] improvements to the text --- design/stateless.md | 127 +++++++++++++++++++++++++++++++++----------- 1 file changed, 97 insertions(+), 30 deletions(-) diff --git a/design/stateless.md b/design/stateless.md index e16d98d21..38be73a08 100644 --- a/design/stateless.md +++ b/design/stateless.md @@ -9,7 +9,7 @@ in `2026-06-30` version. It focuses on the following SEPs: These proposals together define a stateless and sessionless protocol that is largely incompatible with its previous version. This poses challenges given our commitment to -not introducing backwards incompatible changes to the SDK. +not introduce backwards incompatible API changes to the SDK. ## Design decisions @@ -22,11 +22,13 @@ support - only servers that will enable it will be able to support the new versi This will simplify the implementation and provide opt-in mechanism for the new behavior. Such opt-in is needed, because it's not possible to automatically enable the new protocol version for all SDK users, especially if they are using functionality that is removed -in the new version. `Stateless` mode already disallows many features that were removed. +in it. `Stateless` mode already disallows many such features that are removed. Any backwards incompatible behavior changes will be guarded by `MCPGODEBUG` flags to give developers more time to adapt. -Note: For v2, it would make sense to rename `ClientSession` to avoid confusion. +> [!NOTE] +> Deprecated APIs: +> For v2, it would make sense to rename `ClientSession` to avoid confusion. `ServerSession` should be removed. ### Keeping the session-based APIs @@ -53,8 +55,11 @@ we need adjust the way we establish the protocol version and the capabilities of #### Protocol version For the server side, the protocol version will come in each request. It will be used to populate -`ServerSession.state.InitializeParams` and used for the call duration. Initialize request with -`2026-06-30` protocol version specified will be rejected. +`ServerSession.state.InitializeParams` and used for the call duration. An `initialize` request with +`2026-06-30` protocol version specified will be rejected with `Method not found` (-32601), since the +initialization handshake does not exist in the new protocol version. + +TODO: Consider whether servers should be allowed to limit the protocol versions they advertise as supported. For client side, the protocol version must be provided in each request. We cannot depend on the initialization for provide a negotiated version. Instead, the server returns @@ -82,7 +87,7 @@ servers. See [Server Capabilities](#server-capabilities) for details. #### Server Capabilities SEP-2575 introduces `server/discover` as a replacement for capability exchange during initialization. -It returns `supportedVersions`, `capabilities`, `serverInfo`, +It will return `supportedVersions`, `capabilities`, `serverInfo`, and `instructions` — effectively the same information that was in `InitializeResult`, plus the list of supported protocol versions. @@ -108,12 +113,25 @@ The handler will be registered with the `missingParamsOK` flag and will be allow pre-initialization. It will be implemented on `Server` (not `ServerSession`) since it is not session-scoped. The handler will use `Server.capabilities()` and `Server.impl` to construct the response. - -On the client side, `server/discover` will be called during connection establishment to determine the protocol version to be used. -If the server returns `Method not found`, the client knows the server is pre-`2026-06-30` and should -fall back to the initialization handshake. -The capabilities and protocol version from the `server/discover` result will be saved tp `ClientSession.state.InitializeResult`. -This ensures that existing client code that reads `ClientSession.InitializeResult` will continue to work without changes. +The `2026-06-30` protocol version will only be returned as supported if the server runs in +the `Stateless` mode. + +On the client side, `server/discover` will be called during connection establishment to determine +the protocol version to be used. The client will send `server/discover` with `2026-06-30` as the +protocol version in both the `MCP-Protocol-Version` header and the `_meta` payload. The full +per-request `_meta` fields (`clientInfo`, `clientCapabilities`) will also be included, using the +values from `ClientOptions`. This means the transport's `setMCPHeaders` must be able to set the +`MCP-Protocol-Version` header before `initializedResult` is populated. + +If the server returns `Method not found` or `UnsupportedProtocolVersionError`, the client knows +the server is pre-`2026-06-30` and should fall back to the initialization handshake. +This will result in an additional round-trip for older servers. + +The capabilities and protocol version from the `server/discover` result will be saved to +`ClientSession.state.InitializeResult`, with `ProtocolVersion` set to the latest version from +`DiscoverResult.SupportedVersions` that the client also supports. +This ensures that existing client code that reads `ClientSession.InitializeResult` will continue +to work without changes. > [!WARNING] > Deprecated APIs: @@ -131,7 +149,7 @@ SEP-2575 moves client capabilities from session-level negotiation to per-request On the server side, the stateless session handler will extract these fields from each incoming request's `_meta` and populate `ServerSession.state.InitializeParams` with the values for the duration of the call. This ensures that existing server code that reads `ServerSession.InitializeParams()` -continues to work without changes. The extraction will happen in the `ServerSession.handle()` method, +will continue to work without changes. The extraction will happen in the `ServerSession.handle()` method, before dispatching to the registered handler. If a server receives a request with missing `_meta` fields, it will return `INVALID_PARAMS` (-32602) when operating with `>= 2026-06-30`. @@ -158,12 +176,31 @@ The standalone SSE stream was used for two purposes: 1. Server-to-client notifications outside the context of a request (e.g., `notifications/tools/list_changed`) 2. Server-to-client requests (e.g., `ping`, sampling, elicitation) -Purpose (1) is replaced by a new `subscriptions/listen` RPC method. -Purpose (2) is replaced by MRTR (see [Changed paradigm for server to client communication](#changed-paradigm-for-server-to-client-communication)). +Purpose (1) will be replaced by a new `subscriptions/listen` RPC method. +Purpose (2) will be replaced by MRTR ([SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)). On the client side, the streamable client transport currently opens a GET SSE stream after initialization. For `>= 2026-06-30` protocol versions, this step will be skipped. The client -will instead use `subscriptions/listen` if it needs to receive server-initiated notifications. +will instead send `subscriptions/listen` during `Client.Connect()` if any of the following +`ClientOptions` handlers are configured: + +* `ToolListChangedHandler` → subscribes to `toolsListChanged` +* `PromptListChangedHandler` → subscribes to `promptsListChanged` +* `ResourceListChangedHandler` → subscribes to `resourcesListChanged` +* `LoggingMessageHandler` → subscribes with `logLevel` + +If none of these handlers are set, no `subscriptions/listen` call will be made during `Connect()`. +Presence of `ResourceUpdateHandler` will not automatically trigger `subscriptions/listen` during +`Connect()`, as Resource notifications require URIs to be specified. Instead, each call to +`ClientSession.Subscribe()` will start a new `subscriptions/listen` stream for the specified +Resource. Calling `ClientSession.Unsubscribe()` will cancel that stream. +On the server side, the server will return `Method not found` (-32601) for `resource/subscribe` and +`resource/unsubscribe` when `>= 2026-06-30`. + +> [!WARNING] +> Deprecated APIs: +> - `ClientSession.Subscribe` +> - `ClientSession.Unsubscribe` ### Ping @@ -190,20 +227,14 @@ For `>= 2026-06-30`: For the new protocol version, `Server.Connect()` should skip `startKeepalive` when operating in stateless mode. -Removal will be gated by MCPGODEBUG flag `mcpping` defaulting to `mcpping=enabled` (keep the -method) initially, switching to `mcpping=disabled` to remove it. The keepalive graceful -degradation (`ErrMethodNotFound` → silently stop) means that even without the flag, clients -connecting to new servers will work correctly. - > [!WARNING] > Deprecated APIs: > - `ClientSession.Ping` > - `ServerSession.Ping` - ### SDK connection establishment flow changes -#### Original flow +#### Original flow (HTTP) ```mermaid sequenceDiagram @@ -214,12 +245,47 @@ sequenceDiagram c->>s: initialize {"protocolVersion": "2025-11-25"} s->>c: initialized c->>s: notifications/initialized - c->>s: hanging standalone SSE stream + c->>s: hanging standalone SSE stream (GET) note over c: Client.Connect() finished note over c,s: Operation ``` -#### Updated flow +#### Updated flow (HTTP) + +```mermaid +sequenceDiagram + participant c as Client SDK + participant s as Server SDK + + Note over c: Client.Connect() + c->>s: server/discover (MCP-Protocol-Version: 2026-06-30) + + alt 2026-06-30 version supported + s->>c: {supportedVersions, capabilities, serverInfo, instructions} + opt if notification handlers configured + c->>s: subscriptions/listen (POST, hanging SSE) + s->>c: notifications/subscriptions/acknowledged + end + note over c: Client.Connect() finished + note over c,s: Operation using 2026-06-30 version + else 2026-06-30 version not supported + s->>c: Method not found / UnsupportedProtocolVersionError + note over c,s: fall back to legacy flow + c->>s: initialize {"protocolVersion": "2025-11-25"} + s->>c: initialized + c->>s: notifications/initialized + c->>s: hanging standalone SSE stream (GET) + note over c: Client.Connect() finished + note over c,s: Operation using 2025-11-25 version + end +``` + +#### Updated flow (STDIO) + +STDIO follows the same logic as HTTP but without HTTP-specific mechanisms (headers, SSE streams). +The protocol version is conveyed only via the `_meta` field. The SEP recommends using +`server/discover` for STDIO to determine the server's version, since there is no per-request +HTTP error status to detect version mismatches. ```mermaid sequenceDiagram @@ -227,22 +293,23 @@ sequenceDiagram participant s as Server SDK Note over c: Client.Connect() - c->>s: server/discover + c->>s: server/discover {_meta: {protocolVersion: "2026-06-30"}} alt 2026-06-30 version supported - s->>c: {supported: ["2026-06-30"]} + s->>c: {supportedVersions, capabilities, serverInfo, instructions} opt if notification handlers configured - c->>s: hanging subscribe/listen + c->>s: subscriptions/listen + s->>c: notifications/subscriptions/acknowledged end note over c: Client.Connect() finished note over c,s: Operation using 2026-06-30 version else 2026-06-30 version not supported - s->>c: failed or "2026-06-30" not in supported list + s->>c: Method not found / UnsupportedProtocolVersionError note over c,s: fall back to legacy flow c->>s: initialize {"protocolVersion": "2025-11-25"} s->>c: initialized c->>s: notifications/initialized - c->>s: hanging standalone SSE stream note over c: Client.Connect() finished note over c,s: Operation using 2025-11-25 version end +``` \ No newline at end of file From 2d9221d441075a86a12c90dd1d11e81eedb954b2 Mon Sep 17 00:00:00 2001 From: Maciek Kisiel Date: Wed, 6 May 2026 15:35:23 +0000 Subject: [PATCH 3/5] describe removal of sessions and resumability --- design/stateless.md | 85 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/design/stateless.md b/design/stateless.md index 38be73a08..e2f519b23 100644 --- a/design/stateless.md +++ b/design/stateless.md @@ -3,7 +3,6 @@ This document discusses the changes needed to fully support protocol changes introduced in `2026-06-30` version. It focuses on the following SEPs: -* [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322) * [SEP-2567: Sessionless MCP via Explicit State Handles](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567) * [SEP-2575: Make MCP Stateless](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575) @@ -201,6 +200,7 @@ On the server side, the server will return `Method not found` (-32601) for `reso > Deprecated APIs: > - `ClientSession.Subscribe` > - `ClientSession.Unsubscribe` +> - `StreamableClientTransport.DisableStandaloneSSE` ### Ping @@ -231,6 +231,7 @@ For `>= 2026-06-30`: > Deprecated APIs: > - `ClientSession.Ping` > - `ServerSession.Ping` +> - `ServerOptions.Keepalive` ### SDK connection establishment flow changes @@ -312,4 +313,84 @@ sequenceDiagram note over c: Client.Connect() finished note over c,s: Operation using 2025-11-25 version end -``` \ No newline at end of file +``` + +## Removing sessions + +SEP 2567 removes the concept of sessions from the MCP protocol. Some adjustments will be +needed to the `Stateless` mode to fully align it with the proposal. + +### `MCP-Session-Id` header removal + +Server in stateless mode will ignore the `MCP-Session-Id` header in the requests. +Currently it doesn't reject requests that specify this header, but it reads the +value and saves it in the transport to be used in other places. +The `ServerOptions.GetSessionID` will not be called by servers in the stateless +mode anymore. The `MCP-Session-Id` will also not be set on the response to +`initialize`, but this is a consequence of removal of the initialization handshake. + +These behavior changes are backwards incompatible and will be accompanied by a `MCPGODEBUG` flag. + +> [!WARNING] +> Deprecated APIs: +> - `ServerOptions.GetSessionID` +> - `StreamableHTTPOptions.SessionTimeout` +> - `StreamableServerTransport.SessionID` +> - `Connection.SessionID` +> - `ErrSessionMissing` +> - `ServerSessionState` +> - `Server.Sessions` +> - `Session.ID` + +### DELETE method returns 405 + +The `DELETE` will return `405 Method Not Allowed` for all requests to the server in stateless mode. +This is in line with SEP-2575 that only `POST` requests should be supported. Currently it returns +`400 Bad Request` if `MCP-Session-Id` header is not provided and `204 No Content` otherwise. +The change will be protected by the same `MCPGODEBUG` flag as removal of the session related header. + +## Removing stream resumability + +SEP-2575 removes stream resumability. All related functionality will be deprecated. + +For `>= 2026-06-30`, the following behavioral changes will apply: + +* **Event IDs on SSE streams**: The server will stop writing event IDs (`id:` fields) to SSE + response streams. Currently, event IDs are only written when an `EventStore` is configured + (`streamable.go:1456-1458`). For the new protocol version, event IDs will not be written + regardless of whether an `EventStore` is provided. + +* **EventStore usage**: The server will not call `EventStore.Open`, `EventStore.Append`, or + `EventStore.SessionClosed` for `>= 2026-06-30` requests, even if an `EventStore` is configured + on the handler. Currently, the `EventStore` is called unconditionally when present + (`streamable.go:864-866`, `1446-1448`), including in stateless mode, which leads to + wasteful write-then-immediately-cleanup cycles. For the new protocol version, the `EventStore` + will be ignored entirely. + +* **`Last-Event-ID` on POST requests**: The server currently rejects POST requests that include + a `Last-Event-ID` header with `400 Bad Request` (`streamable.go:1100-1101`). This behavior + will remain unchanged — `Last-Event-ID` on POST was never valid and will continue to be + rejected. + +* **Client reconnection**: The client will stop sending `Last-Event-ID` on reconnection + attempts (`streamable.go:2207-2208`) and will stop capturing event IDs from SSE streams + (`streamable.go:2093-2094`). Reconnection for `subscriptions/listen` streams will still + be supported (re-establishing the subscription from scratch), but without `Last-Event-ID` + based replay. + +* **`CloseSSEStream` callback**: Calling `RequestExtra.CloseSSEStream` will be a no-op for + `>= 2026-06-30`. This mechanism (SEP-1699) was designed to trigger client reconnection + with `Last-Event-ID` for server-initiated disconnect scenarios. Without resumability, closing + the SSE stream is equivalent to cancelling the request, not triggering a reconnection. + +> [!WARNING] +> Deprecated APIs: +> - `EventStore` +> - `MemoryEventStore` +> - `MemoryEventStoreOptions` +> - `NewMemoryEventStore` +> - `ErrEventsPurged` +> - `StreamableHTTPOptions.EventStore` +> - `StreamableServerTransport.EventStore` +> - `RequestExtra.CloseSSEStream` +> - `CloseSSEStreamArgs` \ No newline at end of file From 3a8568ddc9b77c01427a5ae8aca27302da3e0eb8 Mon Sep 17 00:00:00 2001 From: Maciek Kisiel Date: Thu, 14 May 2026 10:18:13 +0000 Subject: [PATCH 4/5] Update the design to take a more holistic approach. --- design/stateless.md | 166 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 131 insertions(+), 35 deletions(-) diff --git a/design/stateless.md b/design/stateless.md index e2f519b23..7455b57e0 100644 --- a/design/stateless.md +++ b/design/stateless.md @@ -14,9 +14,12 @@ not introduce backwards incompatible API changes to the SDK. ### Stateless mode reuse -On the server side, there already exists a `Stateless` mode that works similarly to -what the proposals define. We will make it a dependency of the new protocol version -support - only servers that will enable it will be able to support the new version. +On the HTTP server side, there already exists a `StreamableHTTPOptions.Stateless` mode that +works similarly to what the proposals define. We will make it a dependency of the new protocol +version support for the HTTP transport — only HTTP servers that enable it will be able to +serve `>= 2026-06-30` requests. For STDIO and other transports, no transport-level option is +needed; protocol-level behavior is driven by per-request detection (see +[Per-request protocol detection](#per-request-protocol-detection)). This will simplify the implementation and provide opt-in mechanism for the new behavior. Such opt-in is needed, because it's not possible to automatically enable the new protocol @@ -38,6 +41,55 @@ Even though SEP-2567 removes the concept of sessions from the protocol, we canno these objects. Instead, we will adjust their lifetime of the server side to be per-request and attempt to fill the session state on both sides with the most appropriate data available. +### Per-request protocol detection + +The presence of `io.modelcontextprotocol/protocolVersion` in a request's `_meta` implicitly +signals that the request follows the new protocol. No explicit flag is needed at the protocol +level — `ServerSession.handle()` detects the field and adjusts behavior per-request. + +The detection mechanism works as follows: + +1. Before the initialization gate, `ServerSession.handle()` performs a lightweight partial + unmarshal of `_meta` from the raw `jreq.Params`: + + ```go + var meta struct { + Meta Meta `json:"_meta"` + } + internaljson.Unmarshal(jreq.Params, &meta) + ``` + +2. If `io.modelcontextprotocol/protocolVersion` is present in `meta.Meta`, the initialization + gate is skipped (the request is allowed regardless of `ServerSession.state.InitializeParams` + being `nil`). Required `_meta` fields (`clientInfo`, `clientCapabilities`) are validated. + +3. New accessor methods on `ServerRequest[P]` provide typed per-request access: + + ```go + func (r *ServerRequest[P]) ProtocolVersion() string + func (r *ServerRequest[P]) ClientCapabilities() *ClientCapabilities + func (r *ServerRequest[P]) ClientInfo() *Implementation + ``` + + These read from `r.Params.GetMeta()` with fallback to `r.Session.InitializeParams()` + for old protocol requests. Since validation already happened in `handle()`, the + accessors assume data is well-formed and do not return errors. + +4. `ServerSession.InitializeParams()` returns `nil` for new-protocol sessions — there is no + pre-population of session state. Returning `nil` is a clear signal to callers that they + should use the `ServerRequest` accessors instead. + +This mechanism is transport-agnostic: it works on HTTP and STDIO identically. A single +session can serve both old and new protocol requests, with each request handled according +to its own `_meta`. For HTTP, `StreamableHTTPOptions.Stateless` remains needed for +transport-level concerns (per-request session lifecycle, HTTP method gating). For STDIO, +no transport-level option is needed. + +The body-peeking and state pre-population in `ephemeralConnectOpts` becomes unnecessary +for new-protocol requests since `handle()` skips the initialization gate based on `_meta`. +Pre-population remains for old-protocol requests on stateless HTTP servers (where the +client skips `initialize` but doesn't send `_meta`). + ## Connection establishment There are several changes affecting how the connection from the client to the server is established. @@ -53,21 +105,21 @@ we need adjust the way we establish the protocol version and the capabilities of #### Protocol version -For the server side, the protocol version will come in each request. It will be used to populate -`ServerSession.state.InitializeParams` and used for the call duration. An `initialize` request with -`2026-06-30` protocol version specified will be rejected with `Method not found` (-32601), since the -initialization handshake does not exist in the new protocol version. +For the server side, the protocol version will come in each request's `_meta` field +(`io.modelcontextprotocol/protocolVersion`). It is detected via the partial unmarshal in +`ServerSession.handle()` (see [Per-request protocol detection](#per-request-protocol-detection)) +and is not stored in session state. Per-request access is provided by the new +`req.ProtocolVersion()` accessor on `ServerRequest`. -TODO: Consider whether servers should be allowed to limit the protocol versions they advertise as supported. +An `initialize` request with `2026-06-30` protocol version specified will be rejected with +`Method not found` (-32601), since the initialization handshake does not exist in the new +protocol version. For client side, the protocol version must be provided in each request. We cannot depend on the initialization for provide a negotiated version. Instead, the server returns `UnsupportedProtocolVersionError` if the version specified by the client is not supported. -The error also contains a list of `supported` versions. If any of them is supported by the client, -then one of two things will happen: - -* the version will be saved to `ClientSession.state.InitializeResult` and used in subsequent requests if it is `>= 2026-06-30` -* normal initialization will be performed if it is `< 2026-06-30` +The exact procedure for the determination of the protocol version is presented in more detail +in [SDK connection establishment flow changes](#sdk-connection-establishment-flow-changes). New types in `protocol.go`: @@ -80,12 +132,9 @@ type UnsupportedProtocolVersionData struct { } ``` -The protocol version may also be determined by calling `ServerDiscover` with sufficiently modern -servers. See [Server Capabilities](#server-capabilities) for details. - #### Server Capabilities -SEP-2575 introduces `server/discover` as a replacement for capability exchange during initialization. +SEP-2575 introduces `server/discover` as a convenience replacement for capability exchange during initialization. It will return `supportedVersions`, `capabilities`, `serverInfo`, and `instructions` — effectively the same information that was in `InitializeResult`, plus the list of supported protocol versions. @@ -108,12 +157,35 @@ type DiscoverResult struct { } ``` -The handler will be registered with the `missingParamsOK` flag and will be allowed -pre-initialization. -It will be implemented on `Server` (not `ServerSession`) since it is not session-scoped. -The handler will use `Server.capabilities()` and `Server.impl` to construct the response. -The `2026-06-30` protocol version will only be returned as supported if the server runs in -the `Stateless` mode. +The handler will be registered with the `missingParamsOK` flag and added to the +pre-initialization allow-list in `ServerSession.handle()` alongside `initialize`, `ping`, +and `notifications/initialized`. It will be implemented on `Server` (not `ServerSession`) +since it is not session-scoped. The handler will use `Server.capabilities()` and +`Server.impl` to construct the response. + +To determine which versions to include in `supportedVersions`, the server checks whether +the transport supports each version via a new exported interface: + +```go +// ProtocolVersionSupporter may be implemented by a Transport to declare which +// protocol versions it can serve. If a Transport does not implement this +// interface, all SDK-supported versions are assumed to be supported. +type ProtocolVersionSupporter interface { + SupportsProtocolVersion(version string) bool +} +``` + +`Server.Connect()` checks whether the passed `Transport` implements +`ProtocolVersionSupporter`. If it does, the result is used to filter the SDK's +`supportedProtocolVersions` list. The filtered list is stored on the `ServerSession` +at connection time and used by the `server/discover` handler to build +`DiscoverResult.SupportedVersions`. + +`StreamableServerTransport` implements this interface: it returns `true` for `2026-06-30` +only when `Stateless` is `true`. STDIO transports (`StdioTransport`, `IOTransport`) and +`InMemoryTransport` do not need to implement the interface — the default behavior assumes +they support all SDK versions, which matches their capabilities. Custom transports may +implement the interface to constrain which versions they advertise. On the client side, `server/discover` will be called during connection establishment to determine the protocol version to be used. The client will send `server/discover` with `2026-06-30` as the @@ -145,13 +217,34 @@ SEP-2575 moves client capabilities from session-level negotiation to per-request * `io.modelcontextprotocol/clientCapabilities` (`ClientCapabilities`, required) * `io.modelcontextprotocol/logLevel` (`LoggingLevel`, optional) -On the server side, the stateless session handler will extract these fields from each incoming -request's `_meta` and populate `ServerSession.state.InitializeParams` with the values for the -duration of the call. This ensures that existing server code that reads `ServerSession.InitializeParams()` -will continue to work without changes. The extraction will happen in the `ServerSession.handle()` method, -before dispatching to the registered handler. -If a server receives a request with missing `_meta` fields, it will return `INVALID_PARAMS` -(-32602) when operating with `>= 2026-06-30`. +On the server side, `ServerSession.handle()` validates that the required `_meta` fields +(`protocolVersion`, `clientInfo`, `clientCapabilities`) are present for `>= 2026-06-30` requests. +If a request is missing required `_meta` fields, the server will return `INVALID_PARAMS` +(-32602). + +The fields are not copied into session state. Instead, new accessor methods on +`ServerRequest[P]` provide typed per-request access: + +```go +func (r *ServerRequest[P]) ProtocolVersion() string +func (r *ServerRequest[P]) ClientCapabilities() *ClientCapabilities +func (r *ServerRequest[P]) ClientInfo() *Implementation +``` + +These read from `r.Params.GetMeta()` with fallback to `r.Session.InitializeParams()` for +old protocol requests. Existing handler code should migrate from the session-level accessor +to the request-level accessor: + +```go +// Old: +caps := req.Session.InitializeParams().Capabilities +// New: +caps := req.ClientCapabilities() +``` + +`ServerSession.InitializeParams()` returns `nil` for new-protocol sessions (no state +pre-population). For old-protocol sessions, it continues to return the `InitializeParams` +from the `initialize` handshake. > [!WARNING] > Deprecated APIs: @@ -217,15 +310,18 @@ Currently, `ping` is used in two ways: For `>= 2026-06-30`: * Sending a `ping` request will return `Method not found` (-32601). -* `ServerSession.Ping()` will return an error for stateless sessions (already the case: +* `ServerSession.Ping()` will return an error for stateless HTTP transport (already the case: `streamable.go:1367-1369`). * `ClientSession.Ping()` will continue to exist for backwards compatibility but will return an error if the server doesn't support it (`ErrMethodNotFound` from keepalive is already handled gracefully — `shared.go:607-610`). -* The keepalive mechanism will be disabled for stateless sessions. Currently, keepalive starts - even for stateless sessions and races with `defer session.Close()` (`streamable_test.go:366-374`). - For the new protocol version, `Server.Connect()` should skip `startKeepalive` when operating - in stateless mode. +* The keepalive mechanism does not need explicit disabling for the new protocol. The existing + graceful degradation handles it: when keepalive sends a `ping` and the peer returns + `Method not found`, `startKeepalive` stops silently (`shared.go:607-610`). For stateless + HTTP sessions, keepalive remains pointless (sessions are ephemeral and the keepalive races + with `defer session.Close()`); to surface this misconfiguration, `StreamableHTTPHandler` + will log a warning if it receives a `Server` with `KeepAlive` set while operating in + `Stateless` mode. > [!WARNING] > Deprecated APIs: From 2f12ea4b3bed911357cdd8ee975021a89bdda987 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Tue, 27 Oct 2026 11:22:59 +0000 Subject: [PATCH 5/5] design: update stateless.md to match the 2026-07-28 implementation The design was written against the draft 2026-06-30 version, and the implementation that landed since differs from it in several places: dedicated error codes, server/discover result shape and fallback rules, subscriptions/listen gating, session and resumability behavior in stateless mode, and which APIs were actually deprecated. Describe what the SDK does today, and list the differences from the original proposal for reviewers of the earlier version. --- design/stateless.md | 763 ++++++++++++++++++++++++-------------------- 1 file changed, 425 insertions(+), 338 deletions(-) diff --git a/design/stateless.md b/design/stateless.md index 7455b57e0..fc39f8466 100644 --- a/design/stateless.md +++ b/design/stateless.md @@ -1,130 +1,153 @@ -# Stateless MCP for 2026-06-30 protocol version +# Stateless MCP for the 2026-07-28 protocol version -This document discusses the changes needed to fully support protocol changes introduced -in `2026-06-30` version. It focuses on the following SEPs: +This document describes how the SDK supports the stateless and sessionless +protocol introduced in protocol version `2026-07-28`. It was first written as a +proposal against the draft version `2026-06-30`, and has since been updated to +match the implementation. [Changes from the original +proposal](#changes-from-the-original-proposal) lists where the two differ. + +It focuses on the following SEPs: * [SEP-2567: Sessionless MCP via Explicit State Handles](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567) * [SEP-2575: Make MCP Stateless](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575) -These proposals together define a stateless and sessionless protocol that is largely -incompatible with its previous version. This poses challenges given our commitment to -not introduce backwards incompatible API changes to the SDK. +Related changes are covered elsewhere: -## Design decisions +* Multi round-trip requests ([SEP-2322](https://github.com/CaitieM20/modelcontextprotocol/blob/de6d76fba3078eda957dadb3cec51ca8ab851b5c/seps/2322-MRTR.md)), + which replace server-to-client requests, are described in [mrtr.md](mrtr.md). +* The standard HTTP headers of [SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243) + and the user-facing behavior are documented in [docs/protocol.md](../docs/protocol.md). +* The deprecation of roots, sampling and logging is defined by + [SEP-2577](https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging). -### Stateless mode reuse +These proposals together define a protocol that is largely incompatible with +its previous versions. This poses challenges given our commitment to not +introduce backwards incompatible API changes to the SDK. -On the HTTP server side, there already exists a `StreamableHTTPOptions.Stateless` mode that -works similarly to what the proposals define. We will make it a dependency of the new protocol -version support for the HTTP transport — only HTTP servers that enable it will be able to -serve `>= 2026-06-30` requests. For STDIO and other transports, no transport-level option is -needed; protocol-level behavior is driven by per-request detection (see -[Per-request protocol detection](#per-request-protocol-detection)). - -This will simplify the implementation and provide opt-in mechanism for the new behavior. -Such opt-in is needed, because it's not possible to automatically enable the new protocol -version for all SDK users, especially if they are using functionality that is removed -in it. `Stateless` mode already disallows many such features that are removed. -Any backwards incompatible behavior changes will be guarded by `MCPGODEBUG` flags -to give developers more time to adapt. +## Design decisions -> [!NOTE] -> Deprecated APIs: -> For v2, it would make sense to rename `ClientSession` to avoid confusion. -`ServerSession` should be removed. +### Stateless mode reuse -### Keeping the session-based APIs +On the HTTP server side, `StreamableHTTPOptions.Stateless` already worked +similarly to what the proposals define. It is a prerequisite for the new +protocol version over HTTP: only handlers with `Stateless` set serve +`>= 2026-07-28` requests. This is expressed through the +[`ProtocolVersionSupporter`](#server-capabilities-serverdiscover) interface: -Session objects (`ServerSession`/`ClientSession`) are the core part of the Go MCP SDK API. -They are returned from `Connect` functions and they are used to interact with the peer. -Even though SEP-2567 removes the concept of sessions from the protocol, we cannot simply remove -these objects. Instead, we will adjust their lifetime of the server side to be per-request -and attempt to fill the session state on both sides with the most appropriate data available. +* `StreamableServerTransport` supports `>= 2026-07-28` only when `Stateless` is + true. +* `SSEServerTransport` supports only versions below `2026-07-28`, since + `2026-07-28` defines only the stdio and Streamable HTTP bindings. +* Stdio and in-memory transports don't implement the interface, so they support + every version. No transport-level option is needed for them. -### Per-request protocol detection +A stateful `StreamableHTTPHandler` rejects new-protocol requests other than +`server/discover` with `CodeUnsupportedProtocolVersion`, advertising only the +legacy versions, and logs a warning pointing the operator at +`StreamableHTTPOptions.Stateless`. -The presence of `io.modelcontextprotocol/protocolVersion` in a request's `_meta` implicitly -signals that the request follows the new protocol. No explicit flag is needed at the protocol -level — `ServerSession.handle()` detects the field and adjusts behavior per-request. +The protocol version itself is not opt-in. `2026-07-28` is the latest supported +version and the client's default. A client can request an older version with +`ClientSessionOptions.ProtocolVersion`, and a server can narrow the versions it +serves with `ServerOptions.SupportedProtocolVersions`. -The detection mechanism works as follows: +Backwards incompatible behavior changes are guarded by `MCPGODEBUG` settings; +see [MCPGODEBUG settings](#mcpgodebug-settings). -1. Before the initialization gate, `ServerSession.handle()` performs a lightweight partial - unmarshal of `_meta` from the raw `jreq.Params`: +### Keeping the session-based APIs - ```go - var meta struct { - Meta Meta `json:"_meta"` - } - internaljson.Unmarshal(jreq.Params, &meta) - ``` +Session objects (`ServerSession`/`ClientSession`) are the core part of the Go +MCP SDK API. They are returned from `Connect` functions and they are used to +interact with the peer. Even though SEP-2567 removes the concept of sessions +from the protocol, we cannot remove these objects. Instead: -2. If `io.modelcontextprotocol/protocolVersion` is present in `meta.Meta`, the initialization - gate is skipped (the request is allowed regardless of `ServerSession.state.InitializeParams` - being `nil`). Required `_meta` fields (`clientInfo`, `clientCapabilities`) are validated. +* On stateless HTTP servers, a `ServerSession` lives for a single request. +* On stdio, a single `ServerSession` can serve both legacy and new-protocol + requests, each handled according to its own `_meta`. +* Session state on both sides is filled with the closest data available: the + server derives `InitializeParams` from the first new-protocol request (or + `server/discover`), and the client derives `InitializeResult` from the + `server/discover` result. -3. New accessor methods on `ServerRequest[P]` provide typed per-request access: +### Per-request protocol detection - ```go - func (r *ServerRequest[P]) ProtocolVersion() string - func (r *ServerRequest[P]) ClientCapabilities() *ClientCapabilities - func (r *ServerRequest[P]) ClientInfo() *Implementation - ``` +A call whose `_meta` carries `io.modelcontextprotocol/protocolVersion` follows +the new protocol. `ServerSession.handle` classifies every incoming message +before the initialization gate: + +1. Notifications, calls without `_meta`, and calls with an empty or absent + protocol version are legacy. +2. A supported version below `2026-07-28` is legacy. +3. A version the SDK doesn't support is treated as new-protocol and rejected + with `CodeUnsupportedProtocolVersion`, carrying the server's supported + versions. +4. Otherwise the request is new-protocol. `clientCapabilities` is required; + if it is missing or invalid, the server returns `INVALID_PARAMS` (-32602). + `clientInfo` and `logLevel` are optional, but an invalid `clientInfo` is + also rejected. + +New-protocol requests skip the initialization gate. On a session that was never +initialized, the first such request stores its derived `InitializeParams` and +negotiated version in the session state. A server configured with only +`>= 2026-07-28` versions rejects legacy calls with +`CodeUnsupportedProtocolVersion`. + +Methods that the new protocol removes return `Method not found` (-32601) when +called with new-protocol `_meta`: `initialize`, `ping`, +`notifications/initialized`, `notifications/roots/list_changed`, +`logging/setLevel`, `resources/subscribe` and `resources/unsubscribe`. + +Handlers read per-request client data through accessors on `ServerRequest`: - These read from `r.Params.GetMeta()` with fallback to `r.Session.InitializeParams()` - for old protocol requests. Since validation already happened in `handle()`, the - accessors assume data is well-formed and do not return errors. +```go +func (r *ServerRequest[P]) ProtocolVersion() string +func (r *ServerRequest[P]) ClientCapabilities() *ClientCapabilities +func (r *ServerRequest[P]) ClientInfo() *Implementation +``` -4. `ServerSession.InitializeParams()` returns `nil` for new-protocol sessions — there is no - pre-population of session state. Returning `nil` is a clear signal to callers that they - should use the `ServerRequest` accessors instead. +They read the request's `_meta` first and fall back to +`r.Session.InitializeParams()` for legacy requests. Existing handler code +should migrate from the session-level accessor: -This mechanism is transport-agnostic: it works on HTTP and STDIO identically. A single -session can serve both old and new protocol requests, with each request handled according -to its own `_meta`. For HTTP, `StreamableHTTPOptions.Stateless` remains needed for -transport-level concerns (per-request session lifecycle, HTTP method gating). For STDIO, -no transport-level option is needed. +```go +// Old: +caps := req.Session.InitializeParams().Capabilities +// New: +caps := req.ClientCapabilities() +``` -The body-peeking and state pre-population in `ephemeralConnectOpts` becomes unnecessary -for new-protocol requests since `handle()` skips the initialization gate based on `_meta`. -Pre-population remains for old-protocol requests on stateless HTTP servers (where the -client skips `initialize` but doesn't send `_meta`). +`ServerSession.InitializeParams()` keeps working for new-protocol sessions: it +returns the parameters derived from the first new-protocol request. It is not +deprecated. ## Connection establishment -There are several changes affecting how the connection from the client to the server is established. - ### Initialization handshake removal -> [!WARNING] -> Deprecated APIs: -> - `ServerOptions.InitializedHandler` +SEP-2575 removes the initialization handshake. The SDK keeps `initialize` for +legacy clients only: -SEP-2575 removes the initialization handshake from the protocol. This means that -we need adjust the way we establish the protocol version and the capabilities of the peer. +* Version negotiation in `initialize` is capped below `2026-07-28`. A client + that asks for `2026-07-28` through `initialize` is answered with the newest + legacy version the server supports. +* An `initialize` request that carries new-protocol `_meta` gets + `Method not found`. +* `ServerOptions.InitializedHandler` is called only on the legacy path, since + new-protocol clients never send `notifications/initialized`. -#### Protocol version +#### Protocol version and errors -For the server side, the protocol version will come in each request's `_meta` field -(`io.modelcontextprotocol/protocolVersion`). It is detected via the partial unmarshal in -`ServerSession.handle()` (see [Per-request protocol detection](#per-request-protocol-detection)) -and is not stored in session state. Per-request access is provided by the new -`req.ProtocolVersion()` accessor on `ServerRequest`. - -An `initialize` request with `2026-06-30` protocol version specified will be rejected with -`Method not found` (-32601), since the initialization handshake does not exist in the new -protocol version. - -For client side, the protocol version must be provided in each request. We cannot depend -on the initialization for provide a negotiated version. Instead, the server returns -`UnsupportedProtocolVersionError` if the version specified by the client is not supported. -The exact procedure for the determination of the protocol version is presented in more detail -in [SDK connection establishment flow changes](#sdk-connection-establishment-flow-changes). - -New types in `protocol.go`: +On the server, the protocol version comes from each request's `_meta` +(see [Per-request protocol detection](#per-request-protocol-detection)) and, +over HTTP, from the `Mcp-Protocol-Version` header. The SDK defines dedicated +error codes for the new protocol: ```go -const ErrCodeUnsupportedProtocolVersion = -32602 // INVALID_PARAMS per SEP-2575 +const ( + CodeHeaderMismatch = -32020 + CodeMissingRequiredClientCapabilities = -32021 + CodeUnsupportedProtocolVersion = -32022 +) type UnsupportedProtocolVersionData struct { Supported []string `json:"supported"` @@ -132,17 +155,14 @@ type UnsupportedProtocolVersionData struct { } ``` -#### Server Capabilities - -SEP-2575 introduces `server/discover` as a convenience replacement for capability exchange during initialization. -It will return `supportedVersions`, `capabilities`, `serverInfo`, -and `instructions` — effectively the same information that was in `InitializeResult`, plus -the list of supported protocol versions. +On the client, the protocol version is set on every request; there is no +handshake to negotiate it. The procedure is described in [SDK connection +establishment flow](#sdk-connection-establishment-flow). -It will also be available to old clients — it is harmless and -does not require initialization. +#### Server capabilities: `server/discover` -New types in `protocol.go`: +SEP-2575 introduces `server/discover` as a replacement for the capability +exchange of `initialize`: ```go type DiscoverParams struct { @@ -150,186 +170,208 @@ type DiscoverParams struct { } type DiscoverResult struct { + completeResultWithType + Meta `json:"_meta,omitempty"` + Cacheable SupportedVersions []string `json:"supportedVersions"` Capabilities *ServerCapabilities `json:"capabilities"` - ServerInfo *Implementation `json:"serverInfo"` Instructions string `json:"instructions,omitempty"` } ``` -The handler will be registered with the `missingParamsOK` flag and added to the -pre-initialization allow-list in `ServerSession.handle()` alongside `initialize`, `ping`, -and `notifications/initialized`. It will be implemented on `Server` (not `ServerSession`) -since it is not session-scoped. The handler will use `Server.capabilities()` and -`Server.impl` to construct the response. +Server info is not a result field: like every new-protocol result, it is +carried in `_meta` under `io.modelcontextprotocol/serverInfo`. + +The handler is implemented on `Server` and registered with `missingParamsOK`. +It answers only new-protocol requests; a `server/discover` without a `_meta` +protocol version gets `Method not found`. A stateful `StreamableHTTPHandler` +lets `server/discover` through, so that its answer (which lists only legacy +versions) tells the client to fall back. -To determine which versions to include in `supportedVersions`, the server checks whether -the transport supports each version via a new exported interface: +`supportedVersions` is the server's version list, narrowed by +`ServerOptions.SupportedProtocolVersions` and by the transport through an +optional interface: ```go -// ProtocolVersionSupporter may be implemented by a Transport to declare which -// protocol versions it can serve. If a Transport does not implement this -// interface, all SDK-supported versions are assumed to be supported. +// ProtocolVersionSupporter is an optional capability that a [Transport] may +// implement to declare which MCP protocol versions it can serve. +// +// [Server.Connect] consults this interface to filter the +// list of versions advertised in server/discover responses. Transports that +// do not implement this interface are assumed to support every protocol +// version known to the SDK. type ProtocolVersionSupporter interface { SupportsProtocolVersion(version string) bool } ``` -`Server.Connect()` checks whether the passed `Transport` implements -`ProtocolVersionSupporter`. If it does, the result is used to filter the SDK's -`supportedProtocolVersions` list. The filtered list is stored on the `ServerSession` -at connection time and used by the `server/discover` handler to build -`DiscoverResult.SupportedVersions`. - -`StreamableServerTransport` implements this interface: it returns `true` for `2026-06-30` -only when `Stateless` is `true`. STDIO transports (`StdioTransport`, `IOTransport`) and -`InMemoryTransport` do not need to implement the interface — the default behavior assumes -they support all SDK versions, which matches their capabilities. Custom transports may -implement the interface to constrain which versions they advertise. - -On the client side, `server/discover` will be called during connection establishment to determine -the protocol version to be used. The client will send `server/discover` with `2026-06-30` as the -protocol version in both the `MCP-Protocol-Version` header and the `_meta` payload. The full -per-request `_meta` fields (`clientInfo`, `clientCapabilities`) will also be included, using the -values from `ClientOptions`. This means the transport's `setMCPHeaders` must be able to set the -`MCP-Protocol-Version` header before `initializedResult` is populated. - -If the server returns `Method not found` or `UnsupportedProtocolVersionError`, the client knows -the server is pre-`2026-06-30` and should fall back to the initialization handshake. -This will result in an additional round-trip for older servers. - -The capabilities and protocol version from the `server/discover` result will be saved to -`ClientSession.state.InitializeResult`, with `ProtocolVersion` set to the latest version from -`DiscoverResult.SupportedVersions` that the client also supports. -This ensures that existing client code that reads `ClientSession.InitializeResult` will continue -to work without changes. - -> [!WARNING] -> Deprecated APIs: -> - `ClientSession.InitializeResult` - -#### Client Capabilities - -SEP-2575 moves client capabilities from session-level negotiation to per-request `_meta` fields: +`Server.Connect` filters the list once and stores it on the `ServerSession`. -* `io.modelcontextprotocol/protocolVersion` (string, required) -* `io.modelcontextprotocol/clientInfo` (`Implementation`, required) -* `io.modelcontextprotocol/clientCapabilities` (`ClientCapabilities`, required) -* `io.modelcontextprotocol/logLevel` (`LoggingLevel`, optional) - -On the server side, `ServerSession.handle()` validates that the required `_meta` fields -(`protocolVersion`, `clientInfo`, `clientCapabilities`) are present for `>= 2026-06-30` requests. -If a request is missing required `_meta` fields, the server will return `INVALID_PARAMS` -(-32602). - -The fields are not copied into session state. Instead, new accessor methods on -`ServerRequest[P]` provide typed per-request access: +On the client, `Client.Connect` calls `server/discover` as described in [SDK +connection establishment flow](#sdk-connection-establishment-flow). On success, +it stores a synthesized `InitializeResult` (capabilities, instructions, +negotiated version and the server info from `_meta`) in the session, so +existing code that reads `ClientSession.InitializeResult` keeps working. +Applications can also call discover directly: ```go -func (r *ServerRequest[P]) ProtocolVersion() string -func (r *ServerRequest[P]) ClientCapabilities() *ClientCapabilities -func (r *ServerRequest[P]) ClientInfo() *Implementation +func (cs *ClientSession) Discover(ctx context.Context, params *DiscoverParams) (*DiscoverResult, error) ``` -These read from `r.Params.GetMeta()` with fallback to `r.Session.InitializeParams()` for -old protocol requests. Existing handler code should migrate from the session-level accessor -to the request-level accessor: - -```go -// Old: -caps := req.Session.InitializeParams().Capabilities -// New: -caps := req.ClientCapabilities() -``` +It returns the result unchanged, including cache hints and server info, and +does not update `InitializeResult`. On legacy sessions it returns +`Method not found` without a round trip. -`ServerSession.InitializeParams()` returns `nil` for new-protocol sessions (no state -pre-population). For old-protocol sessions, it continues to return the `InitializeParams` -from the `initialize` handshake. +#### Client capabilities and per-request `_meta` -> [!WARNING] -> Deprecated APIs: -> - `ServerSession.InitializeParams` -> - `ServerOptions.InitializedHandler` +SEP-2575 moves client data from the handshake to per-request `_meta` keys, +exported as constants: -On the client side, `ClientSession` will automatically populate these `_meta` fields on every -outgoing request when operating with `>= 2026-06-30` protocol version. The values will come from -`ClientOptions` (for `clientInfo` and `clientCapabilities`) and from the negotiated -protocol version. +| Constant | Key | Notes | +|---|---|---| +| `MetaKeyProtocolVersion` | `io.modelcontextprotocol/protocolVersion` | required | +| `MetaKeyClientCapabilities` | `io.modelcontextprotocol/clientCapabilities` | required | +| `MetaKeyClientInfo` | `io.modelcontextprotocol/clientInfo` | optional | +| `MetaKeyLogLevel` | `io.modelcontextprotocol/logLevel` | optional; deprecated by SEP-2577 | +| `MetaKeyServerInfo` | `io.modelcontextprotocol/serverInfo` | set by the server on results | +| `MetaKeySubscriptionID` | `io.modelcontextprotocol/subscriptionId` | see [`subscriptions/listen`](#standalone-sse-stream-removal-and-subscriptionslisten) | -### SSE standalone stream removal +On new-protocol sessions, `ClientSession` adds the protocol version, client +info (when set) and client capabilities to every outgoing request, without +overwriting keys the caller already set. It does not set `logLevel`. The +streamable client transport sets the `Mcp-Protocol-Version` header from the +message's `_meta`, falling back to the negotiated version, so the header is +correct before any session state exists. -SEP-2575 removes the HTTP GET endpoint used for standalone SSE streaming. In the new protocol -version, GET requests MUST return `405 Method Not Allowed`. This is already the behavior in -`Stateless` mode (`streamable.go:336-342`), so no new server-side changes are needed for -stateless servers. +Over HTTP, the server checks the header against `_meta`. A missing header or a +mismatch returns `CodeHeaderMismatch`; a missing `_meta` version returns +`INVALID_PARAMS`. For new-protocol requests, `Method not found` maps to HTTP +404 and `INVALID_PARAMS` to HTTP 400. -The standalone SSE stream was used for two purposes: +### Standalone SSE stream removal and `subscriptions/listen` -1. Server-to-client notifications outside the context of a request (e.g., `notifications/tools/list_changed`) -2. Server-to-client requests (e.g., `ping`, sampling, elicitation) +SEP-2575 removes the HTTP GET endpoint used for standalone SSE streaming. A +stateless `StreamableHTTPHandler` answers GET and DELETE with +`405 Method Not Allowed` and `Allow: POST`, at every protocol version. The +streamable client transport doesn't open the GET stream once the negotiated +version is `>= 2026-07-28`, so `StreamableClientTransport.DisableStandaloneSSE` +only matters for legacy versions. -Purpose (1) will be replaced by a new `subscriptions/listen` RPC method. -Purpose (2) will be replaced by MRTR ([SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)). +The standalone stream had two uses: -On the client side, the streamable client transport currently opens a GET SSE stream after -initialization. For `>= 2026-06-30` protocol versions, this step will be skipped. The client -will instead send `subscriptions/listen` during `Client.Connect()` if any of the following -`ClientOptions` handlers are configured: +1. Server-to-client notifications outside a request (e.g. + `notifications/tools/list_changed`). These now go through + `subscriptions/listen`. +2. Server-to-client requests (e.g. sampling, elicitation). These are replaced + by [MRTR](#server-to-client-requests). -* `ToolListChangedHandler` → subscribes to `toolsListChanged` -* `PromptListChangedHandler` → subscribes to `promptsListChanged` -* `ResourceListChangedHandler` → subscribes to `resourcesListChanged` -* `LoggingMessageHandler` → subscribes with `logLevel` +`subscriptions/listen` takes a set of notification types: -If none of these handlers are set, no `subscriptions/listen` call will be made during `Connect()`. -Presence of `ResourceUpdateHandler` will not automatically trigger `subscriptions/listen` during -`Connect()`, as Resource notifications require URIs to be specified. Instead, each call to -`ClientSession.Subscribe()` will start a new `subscriptions/listen` stream for the specified -Resource. Calling `ClientSession.Unsubscribe()` will cancel that stream. -On the server side, the server will return `Method not found` (-32601) for `resource/subscribe` and -`resource/unsubscribe` when `>= 2026-06-30`. +```go +type NotificationSubscriptions struct { + ToolsListChanged bool `json:"toolsListChanged,omitempty"` + PromptsListChanged bool `json:"promptsListChanged,omitempty"` + ResourcesListChanged bool `json:"resourcesListChanged,omitempty"` + ResourceSubscriptions []string `json:"resourceSubscriptions,omitempty"` +} +``` -> [!WARNING] -> Deprecated APIs: -> - `ClientSession.Subscribe` -> - `ClientSession.Unsubscribe` -> - `StreamableClientTransport.DisableStandaloneSSE` +At the end of a successful `server/discover`, `Client.Connect` starts a +background `subscriptions/listen` for each list-changed type whose handler is +set **and** whose `ListChanged` capability the server advertises: + +* `ToolListChangedHandler` → `ToolsListChanged` +* `PromptListChangedHandler` → `PromptsListChanged` +* `ResourceListChangedHandler` → `ResourcesListChanged` + +If none apply, no listen is started. `Connect` doesn't wait for the +acknowledgement. `LoggingMessageHandler` doesn't trigger a listen: the log +level travels with each request instead (see [Logging](#logging)). +`ResourceUpdatedHandler` doesn't trigger one either, because resource +notifications need URIs. Instead, on new-protocol sessions: + +* `ClientSession.Subscribe` starts a listen for that one URI and returns + without waiting. Subscribing to a URI twice is a no-op. +* `ClientSession.Unsubscribe` cancels that listen. +* If a listen ends for any other reason, the subscription is dropped; the SDK + doesn't resubscribe automatically, so the application calls `Subscribe` + again. + +On the server, `subscriptionsListen` keeps only the types allowed by the +server's capabilities, sends `notifications/subscriptions/acknowledged` with +the allowed set and a subscription ID in `_meta`, and holds the request open +until it is cancelled. List-changed and resource-updated notifications reach a +new-protocol session only through a matching listen, tagged with +`io.modelcontextprotocol/subscriptionId`. Over HTTP, listen responses always +use SSE, and `StreamableHTTPOptions.StreamKeepAlive` (default 30s) sends SSE +comments on them. + +### Server-to-client requests + +Servers can no longer send requests to clients. On a `>= 2026-07-28` request, +`ServerSession.CreateMessage`, `CreateMessageWithTools`, `Elicit` and +`ListRoots` return an error telling the handler to return an `InputRequests` +map instead. The client fulfills the input requests and retries the call; for +legacy clients, server middleware turns the input requests back into real +server-to-client calls. See [mrtr.md](mrtr.md). A stateless HTTP server +rejects every server-initiated request, whatever the version. + +### Logging + +The log level travels with each request in `_meta` +(`io.modelcontextprotocol/logLevel`). `ServerSession.Log` uses the request's +level, then the session's legacy `logging/setLevel` level; with neither, the +message is dropped. Messages are sent on the request's stream. +`logging/setLevel` returns `Method not found` for new-protocol requests. The +logging APIs are deprecated by SEP-2577 (see [Deprecations](#deprecations)). ### Ping -SEP-2575 removes `ping` in both directions. Server-to-client ping is removed because servers -can no longer independently send requests. Client-to-server ping is removed because any normal -RPC call proves server liveness, and transport-layer mechanisms (HTTP keep-alives, SSE comments) -handle connection-health checks. - -Currently, `ping` is used in two ways: - -1. Explicit calls via `ServerSession.Ping()` / `ClientSession.Ping()` (`server.go:1167`, `client.go:966`) -2. Keepalive mechanism via `startKeepalive()` (`shared.go:577-622`) - -For `>= 2026-06-30`: - -* Sending a `ping` request will return `Method not found` (-32601). -* `ServerSession.Ping()` will return an error for stateless HTTP transport (already the case: - `streamable.go:1367-1369`). -* `ClientSession.Ping()` will continue to exist for backwards compatibility but will return - an error if the server doesn't support it (`ErrMethodNotFound` from keepalive is already - handled gracefully — `shared.go:607-610`). -* The keepalive mechanism does not need explicit disabling for the new protocol. The existing - graceful degradation handles it: when keepalive sends a `ping` and the peer returns - `Method not found`, `startKeepalive` stops silently (`shared.go:607-610`). For stateless - HTTP sessions, keepalive remains pointless (sessions are ephemeral and the keepalive races - with `defer session.Close()`); to surface this misconfiguration, `StreamableHTTPHandler` - will log a warning if it receives a `Server` with `KeepAlive` set while operating in - `Stateless` mode. - -> [!WARNING] -> Deprecated APIs: -> - `ClientSession.Ping` -> - `ServerSession.Ping` -> - `ServerOptions.Keepalive` - -### SDK connection establishment flow changes +SEP-2575 removes `ping` in both directions. Server-to-client ping is removed +because servers can no longer send requests. Client-to-server ping is removed +because any normal call proves server liveness, and transport-layer mechanisms +handle connection health. + +For `>= 2026-07-28`: + +* A `ping` request returns `Method not found`. +* `ClientSession.Ping` returns `Method not found` locally, without a round + trip. +* The client keepalive (`ClientOptions.KeepAlive`) is started only on the + legacy `initialize` path. +* `ServerSession.Ping` and the server keepalive (`ServerOptions.KeepAlive`) + are not version-gated. Over stateless HTTP, `Ping` fails like any other + server-initiated request. `KeepAlive` should not be enabled for + `2026-07-28`. + +### Cancellation + +On stdio-style transports (`StdioTransport`, `CommandTransport`, `IOTransport`, +`InMemoryTransport`), a `>= 2026-07-28` session sends no response to a request +that the peer cancelled with `notifications/cancelled`; this includes a +cancelled `subscriptions/listen`. Earlier versions still answer cancelled +requests. + +Over HTTP, the client cancels a request by closing it. Handlers observe that +only for listen requests, or for new-protocol requests when +`StreamableHTTPOptions.PropagateRequestCancellation` is set. + +### SDK connection establishment flow + +`Client.Connect` uses `server/discover` when the requested version (the +latest by default, or `ClientSessionOptions.ProtocolVersion`) is +`>= 2026-07-28`: + +1. Send `server/discover` with the requested version in `_meta` and, over + HTTP, in the `Mcp-Protocol-Version` header, along with `clientInfo` and + `clientCapabilities`. The negotiated version is the requested one if the + server lists it, otherwise the highest version both sides support. +2. If the server answers `CodeUnsupportedProtocolVersion` with a supported + list that contains a `>= 2026-07-28` version both sides support, retry + once with that version. +3. On any other error, or if no common `>= 2026-07-28` version exists, fall + back to `initialize` with `2025-11-25`. Older servers cost one extra round + trip. #### Original flow (HTTP) @@ -340,7 +382,7 @@ sequenceDiagram Note over c: Client.Connect() c->>s: initialize {"protocolVersion": "2025-11-25"} - s->>c: initialized + s->>c: InitializeResult c->>s: notifications/initialized c->>s: hanging standalone SSE stream (GET) note over c: Client.Connect() finished @@ -355,34 +397,35 @@ sequenceDiagram participant s as Server SDK Note over c: Client.Connect() - c->>s: server/discover (MCP-Protocol-Version: 2026-06-30) - - alt 2026-06-30 version supported - s->>c: {supportedVersions, capabilities, serverInfo, instructions} - opt if notification handlers configured - c->>s: subscriptions/listen (POST, hanging SSE) - s->>c: notifications/subscriptions/acknowledged + c->>s: server/discover (Mcp-Protocol-Version: 2026-07-28) + + alt 2026-07-28 supported + s->>c: DiscoverResult {supportedVersions, capabilities, instructions, _meta.serverInfo} + opt list-changed handlers set and advertised by the server + c->>s: subscriptions/listen (POST, SSE stream, not awaited) end note over c: Client.Connect() finished - note over c,s: Operation using 2026-06-30 version - else 2026-06-30 version not supported - s->>c: Method not found / UnsupportedProtocolVersionError - note over c,s: fall back to legacy flow + s-->>c: notifications/subscriptions/acknowledged + note over c,s: Operation using 2026-07-28 + else another >= 2026-07-28 version supported + s->>c: CodeUnsupportedProtocolVersion {supported, requested} + c->>s: server/discover with the negotiated version (once) + else not supported + s->>c: Method not found, other error, or only legacy versions + note over c,s: fall back to the legacy flow c->>s: initialize {"protocolVersion": "2025-11-25"} - s->>c: initialized + s->>c: InitializeResult c->>s: notifications/initialized - c->>s: hanging standalone SSE stream (GET) + c->>s: hanging standalone SSE stream (GET, stateful servers) note over c: Client.Connect() finished - note over c,s: Operation using 2025-11-25 version + note over c,s: Operation using 2025-11-25 end ``` #### Updated flow (STDIO) -STDIO follows the same logic as HTTP but without HTTP-specific mechanisms (headers, SSE streams). -The protocol version is conveyed only via the `_meta` field. The SEP recommends using -`server/discover` for STDIO to determine the server's version, since there is no per-request -HTTP error status to detect version mismatches. +STDIO follows the same logic without the HTTP-specific mechanisms (headers, +SSE streams). The protocol version is conveyed only in `_meta`. ```mermaid sequenceDiagram @@ -390,97 +433,80 @@ sequenceDiagram participant s as Server SDK Note over c: Client.Connect() - c->>s: server/discover {_meta: {protocolVersion: "2026-06-30"}} - - alt 2026-06-30 version supported - s->>c: {supportedVersions, capabilities, serverInfo, instructions} - opt if notification handlers configured - c->>s: subscriptions/listen - s->>c: notifications/subscriptions/acknowledged + c->>s: server/discover {_meta: {protocolVersion: "2026-07-28", ...}} + + alt 2026-07-28 supported + s->>c: DiscoverResult {supportedVersions, capabilities, instructions, _meta.serverInfo} + opt list-changed handlers set and advertised by the server + c->>s: subscriptions/listen (not awaited) end note over c: Client.Connect() finished - note over c,s: Operation using 2026-06-30 version - else 2026-06-30 version not supported - s->>c: Method not found / UnsupportedProtocolVersionError - note over c,s: fall back to legacy flow + s-->>c: notifications/subscriptions/acknowledged + note over c,s: Operation using 2026-07-28 + else not supported + s->>c: Method not found, other error, or only legacy versions + note over c,s: fall back to the legacy flow c->>s: initialize {"protocolVersion": "2025-11-25"} - s->>c: initialized + s->>c: InitializeResult c->>s: notifications/initialized note over c: Client.Connect() finished - note over c,s: Operation using 2025-11-25 version + note over c,s: Operation using 2025-11-25 end ``` ## Removing sessions -SEP 2567 removes the concept of sessions from the MCP protocol. Some adjustments will be -needed to the `Stateless` mode to fully align it with the proposal. +SEP-2567 removes the concept of sessions from the protocol. `Stateless` mode +was adjusted to match, for every protocol version. -### `MCP-Session-Id` header removal +### `Mcp-Session-Id` header removal -Server in stateless mode will ignore the `MCP-Session-Id` header in the requests. -Currently it doesn't reject requests that specify this header, but it reads the -value and saves it in the transport to be used in other places. -The `ServerOptions.GetSessionID` will not be called by servers in the stateless -mode anymore. The `MCP-Session-Id` will also not be set on the response to -`initialize`, but this is a consequence of removal of the initialization handshake. +A stateless server doesn't read the `Mcp-Session-Id` request header, doesn't +call `ServerOptions.GetSessionID`, and never sets `Mcp-Session-Id` on +responses. `MCPGODEBUG=allowsessionsinstateless=1` restores the previous +behavior for legacy-protocol requests; `2026-07-28` requests never get a +session ID. -These behavior changes are backwards incompatible and will be accompanied by a `MCPGODEBUG` flag. +### DELETE method returns 405 + +A stateless server answers `DELETE` with `405 Method Not Allowed`, in line +with SEP-2575 allowing only `POST`. Previously it returned `400 Bad Request` +without an `Mcp-Session-Id` header and `204 No Content` otherwise; the same +`allowsessionsinstateless` setting restores that. -> [!WARNING] -> Deprecated APIs: +> [!NOTE] +> These APIs remain and are not deprecated, but they only apply to stateful +> servers and legacy protocol versions: > - `ServerOptions.GetSessionID` > - `StreamableHTTPOptions.SessionTimeout` > - `StreamableServerTransport.SessionID` > - `Connection.SessionID` > - `ErrSessionMissing` -> - `ServerSessionState` -> - `Server.Sessions` -> - `Session.ID` - -### DELETE method returns 405 - -The `DELETE` will return `405 Method Not Allowed` for all requests to the server in stateless mode. -This is in line with SEP-2575 that only `POST` requests should be supported. Currently it returns -`400 Bad Request` if `MCP-Session-Id` header is not provided and `204 No Content` otherwise. -The change will be protected by the same `MCPGODEBUG` flag as removal of the session related header. +> - `ServerSession.ID`, `ClientSession.ID` ## Removing stream resumability -SEP-2575 removes stream resumability. All related functionality will be deprecated. - -For `>= 2026-06-30`, the following behavioral changes will apply: - -* **Event IDs on SSE streams**: The server will stop writing event IDs (`id:` fields) to SSE - response streams. Currently, event IDs are only written when an `EventStore` is configured - (`streamable.go:1456-1458`). For the new protocol version, event IDs will not be written - regardless of whether an `EventStore` is provided. - -* **EventStore usage**: The server will not call `EventStore.Open`, `EventStore.Append`, or - `EventStore.SessionClosed` for `>= 2026-06-30` requests, even if an `EventStore` is configured - on the handler. Currently, the `EventStore` is called unconditionally when present - (`streamable.go:864-866`, `1446-1448`), including in stateless mode, which leads to - wasteful write-then-immediately-cleanup cycles. For the new protocol version, the `EventStore` - will be ignored entirely. +SEP-2575 removes stream resumability. For `>= 2026-07-28`: + +* **Event IDs and EventStore**: the server doesn't call `EventStore.Open`, + doesn't write the priming event, doesn't call `EventStore.Append`, and + doesn't write SSE event IDs, even if an `EventStore` is configured. + `EventStore.SessionClosed` is still called when a connection closes. +* **`Last-Event-ID` on POST**: rejected with `400 Bad Request` at every + version, as before. It was never valid. +* **Client reconnection**: the client isn't version-gated, but since the + server sends no event IDs there is nothing to resume. When a call's stream + breaks, the request is lost and the caller re-issues it. Listen streams are + not re-established automatically (see + [`subscriptions/listen`](#standalone-sse-stream-removal-and-subscriptionslisten)). +* **`CloseSSEStream`**: `RequestExtra.CloseSSEStream` is a no-op. The SEP-1699 + mechanism it implements triggered client reconnection with `Last-Event-ID`; + without resumability, closing the stream is the same as cancelling the + request. -* **`Last-Event-ID` on POST requests**: The server currently rejects POST requests that include - a `Last-Event-ID` header with `400 Bad Request` (`streamable.go:1100-1101`). This behavior - will remain unchanged — `Last-Event-ID` on POST was never valid and will continue to be - rejected. - -* **Client reconnection**: The client will stop sending `Last-Event-ID` on reconnection - attempts (`streamable.go:2207-2208`) and will stop capturing event IDs from SSE streams - (`streamable.go:2093-2094`). Reconnection for `subscriptions/listen` streams will still - be supported (re-establishing the subscription from scratch), but without `Last-Event-ID` - based replay. - -* **`CloseSSEStream` callback**: Calling `RequestExtra.CloseSSEStream` will be a no-op for - `>= 2026-06-30`. This mechanism (SEP-1699) was designed to trigger client reconnection - with `Last-Event-ID` for server-initiated disconnect scenarios. Without resumability, closing - the SSE stream is equivalent to cancelling the request, not triggering a reconnection. - -> [!WARNING] -> Deprecated APIs: +> [!NOTE] +> These APIs remain and are not deprecated, but they only apply to protocol +> versions before `2026-07-28`: > - `EventStore` > - `MemoryEventStore` > - `MemoryEventStoreOptions` @@ -488,5 +514,66 @@ For `>= 2026-06-30`, the following behavioral changes will apply: > - `ErrEventsPurged` > - `StreamableHTTPOptions.EventStore` > - `StreamableServerTransport.EventStore` -> - `RequestExtra.CloseSSEStream` -> - `CloseSSEStreamArgs` \ No newline at end of file +> - `RequestExtra.CloseSSEStream` (its doc comment notes the protocol +> deprecation) +> - `CloseSSEStreamArgs` + +## Deprecations + +The only APIs marked `// Deprecated:` for this protocol version are the roots, +sampling and logging APIs, deprecated by SEP-2577 as of `2026-07-28`. They keep +working for at least twelve months. They include: + +* Roots: `Client.AddRoots`, `Client.RemoveRoots`, `ServerSession.ListRoots`, + `ServerOptions.RootsListChangedHandler`, and the related types. +* Sampling: `ClientOptions.CreateMessageHandler`, + `ClientOptions.CreateMessageWithToolsHandler`, `ServerSession.CreateMessage`, + `ServerSession.CreateMessageWithTools`, and the related types. +* Logging: `ClientOptions.LoggingMessageHandler`, + `ClientSession.SetLoggingLevel`, `ServerSession.Log`, `LoggingHandler`, + `MetaKeyLogLevel`, and the related types. + +Elicitation, ping, keepalive, subscriptions, sessions, `InitializeResult`, +`InitializeParams` and the event store are not deprecated. For v2, it would +make sense to rename `ClientSession` and remove `ServerSession`. + +## MCPGODEBUG settings + +These settings restore earlier behavior changed by the stateless work. See +[docs/mcpgodebug.md](../docs/mcpgodebug.md) for details and removal plans. + +| Setting | Added | Restores | +|---|---|---| +| `allowsessionsinstateless` | 1.7.0 | Session IDs and `DELETE` in stateless mode | +| `noprotocolerrorbody` | 1.7.0 | Client ignores JSON-RPC error bodies on non-2xx responses, and any non-transient error fails the connection | +| `plaintextstatefulrejection` | 1.8.0 | Plain-text 400 from a stateful handler for new-protocol requests | +| `fatalnewprotocolerrors` | 1.9.0 | A failed `2026-07-28` call fails the whole streamable HTTP connection | + +## Changes from the original proposal + +* The target version is `2026-07-28`, not the draft `2026-06-30`. +* Unsupported versions return the dedicated `CodeUnsupportedProtocolVersion` + (-32022) instead of `INVALID_PARAMS`, alongside `CodeHeaderMismatch` and + `CodeMissingRequiredClientCapabilities`. +* `clientInfo` is optional in `_meta`; only `clientCapabilities` is required. +* `ServerSession.InitializeParams` returns parameters derived from the request + instead of `nil`. +* An `initialize` that asks for `2026-07-28` is negotiated down to a legacy + version instead of being rejected. +* `DiscoverResult` has no `serverInfo` field (server info is in `_meta`) and + embeds `Cacheable`. `server/discover` is not available to legacy clients. +* The client falls back to `initialize` on any discover error, and retries + discover once on `CodeUnsupportedProtocolVersion`. +* At connect, a list-changed listen also requires the server to advertise + `ListChanged`. Logging is not part of `subscriptions/listen`; the log level + travels with each request. Listen streams are not resubscribed + automatically. +* `ServerSession.Ping` and the server keepalive are not version-gated, and + `StreamableHTTPHandler` logs no warning for `KeepAlive` in stateless mode. +* Ignoring session IDs and answering `DELETE` with 405 apply to stateless mode + at every version, not only `2026-07-28`. +* Client-side resumability is not version-gated, and + `EventStore.SessionClosed` is still called. +* Apart from the SEP-2577 APIs, none of the APIs the proposal listed as + deprecated were deprecated. Those that only apply to legacy versions are + listed in the notes above.