Skip to content

Commit 2271620

Browse files
halter73Copilot
andcommitted
Add forward compat guidance, HTTP message delivery docs, and enhance transports.md
Add 'Forward and backward compatibility' section to sessions.md explaining why servers should set Stateless explicitly. Add 'How Streamable HTTP delivers messages' section defining solicited (POST response streams) vs unsolicited (GET stream) message delivery. Enhance transports.md with message flow overview, SSE backpressure explanation, and backpressure row in the transport comparison table. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4f885d2 commit 2271620

File tree

2 files changed

+52
-15
lines changed

2 files changed

+52
-15
lines changed

docs/concepts/sessions/sessions.md

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ uid: sessions
77

88
# Sessions
99

10-
The MCP [Streamable HTTP transport] uses an `Mcp-Session-Id` HTTP header to associate multiple requests with a single logical session. However, **we recommend most servers disable sessions entirely by setting <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> to `true`**. Stateless mode avoids the complexity, memory overhead, and deployment constraints that come with sessions. Sessions are only necessary when the server needs to send requests _to_ the client, push unsolicited notifications, or maintain per-client state across requests.
10+
The MCP [Streamable HTTP transport] uses an `Mcp-Session-Id` HTTP header to associate multiple requests with a single logical session. However, **we recommend most servers disable sessions entirely by setting <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> to `true`**. Stateless mode avoids the complexity, memory overhead, and deployment constraints that come with sessions. Sessions are only necessary when the server needs to send requests _to_ the client, push [unsolicited notifications](#how-streamable-http-delivers-messages), or maintain per-client state across requests.
1111

1212
When sessions are enabled (the current C# SDK default), the server creates and tracks an in-memory session for each client, while the client automatically includes the session ID in subsequent requests. The [MCP specification requires](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) that clients use sessions when a server's `initialize` response includes an `Mcp-Session-Id` header — this is not optional for the client. Session expiry detection and reconnection are the responsibility of the application using the client SDK (see [Client-side session behavior](#client-side-session-behavior)).
1313

@@ -16,15 +16,27 @@ When sessions are enabled (the current C# SDK default), the server creates and t
1616
**Quick guide — which mode should I use?**
1717

1818
- Does your server need to send requests _to_ the client (sampling, elicitation, roots)? → **Use stateful.**
19-
- Does your server send unsolicited notifications or support resource subscriptions? → **Use stateful.**
19+
- Does your server send [unsolicited notifications](#how-streamable-http-delivers-messages) or support resource subscriptions? → **Use stateful.**
2020
- Do you need to support clients that only speak the [legacy SSE transport](#legacy-sse-transport)? → **Use stateful** with <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> (disabled by default due to [backpressure concerns](#request-backpressure)).
2121
- Does your server manage per-client state that concurrent agents must not share (isolated environments, parallel workspaces)? → **Use stateful.**
2222
- Are you debugging a typically-stdio server over HTTP and want editors to be able to reset state by reconnecting? → **Use stateful.**
2323
- Otherwise → **Use stateless** (`options.Stateless = true`).
2424

2525
<!-- mlc-disable-next-line -->
2626
> [!NOTE]
27-
> **Why isn't stateless the C# SDK default?** Stateful mode remains the default for backward compatibility and because it is the only HTTP mode with full feature parity with [stdio](xref:transports) (server-to-client requests, unsolicited notifications, subscriptions). Stateless is the recommended choice when you don't need those features. If your server _does_ depend on stateful behavior, consider setting `Stateless = false` explicitly so your code is resilient to a potential future default change once [MRTR](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) or similar mechanisms bring server-to-client interactions to stateless mode.
27+
> **Why isn't stateless the C# SDK default?** Stateful mode remains the default for backward compatibility and because it is the only HTTP mode with full feature parity with [stdio](xref:transports) (server-to-client requests, unsolicited notifications, subscriptions). Stateless is the recommended choice when you don't need those features — see [Forward and backward compatibility](#forward-and-backward-compatibility) for guidance on choosing an explicit setting.
28+
29+
## Forward and backward compatibility
30+
31+
The `Stateless` property is the single most important setting for forward-proofing your MCP server. The current C# SDK default is `Stateless = false` (sessions enabled), but **we expect this default to change** once mechanisms like [MRTR](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) bring server-to-client interactions (sampling, elicitation, roots) to stateless mode. We recommend every server set `Stateless` explicitly rather than relying on the default:
32+
33+
- **`Stateless = true`** — the best forward-compatible choice. Your server opts out of sessions entirely. No matter how the SDK default changes in the future, your behavior stays the same. If you don't need [unsolicited notifications](#how-streamable-http-delivers-messages), server-to-client requests, or session-scoped state, this is the setting to use today.
34+
35+
- **`Stateless = false`** — the right choice when your server depends on sessions for features like sampling, elicitation, roots, unsolicited notifications, or per-client isolation. Setting this explicitly protects your server from a future default change. The [MCP specification requires](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) that clients use sessions when a server's `initialize` response includes an `Mcp-Session-Id` header, so compliant clients will always honor your server's session. Once [MRTR](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) or a similar mechanism is available, you may be able to migrate server-to-client interactions to stateless mode and drop sessions entirely — but until then, explicit `Stateless = false` is the safe choice. See [Stateless alternatives for server-to-client interactions](#stateless-alternatives-for-server-to-client-interactions) for more on MRTR.
36+
37+
<!-- mlc-disable-next-line -->
38+
> [!TIP]
39+
> If you're not sure which to pick, start with `Stateless = true`. You can switch to `Stateless = false` later if you discover you need server-to-client requests or unsolicited notifications. Either way, setting the property explicitly means your server's behavior won't silently change when the SDK default is updated.
2840
2941
## Stateless mode (recommended)
3042

@@ -61,7 +73,7 @@ When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless>
6173
- [Roots](xref:roots) (`RequestRootsAsync`)
6274

6375
The proposed [MRTR mechanism](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) is designed to bring these capabilities to stateless mode, but it is not yet available.
64-
- **Unsolicited server-to-client notifications** (e.g., resource update notifications, logging messages) are not supported. Every notification must be part of a direct response to a client request.
76+
- **[Unsolicited](#how-streamable-http-delivers-messages) server-to-client notifications** (e.g., resource update notifications, logging messages) are not supported. Every notification must be part of a direct response to a client POST request — see [How Streamable HTTP delivers messages](#how-streamable-http-delivers-messages) for why.
6577
- **No concurrent client isolation.** Every request is independent — the server cannot distinguish between two agents calling the same tool simultaneously, and there is no mechanism to maintain separate state per client.
6678
- **No state reset on reconnect.** Stateless servers have no concept of "the previous connection." There is no session to close and no fresh session to start. If your server holds any external state, you must manage cleanup through other means.
6779
- [Tasks](xref:tasks) **are supported** — the task store is shared across ephemeral server instances. However, task-augmented sampling and elicitation are disabled because they require server-to-client requests.
@@ -78,11 +90,7 @@ Use stateless mode when your server:
7890
- Needs to scale horizontally behind a load balancer without session affinity
7991
- Is deployed to serverless environments (Azure Functions, AWS Lambda, etc.)
8092

81-
Most MCP servers fall into this category. Tools that call APIs, query databases, process data, or return computed results are all natural fits for stateless mode.
82-
83-
<!-- mlc-disable-next-line -->
84-
> [!TIP]
85-
> If you're unsure whether you need sessions, start with stateless mode. You can always switch to stateful mode later if you need server-to-client requests or other session features.
93+
Most MCP servers fall into this category. Tools that call APIs, query databases, process data, or return computed results are all natural fits for stateless mode. See [Forward and backward compatibility](#forward-and-backward-compatibility) for guidance on choosing between stateless and stateful mode.
8694

8795
### Stateless alternatives for server-to-client interactions
8896

@@ -99,7 +107,7 @@ This means servers that need user confirmation, LLM reasoning, or other client i
99107
When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> is `false` (the default), the server assigns an `Mcp-Session-Id` to each client during the `initialize` handshake. The client must include this header in all subsequent requests. The server maintains an in-memory session for each connected client, enabling:
100108

101109
- Server-to-client requests (sampling, elicitation, roots) via an open HTTP response stream
102-
- Unsolicited notifications (resource updates, logging messages)
110+
- [Unsolicited notifications](#how-streamable-http-delivers-messages) (resource updates, logging messages) via the GET stream
103111
- Resource subscriptions
104112
- Session-scoped state (e.g., `RunSessionHandler`, state that persists across multiple requests within a session)
105113

@@ -108,7 +116,7 @@ When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless>
108116
Use stateful mode when your server needs one or more of:
109117

110118
- **Server-to-client requests**: Tools that call `ElicitAsync`, `SampleAsync`, or `RequestRootsAsync` to interact with the client
111-
- **Unsolicited notifications**: Sending resource-changed notifications or log messages without a preceding client request
119+
- **[Unsolicited notifications](#how-streamable-http-delivers-messages)**: Sending resource-changed notifications or log messages outside the context of any active request handler — these require the [GET stream](#how-streamable-http-delivers-messages)
112120
- **Resource subscriptions**: Clients subscribing to resource changes and receiving updates
113121
- **Legacy SSE client support**: Clients that only speak the [legacy SSE transport](#legacy-sse-transport) — requires <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> (disabled by default)
114122
- **Session-scoped state**: Logic that must persist across multiple requests within the same session
@@ -126,7 +134,7 @@ The [deployment considerations](#deployment-considerations) below are real conce
126134
| **Server restarts** | No impact — each request is independent | All sessions lost; clients must reinitialize |
127135
| **Memory** | Per-request only | Per-session (default: up to 10,000 sessions × 2 hours) |
128136
| **Server-to-client requests** | Not supported (see [MRTR proposal](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) for a stateless alternative) | Supported (sampling, elicitation, roots) |
129-
| **Unsolicited notifications** | Not supported | Supported (resource updates, logging) |
137+
| **[Unsolicited notifications](#how-streamable-http-delivers-messages)** | Not supported | Supported (resource updates, logging) |
130138
| **Resource subscriptions** | Not supported | Supported |
131139
| **Client compatibility** | Works with all Streamable HTTP clients | Also supports legacy SSE-only clients via <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> (disabled by default), but some Streamable HTTP clients [may not send `Mcp-Session-Id` correctly](#deployment-considerations) |
132140
| **Local development** | Works, but no way to reset server state from the editor | Editors can reset state by starting a new session without restarting the process |
@@ -138,6 +146,24 @@ The [deployment considerations](#deployment-considerations) below are real conce
138146

139147
### Streamable HTTP
140148

149+
#### How Streamable HTTP delivers messages
150+
151+
Understanding how messages flow between client and server over HTTP is key to understanding why sessions exist and when you can avoid them.
152+
153+
**POST response streams (solicited messages).** Every JSON-RPC request from the client arrives as an HTTP POST. The server holds the POST response body open as a [Server-Sent Events (SSE)](https://html.spec.whatwg.org/multipage/server-sent-events.html) stream and writes messages back to it: the JSON-RPC response, any intermediate messages the handler produces (progress notifications, log messages), and — critically — any **server-to-client requests** the handler makes during execution, such as sampling, elicitation, or roots requests. This is a **solicited** interaction: the client's POST request solicited the server's response, and the server writes everything related to that request into the same HTTP response body. The POST response completes when the final JSON-RPC response is sent.
154+
155+
**The GET stream (unsolicited messages).** The client can optionally open a long-lived HTTP GET request to the same MCP endpoint. This stream is the **only** channel for **unsolicited** messages — notifications or server-to-client requests that the server initiates _outside the context of any active request handler_. For example:
156+
157+
- A resource-changed notification fired by a background file watcher
158+
- A log message emitted asynchronously after all request handlers have returned
159+
- A server-to-client request that isn't triggered by a tool call
160+
161+
These messages are "unsolicited" because no client POST solicited them. There is no POST response body to write them to — because outside of POST requests that solicit the server 1:1 with a JSON-RPC request, there is simply no HTTP response body stream available. The GET stream fills this gap.
162+
163+
**No GET stream = messages silently dropped.** Clients are not required to open a GET stream. If the client hasn't opened one, the server has no delivery path for unsolicited messages and silently drops them. This is by design in the [Streamable HTTP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) — unsolicited messages are best-effort.
164+
165+
**Why stateless mode can't support unsolicited messages.** In stateless mode, the GET endpoint is not mapped at all. Every message the server sends must be part of a POST response — there is no other HTTP response body to write to. This is also why server-to-client requests (sampling, elicitation, roots) are disabled: the server could initiate a request down the POST response stream during a handler, but the client's response to that request would arrive as a _new_ POST — which in stateless mode creates a completely independent server context with no connection to the original handler. The server has no way to correlate the client's reply with the handler that asked the question. Sessions solve this by keeping the handler alive across multiple HTTP round-trips within the same in-memory session.
166+
141167
#### Session lifecycle
142168

143169
A session begins when a client sends an `initialize` JSON-RPC request without an `Mcp-Session-Id` header. The server:

0 commit comments

Comments
 (0)