You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Legacy SSE endpoints (/sse and /message) are now disabled by default
because the SSE transport has no built-in HTTP-level backpressure --
POST returns 202 Accepted immediately without waiting for handler
completion. This means default stateful and stateless modes now provide
identical backpressure characteristics.
To opt in, set HttpServerTransportOptions.EnableLegacySse = true (marked
[Obsolete] with MCP9003) or use the AppContext switch
ModelContextProtocol.AspNetCore.EnableLegacySse. SSE endpoints remain
always disabled in stateless mode regardless of this setting.
Update sessions.md, transports.md, and list-of-diagnostics.md to
document the change, and migrate HttpTaskIntegrationTests to use
Streamable HTTP since they were only incidentally using SSE.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy file name to clipboardExpand all lines: docs/concepts/sessions/sessions.md
+17-12Lines changed: 17 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,7 +15,7 @@ The MCP [Streamable HTTP transport] uses an `Mcp-Session-Id` HTTP header to asso
15
15
16
16
- Does your server need to send requests _to_ the client (sampling, elicitation, roots)? → **Use stateful.**
17
17
- Does your server send unsolicited notifications or support resource subscriptions? → **Use stateful.**
18
-
- Do you need to support clients that only speak the [legacy SSE transport](#sse-legacy)? → **Use stateful.**(Legacy SSE endpoints are disabled in stateless mode.)
18
+
- Do you need to support clients that only speak the [legacy SSE transport](#sse-legacy)? → **Use stateful**with <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> (disabled by default due to [backpressure concerns](#sse-legacy-1)).
19
19
- Does your server manage per-client state that concurrent agents must not share (isolated environments, parallel workspaces)? → **Use stateful.**
20
20
- Are you debugging a typically-stdio server over HTTP and want editors to be able to reset state by reconnecting? → **Use stateful.**
@@ -52,7 +52,7 @@ When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless>
52
52
-<xref:ModelContextProtocol.McpSession.SessionId> is `null`, and the `Mcp-Session-Id` header is not sent or expected
53
53
- Each HTTP request creates a fresh server context — no state carries over between requests
54
54
-<xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.ConfigureSessionOptions> still works, but is called **per HTTP request** rather than once per session (see [Per-request configuration in stateless mode](#per-request-configuration-in-stateless-mode))
55
-
- The `GET` and `DELETE` MCP endpoints are not mapped, and the [legacy SSE endpoints](#sse-legacy) (`/sse` and `/message`) are disabled — clients that only support the legacy SSE transport cannot connect
55
+
- The `GET` and `DELETE` MCP endpoints are not mapped, and [legacy SSE endpoints](#sse-legacy) (`/sse` and `/message`) are always disabled in stateless mode — clients that only support the legacy SSE transport cannot connect
56
56
-**Server-to-client requests are disabled**, including:
57
57
-[Sampling](xref:sampling) (`SampleAsync`)
58
58
-[Elicitation](xref:elicitation) (`ElicitAsync`)
@@ -101,7 +101,7 @@ This means servers that need user confirmation, LLM reasoning, or other client i
101
101
102
102
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:
103
103
104
-
- Server-to-client requests (sampling, elicitation, roots) via an open SSE stream
104
+
- Server-to-client requests (sampling, elicitation, roots) via an open HTTP response stream
- Session-scoped state (e.g., `RunSessionHandler`, state that persists across multiple requests within a session)
@@ -113,7 +113,7 @@ Use stateful mode when your server needs one or more of:
113
113
-**Server-to-client requests**: Tools that call `ElicitAsync`, `SampleAsync`, or `RequestRootsAsync` to interact with the client
114
114
-**Unsolicited notifications**: Sending resource-changed notifications or log messages without a preceding client request
115
115
-**Resource subscriptions**: Clients subscribing to resource changes and receiving updates
116
-
-**Legacy SSE client support**: Clients that only speak the [legacy SSE transport](#sse-legacy)(the `/sse` and `/message` endpoints are only available in stateful mode)
116
+
-**Legacy SSE client support**: Clients that only speak the [legacy SSE transport](#sse-legacy)— requires <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> (disabled by default)
117
117
-**Session-scoped state**: Logic that must persist across multiple requests within the same session
118
118
-**Concurrent client isolation**: Multiple agents or editor instances connecting simultaneously, where per-client state must not leak between users — separate working environments, independent scratch state, or parallel simulations where each participant needs its own context. The server — not the model — controls when sessions are created, so the harness decides the boundaries of isolation.
119
119
-**Local development and debugging**: Testing a typically-stdio server over HTTP where you want to attach a debugger, see log output on stdout, and have editors like Claude Code, GitHub Copilot in VS Code, and Cursor reset the server's state by starting a new session — without requiring a process restart. This closely mirrors the stdio experience where restarting the server process gives the client a clean slate.
@@ -131,7 +131,7 @@ The [deployment considerations](#deployment-considerations) below are real conce
131
131
|**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) |
132
132
|**Unsolicited notifications**| Not supported | Supported (resource updates, logging) |
133
133
|**Resource subscriptions**| Not supported | Supported |
134
-
|**Client compatibility**| Works with all Streamable HTTP clients | Also supports legacy SSE-only clients, but some Streamable HTTP clients [may not send `Mcp-Session-Id` correctly](#deployment-considerations)|
134
+
|**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)|
135
135
|**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 |
136
136
|**Concurrent client isolation**| No distinction between clients — all requests are independent | Each client gets its own session with isolated state |
137
137
|**State reset on reconnect**| No concept of reconnection — every request stands alone | Client reconnection starts a new session with a clean slate |
@@ -208,7 +208,10 @@ Stateful sessions introduce several challenges for production, internet-facing s
208
208
209
209
### SSE (legacy)
210
210
211
-
The legacy [SSE (Server-Sent Events)](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) transport is also supported by `MapMcp()` and always uses stateful mode. Legacy SSE endpoints (`/sse` and `/message`) are only mapped when `Stateless = false` (the default), because the GET and POST requests must be handled by the same server process sharing in-memory session state.
211
+
The legacy [SSE (Server-Sent Events)](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) transport is also supported by `MapMcp()` and always uses stateful mode. Legacy SSE endpoints (`/sse` and `/message`) are **disabled by default** because the SSE transport has [no built-in HTTP-level backpressure](#sse-legacy-1). To enable them, set <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> to `true` — this property is marked `[Obsolete]` with a diagnostic warning (`MCP9003`) to signal that it should only be used when you need to support legacy SSE-only clients and understand the backpressure implications. Alternatively, set the `ModelContextProtocol.AspNetCore.EnableLegacySse`[AppContext switch](https://learn.microsoft.com/dotnet/api/system.appcontext) to `true`.
212
+
213
+
> [!NOTE]
214
+
> Setting `EnableLegacySse = true` while `Stateless = true` throws an `InvalidOperationException` at startup, because SSE requires in-memory session state shared between the GET and POST requests.
212
215
213
216
#### How SSE sessions work
214
217
@@ -472,7 +475,7 @@ For task-augmented requests, the specification [requires](https://modelcontextpr
472
475
473
476
### Request backpressure
474
477
475
-
How well the server is protected against a flood of concurrent requests depends on the session mode and which advanced features are enabled. The key factor is whether the HTTPPOST response stays open while the handler runs — because when it does, HTTP/2's `MaxStreamsPerConnection` (default: **100**) naturally limits how many concurrent handlers a single client connection can drive.
478
+
How well the server is protected against a flood of concurrent requests depends on the session mode and which advanced features are enabled. **In the default configuration, stateful and stateless modes provide identical HTTP-level backpressure** — both hold the POST response open while the handler runs, so HTTP/2's `MaxStreamsPerConnection` (default: **100**) naturally limits concurrent handlers per connection. The unbounded cases (legacy SSE, `EventStreamStore`, Tasks) are all **opt-in** advanced features.
476
479
477
480
#### Default stateful mode (no EventStreamStore, no tasks)
478
481
@@ -486,7 +489,9 @@ One difference from gRPC: handler cancellation tokens are linked to the **sessio
486
489
487
490
For comparison, ASP.NET Core SignalR limits concurrent hub invocations per client to **1** by default (`MaximumParallelInvocationsPerClient`). Default stateful MCP is less restrictive but still bounded by HTTP/2 stream limits.
488
491
489
-
#### SSE (legacy)
492
+
#### SSE (legacy — opt-in only)
493
+
494
+
Legacy SSE endpoints are [disabled by default](#sse-legacy) and must be explicitly enabled via <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse>. This is the primary reason they are disabled — the SSE transport has no built-in HTTP-level backpressure.
490
495
491
496
The legacy SSE transport separates the request and response channels: clients POST JSON-RPC messages to `/message` and receive responses through a long-lived GET SSE stream on `/sse`. The POST endpoint returns **202 Accepted immediately** after queuing the message — it does not wait for the handler to complete. This means there is **no HTTP-level backpressure** on handler concurrency, because each POST frees its connection immediately regardless of how long the handler runs.
492
497
@@ -523,15 +528,15 @@ For servers using the built-in automatic task handlers without external work dis
523
528
524
529
#### Stateless mode
525
530
526
-
Stateless mode has the strongest backpressure story. Each handler's lifetime is the HTTP request's lifetime — `McpServer.DisposeAsync()` awaits all in-flight handlers before the POST response completes. This means Kestrel's connection limits, HTTP/2 `MaxStreamsPerConnection`, request timeouts, and rate-limiting middleware all apply naturally — identical to a standard ASP.NET Core minimal API or controller action.
531
+
Stateless mode provides the same HTTP-level backpressure as default stateful mode. In both modes, each POST is held open until the handler responds. The one difference is cancellation: in stateless mode, the handler's `CancellationToken` is `HttpContext.RequestAborted`, so if a client disconnects mid-flight, the handler is cancelled immediately — identical to a standard ASP.NET Core minimal API or controller action. In default stateful mode, the handler's token is session-scoped, so a disconnected client's handler continues running until it completes or the session is terminated (see [Handler cancellation tokens](#handler-cancellation-tokens) above).
527
532
528
533
#### Summary
529
534
530
535
| Configuration | POST held open? | Backpressure mechanism | Concurrent handler limit per connection |
Copy file name to clipboardExpand all lines: docs/concepts/transports/transports.md
+12-6Lines changed: 12 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -113,7 +113,7 @@ await using var client = await McpClient.ResumeSessionAsync(transport, new Resum
113
113
114
114
#### Streamable HTTP server (ASP.NET Core)
115
115
116
-
Use the `ModelContextProtocol.AspNetCore` package to host an MCP server over HTTP. The <xref:Microsoft.AspNetCore.Builder.McpEndpointRouteBuilderExtensions.MapMcp*> method maps the Streamable HTTP endpoint at the specified route (root by default). It also maps legacy SSE endpoints at `{route}/sse` and `{route}/message` for backward compatibility.
116
+
Use the `ModelContextProtocol.AspNetCore` package to host an MCP server over HTTP. The <xref:Microsoft.AspNetCore.Builder.McpEndpointRouteBuilderExtensions.MapMcp*> method maps the Streamable HTTP endpoint at the specified route (root by default).
117
117
118
118
```csharp
119
119
varbuilder=WebApplication.CreateBuilder(args);
@@ -141,7 +141,7 @@ A custom route can be specified. For example, the [AspNetCoreMcpPerSessionTools]
141
141
app.MapMcp("/mcp");
142
142
```
143
143
144
-
When using a custom route, Streamable HTTP clients should connect directly to that route (e.g., `https://host/mcp`), while SSE clients should connect to `{route}/sse` (e.g., `https://host/mcp/sse`).
144
+
When using a custom route, Streamable HTTP clients should connect directly to that route (e.g., `https://host/mcp`), while SSE clients (when [legacy SSE is enabled](xref:sessions#sse-legacy)) should connect to `{route}/sse` (e.g., `https://host/mcp/sse`).
The ASP.NET Core integration supports SSE transport alongside Streamable HTTP. The same `MapMcp()` endpoint handles both protocols — clients connecting with SSE are automatically served using the legacy SSE mechanism. SSE requires stateful mode (the default); legacy SSE endpoints are not mapped when `Stateless = true`.
181
+
The ASP.NET Core integration supports SSE transport alongside Streamable HTTP. Legacy SSE endpoints (`/sse` and `/message`) are **disabled by default** due to [backpressure concerns](xref:sessions#sse-legacy-1). To enable them, set <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> to `true`. SSE always requires stateful mode; legacy SSE endpoints are never mapped when `Stateless = true`.
// SSE requires stateful mode (the default). Set explicitly for forward compatibility.
190
190
options.Stateless=false;
191
+
192
+
#pragmawarningdisable MCP9003 // EnableLegacySse is obsolete
193
+
// Enable legacy SSE endpoints for clients that don't support Streamable HTTP.
194
+
// See sessions doc for backpressure implications.
195
+
options.EnableLegacySse=true;
196
+
#pragmawarningrestore MCP9003
191
197
})
192
198
.WithTools<MyTools>();
193
199
194
200
varapp=builder.Build();
195
201
196
-
// MapMcp() serves both Streamable HTTPand legacy SSE.
197
-
//SSE clients connect to /sse (or {route}/sse for custom routes).
202
+
// MapMcp() serves Streamable HTTP. Legacy SSE (/sse and /message) is also
203
+
//available because EnableLegacySse is set to true above.
198
204
app.MapMcp();
199
205
app.Run();
200
206
```
201
207
202
-
No additional configuration is needed. When a client connects using the SSE protocol, the server responds with an SSE stream for server-to-client messages and accepts client-to-server messages via a separate POST endpoint.
208
+
See [Sessions — SSE (legacy)](xref:sessions#sse-legacy) for details on SSE session lifetime, configuration, and backpressure implications.
Copy file name to clipboardExpand all lines: docs/list-of-diagnostics.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -36,3 +36,4 @@ When APIs are marked as obsolete, a diagnostic is emitted to warn users that the
36
36
| :------------ | :----- | :---------- |
37
37
|`MCP9001`| In place | The `EnumSchema` and `LegacyTitledEnumSchema` APIs are deprecated as of specification version 2025-11-25. Use the current schema APIs instead. |
38
38
|`MCP9002`| Removed | The `AddXxxFilter` extension methods on `IMcpServerBuilder` (e.g., `AddListToolsFilter`, `AddCallToolFilter`, `AddIncomingMessageFilter`) were superseded by `WithRequestFilters()` and `WithMessageFilters()`. |
39
+
|`MCP9003`| In place |<xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> opts into the legacy SSE transport which has no built-in HTTP-level backpressure. Use Streamable HTTP instead. See [Sessions — SSE (legacy)](xref:sessions#sse-legacy) for details. |
publicconststringEnableLegacySse_Message="Legacy SSE transport has no built-in request backpressure and should only be used with completely trusted clients in isolated processes. Use Streamable HTTP instead.";
0 commit comments