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
Document DI service lifetimes and scoping behavior
Add 'Service lifetimes and DI scopes' section to sessions.md covering
how ScopeRequests controls per-handler scoping in stateful HTTP, how
stateless HTTP reuses ASP.NET Core's request scope, and how stdio
defaults to per-handler scoping but is configurable. Includes summary
table and cross-link from the stdio section.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Session-scoped state (e.g., per-session DI scopes, RunSessionHandler)
96
+
- Session-scoped state (e.g., `RunSessionHandler`, state that persists across multiple requests within a session)
95
97
96
98
### When to use stateful mode
97
99
@@ -101,7 +103,10 @@ Use stateful mode when your server needs one or more of:
101
103
-**Unsolicited notifications**: Sending resource-changed notifications or log messages without a preceding client request
102
104
-**Resource subscriptions**: Clients subscribing to resource changes and receiving updates
103
105
-**Session-scoped state**: Logic that must persist across multiple requests within the same session
104
-
-**Debugging stdio servers over HTTP**: When you want to test a typically stateful stdio server over HTTP while supporting concurrent connections from editors like Claude Code, GitHub Copilot in VS Code, Cursor, etc., sessions let you distinguish between them
106
+
-**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.
107
+
-**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.
108
+
109
+
The [deployment footguns](#deployment-footguns) below are real concerns for production, internet-facing services — but many MCP servers don't run in that context. For single-instance servers, internal tools, and dev/test clusters, session affinity and memory overhead are largely irrelevant, and sessions provide the richest feature set with no practical downsides.
105
110
106
111
### Deployment footguns
107
112
@@ -131,13 +136,22 @@ The SDK does not limit how long a handler can run or how many requests can be pr
131
136
132
137
Stateless mode is significantly more resilient here because each tool call is a standard HTTP request-response. This means Kestrel and IIS connection limits, request timeouts, and rate-limiting middleware all apply naturally. The <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.IdleTimeout> and <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.MaxIdleSessionCount> settings help protect against non-malicious overuse (e.g., a buggy client creating too many sessions), but they are not a substitute for HTTP-level protections.
133
138
139
+
### Convenience pitfalls of statelessness
140
+
141
+
Stateless mode trades features for simplicity. Before choosing it, consider what you give up:
142
+
143
+
-**No server-to-client requests.** Sampling, elicitation, and roots all require the server to send a JSON-RPC request back to the client over a persistent connection. Stateless mode has no such connection. The proposed [MRTR mechanism](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) is designed to solve this, but it is not yet available.
144
+
-**No push notifications.** The server cannot send unsolicited messages — log entries, resource-change events, or progress updates outside the scope of a tool call response. Every notification must be part of a direct response to a client request.
145
+
-**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.
146
+
-**No state reset on reconnect.** When a client disconnects and reconnects (e.g., an editor restarting), stateless servers have no concept of "the previous connection." There is no session to close and no fresh session to start — because there was never a session to begin with. If your server holds any external state, you must manage cleanup through other means.
147
+
134
148
## stdio transport
135
149
136
150
The [stdio transport](xref:transports) is inherently single-session. The client launches the server as a child process and communicates over stdin/stdout. There is exactly one session per process, the session starts when the process starts, and it ends when the process exits.
137
151
138
152
Because there is only one connection, stdio servers don't need session IDs or any explicit session management. The session is implicit in the process boundary. This makes stdio the simplest transport to use, and it naturally supports all server-to-client features (sampling, elicitation, roots) because there is always exactly one client connected.
139
153
140
-
However, stdio servers cannot be shared between multiple clients. Each client needs its own server process. This is fine for local tool integrations (IDEs, CLI tools) but not suitable for remote or multi-tenant scenarios — use [Streamable HTTP](xref:transports) for those.
154
+
However, stdio servers cannot be shared between multiple clients. Each client needs its own server process. This is fine for local tool integrations (IDEs, CLI tools) but not suitable for remote or multi-tenant scenarios — use [Streamable HTTP](xref:transports) for those. For details on how DI scopes work with stdio, see [Service lifetimes and DI scopes](#service-lifetimes-and-di-scopes).
How the server resolves scoped services depends on the transport and session mode. The <xref:ModelContextProtocol.Server.McpServerOptions.ScopeRequests> property controls whether the server creates a new `IServiceProvider` scope for each handler invocation.
299
+
300
+
### Stateful HTTP
301
+
302
+
In stateful mode, the server's <xref:ModelContextProtocol.McpServer.Services> is the application-level `IServiceProvider` — not a per-request scope. Because the server outlives individual HTTP requests, <xref:ModelContextProtocol.Server.McpServerOptions.ScopeRequests> defaults to `true`: each handler invocation (tool call, resource read, etc.) creates a new async scope via `IServiceScopeFactory.CreateAsyncScope()`. The scoped `IServiceProvider` is available on <xref:ModelContextProtocol.Server.RequestContext`1.Services>.
303
+
304
+
This means:
305
+
306
+
-**Scoped services** are created fresh for each handler invocation and disposed when the handler completes
307
+
-**Singleton services** resolve from the application container as usual
308
+
-**Transient services** create a new instance per resolution, as usual
309
+
310
+
### Stateless HTTP
311
+
312
+
In stateless mode, the server uses ASP.NET Core's per-request `HttpContext.RequestServices` as its service provider, and <xref:ModelContextProtocol.Server.McpServerOptions.ScopeRequests> is automatically set to `false`. No additional scopes are created — handlers share the same HTTP request scope that middleware and other ASP.NET Core components use.
313
+
314
+
This means:
315
+
316
+
-**Scoped services** behave exactly like any other ASP.NET Core request-scoped service — middleware can set state on a scoped service and the tool handler will see it
317
+
- The DI lifetime model is identical to a standard ASP.NET Core controller or minimal API endpoint
318
+
319
+
### stdio
320
+
321
+
The stdio transport creates a single server for the lifetime of the process. The server's <xref:ModelContextProtocol.McpServer.Services> is the application-level `IServiceProvider`. By default, <xref:ModelContextProtocol.Server.McpServerOptions.ScopeRequests> is `true`, so each handler invocation gets its own scope — the same behavior as stateful HTTP.
322
+
323
+
You can set <xref:ModelContextProtocol.Server.McpServerOptions.ScopeRequests> to `false` if you want handlers to resolve services directly from the root container. This can be useful for performance-sensitive scenarios where scope creation overhead matters, but be aware that scoped services will then behave like singletons for the lifetime of the process.
324
+
325
+
```csharp
326
+
builder.Services.AddMcpServer(options=>
327
+
{
328
+
// Disable per-handler scoping. Scoped services will resolve from the root container.
|**stdio**| Application services |`true` (default, configurable) | New async scope per handler invocation |
342
+
282
343
## User binding
283
344
284
345
When authentication is configured, the server automatically binds sessions to the authenticated user. This prevents one user from hijacking another user's session.
@@ -357,3 +418,6 @@ This is useful for clients that may experience transient network issues. Without
357
418
|**Unsolicited notifications**| Not supported | Supported (resource updates, logging) |
358
419
|**Resource subscriptions**| Not supported | Supported |
359
420
|**Client compatibility**| Works with all clients | Requires clients to track and send `Mcp-Session-Id`|
421
+
|**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 |
422
+
|**Concurrent client isolation**| No distinction between clients — all requests are independent | Each client gets its own session with isolated state |
423
+
|**State reset on reconnect**| No concept of reconnection — every request stands alone | Client reconnection starts a new session with a clean slate |
0 commit comments