From 938e1b2541b9b4cd4ad2c334daa42b8e72e9e0a9 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Fri, 24 Jul 2026 20:35:40 -0700 Subject: [PATCH 1/9] Refine 2.0.0 concept documentation - tasks.md: restore the intended information architecture by moving the "Compatibility with v1 experimental Tasks" section under "Architecture notes". - capabilities.md: correct the client variable reference (mcpClient -> client). - index.md: list "Stateless and Stateful" under Base protocol to match toc.yml. - getting-started.md, transports.md: standardize link text on "Stateless and Stateful" (the concept page's name) rather than "Sessions". - sampling.md, roots.md, logging.md: add deprecation notices for the Roots/Sampling/Logging utilities (SEP-2577); for sampling and roots, point to the stateless-compatible Multi round-trip requests (MRTR) alternative. - list-of-diagnostics.md: link SEP-2577 from the MCP9005 row. - logging.md: correct the Trace-level note. Trace maps to the MCP `debug` level when sent to the client; it is not silently dropped (McpServerImpl.ToLoggingLevel: LogLevel.Trace => LoggingLevel.Debug). - Normalize stray trailing spaces in two docs-sample snippet markers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 782b5bad-4046-49ec-93a6-72768b5b7521 --- docs/concepts/capabilities/capabilities.md | 4 +- docs/concepts/getting-started.md | 2 +- docs/concepts/index.md | 2 +- docs/concepts/logging/logging.md | 7 +++- .../samples/server/Tools/LoggingTools.cs | 2 +- .../samples/server/Tools/LongRunningTools.cs | 4 +- docs/concepts/roots/roots.md | 5 ++- docs/concepts/sampling/sampling.md | 5 ++- docs/concepts/tasks/tasks.md | 38 +++++++++---------- docs/concepts/transports/transports.md | 8 ++-- docs/list-of-diagnostics.md | 2 +- 11 files changed, 44 insertions(+), 35 deletions(-) diff --git a/docs/concepts/capabilities/capabilities.md b/docs/concepts/capabilities/capabilities.md index 76cc37512..d4c7642a9 100644 --- a/docs/concepts/capabilities/capabilities.md +++ b/docs/concepts/capabilities/capabilities.md @@ -83,11 +83,11 @@ if (client.ServerCapabilities.Resources is { Subscribe: true }) // Check if the server supports prompts with list-changed notifications if (client.ServerCapabilities.Prompts is { ListChanged: true }) { - mcpClient.RegisterNotificationHandler( + client.RegisterNotificationHandler( NotificationMethods.PromptListChangedNotification, async (notification, ct) => { - var prompts = await mcpClient.ListPromptsAsync(cancellationToken: ct); + var prompts = await client.ListPromptsAsync(cancellationToken: ct); }); } diff --git a/docs/concepts/getting-started.md b/docs/concepts/getting-started.md index 5ff681603..73901c14f 100644 --- a/docs/concepts/getting-started.md +++ b/docs/concepts/getting-started.md @@ -87,7 +87,7 @@ builder.Services.AddMcpServer() { // Stateless mode is recommended for servers that don't need // server-to-client requests like sampling or elicitation. - // See the Sessions documentation for details. + // See the Stateless and Stateful documentation for details. options.Stateless = true; }) .WithToolsFromAssembly(); diff --git a/docs/concepts/index.md b/docs/concepts/index.md index 5817426af..b3185056a 100644 --- a/docs/concepts/index.md +++ b/docs/concepts/index.md @@ -20,6 +20,7 @@ To install the SDK and build your first MCP client and server, see [Getting star | - | - | | [Capabilities](capabilities/capabilities.md) | Learn how client and server capabilities are negotiated during initialization, including protocol version negotiation. | | [Transports](transports/transports.md) | Learn how to configure stdio, Streamable HTTP, and SSE transports for client-server communication. | +| [Stateless and Stateful](stateless/stateless.md) | Learn when to use stateless vs. stateful mode for HTTP servers and how to configure sessions. | | [Ping](ping/ping.md) | Learn how to verify connection health using the ping mechanism. | | [Progress tracking](progress/progress.md) | Learn how to track progress for long-running operations through notification messages. | | [Cancellation](cancellation/cancellation.md) | Learn how to cancel in-flight MCP requests using cancellation tokens and notifications. | @@ -43,7 +44,6 @@ To install the SDK and build your first MCP client and server, see [Getting star | [Completions](completions/completions.md) | Learn how to implement argument auto-completion for prompts and resource templates. | | [Logging](logging/logging.md) | Learn how to implement logging in MCP servers and how clients can consume log messages. | | [Pagination](pagination/pagination.md) | Learn how to use cursor-based pagination when listing tools, prompts, and resources. | -| [Stateless and Stateful](stateless/stateless.md) | Learn when to use stateless vs. stateful mode for HTTP servers and how to configure sessions. | | [HTTP Context](httpcontext/httpcontext.md) | Learn how to access the underlying `HttpContext` for a request. | | [MCP Server Handler Filters](filters.md) | Learn how to add filters to the handler pipeline. Filters let you wrap the original handler with additional functionality. | diff --git a/docs/concepts/logging/logging.md b/docs/concepts/logging/logging.md index 8d705cf88..2b381aa54 100644 --- a/docs/concepts/logging/logging.md +++ b/docs/concepts/logging/logging.md @@ -11,6 +11,9 @@ MCP servers can expose log messages to clients through the [Logging utility]. [Logging utility]: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging +> [!IMPORTANT] +> Logging is **deprecated** as of MCP specification revision `2026-07-28` ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577), [MCP9005](xref:list-of-diagnostics#obsolete-apis)) and may be removed in a future version. + This document describes how to implement logging in MCP servers and how clients can consume log messages. ### Logging levels @@ -32,8 +35,8 @@ different set of logging levels. The following table shows the levels and how th | `emergency` | | System is unusable | | **Note:** .NET's [ILogger] also supports a `Trace` level (more verbose than Debug) log level. -As there is no equivalent level in the MCP logging levels, Trace level logs messages are silently -dropped when sending messages to the client. +As there is no more verbose level in the MCP logging levels, Trace level log messages are mapped to +the MCP `debug` level when sent to the client. [ILogger]: https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.ilogger [ILoggerProvider]: https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.iloggerprovider diff --git a/docs/concepts/logging/samples/server/Tools/LoggingTools.cs b/docs/concepts/logging/samples/server/Tools/LoggingTools.cs index 8ddb9e4df..edb39168b 100644 --- a/docs/concepts/logging/samples/server/Tools/LoggingTools.cs +++ b/docs/concepts/logging/samples/server/Tools/LoggingTools.cs @@ -16,7 +16,7 @@ public static async Task LoggingTool( var progressToken = context.Params.ProgressToken; var stepDuration = duration / steps; - // + // ILoggerProvider loggerProvider = context.Server.AsClientLoggerProvider(); ILogger logger = loggerProvider.CreateLogger("LoggingTools"); // diff --git a/docs/concepts/progress/samples/server/Tools/LongRunningTools.cs b/docs/concepts/progress/samples/server/Tools/LongRunningTools.cs index e02889a05..e3a21db7a 100644 --- a/docs/concepts/progress/samples/server/Tools/LongRunningTools.cs +++ b/docs/concepts/progress/samples/server/Tools/LongRunningTools.cs @@ -22,7 +22,7 @@ public static async Task LongRunningTool( { await Task.Delay(stepDuration * 1000); - // + // if (progressToken is not null) { await server.SendNotificationAsync("notifications/progress", new ProgressNotificationParams @@ -36,7 +36,7 @@ public static async Task LongRunningTool( }, }); } - // + // } return $"Long running tool completed. Duration: {duration} seconds. Steps: {steps}."; diff --git a/docs/concepts/roots/roots.md b/docs/concepts/roots/roots.md index ec26d435d..a7a8ed2e4 100644 --- a/docs/concepts/roots/roots.md +++ b/docs/concepts/roots/roots.md @@ -11,6 +11,9 @@ MCP [roots] allow clients to inform servers about the relevant locations in the [roots]: https://modelcontextprotocol.io/specification/2025-11-25/client/roots +> [!IMPORTANT] +> Roots are **deprecated** as of MCP specification revision `2026-07-28` ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577), [MCP9005](xref:list-of-diagnostics#obsolete-apis)) and may be removed in a future version. + ### Overview Roots provide a mechanism for the client to tell the server which directories, projects, or repositories are relevant to the current session. A server might use roots to: @@ -57,7 +60,7 @@ await using var client = await McpClient.CreateAsync(transport, options); ### Requesting roots from the server -Servers can request the client's root list using . This is a server-to-client request, so it requires [stateful mode or stdio](xref:stateless) — it is not available in [stateless mode](xref:stateless#stateless-mode-recommended). +Servers can request the client's root list using . This is a server-to-client request, so it requires [stateful mode or stdio](xref:stateless) — it is not available in [stateless mode](xref:stateless#stateless-mode-recommended). For a stateless-compatible alternative, throw `InputRequiredException` from your handler through MRTR — see [Multi round-trip requests (MRTR)](#multi-round-trip-requests-mrtr). ```csharp [McpServerTool, Description("Lists the user's project roots")] diff --git a/docs/concepts/sampling/sampling.md b/docs/concepts/sampling/sampling.md index bdeeaf910..3dcbd44c5 100644 --- a/docs/concepts/sampling/sampling.md +++ b/docs/concepts/sampling/sampling.md @@ -11,8 +11,11 @@ MCP [sampling] allows servers to request LLM completions from the client. This e [sampling]: https://modelcontextprotocol.io/specification/2025-11-25/client/sampling +> [!IMPORTANT] +> Sampling is **deprecated** as of MCP specification revision `2026-07-28` ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577), [MCP9005](xref:list-of-diagnostics#obsolete-apis)) and may be removed in a future version. + > [!NOTE] -> Sampling is a **server-to-client request** — the server sends a request back to the client over an open connection. This requires [stateful mode or stdio](xref:stateless). Sampling is not available in [stateless mode](xref:stateless#stateless-mode-recommended) because stateless servers cannot send requests to clients. +> Sampling is a **server-to-client request** — the server sends a request back to the client over an open connection. This requires [stateful mode or stdio](xref:stateless). Sampling is not available in [stateless mode](xref:stateless#stateless-mode-recommended) because stateless servers cannot send requests to clients. For a stateless-compatible alternative, throw `InputRequiredException` from your handler through MRTR — see [Multi round-trip requests (MRTR)](#multi-round-trip-requests-mrtr). ### How sampling works diff --git a/docs/concepts/tasks/tasks.md b/docs/concepts/tasks/tasks.md index 1bbcfea1a..c1ab0c23a 100644 --- a/docs/concepts/tasks/tasks.md +++ b/docs/concepts/tasks/tasks.md @@ -14,25 +14,6 @@ Tasks are provided by the `ModelContextProtocol.Extensions.Tasks` package and re version `2026-07-28` or later. The implementation follows [SEP-2663 (Tasks Extension)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md). -### Compatibility with v1 experimental Tasks - -The Tasks extension in v2.0.0 replaces the experimental Tasks implementation shipped in v1.3.0 and -v1.4.x. The implementations are not compatible at either the API or protocol level. A v2 Tasks -client or server must use a connection negotiated to `2026-07-28` or later; it cannot fall back to -the down-level implementation. - -On a connection negotiated to the down-level `2025-11-25` protocol: - -- A v2 client calling a v1 server receives an ordinary tool result. The v2 client does not opt in - to the down-level Tasks protocol, and `GetTaskAsync` rejects use before a `2026-07-28` - connection is negotiated. -- A v1 client calling a v2 server likewise receives an ordinary tool result. The v2 server does - not create Tasks on a down-level connection, and its `tasks/get` endpoint rejects the legacy - request with a method-not-found error. - -Upgrade both peers to the v2 Tasks extension before using Tasks. The extension provides no -compatibility bridge for the previous experimental API. - ### Overview A client opts into tasks on a per-request basis by including the `io.modelcontextprotocol/tasks` @@ -346,6 +327,25 @@ scope, the SDK intentionally skips the normal client-capability negotiation chec the client opted in by including the extension marker in the originating request, so it's responsible for handling — or rejecting — the input requests surfaced through `tasks/get`. +#### Compatibility with v1 experimental Tasks + +The Tasks extension in v2.0.0 replaces the experimental Tasks implementation shipped in v1.3.0 and +v1.4.x. The implementations are not compatible at either the API or protocol level. A v2 Tasks +client or server must use a connection negotiated to `2026-07-28` or later; it cannot fall back to +the down-level implementation. + +On a connection negotiated to the down-level `2025-11-25` protocol: + +- A v2 client calling a v1 server receives an ordinary tool result. The v2 client does not opt in + to the down-level Tasks protocol, and `GetTaskAsync` rejects use before a `2026-07-28` + connection is negotiated. +- A v1 client calling a v2 server likewise receives an ordinary tool result. The v2 server does + not create Tasks on a down-level connection, and its `tasks/get` endpoint rejects the legacy + request with a method-not-found error. + +Upgrade both peers to the v2 Tasks extension before using Tasks. The extension provides no +compatibility bridge for the previous experimental API. + ### Known limitations - **Server-push task status notifications (SEP-2575)**: not yet implemented. Clients rely on diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md index 1d924808e..63dcf789d 100644 --- a/docs/concepts/transports/transports.md +++ b/docs/concepts/transports/transports.md @@ -183,7 +183,7 @@ app.MapMcp(); app.Run(); ``` -By default, the HTTP transport runs **statelessly** — the server does not assign an `Mcp-Session-Id` or track transport session state in memory. This simplifies deployment, enables horizontal scaling without session affinity, and matches the `2026-07-28` Streamable HTTP wire format. Set `Stateless = false` explicitly when your server needs stateful sessions for unsolicited notifications, resource subscriptions, or per-client isolation. For a detailed guide on when to use stateless vs. stateful mode, configure session options, and understand [cancellation and disposal](xref:stateless#cancellation-and-disposal) behavior during shutdown, see [Sessions](xref:stateless). +By default, the HTTP transport runs **statelessly** — the server does not assign an `Mcp-Session-Id` or track transport session state in memory. This simplifies deployment, enables horizontal scaling without session affinity, and matches the `2026-07-28` Streamable HTTP wire format. Set `Stateless = false` explicitly when your server needs stateful sessions for unsolicited notifications, resource subscriptions, or per-client isolation. For a detailed guide on when to use stateless vs. stateful mode, configure session options, and understand [cancellation and disposal](xref:stateless#cancellation-and-disposal) behavior during shutdown, see [Stateless and Stateful](xref:stateless). #### Host name validation @@ -328,7 +328,7 @@ app.MapMcp(); app.Run(); ``` -See [Sessions — Legacy SSE transport](xref:stateless#legacy-sse-transport) for details on SSE session lifetime and configuration. +See [Stateless and Stateful — Legacy SSE transport](xref:stateless#legacy-sse-transport) for details on SSE session lifetime and configuration. ### Transport mode comparison @@ -345,7 +345,7 @@ See [Sessions — Legacy SSE transport](xref:stateless#legacy-sse-transport) for | Authentication | Process-level | HTTP auth (OAuth, headers) | HTTP auth (OAuth, headers) | HTTP auth (OAuth, headers) | | Best for | Local tools, IDE integrations | Remote servers, production deployments | Local HTTP debugging, server-to-client features | Legacy client compatibility | -For a detailed comparison of stateless vs. stateful mode — including deployment trade-offs, security considerations, and configuration — see [Sessions](xref:stateless). +For a detailed comparison of stateless vs. stateful mode — including deployment trade-offs, security considerations, and configuration — see [Stateless and Stateful](xref:stateless). ### In-memory transport @@ -380,7 +380,7 @@ var echo = tools.First(t => t.Name == "echo"); Console.WriteLine(await echo.InvokeAsync(new() { ["arg"] = "Hello World" })); ``` -Like [stdio](#stdio-transport), the in-memory transport is inherently single-session — there is no `Mcp-Session-Id` header, and server-to-client requests (sampling, elicitation, roots) work naturally over the bidirectional pipe. This makes it ideal for testing servers that depend on these features. For information about how session behavior varies across transports, see [Sessions](xref:stateless). +Like [stdio](#stdio-transport), the in-memory transport is inherently single-session — there is no `Mcp-Session-Id` header, and server-to-client requests (sampling, elicitation, roots) work naturally over the bidirectional pipe. This makes it ideal for testing servers that depend on these features. For information about how session behavior varies across transports, see [Stateless and Stateful](xref:stateless). ## Cross-application access diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index 284f24f94..e65646d1f 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -43,6 +43,6 @@ When APIs are marked as obsolete, a diagnostic is emitted to warn users that the | `MCP9002` | Removed | The `AddXxxFilter` extension methods on `IMcpServerBuilder` (for example, `AddListToolsFilter`, `AddCallToolFilter`, `AddIncomingMessageFilter`) were superseded by `WithRequestFilters()` and `WithMessageFilters()`. | | `MCP9003` | In place | The `RequestContext(McpServer, JsonRpcRequest)` constructor is obsolete. Use the overload that accepts a `parameters` argument: `RequestContext(McpServer, JsonRpcRequest, TParams)`. | | `MCP9004` | In place | opts into the legacy SSE transport which has no built-in HTTP-level backpressure. Use Streamable HTTP instead. See [Stateless — Legacy SSE transport](xref:stateless#legacy-sse-transport) for details. | -| `MCP9005` | In place | The Roots, Sampling, and Logging features are deprecated as of specification version 2026-07-28 and may be removed in a future version. See SEP-2577 for more information. | +| `MCP9005` | In place | The Roots, Sampling, and Logging features are deprecated as of specification version 2026-07-28 and may be removed in a future version. See [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) for more information. | | `MCP9006` | In place | The stateful Streamable HTTP configuration knobs on — `EventStreamStore`, `SessionMigrationHandler`, `PerSessionExecutionContext`, `IdleTimeout`, and `MaxIdleSessionCount` — only apply when `Stateless = false`. Starting with the `2026-07-28` protocol revision, Streamable HTTP no longer supports sessions, and the SDK now defaults `Stateless` to `true`. These knobs remain available for back-compat with the legacy stateful Streamable HTTP transport but new code should target the stateless path. | | `MCP9007` | In place | `AuthorizationRedirectDelegate` and `ClientOAuthOptions.AuthorizationRedirectDelegate` are retained for source and binary compatibility but cannot provide the RFC 9207 authorization-response issuer. Use `ClientOAuthOptions.AuthorizationCallbackHandler` for issuer-aware authorization flows. | From d35f190e3cf31c6417e735f236742733662418e3 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Fri, 24 Jul 2026 20:14:38 -0700 Subject: [PATCH 2/9] Fix TasksExtension sample crash: register transport via WithStreamServerTransport The sample registered the in-memory pipe transport by adding an ITransport singleton directly, which does not register the McpServer factory in DI. As a result, serviceProvider.GetRequiredService() threw "No service for type 'ModelContextProtocol.Server.McpServer' has been registered." at startup (regression from the Tasks extraction, #1693). Register the transport through .WithStreamServerTransport(...) on the builder, which calls AddSingleSessionServerDependencies and registers the McpServer factory. The sample now runs the task poll loop to completion. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b938335-418c-4ee7-8287-6c70d9148b89 --- samples/TasksExtension/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/TasksExtension/Program.cs b/samples/TasksExtension/Program.cs index 50ddf0d03..782398949 100644 --- a/samples/TasksExtension/Program.cs +++ b/samples/TasksExtension/Program.cs @@ -24,9 +24,9 @@ var services = new ServiceCollection(); services.AddMcpServer() + .WithStreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()) .WithTools([McpServerTool.Create(SlowTools.RunReport, new() { Name = "run-report" })]) .WithTasks(store); -services.AddSingleton(new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream())); await using var serviceProvider = services.BuildServiceProvider(); var server = serviceProvider.GetRequiredService(); From be78e04dafb8e2a7e403674149cc17f5c12396e7 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Fri, 24 Jul 2026 20:21:58 -0700 Subject: [PATCH 3/9] Sync TasksExtension README with current sample The README described a pre-#1693 two-path client (CallToolAsync auto-poll + CallToolRawAsync manual poll) that no longer exists. Program.cs demonstrates a single CallToolAsTaskAsync manual-poll path. Update the prose and the expected output block to match the current sample. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b938335-418c-4ee7-8287-6c70d9148b89 --- samples/TasksExtension/README.md | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/samples/TasksExtension/README.md b/samples/TasksExtension/README.md index f32506113..7cd5b8b08 100644 --- a/samples/TasksExtension/README.md +++ b/samples/TasksExtension/README.md @@ -6,16 +6,12 @@ The server is configured with an in-memory `IMcpTaskStore`, which is sufficient `[McpServerTool]` method automatically run as a background task when the client opts into the tasks extension on a per-request basis. -The client invokes the same `run-report` tool two ways: - -1. **`CallToolAsync` (auto-poll)** — the SDK injects the opt-in marker into `_meta`, polls - `tasks/get` at the cadence the server suggests, dispatches any input requests through - registered handlers, and returns the final `CallToolResult` (or throws on a terminal - `Failed`/`Cancelled`). -2. **`CallToolRawAsync` (manual poll)** — the caller receives the raw - `ResultOrCreatedTask` and drives the lifecycle directly with - `GetTaskAsync`, `UpdateTaskAsync`, and `CancelTaskAsync`. Useful when you need to surface - progress to a UI or stream task status updates between polls. +The client invokes the `run-report` tool with **`CallToolAsTaskAsync` (manual poll)**. It +receives a `ResultOrCreatedTask` and, when the server runs the call as a +background task, drives the lifecycle directly with `GetTaskAsync` — polling at the server's +suggested cadence until the task reaches a terminal state (`Completed`, `Failed`, or +`Cancelled`). If the server returns an inline result instead of creating a task, the sample +surfaces that result and stops. Both ends of the conversation are connected in-process over an in-memory `Pipe`, so no separate server process or HTTP transport is required. @@ -29,10 +25,7 @@ dotnet run --project samples/TasksExtension/TasksExtension.csproj Expected output: ``` -=== CallToolAsync (auto-poll) === - result: report ready - -=== CallToolRawAsync (manual poll) === +=== CallToolAsTaskAsync (manual poll) === task created: id=… status=Working pollIntervalMs=250 poll 1: still working … … From a9952cbb9b5054432e37dfa28929cc9467414508 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Sun, 26 Jul 2026 23:28:30 -0700 Subject: [PATCH 4/9] docs(mrtr): add "Supporting down-level clients" with confirmation sample Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/concepts/mrtr/mrtr.md | 99 +++++++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/docs/concepts/mrtr/mrtr.md b/docs/concepts/mrtr/mrtr.md index 5a498dac4..20a75410d 100644 --- a/docs/concepts/mrtr/mrtr.md +++ b/docs/concepts/mrtr/mrtr.md @@ -247,20 +247,107 @@ public static string WizardTool( } ``` -### Providing custom error messages +### Supporting down-level clients -When MRTR is not supported, you can provide domain-specific guidance: +When is `false`, the tool +can't obtain client input through an input request — but that doesn't have to be a dead +end. Handle down-level and stateless callers along a spectrum, from a helpful message to a +fully functional non-interactive path. + +#### Return actionable guidance + +At minimum, tell the caller how to proceed instead of failing opaquely. Explain what kind +of session would enable the interactive path, and — when the tool exposes one — how to +invoke the non-interactive path described below: ```csharp if (!server.IsMrtrSupported) { - return "This tool requires interactive input. To use it:\n" - + "1. Connect with a client that negotiates MCP protocol revision 2026-07-28, or\n" - + "2. Use a stateful session using protocol revision 2025-11-25 so the server can resolve the input requests for you.\n" - + "\nStateless sessions using protocol revision 2025-11-25 and earlier cannot resolve MRTR input requests."; + return "This tool needs interactive input. Connect with a client that negotiates MCP " + + "protocol revision 2026-07-28, or use a stateful session using revision 2025-11-25 " + + "so the server can resolve the input requests for you."; } ``` +#### Offer a non-interactive fallback + +When a tool *can* complete without a prompt, expose an explicit argument that lets a +down-level or stateless caller opt in directly. The tool stays usable everywhere: it +elicits confirmation when MRTR is available, and otherwise returns guidance the caller +(or its model) can act on by resending with the argument set. + +The confirmation tool below is just an example of a tool that has a sensible +non-interactive fallback. It also shows how to branch on the elicitation **action** rather +than assuming acceptance: the deserialized +carries an of `"accept"`, +`"decline"`, or `"cancel"`, surfaced through the + shorthand. + +```csharp +[McpServerTool, Description("Deletes a file (with required confirmation).")] +public static string DeleteFile( + McpServer server, + RequestContext context, + [Description("The path of the file to delete")] string path, + [Description("User confirmation to delete the file")] bool confirm = false) +{ + // Handles four client scenarios: + // 1. Explicit opt-in: client sends `confirm: true` + // 2. MRTR round-trip request: client sends `InputResponses["confirm"]` with action `accept` + // 3. MRTR initial request: server elicits input (with automatic SDK down-level bridge) + // 4. Session-less down-level: server returns a guidance message requesting explicit opt-in + + // (1) Explicit opt-in. Works on any client, including down-level session-less + // because a previous response gave instructions for passing `confirm: true`. + // These requests are typically sent after (4) returns a guidance message. + var deletionConfirmed = confirm; + + // (2) MRTR round-trip request. Works with native MRTR support or the automatic down-level + // SDK bridge after (3) throws an `InputRequiredException` to elicit input. + if (!confirm && context.Params?.InputResponses?.TryGetValue("confirm", out var confirmResponse) is true) + { + var confirmResult = confirmResponse.Deserialize(InputResponse.ElicitResultJsonTypeInfo); + deletionConfirmed = confirmResult?.IsAccepted is true; + + if (!deletionConfirmed) return "Deletion cancelled"; + } + + // (1) or (2) Explicit opt-in or confirmation input received; proceed with the deletion + if (deletionConfirmed) + return $"Deleted {path}."; + + // (3) MRTR initial request: elicit input to confirm the deletion. This uses the + // 2026-07-28 MRTR input request, but the SDK provides an automatic bridge to + // a legacy elicitation on a down-level, stateful session. When the bridge can + // be provided, `server.IsMrtrSupported` is `true` and the exception leads to + // a legacy elicitation response automatically. + if (server.IsMrtrSupported) + { + throw new InputRequiredException( + inputRequests: new Dictionary + { + ["confirm"] = InputRequest.ForElicitation(new ElicitRequestParams + { + Message = $"Delete {path}? This cannot be undone.", + RequestedSchema = new(), + }) + }, + requestState: path); // opaque; echoed back to us on the retry + } + + // (4) Down-level and stateless: we can't prompt an elicitation through an MRTR + // round-trip request or an elicitation. Return a natural language response + // with guidance for sending an explicit opt-in. + return $"Deletion requires user confirmation. Confirm by resending with `confirm: true`."; +} +``` + +> [!IMPORTANT] +> A confirmation prompt collects no form fields, but you must still set +> `RequestedSchema = new()`. The empty schema is accepted by native `2026-07-28` MRTR, and +> it's **required** by the down-level stateful elicitation bridge under `2025-11-25`: +> omitting it throws `ArgumentException: "Form mode elicitation requests require a requested schema."`. + ## Compatibility The SDK supports `InputRequiredException` across two protocol revisions and two session modes: From 136e379c313d9be058c78b4b5dabbb46b9cdf1ad Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Mon, 27 Jul 2026 11:16:40 -0700 Subject: [PATCH 5/9] Standardize MCP9004 link text on "Stateless and Stateful" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review feedback: the MCP9004 row linked [Stateless — Legacy SSE transport], but this PR standardizes the link text on "Stateless and Stateful" (the concept page's name) elsewhere. Rename to match. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 782b5bad-4046-49ec-93a6-72768b5b7521 --- docs/list-of-diagnostics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index ba870e523..6dca96f09 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -42,7 +42,7 @@ When APIs are marked as obsolete, a diagnostic is emitted to warn users that the | `MCP9001` | In place | The `EnumSchema` and `LegacyTitledEnumSchema` APIs are deprecated as of specification version 2025-11-25. Use the current schema APIs instead. | | `MCP9002` | Removed | The `AddXxxFilter` extension methods on `IMcpServerBuilder` (for example, `AddListToolsFilter`, `AddCallToolFilter`, `AddIncomingMessageFilter`) were superseded by `WithRequestFilters()` and `WithMessageFilters()`. | | `MCP9003` | In place | The `RequestContext(McpServer, JsonRpcRequest)` constructor is obsolete. Use the overload that accepts a `parameters` argument: `RequestContext(McpServer, JsonRpcRequest, TParams)`. | -| `MCP9004` | In place | opts into the legacy SSE transport which has no built-in HTTP-level backpressure. Use Streamable HTTP instead. See [Stateless — Legacy SSE transport](xref:stateless#legacy-sse-transport) for details. | +| `MCP9004` | In place | opts into the legacy SSE transport which has no built-in HTTP-level backpressure. Use Streamable HTTP instead. See [Stateless and Stateful — Legacy SSE transport](xref:stateless#legacy-sse-transport) for details. | | `MCP9005` | In place | The Roots, Sampling, and Logging features are deprecated as of specification version 2026-07-28 and may be removed in a future version. See [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) for more information. | | `MCP9006` | In place | The stateful Streamable HTTP configuration knobs on — `EventStreamStore`, `SessionMigrationHandler`, `PerSessionExecutionContext`, `IdleTimeout`, and `MaxIdleSessionCount` — only apply when `Stateless = false`. Starting with the `2026-07-28` protocol revision, Streamable HTTP no longer supports sessions, and the SDK now defaults `Stateless` to `true`. These knobs remain available for back-compat with the legacy stateful Streamable HTTP transport but new code should target the stateless path. | | `MCP9007` | In place | `AuthorizationRedirectDelegate` and `ClientOAuthOptions.AuthorizationRedirectDelegate` are retained for source and binary compatibility but cannot provide the RFC 9207 authorization-response issuer. Use `ClientOAuthOptions.AuthorizationCallbackHandler` for issuer-aware authorization flows. | From db8cb3cda512151e6460d343d39ebacf620bbb26 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Mon, 27 Jul 2026 17:37:11 -0700 Subject: [PATCH 6/9] Use a non-destructive confirmation sample in MRTR docs Replace the DeleteFile down-level confirmation example with a CloseSupportTicket example that matches the sample used in the accompanying blog post. The API surface is unchanged (InputRequiredException + InputRequest.ForElicitation, branching on the elicitation action via ElicitResult.IsAccepted); only the illustrative domain changes to a non-destructive action. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 782b5bad-4046-49ec-93a6-72768b5b7521 --- docs/concepts/mrtr/mrtr.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/concepts/mrtr/mrtr.md b/docs/concepts/mrtr/mrtr.md index 20a75410d..a93e44380 100644 --- a/docs/concepts/mrtr/mrtr.md +++ b/docs/concepts/mrtr/mrtr.md @@ -284,12 +284,12 @@ carries an of `"accept" shorthand. ```csharp -[McpServerTool, Description("Deletes a file (with required confirmation).")] -public static string DeleteFile( +[McpServerTool, Description("Closes a support ticket (with confirmation).")] +public static string CloseSupportTicket( McpServer server, RequestContext context, - [Description("The path of the file to delete")] string path, - [Description("User confirmation to delete the file")] bool confirm = false) + [Description("The ID of the ticket to close")] long ticketId, + [Description("User confirmation to close the ticket")] bool confirm = false) { // Handles four client scenarios: // 1. Explicit opt-in: client sends `confirm: true` @@ -300,23 +300,23 @@ public static string DeleteFile( // (1) Explicit opt-in. Works on any client, including down-level session-less // because a previous response gave instructions for passing `confirm: true`. // These requests are typically sent after (4) returns a guidance message. - var deletionConfirmed = confirm; + var closeConfirmed = confirm; // (2) MRTR round-trip request. Works with native MRTR support or the automatic down-level // SDK bridge after (3) throws an `InputRequiredException` to elicit input. if (!confirm && context.Params?.InputResponses?.TryGetValue("confirm", out var confirmResponse) is true) { var confirmResult = confirmResponse.Deserialize(InputResponse.ElicitResultJsonTypeInfo); - deletionConfirmed = confirmResult?.IsAccepted is true; + closeConfirmed = confirmResult?.IsAccepted is true; - if (!deletionConfirmed) return "Deletion cancelled"; + if (!closeConfirmed) return "Ticket close cancelled"; } - // (1) or (2) Explicit opt-in or confirmation input received; proceed with the deletion - if (deletionConfirmed) - return $"Deleted {path}."; + // (1) or (2) Explicit opt-in or confirmation input received; proceed with closing the ticket + if (closeConfirmed) + return $"Closed ticket {ticketId}."; - // (3) MRTR initial request: elicit input to confirm the deletion. This uses the + // (3) MRTR initial request: elicit input to confirm closing the ticket. This uses the // 2026-07-28 MRTR input request, but the SDK provides an automatic bridge to // a legacy elicitation on a down-level, stateful session. When the bridge can // be provided, `server.IsMrtrSupported` is `true` and the exception leads to @@ -328,17 +328,17 @@ public static string DeleteFile( { ["confirm"] = InputRequest.ForElicitation(new ElicitRequestParams { - Message = $"Delete {path}? This cannot be undone.", + Message = $"Close ticket '{ticketId}'? This will notify the requester.", RequestedSchema = new(), }) }, - requestState: path); // opaque; echoed back to us on the retry + requestState: ticketId.ToString()); // opaque; echoed back to us on the retry } // (4) Down-level and stateless: we can't prompt an elicitation through an MRTR // round-trip request or an elicitation. Return a natural language response // with guidance for sending an explicit opt-in. - return $"Deletion requires user confirmation. Confirm by resending with `confirm: true`."; + return "Closing a ticket requires user confirmation. Confirm by resending with `confirm: true`."; } ``` From 6e92ea24be4ff8022cf02ea923449894e5b8d4a1 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Mon, 27 Jul 2026 17:37:18 -0700 Subject: [PATCH 7/9] Remove stale MCPEXP001 note from TasksExtension README Address PR review feedback: the note singled out MCPEXP001, but the sample's Program.cs now suppresses MCPEXP001, MCPEXP002, and MCPEXP004, and the suppression mechanism is already self-documented by the project's and the pragma. Drop the redundant note; keep the durable IMcpTaskStore guidance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 782b5bad-4046-49ec-93a6-72768b5b7521 --- samples/TasksExtension/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/samples/TasksExtension/README.md b/samples/TasksExtension/README.md index 7cd5b8b08..accafa576 100644 --- a/samples/TasksExtension/README.md +++ b/samples/TasksExtension/README.md @@ -34,10 +34,6 @@ Expected output: ## Notes -- The `MCPEXP001` warning is suppressed because the tasks extension is still experimental. The - project's `` already includes it; if you copy this pattern into your own project, - either suppress the diagnostic or wrap the experimental APIs in - `#pragma warning disable MCPEXP001`. - For production deployments — especially stateless HTTP servers — implement `IMcpTaskStore` against durable storage and register it as a singleton (see [docs/concepts/tasks/tasks.md](../../docs/concepts/tasks/tasks.md) for the contract). From 52ab6aa5381704c7a8f1276680dccc9c6f7713a7 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Mon, 27 Jul 2026 19:28:38 -0700 Subject: [PATCH 8/9] Refine CloseSupportTicket MRTR sample to elicit a closeReason Evolve the down-level fallback sample from a boolean confirmation to a required closeReason string. The server proposes a default reason ("completed") in the elicitation schema and branches on the elicitation action via ElicitResult.IsAccepted, so a decline or cancel leaves the ticket open. Behavior validated across the full matrix: native MRTR over stateless HTTP, the legacy elicitation bridge over stateful HTTP, and the stateless down-level guidance path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 782b5bad-4046-49ec-93a6-72768b5b7521 --- docs/concepts/mrtr/mrtr.md | 97 +++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/docs/concepts/mrtr/mrtr.md b/docs/concepts/mrtr/mrtr.md index a93e44380..8730e6f37 100644 --- a/docs/concepts/mrtr/mrtr.md +++ b/docs/concepts/mrtr/mrtr.md @@ -273,63 +273,84 @@ if (!server.IsMrtrSupported) When a tool *can* complete without a prompt, expose an explicit argument that lets a down-level or stateless caller opt in directly. The tool stays usable everywhere: it -elicits confirmation when MRTR is available, and otherwise returns guidance the caller +elicits input when MRTR is available, and otherwise returns guidance the caller (or its model) can act on by resending with the argument set. -The confirmation tool below is just an example of a tool that has a sensible -non-interactive fallback. It also shows how to branch on the elicitation **action** rather -than assuming acceptance: the deserialized -carries an of `"accept"`, -`"decline"`, or `"cancel"`, surfaced through the +The tool below is just an example that has a sensible non-interactive fallback. It also +shows how to branch on the elicitation **action** rather than assuming acceptance: the +deserialized carries an + of `"accept"`, `"decline"`, or +`"cancel"`, surfaced through the shorthand. ```csharp -[McpServerTool, Description("Closes a support ticket (with confirmation).")] +[McpServerTool, Description("Closes a support ticket, recording why it was closed.")] public static string CloseSupportTicket( McpServer server, RequestContext context, [Description("The ID of the ticket to close")] long ticketId, - [Description("User confirmation to close the ticket")] bool confirm = false) + [Description("Why the ticket is being closed")] string? closeReason = null) { // Handles four client scenarios: - // 1. Explicit opt-in: client sends `confirm: true` - // 2. MRTR round-trip request: client sends `InputResponses["confirm"]` with action `accept` - // 3. MRTR initial request: server elicits input (with automatic SDK down-level bridge) - // 4. Session-less down-level: server returns a guidance message requesting explicit opt-in + // 1. Provided up-front: client sends `closeReason` in the initial call + // 2. MRTR round-trip request: client confirms via `InputResponses["closeReason"]` + // 3. MRTR initial request: server proposes a default reason and asks for confirmation + // (with automatic SDK down-level bridge) + // 4. Session-less down-level: server returns a guidance message requesting the reason up-front - // (1) Explicit opt-in. Works on any client, including down-level session-less - // because a previous response gave instructions for passing `confirm: true`. + // The default reason proposed to the caller and used if none is provided. + string defaultCloseReason = "completed"; + + // (1) Provided up-front. Works on any client, including down-level session-less. // These requests are typically sent after (4) returns a guidance message. - var closeConfirmed = confirm; + var confirmedReason = closeReason; // (2) MRTR round-trip request. Works with native MRTR support or the automatic down-level - // SDK bridge after (3) throws an `InputRequiredException` to elicit input. - if (!confirm && context.Params?.InputResponses?.TryGetValue("confirm", out var confirmResponse) is true) + // SDK bridge after (3) throws an `InputRequiredException` to request a `closeReason`. + if (string.IsNullOrWhiteSpace(confirmedReason) && + context.Params?.InputResponses?.TryGetValue("closeReason", out var reasonResponse) is true) { - var confirmResult = confirmResponse.Deserialize(InputResponse.ElicitResultJsonTypeInfo); - closeConfirmed = confirmResult?.IsAccepted is true; + var reasonResult = reasonResponse.Deserialize(InputResponse.ElicitResultJsonTypeInfo); + + // Branch on the elicitation action: `decline` or `cancel` leaves the ticket open. + if (reasonResult?.IsAccepted is not true) return "Ticket close cancelled"; - if (!closeConfirmed) return "Ticket close cancelled"; + // Accepted: use the reason the caller confirmed, falling back to the proposed default. + confirmedReason = reasonResult.Content?.TryGetValue("closeReason", out var reasonValue) is true + ? reasonValue.GetString() + : null; + confirmedReason = string.IsNullOrWhiteSpace(confirmedReason) ? defaultCloseReason : confirmedReason; } - // (1) or (2) Explicit opt-in or confirmation input received; proceed with closing the ticket - if (closeConfirmed) - return $"Closed ticket {ticketId}."; + // (1) or (2) A reason is in hand; proceed with closing the ticket. + if (!string.IsNullOrWhiteSpace(confirmedReason)) + return $"Closed ticket {ticketId}: {confirmedReason}"; - // (3) MRTR initial request: elicit input to confirm closing the ticket. This uses the - // 2026-07-28 MRTR input request, but the SDK provides an automatic bridge to - // a legacy elicitation on a down-level, stateful session. When the bridge can - // be provided, `server.IsMrtrSupported` is `true` and the exception leads to - // a legacy elicitation response automatically. + // (3) MRTR initial request: propose "completed" as the default reason and ask the caller + // to confirm (or adjust) it. This uses the 2026-07-28 MRTR input request, but the SDK + // provides an automatic bridge to a legacy elicitation on a down-level, stateful + // session. When the bridge can be provided, `server.IsMrtrSupported` is `true` and the + // exception leads to a legacy elicitation response automatically. if (server.IsMrtrSupported) { throw new InputRequiredException( inputRequests: new Dictionary { - ["confirm"] = InputRequest.ForElicitation(new ElicitRequestParams + ["closeReason"] = InputRequest.ForElicitation(new ElicitRequestParams { - Message = $"Close ticket '{ticketId}'? This will notify the requester.", - RequestedSchema = new(), + Message = $"Close ticket '{ticketId}'? Accept the default reason or provide your own.", + RequestedSchema = new() + { + Properties = + { + ["closeReason"] = new ElicitRequestParams.StringSchema + { + Title = "Close reason", + Description = "The reason for closing the ticket", + Default = defaultCloseReason, + }, + }, + }, }) }, requestState: ticketId.ToString()); // opaque; echoed back to us on the retry @@ -337,16 +358,18 @@ public static string CloseSupportTicket( // (4) Down-level and stateless: we can't prompt an elicitation through an MRTR // round-trip request or an elicitation. Return a natural language response - // with guidance for sending an explicit opt-in. - return "Closing a ticket requires user confirmation. Confirm by resending with `confirm: true`."; + // with guidance for providing the reason up-front. + return "Closing a ticket requires a reason. Resend with `closeReason`."; } ``` > [!IMPORTANT] -> A confirmation prompt collects no form fields, but you must still set -> `RequestedSchema = new()`. The empty schema is accepted by native `2026-07-28` MRTR, and -> it's **required** by the down-level stateful elicitation bridge under `2025-11-25`: -> omitting it throws `ArgumentException: "Form mode elicitation requests require a requested schema."`. +> Form-mode elicitations must set `RequestedSchema`. This tool proposes a default, so its +> schema declares an optional `closeReason` string with a `Default` the caller can accept or +> override. A confirmation prompt that collects no fields still needs a schema — set +> `RequestedSchema = new()` (an empty object schema): it's accepted by native `2026-07-28` MRTR +> and **required** by the down-level stateful elicitation bridge under `2025-11-25`, which +> otherwise throws `ArgumentException: "Form mode elicitation requests require a requested schema."`. ## Compatibility From 5578272a9a0e5f6686d5db9633250fbec131f15f Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Mon, 27 Jul 2026 19:53:10 -0700 Subject: [PATCH 9/9] Refine docs navigation for API Reference and Experimental APIs Add API Reference and Experimental APIs to the conceptual left-nav (after Extensions), remove Experimental APIs from the top-nav, and add a See also section to the Versioning page linking Experimental APIs and List of diagnostics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 782b5bad-4046-49ec-93a6-72768b5b7521 --- docs/concepts/toc.yml | 4 ++++ docs/toc.yml | 2 -- docs/versioning.md | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/concepts/toc.yml b/docs/concepts/toc.yml index 62c196825..276415e13 100644 --- a/docs/concepts/toc.yml +++ b/docs/concepts/toc.yml @@ -57,3 +57,7 @@ items: uid: tasks - name: Identity and Roles uid: identity +- name: API Reference + uid: ModelContextProtocol +- name: Experimental APIs + uid: experimental diff --git a/docs/toc.yml b/docs/toc.yml index e09c4b54c..953382b64 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -7,7 +7,5 @@ items: href: roadmap.md - name: Versioning href: versioning.md -- name: Experimental APIs - href: experimental.md - name: GitHub href: https://github.com/ModelContextProtocol/csharp-sdk diff --git a/docs/versioning.md b/docs/versioning.md index 1305e5fa7..5e6ea66ec 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -71,3 +71,8 @@ If APIs within the SDK become obsolete due to changes in the MCP spec or other e 3. Within a MAJOR version update, obsolete APIs might be removed. API removals are expected to be rare and avoided wherever possible, and `[Obsolete]` attributes will be applied ahead of the API removal. Beginning with the 1.0.0 release, all obsoletions will use diagnostic codes specific to the MCP SDK APIs, using an `MCP` prefix. + +## See also + +- [Experimental APIs](experimental.md) +- [List of diagnostics](list-of-diagnostics.md)