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
Add tasks, cancellation, and observability coverage to sessions doc
Cover how tasks work in stateless vs stateful mode, the tasks/cancel
vs notifications/cancelled distinction, session-scoped task isolation,
and OpenTelemetry integration (mcp.session.id tag, session/operation
duration histograms, distributed tracing via _meta).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy file name to clipboardExpand all lines: docs/concepts/sessions/sessions.md
+61-1Lines changed: 61 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,6 +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
19
- Does your server manage per-client state that concurrent agents must not share (isolated environments, parallel workspaces)? → **Use stateful.**
19
20
- Are you debugging a typically-stdio server over HTTP and want editors to be able to reset state by reconnecting? → **Use stateful.**
@@ -58,12 +59,13 @@ When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless>
58
59
-<xref:ModelContextProtocol.McpSession.SessionId> is `null`, and the `Mcp-Session-Id` header is not sent or expected
59
60
- Each HTTP request creates a fresh server context — no state carries over between requests
60
61
-<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))
61
-
- The `GET` and `DELETE` MCP endpoints are not mapped, and the legacy `/sse`endpoint is disabled
62
+
- 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
62
63
-**Server-to-client requests are disabled**, including:
63
64
-[Sampling](xref:sampling) (`SampleAsync`)
64
65
-[Elicitation](xref:elicitation) (`ElicitAsync`)
65
66
-[Roots](xref:roots) (`RequestRootsAsync`)
66
67
- Unsolicited server-to-client notifications (e.g., resource update notifications, logging messages) are not supported
68
+
-[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.
67
69
68
70
These restrictions exist because in a stateless deployment, responses from the client could arrive at any server instance — not necessarily the one that sent the request.
69
71
@@ -118,6 +120,7 @@ Use stateful mode when your server needs one or more of:
118
120
-**Server-to-client requests**: Tools that call `ElicitAsync`, `SampleAsync`, or `RequestRootsAsync` to interact with the client
119
121
-**Unsolicited notifications**: Sending resource-changed notifications or log messages without a preceding client request
120
122
-**Resource subscriptions**: Clients subscribing to resource changes and receiving updates
123
+
-**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)
121
124
-**Session-scoped state**: Logic that must persist across multiple requests within the same session
122
125
-**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.
123
126
-**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.
@@ -139,6 +142,7 @@ The [deployment considerations](#deployment-considerations) below are real conce
139
142
|**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 |
140
143
|**Concurrent client isolation**| No distinction between clients — all requests are independent | Each client gets its own session with isolated state |
141
144
|**State reset on reconnect**| No concept of reconnection — every request stands alone | Client reconnection starts a new session with a clean slate |
145
+
|**[Tasks](xref:tasks)**| Supported — shared task store, no per-session isolation | Supported — task store scoped per session |
@@ -345,6 +350,7 @@ When authentication is configured, the server automatically binds sessions to th
345
350
4. If there's a mismatch, the server responds with `403 Forbidden`
346
351
347
352
This binding is automatic — no configuration is needed. If no authentication middleware is configured, user binding is skipped (the session is not bound to any user).
353
+
348
354
## Service lifetimes and DI scopes
349
355
350
356
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.
@@ -422,6 +428,7 @@ In stateful modes (Streamable HTTP, SSE, stdio), a client can cancel a specific
422
428
- The `initialize` request cannot be cancelled (per the MCP specification)
423
429
- Invalid or unknown request IDs are silently ignored
424
430
- In stateless mode, there is no persistent session to receive the notification on, so client-initiated cancellation does not apply
431
+
- For [task-augmented requests](xref:tasks), the MCP specification requires using [`tasks/cancel`](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#cancelling-tasks) instead of `notifications/cancelled`. The SDK uses a separate cancellation token per task (independent of the original HTTP request), so `tasks/cancel` can cancel a task even after the initial request has completed. See [Tasks and session modes](#tasks-and-session-modes) for details.
425
432
426
433
### Server and session disposal
427
434
@@ -445,6 +452,59 @@ For stateless servers, shutdown is even simpler: each request is independent, so
445
452
### Stateless per-request logging
446
453
447
454
In stateless mode, each HTTP request creates and disposes a short-lived `McpServer` instance. This produces session lifecycle log entries at `Trace` level (`session created` / `session disposed`) for every request. These are typically invisible at default log levels but may appear when troubleshooting with verbose logging enabled. There is no user-facing `initialize` handshake in stateless mode — the SDK handles the per-request server lifecycle internally.
455
+
456
+
### Tasks and session modes
457
+
458
+
[Tasks](xref:tasks) enable a "call-now, fetch-later" pattern for long-running tool calls. Task support depends on having an <xref:ModelContextProtocol.IMcpTaskStore> configured (`McpServerOptions.TaskStore`), and behavior differs between session modes.
459
+
460
+
#### Stateless mode
461
+
462
+
Tasks are a natural fit for stateless servers. The client sends a task-augmented `tools/call` request, receives a task ID immediately, and polls for completion with `tasks/get` or `tasks/result` on subsequent independent HTTP requests. Because each request creates an ephemeral `McpServer` that shares the same `IMcpTaskStore`, all task operations work without any persistent session.
463
+
464
+
In stateless mode, there is no `SessionId`, so the task store does not apply session-based isolation. All tasks are accessible from any request to the same server. This is typically fine for single-purpose servers or when authentication middleware already identifies the caller.
465
+
466
+
#### Stateful mode
467
+
468
+
In stateful mode, the `IMcpTaskStore` receives the session's `SessionId` on every operation — `CreateTaskAsync`, `GetTaskAsync`, `ListTasksAsync`, `CancelTaskAsync`, etc. The built-in <xref:ModelContextProtocol.InMemoryMcpTaskStore> enforces session isolation: tasks created in one session cannot be accessed from another.
469
+
470
+
Tasks can outlive individual HTTP requests because the tool executes in the background after returning the initial `CreateTaskResult`. Task cleanup is governed by the task's TTL (time-to-live), not by session termination. However, the `InMemoryMcpTaskStore` loses all tasks if the server process restarts. For durable tasks, implement a custom <xref:ModelContextProtocol.IMcpTaskStore> backed by an external store. See [Fault-tolerant task implementations](xref:tasks#fault-tolerant-task-implementations) for guidance.
471
+
472
+
#### Task cancellation vs request cancellation
473
+
474
+
The MCP specification defines two distinct cancellation mechanisms:
475
+
476
+
-**`notifications/cancelled`** cancels a regular in-flight request by its JSON-RPC request ID. The SDK looks up the handler's `CancellationToken` and cancels it. This is a fire-and-forget notification with no response.
477
+
-**`tasks/cancel`** cancels a task by its task ID. The SDK signals a separate per-task `CancellationToken` (independent of the original request) and updates the task's status to `cancelled` in the store. This is a request-response operation that returns the final task state.
478
+
479
+
For task-augmented requests, the specification [requires](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation) using `tasks/cancel` instead of `notifications/cancelled`.
480
+
481
+
### Observability
482
+
483
+
The SDK automatically integrates with [.NET's OpenTelemetry support](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing) and attaches session metadata to traces and metrics.
484
+
485
+
#### Activity tags
486
+
487
+
Every server-side request activity is tagged with `mcp.session.id` — the session's unique identifier. In stateless mode, this tag is `null` because there is no persistent session. Other tags include `mcp.method.name`, `mcp.protocol.version`, `jsonrpc.request.id`, and operation-specific tags like `gen_ai.tool.name` for tool calls.
488
+
489
+
Use these tags to filter and correlate traces by session in your observability platform (Jaeger, Zipkin, Application Insights, etc.).
490
+
491
+
#### Metrics
492
+
493
+
The SDK records histograms under the `Experimental.ModelContextProtocol` meter:
494
+
495
+
| Metric | Description |
496
+
|--------|-------------|
497
+
|`mcp.server.session.duration`| Duration of the MCP session on the server |
498
+
|`mcp.client.session.duration`| Duration of the MCP session on the client |
499
+
|`mcp.server.operation.duration`| Duration of each request/notification on the server |
500
+
|`mcp.client.operation.duration`| Duration of each request/notification on the client |
501
+
502
+
In stateless mode, each HTTP request is its own "session", so `mcp.server.session.duration` measures individual request lifetimes rather than long-lived session durations.
503
+
504
+
#### Distributed tracing
505
+
506
+
The SDK propagates [W3C trace context](https://www.w3.org/TR/trace-context/) (`traceparent` / `tracestate`) through JSON-RPC messages via the `_meta` field. This means a client's tool call and the server's handling of that call appear as parent-child spans in a distributed trace, regardless of transport.
0 commit comments