Skip to content

Commit 93eecaa

Browse files
halter73Copilot
andcommitted
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>
1 parent b8f4034 commit 93eecaa

File tree

1 file changed

+61
-1
lines changed

1 file changed

+61
-1
lines changed

docs/concepts/sessions/sessions.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ 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+
- 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.)
1819
- Does your server manage per-client state that concurrent agents must not share (isolated environments, parallel workspaces)? → **Use stateful.**
1920
- Are you debugging a typically-stdio server over HTTP and want editors to be able to reset state by reconnecting? → **Use stateful.**
2021
- Otherwise → **Use stateless** (`options.Stateless = true`).
@@ -58,12 +59,13 @@ When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless>
5859
- <xref:ModelContextProtocol.McpSession.SessionId> is `null`, and the `Mcp-Session-Id` header is not sent or expected
5960
- Each HTTP request creates a fresh server context — no state carries over between requests
6061
- <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
6263
- **Server-to-client requests are disabled**, including:
6364
- [Sampling](xref:sampling) (`SampleAsync`)
6465
- [Elicitation](xref:elicitation) (`ElicitAsync`)
6566
- [Roots](xref:roots) (`RequestRootsAsync`)
6667
- 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.
6769

6870
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.
6971

@@ -118,6 +120,7 @@ Use stateful mode when your server needs one or more of:
118120
- **Server-to-client requests**: Tools that call `ElicitAsync`, `SampleAsync`, or `RequestRootsAsync` to interact with the client
119121
- **Unsolicited notifications**: Sending resource-changed notifications or log messages without a preceding client request
120122
- **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)
121124
- **Session-scoped state**: Logic that must persist across multiple requests within the same session
122125
- **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.
123126
- **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
139142
| **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 |
140143
| **Concurrent client isolation** | No distinction between clients — all requests are independent | Each client gets its own session with isolated state |
141144
| **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 |
142146

143147
## Transports and sessions
144148

@@ -328,6 +332,7 @@ builder.Services.AddMcpServer()
328332
})
329333
.WithTools<DefaultTools>();
330334
```
335+
331336
## Security
332337

333338
### User binding
@@ -345,6 +350,7 @@ When authentication is configured, the server automatically binds sessions to th
345350
4. If there's a mismatch, the server responds with `403 Forbidden`
346351

347352
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+
348354
## Service lifetimes and DI scopes
349355

350356
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
422428
- The `initialize` request cannot be cancelled (per the MCP specification)
423429
- Invalid or unknown request IDs are silently ignored
424430
- 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.
425432

426433
### Server and session disposal
427434

@@ -445,6 +452,59 @@ For stateless servers, shutdown is even simpler: each request is independent, so
445452
### Stateless per-request logging
446453

447454
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.
507+
448508
## Advanced features
449509

450510
### Session migration

0 commit comments

Comments
 (0)