Skip to content

Commit 20c6816

Browse files
halter73Copilot
andcommitted
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>
1 parent 7e703f3 commit 20c6816

File tree

1 file changed

+67
-3
lines changed

1 file changed

+67
-3
lines changed

docs/concepts/sessions/sessions.md

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ The MCP [Streamable HTTP transport] uses an `Mcp-Session-Id` HTTP header to asso
1515

1616
- Does your server need to send requests _to_ the client (sampling, elicitation, roots)? → **Use stateful.**
1717
- Does your server send unsolicited notifications or support resource subscriptions? → **Use stateful.**
18+
- Does your server manage per-client state that concurrent agents must not share (isolated environments, parallel workspaces)? → **Use stateful.**
19+
- Are you debugging a typically-stdio server over HTTP and want editors to be able to reset state by reconnecting? → **Use stateful.**
1820
- Otherwise → **Use stateless** (`options.Stateless = true`).
1921

2022
<!-- mlc-disable-next-line -->
@@ -91,7 +93,7 @@ When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless>
9193
- Server-to-client requests (sampling, elicitation, roots) via an open SSE stream
9294
- Unsolicited notifications (resource updates, logging messages)
9395
- Resource subscriptions
94-
- 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)
9597

9698
### When to use stateful mode
9799

@@ -101,7 +103,10 @@ Use stateful mode when your server needs one or more of:
101103
- **Unsolicited notifications**: Sending resource-changed notifications or log messages without a preceding client request
102104
- **Resource subscriptions**: Clients subscribing to resource changes and receiving updates
103105
- **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.
105110

106111
### Deployment footguns
107112

@@ -131,13 +136,22 @@ The SDK does not limit how long a handler can run or how many requests can be pr
131136

132137
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.
133138

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+
134148
## stdio transport
135149

136150
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.
137151

138152
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.
139153

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).
141155

142156
## Session lifecycle (HTTP)
143157

@@ -279,6 +293,53 @@ builder.Services.AddMcpServer()
279293
.WithTools<DefaultTools>();
280294
```
281295

296+
## Service lifetimes and DI scopes
297+
298+
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.
329+
options.ScopeRequests = false;
330+
})
331+
.WithStdioServerTransport()
332+
.WithTools<MyTools>();
333+
```
334+
335+
### Summary
336+
337+
| Mode | Service provider | ScopeRequests | Handler scope |
338+
|------|-----------------|---------------|---------------|
339+
| **Stateful HTTP** | Application services | `true` (default) | New async scope per handler invocation |
340+
| **Stateless HTTP** | `HttpContext.RequestServices` | `false` (forced) | Shared HTTP request scope |
341+
| **stdio** | Application services | `true` (default, configurable) | New async scope per handler invocation |
342+
282343
## User binding
283344

284345
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
357418
| **Unsolicited notifications** | Not supported | Supported (resource updates, logging) |
358419
| **Resource subscriptions** | Not supported | Supported |
359420
| **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

Comments
 (0)