V2 Edit pass (#1728)
This commit is contained in:
parent
2d1b9a3462
commit
b283596df1
24 changed files with 263 additions and 251 deletions
|
|
@ -111,11 +111,11 @@ public static string GetWeather(McpServer server, string location)
|
||||||
|
|
||||||
The `Visibility` property controls which principals can invoke the tool:
|
The `Visibility` property controls which principals can invoke the tool:
|
||||||
|
|
||||||
| Value | Meaning |
|
| Value | Meaning |
|
||||||
| - | - |
|
|-----------------------------|----------------------------------------------------|
|
||||||
| `McpUiToolVisibility.Model` | Only the LLM can call this tool |
|
| `McpUiToolVisibility.Model` | Only the LLM can call this tool |
|
||||||
| `McpUiToolVisibility.App` | Only the app UI can call this tool |
|
| `McpUiToolVisibility.App` | Only the app UI can call this tool |
|
||||||
| Both (or null/empty) | Both the model and app can call the tool (default) |
|
| Both (or null/empty) | Both the model and app can call the tool (default) |
|
||||||
|
|
||||||
## UI resources
|
## UI resources
|
||||||
|
|
||||||
|
|
@ -164,7 +164,7 @@ The MCP Apps spec defines display modes (`inline`, `fullscreen`, `pip`) that con
|
||||||
|
|
||||||
## Host theming
|
## Host theming
|
||||||
|
|
||||||
Hosts pass standardized CSS custom properties (e.g., `--color-background-primary`, `--color-text-primary`) to app iframes. Your HTML can reference these variables to automatically match the host's theme without any server-side configuration.
|
Hosts pass standardized CSS custom properties (for example, `--color-background-primary`, `--color-text-primary`) to app iframes. Your HTML can reference these variables to automatically match the host's theme without any server-side configuration.
|
||||||
|
|
||||||
See the [MCP Apps specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) for the full list of CSS variables.
|
See the [MCP Apps specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) for the full list of CSS variables.
|
||||||
|
|
||||||
|
|
@ -176,10 +176,10 @@ The default Content Security Policy restricts external script and style loads. F
|
||||||
|
|
||||||
The <xref:ModelContextProtocol.Extensions.Apps.McpApps> class provides constants for protocol values:
|
The <xref:ModelContextProtocol.Extensions.Apps.McpApps> class provides constants for protocol values:
|
||||||
|
|
||||||
| Constant | Value | Usage |
|
| Constant | Value | Usage |
|
||||||
| - | - | - |
|
|------------------------|------------------------------|-------------------------------------------|
|
||||||
| `McpApps.HtmlMimeType` | `text/html;profile=mcp-app` | MIME type for UI resources |
|
| `McpApps.HtmlMimeType` | `text/html;profile=mcp-app` | MIME type for UI resources |
|
||||||
| `McpApps.ExtensionId` | `io.modelcontextprotocol/ui` | Key in `extensions` capability dictionary |
|
| `McpApps.ExtensionId` | `io.modelcontextprotocol/ui` | Key in `extensions` capability dictionary |
|
||||||
|
|
||||||
## Serialization
|
## Serialization
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ The cancellation notification includes:
|
||||||
- **RequestId**: The ID of the request to cancel, allowing the receiver to correlate the cancellation with the correct in-flight request.
|
- **RequestId**: The ID of the request to cancel, allowing the receiver to correlate the cancellation with the correct in-flight request.
|
||||||
- **Reason**: An optional human-readable reason for the cancellation.
|
- **Reason**: An optional human-readable reason for the cancellation.
|
||||||
|
|
||||||
Cancellation notifications can be observed by registering a handler. For broader interception of notifications and other messages, <xref:ModelContextProtocol.Server.McpMessageFilter> delegates can be added to the <xref:ModelContextProtocol.Server.McpMessageFilters.IncomingFilters> collection in <xref:ModelContextProtocol.Server.McpServerOptions.Filters>.
|
Cancellation notifications can be observed by registering a handler. For broader interception of notifications and other messages, you can add <xref:ModelContextProtocol.Server.McpMessageFilter> delegates to the <xref:ModelContextProtocol.Server.McpMessageFilters.IncomingFilters> collection in <xref:ModelContextProtocol.Server.McpServerOptions.Filters>.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
mcpClient.RegisterNotificationHandler(
|
mcpClient.RegisterNotificationHandler(
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ var options = new McpClientOptions
|
||||||
await using var client = await McpClient.CreateAsync(transport, options);
|
await using var client = await McpClient.CreateAsync(transport, options);
|
||||||
```
|
```
|
||||||
|
|
||||||
Handlers for each capability (roots, sampling, elicitation) are covered in their respective documentation pages.
|
Handlers for each capability (roots, sampling, and elicitation) are covered in their respective documentation pages.
|
||||||
|
|
||||||
### Server capabilities
|
### Server capabilities
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ Server capabilities are automatically inferred from the configured features. For
|
||||||
|
|
||||||
Before using an optional feature, check whether the other side declared the corresponding capability.
|
Before using an optional feature, check whether the other side declared the corresponding capability.
|
||||||
|
|
||||||
#### Checking server capabilities from the client
|
#### Check server capabilities from the client
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
await using var client = await McpClient.CreateAsync(transport);
|
await using var client = await McpClient.CreateAsync(transport);
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ MCP [completions] allow servers to provide argument auto-completion suggestions
|
||||||
|
|
||||||
Completions work with two types of references:
|
Completions work with two types of references:
|
||||||
|
|
||||||
- **Prompt argument completions**: Suggest values for prompt parameters (e.g., language names, style options)
|
- **Prompt argument completions**: Suggest values for prompt parameters (for example, language names, style options)
|
||||||
- **Resource template argument completions**: Suggest values for URI template parameters (e.g., file paths, resource IDs)
|
- **Resource template argument completions**: Suggest values for URI template parameters (for example, file paths, resource IDs)
|
||||||
|
|
||||||
The server returns a <xref:ModelContextProtocol.Protocol.Completion> object containing a list of suggested values, an optional total count, and a flag indicating if more values are available.
|
The server returns a <xref:ModelContextProtocol.Protocol.Completion> object containing a list of suggested values, an optional total count, and a flag indicating if more values are available.
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ builder.Services.AddMcpServer()
|
||||||
|
|
||||||
### Automatic completions with AllowedValuesAttribute
|
### Automatic completions with AllowedValuesAttribute
|
||||||
|
|
||||||
For parameters with a known set of valid values, you can use `System.ComponentModel.DataAnnotations.AllowedValuesAttribute` on `string` parameters of prompts or resource templates. The server will automatically surface those values as completions without needing a custom completion handler.
|
For parameters with a known set of valid values, you can use `System.ComponentModel.DataAnnotations.AllowedValuesAttribute` on `string` parameters of prompts or resource templates. The server automatically surfaces those values as completions without needing a custom completion handler.
|
||||||
|
|
||||||
#### Prompt parameters
|
#### Prompt parameters
|
||||||
|
|
||||||
|
|
@ -115,11 +115,11 @@ public class MyResources
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
With these attributes in place, when a client sends a `completion/complete` request for the `language` or `section` argument, the server will automatically filter and return matching values based on what the user has typed so far. This approach can be combined with a custom completion handler registered via `WithCompleteHandler`; the handler's results are returned first, followed by any matching `AllowedValues`.
|
With these attributes in place, when a client sends a `completion/complete` request for the `language` or `section` argument, the server automatically filters and returns matching values based on what the user has typed so far. This approach can be combined with a custom completion handler registered via `WithCompleteHandler`; the handler's results are returned first, followed by any matching `AllowedValues`.
|
||||||
|
|
||||||
### Requesting completions on the client
|
### Requesting completions on the client
|
||||||
|
|
||||||
Clients request completions using <xref:ModelContextProtocol.Client.McpClient.CompleteAsync*>. Provide a reference to the prompt or resource template, the argument name, and the current partial value:
|
Clients request completions using <xref:ModelContextProtocol.Client.McpClient.CompleteAsync*>. Provide a reference to the prompt or resource template, the argument name, and the current partial value.
|
||||||
|
|
||||||
#### Prompt argument completions
|
#### Prompt argument completions
|
||||||
|
|
||||||
|
|
@ -142,7 +142,7 @@ if (result.Completion.HasMore == true)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Resource template argument completions
|
#### Resource-template argument completions
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Get completions for a resource template argument
|
// Get completions for a resource template argument
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ uid: elicitation
|
||||||
|
|
||||||
## Elicitation
|
## Elicitation
|
||||||
|
|
||||||
The **elicitation** feature allows servers to request additional information from users during interactions. This enables more dynamic and interactive AI experiences, making it easier to gather necessary context before executing tasks.
|
The **elicitation** feature allows servers to request additional information from users during interactions. This feature enables more dynamic and interactive AI experiences, making it easier to gather necessary context before executing tasks.
|
||||||
|
|
||||||
The protocol supports two modes of elicitation:
|
The protocol supports two modes of elicitation:
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ so tools can simply add a parameter of type <xref:ModelContextProtocol.Server.Mc
|
||||||
|
|
||||||
#### Form Mode Elicitation (In-Band)
|
#### Form Mode Elicitation (In-Band)
|
||||||
|
|
||||||
For form-based elicitation, the MCP Server must specify the schema of each input value it is requesting from the user.
|
For form-based elicitation, the MCP Server must specify the schema of each input value it's requesting from the user.
|
||||||
Primitive types (string, number, Boolean) and enum types are supported for elicitation requests.
|
Primitive types (string, number, Boolean) and enum types are supported for elicitation requests.
|
||||||
The schema might include a description to help the user understand what's being requested.
|
The schema might include a description to help the user understand what's being requested.
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ var options = new McpClientOptions
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
The `ElicitationHandler` is an asynchronous method that will be called when the server requests additional information. The handler should check the `Mode` of the request:
|
The `ElicitationHandler` is an asynchronous method that's called when the server requests additional information. The handler should check the `Mode` of the request:
|
||||||
|
|
||||||
- **Form Mode**: Present the form defined by `RequestedSchema` to the user. Return the user's input in the `Content` of the result.
|
- **Form Mode**: Present the form defined by `RequestedSchema` to the user. Return the user's input in the `Content` of the result.
|
||||||
- **URL Mode**: Present the `Message` and `Url` to the user. Ask for consent to open the URL. If the user consents, open the URL and return `Action="accept"`. If the user declines, return `Action="decline"`.
|
- **URL Mode**: Present the `Message` and `Url` to the user. Ask for consent to open the URL. If the user consents, open the URL and return `Action="accept"`. If the user declines, return `Action="decline"`.
|
||||||
|
|
@ -223,11 +223,11 @@ public static string ElicitWithMrtr(
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> See [Multi Round-Trip Requests (MRTR)](xref:mrtr) for the full protocol details, including multiple round trips, concurrent input requests, and the compatibility matrix.
|
> For the full protocol details, including multiple round trips, concurrent input requests, and the compatibility matrix, see [Multi Round-Trip Requests (MRTR)](xref:mrtr).
|
||||||
|
|
||||||
### URL Elicitation Required Error
|
### URL Elicitation Required Error
|
||||||
|
|
||||||
When a tool cannot proceed without first completing a URL-mode elicitation (for example, when third-party OAuth authorization is needed), and calling `ElicitAsync` is not practical (for example in [stateless](xref:stateless) mode where server-to-client requests are disabled), the server may throw a <xref:ModelContextProtocol.UrlElicitationRequiredException>. This is a specialized error (JSON-RPC error code `-32042`) that signals to the client that one or more URL-mode elicitations must be completed before the original request can be retried.
|
When a tool cannot proceed without first completing a URL-mode elicitation (for example, when third-party OAuth authorization is needed), and calling `ElicitAsync` is not practical (for example, in [stateless](xref:stateless) mode where server-to-client requests are disabled), the server might throw a <xref:ModelContextProtocol.UrlElicitationRequiredException>. This is a specialized error (JSON-RPC error code `-32042`) that signals to the client that one or more URL-mode elicitations must be completed before the original request can be retried.
|
||||||
|
|
||||||
#### Throwing UrlElicitationRequiredException on the Server
|
#### Throwing UrlElicitationRequiredException on the Server
|
||||||
|
|
||||||
|
|
@ -271,10 +271,10 @@ The exception can include multiple elicitations if the operation requires author
|
||||||
|
|
||||||
When the client calls a tool and receives a `UrlElicitationRequiredException`, it should:
|
When the client calls a tool and receives a `UrlElicitationRequiredException`, it should:
|
||||||
|
|
||||||
1. Present each URL elicitation to the user (showing the URL and message)
|
1. Present each URL elicitation to the user (showing the URL and message).
|
||||||
2. Get user consent before opening each URL
|
2. Get user consent before opening each URL.
|
||||||
3. Optionally wait for completion notifications from the server
|
3. Optionally wait for completion notifications from the server.
|
||||||
4. Retry the original request after the user completes the out-of-band interactions
|
4. Retry the original request after the user completes the out-of-band interactions.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
try
|
try
|
||||||
|
|
@ -339,6 +339,6 @@ await using var completionHandler = client.RegisterNotificationHandler(
|
||||||
|
|
||||||
This pattern is particularly useful for:
|
This pattern is particularly useful for:
|
||||||
|
|
||||||
- **Third-party OAuth flows**: When the MCP server needs to obtain tokens from external services on behalf of the user
|
- **Third-party OAuth flows**: When the MCP server needs to obtain tokens from external services on behalf of the user.
|
||||||
- **Payment processing**: When user confirmation is required through a secure payment interface
|
- **Payment processing**: When user confirmation is required through a secure payment interface.
|
||||||
- **Sensitive credential collection**: When API keys or other secrets must be entered directly on a trusted server page rather than through the MCP client
|
- **Sensitive credential collection**: When API keys or other secrets must be entered directly on a trusted server page rather than through the MCP client.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ uid: filters
|
||||||
The MCP Server provides two levels of filters for intercepting and modifying request processing:
|
The MCP Server provides two levels of filters for intercepting and modifying request processing:
|
||||||
|
|
||||||
1. **Message Filters** - Low-level filters (`AddIncomingFilter`, `AddOutgoingFilter`) configured via `WithMessageFilters(...)` that intercept all JSON-RPC messages before routing.
|
1. **Message Filters** - Low-level filters (`AddIncomingFilter`, `AddOutgoingFilter`) configured via `WithMessageFilters(...)` that intercept all JSON-RPC messages before routing.
|
||||||
2. **Request-Specific Filters** - Handler-level filters (e.g., `AddListToolsFilter`, `AddCallToolFilter`) configured via `WithRequestFilters(...)` that target specific MCP operations.
|
2. **Request-Specific Filters** - Handler-level filters (for example, `AddListToolsFilter`, `AddCallToolFilter`) configured via `WithRequestFilters(...)` that target specific MCP operations.
|
||||||
|
|
||||||
The filters are stored in `McpServerOptions.Filters`.
|
The filters are stored in `McpServerOptions.Filters`.
|
||||||
|
|
||||||
|
|
@ -18,17 +18,19 @@ The filters are stored in `McpServerOptions.Filters`.
|
||||||
|
|
||||||
The following request filter methods are available on `IMcpRequestFilterBuilder` inside `WithRequestFilters(...)`:
|
The following request filter methods are available on `IMcpRequestFilterBuilder` inside `WithRequestFilters(...)`:
|
||||||
|
|
||||||
- `AddListResourceTemplatesFilter` - Filter for list resource templates handlers
|
| Method | Filters for... |
|
||||||
- `AddListToolsFilter` - Filter for list tools handlers
|
|-------------------------------------|----------------------------------|
|
||||||
- `AddCallToolFilter` - Filter for call tool handlers
|
| `AddListResourceTemplatesFilter` | List resource templates handlers |
|
||||||
- `AddListPromptsFilter` - Filter for list prompts handlers
|
| `AddListToolsFilter` | List tools handlers |
|
||||||
- `AddGetPromptFilter` - Filter for get prompt handlers
|
| `AddCallToolFilter` | Call tool handlers |
|
||||||
- `AddListResourcesFilter` - Filter for list resources handlers
|
| `AddListPromptsFilter` | List prompts handlers |
|
||||||
- `AddReadResourceFilter` - Filter for read resource handlers
|
| `AddGetPromptFilter` | Get prompt handlers |
|
||||||
- `AddCompleteFilter` - Filter for completion handlers
|
| `AddListResourcesFilter` | List resources handlers |
|
||||||
- `AddSubscribeToResourcesFilter` - Filter for resource subscription handlers
|
| `AddReadResourceFilter` | Read resource handlers |
|
||||||
- `AddUnsubscribeFromResourcesFilter` - Filter for resource unsubscription handlers
|
| `AddCompleteFilter` | Completion handlers |
|
||||||
- `AddSetLoggingLevelFilter` - Filter for logging level handlers
|
| `AddSubscribeToResourcesFilter` | Resource subscription handlers |
|
||||||
|
| `AddUnsubscribeFromResourcesFilter` | Resource unsubscription handlers |
|
||||||
|
| `AddSetLoggingLevelFilter` | Logging level handlers |
|
||||||
|
|
||||||
## Message Filters
|
## Message Filters
|
||||||
|
|
||||||
|
|
@ -308,6 +310,8 @@ Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filt
|
||||||
|
|
||||||
## Common Use Cases
|
## Common Use Cases
|
||||||
|
|
||||||
|
Filters are commonly used for [logging](#logging), [error handling](#error-handling), [performance monitoring](#performance-monitoring), and [caching](#caching).
|
||||||
|
|
||||||
### Logging
|
### Logging
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
|
@ -406,7 +410,7 @@ services.AddMcpServer()
|
||||||
.WithTools<WeatherTools>();
|
.WithTools<WeatherTools>();
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important**: You should always call `AddAuthorizationFilters()` when using ASP.NET Core integration if you want to use authorization attributes like `[Authorize]` on your MCP server tools, prompts, or resources.
|
**Important**: If you want to use authorization attributes like `[Authorize]` on your MCP server tools, prompts, or resources, you should always call `AddAuthorizationFilters()` when using ASP.NET Core integration.
|
||||||
|
|
||||||
### Authorization Attributes Support
|
### Authorization Attributes Support
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ This guide walks you through installing the MCP C# SDK and building a minimal MC
|
||||||
The SDK ships as three NuGet packages. Pick the one that matches your scenario:
|
The SDK ships as three NuGet packages. Pick the one that matches your scenario:
|
||||||
|
|
||||||
| Package | Use when... |
|
| Package | Use when... |
|
||||||
| - | - |
|
|---------|-------------|
|
||||||
| **[ModelContextProtocol.Core](https://www.nuget.org/packages/ModelContextProtocol.Core/absoluteLatest)** | You only need the client or low-level server APIs and want the **minimum set of dependencies**. |
|
| **[ModelContextProtocol.Core](https://www.nuget.org/packages/ModelContextProtocol.Core/absoluteLatest)** | You only need the client or low-level server APIs and want the **minimum set of dependencies**. |
|
||||||
| **[ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol/absoluteLatest)** | You're building a client or a **stdio-based** server and want hosting, dependency injection, and attribute-based tool/prompt/resource discovery. References `ModelContextProtocol.Core`. **This is the right starting point for most projects.** |
|
| **[ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol/absoluteLatest)** | You're building a client or a **stdio-based** server and want hosting, dependency injection, and attribute-based tool/prompt/resource discovery. References `ModelContextProtocol.Core`. **This is the right starting point for most projects.** |
|
||||||
| **[ModelContextProtocol.AspNetCore](https://www.nuget.org/packages/ModelContextProtocol.AspNetCore/absoluteLatest)** | You're building an **HTTP-based** MCP server hosted in ASP.NET Core. References `ModelContextProtocol`, so you get everything above plus the HTTP transport. |
|
| **[ModelContextProtocol.AspNetCore](https://www.nuget.org/packages/ModelContextProtocol.AspNetCore/absoluteLatest)** | You're building an **HTTP-based** MCP server hosted in ASP.NET Core. References `ModelContextProtocol`, so you get everything above plus the HTTP transport. |
|
||||||
|
|
@ -29,7 +29,7 @@ The SDK ships as three NuGet packages. Pick the one that matches your scenario:
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> You can also use the [MCP Server project template](https://learn.microsoft.com/dotnet/ai/quickstarts/build-mcp-server) to quickly scaffold a new MCP server project.
|
> You can also use the [MCP Server project template](https://learn.microsoft.com/dotnet/ai/quickstarts/build-mcp-server) to quickly scaffold a new MCP server project.
|
||||||
|
|
||||||
Create a new console app, add the required packages, and replace `Program.cs` with the code below to get a working MCP server that exposes a single tool over stdio:
|
Create a new console app and add the required packages:
|
||||||
|
|
||||||
```
|
```
|
||||||
dotnet new console
|
dotnet new console
|
||||||
|
|
@ -37,6 +37,8 @@ dotnet add package ModelContextProtocol
|
||||||
dotnet add package Microsoft.Extensions.Hosting
|
dotnet add package Microsoft.Extensions.Hosting
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Replace `Program.cs` with the following code to get a working MCP server that exposes a single tool over stdio:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
@ -66,13 +68,15 @@ public static class EchoTool
|
||||||
|
|
||||||
The call to `WithToolsFromAssembly` discovers every class marked with `[McpServerToolType]` in the assembly and registers every `[McpServerTool]` method as a tool. Prompts and resources work the same way with `[McpServerPromptType]` / `[McpServerPrompt]` and `[McpServerResourceType]` / `[McpServerResource]`.
|
The call to `WithToolsFromAssembly` discovers every class marked with `[McpServerToolType]` in the assembly and registers every `[McpServerTool]` method as a tool. Prompts and resources work the same way with `[McpServerPromptType]` / `[McpServerPrompt]` and `[McpServerResourceType]` / `[McpServerResource]`.
|
||||||
|
|
||||||
For HTTP-based servers using ASP.NET Core:
|
For HTTP-based servers using ASP.NET Core, add the following packages:
|
||||||
|
|
||||||
```
|
```
|
||||||
dotnet new web
|
dotnet new web
|
||||||
dotnet add package ModelContextProtocol.AspNetCore
|
dotnet add package ModelContextProtocol.AspNetCore
|
||||||
```
|
```
|
||||||
|
|
||||||
|
And add the following code:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
@ -109,21 +113,23 @@ For production servers, configure the exact public host names for the deployment
|
||||||
|
|
||||||
#### Browser cross-origin access
|
#### Browser cross-origin access
|
||||||
|
|
||||||
**Only** enable CORS if you intentionally want browser-based cross-origin access to this server.
|
**Only** enable cross-origin requests (CORS) if you intentionally want browser-based cross-origin access to this server.
|
||||||
|
|
||||||
CORS is not a substitute for host name validation. When browser-based cross-origin access is required, limit which browser origins can call the MCP endpoint by using the most restrictive ASP.NET Core CORS policy possible. See [Enable Cross-Origin Requests (CORS) in ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/aspnet/core/security/cors).
|
CORS is not a substitute for host name validation. When browser-based cross-origin access is required, limit which browser origins can call the MCP endpoint by using the most restrictive ASP.NET Core CORS policy possible. See [Enable Cross-Origin Requests (CORS) in ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/aspnet/core/security/cors).
|
||||||
|
|
||||||
For the full HTTP security examples, including `AllowedHosts` and restrictive CORS on `MapMcp`, see [Streamable HTTP transport](transports/transports.md#browser-cross-origin-access).
|
For the full HTTP security examples, including `AllowedHosts` and restrictive CORS on `MapMcp`, see [Streamable HTTP transport](transports/transports.md#browser-cross-origin-access).
|
||||||
|
|
||||||
### Building an MCP client
|
### Building an MCP client
|
||||||
|
|
||||||
Create a new console app, add the package, and replace `Program.cs` with the code below. This client connects to the MCP "everything" reference server, lists its tools, and calls one:
|
Create a new console app and add the following packages:
|
||||||
|
|
||||||
```
|
```
|
||||||
dotnet new console
|
dotnet new console
|
||||||
dotnet add package ModelContextProtocol
|
dotnet add package ModelContextProtocol
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Replace `Program.cs` with the following code. This client connects to the MCP "everything" reference server, lists its tools, and calls one:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
using ModelContextProtocol.Client;
|
using ModelContextProtocol.Client;
|
||||||
using ModelContextProtocol.Protocol;
|
using ModelContextProtocol.Protocol;
|
||||||
|
|
@ -157,7 +163,7 @@ Clients can connect to any MCP server, not just ones created with this library.
|
||||||
|
|
||||||
#### Using tools with an LLM
|
#### Using tools with an LLM
|
||||||
|
|
||||||
`McpClientTool` inherits from `AIFunction`, so the tools returned by `ListToolsAsync` can be handed directly to any `IChatClient`:
|
`McpClientTool` inherits from [`AIFunction`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.ai.aifunction), so the tools returned by `ListToolsAsync` can be given directly to any `IChatClient`:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Get available tools.
|
// Get available tools.
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,11 @@ and the `GetHttpHeaders` method accessing the current [HttpContext] to retrieve
|
||||||
|
|
||||||
When using the legacy SSE transport, be aware that the `HttpContext` returned by `IHttpContextAccessor` references the long-lived SSE connection request — not the individual `POST` request that triggered the tool call. This means:
|
When using the legacy SSE transport, be aware that the `HttpContext` returned by `IHttpContextAccessor` references the long-lived SSE connection request — not the individual `POST` request that triggered the tool call. This means:
|
||||||
|
|
||||||
- The `HttpContext.User` may contain stale claims if the client's token was refreshed after the SSE connection was established.
|
- The `HttpContext.User` might contain stale claims if the client's token was refreshed after the SSE connection was established.
|
||||||
- Request headers, query strings, and other per-request metadata will reflect the initial SSE connection, not the current operation.
|
- Request headers, query strings, and other per-request metadata will reflect the initial SSE connection, not the current operation.
|
||||||
|
|
||||||
The Streamable HTTP transport does not have this issue because each tool call is its own HTTP request, so `IHttpContextAccessor.HttpContext` always reflects the current request. In [stateless](xref:stateless) mode, this is guaranteed since every request creates a fresh server context.
|
The Streamable HTTP transport does not have this issue because each tool call is its own HTTP request, so `IHttpContextAccessor.HttpContext` always reflects the current request. In [stateless](xref:stateless) mode, this is guaranteed since every request creates a fresh server context.
|
||||||
|
|
||||||
<!-- mlc-disable-next-line -->
|
<!-- mlc-disable-next-line -->
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The server validates that the user identity has not changed between the session-initiating request and subsequent requests (using the `sub`, `NameIdentifier`, or `UPN` claim). If the user identity changes, the request is rejected with `403 Forbidden`. However, other claims (roles, permissions, custom claims) are not re-validated and may become stale over the lifetime of a session.
|
> The server validates that the user identity has not changed between the session-initiating request and subsequent requests (using the `sub`, `NameIdentifier`, or `UPN` claim). If the user identity changes, the request is rejected with `403 Forbidden`. However, other claims (roles, permissions, custom claims) are not re-validated and might become stale over the lifetime of a session.
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ The SDK registers `ClaimsPrincipal` as one of the built-in services available du
|
||||||
2. Automatically resolves it from the current request's `User` property at invocation time.
|
2. Automatically resolves it from the current request's `User` property at invocation time.
|
||||||
3. Passes `null` if no authenticated user is present (when the parameter is nullable).
|
3. Passes `null` if no authenticated user is present (when the parameter is nullable).
|
||||||
|
|
||||||
This behavior is transport-agnostic. For HTTP transports, the `ClaimsPrincipal` comes from ASP.NET Core authentication. For other transports (like stdio), it will be `null` unless you set it explicitly via a message filter.
|
This behavior is transport-agnostic. For HTTP transports, the `ClaimsPrincipal` comes from ASP.NET Core authentication. For other transports (like stdio), it's `null` unless you set it explicitly via a message filter.
|
||||||
|
|
||||||
## Accessing Identity in Filters
|
## Accessing Identity in Filters
|
||||||
|
|
||||||
|
|
@ -132,7 +132,7 @@ When authorization fails, the SDK automatically:
|
||||||
- **For list operations**: Removes unauthorized items from the results so users only see what they can access.
|
- **For list operations**: Removes unauthorized items from the results so users only see what they can access.
|
||||||
- **For individual operations**: Returns a JSON-RPC error indicating access is forbidden.
|
- **For individual operations**: Returns a JSON-RPC error indicating access is forbidden.
|
||||||
|
|
||||||
See [Filters](xref:filters) for more details on authorization filters and their execution order.
|
For more details on authorization filters and their execution order, see [Filters](xref:filters).
|
||||||
|
|
||||||
## Using `IHttpContextAccessor` (HTTP-Only Alternative)
|
## Using `IHttpContextAccessor` (HTTP-Only Alternative)
|
||||||
|
|
||||||
|
|
@ -156,7 +156,7 @@ public class HttpContextTools(IHttpContextAccessor contextAccessor)
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> `IHttpContextAccessor` only works with HTTP transports. For transport-agnostic identity access, use `ClaimsPrincipal` parameter injection instead.
|
> `IHttpContextAccessor` only works with HTTP transports. For transport-agnostic identity access, use `ClaimsPrincipal` parameter injection instead.
|
||||||
|
|
||||||
See [HTTP Context](xref:httpcontext) for more details, including important caveats about stale `HttpContext` with the legacy SSE transport.
|
For more details, including important caveats about stale `HttpContext` with the legacy SSE transport, see [HTTP Context](xref:httpcontext).
|
||||||
|
|
||||||
## Transport Considerations
|
## Transport Considerations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ Welcome to the conceptual documentation for the Model Context Protocol SDK. Here
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
### [Getting Started](getting-started.md)
|
### Getting Started
|
||||||
|
|
||||||
Install the SDK and build your first MCP client and server.
|
To install the SDK and build your first MCP client and server, see [Getting started](getting-started.md).
|
||||||
|
|
||||||
### Deployment
|
### Deployment
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,16 @@ MCP uses the logging levels defined in [RFC 5424](https://datatracker.ietf.org/d
|
||||||
The MCP C# SDK uses the standard .NET [ILogger] and [ILoggerProvider] abstractions, which support a slightly
|
The MCP C# SDK uses the standard .NET [ILogger] and [ILoggerProvider] abstractions, which support a slightly
|
||||||
different set of logging levels. The following table shows the levels and how they map to standard .NET logging levels.
|
different set of logging levels. The following table shows the levels and how they map to standard .NET logging levels.
|
||||||
|
|
||||||
| Level | .NET | Description | Example Use Case |
|
| Level | .NET | Description | Example use case |
|
||||||
|-----------|------|-----------------------------------|------------------------------|
|
|-------------|------|----------------------------------|----------------------------|
|
||||||
| debug | ✓ | Detailed debugging information | Function entry/exit points |
|
| `debug` | ✓ | Detailed debugging information | Function entry/exit points |
|
||||||
| info | ✓ | General informational messages | Operation progress updates |
|
| `info` | ✓ | General informational messages | Operation progress updates |
|
||||||
| notice | | Normal but significant events | Configuration changes |
|
| `notice` | | Normal but significant events | Configuration changes |
|
||||||
| warning | ✓ | Warning conditions | Deprecated feature usage |
|
| `warning` | ✓ | Warning conditions | Deprecated feature usage |
|
||||||
| error | ✓ | Error conditions | Operation failures |
|
| `error` | ✓ | Error conditions | Operation failures |
|
||||||
| critical | ✓ | Critical conditions | System component failures |
|
| `critical` | ✓ | Critical conditions | System component failures |
|
||||||
| alert | | Action must be taken immediately | Data corruption detected |
|
| `alert` | | Action must be taken immediately | Data corruption detected |
|
||||||
| emergency | | System is unusable | |
|
| `emergency` | | System is unusable | |
|
||||||
|
|
||||||
**Note:** .NET's [ILogger] also supports a `Trace` level (more verbose than Debug) log level.
|
**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
|
As there is no equivalent level in the MCP logging levels, Trace level logs messages are silently
|
||||||
|
|
@ -51,7 +51,7 @@ messages as there might not be an open connection to the client on which the log
|
||||||
|
|
||||||
The C# SDK provides an extension method <xref:Microsoft.Extensions.DependencyInjection.McpServerBuilderExtensions.WithSetLoggingLevelHandler*> on <xref:Microsoft.Extensions.DependencyInjection.IMcpServerBuilder> to allow the
|
The C# SDK provides an extension method <xref:Microsoft.Extensions.DependencyInjection.McpServerBuilderExtensions.WithSetLoggingLevelHandler*> on <xref:Microsoft.Extensions.DependencyInjection.IMcpServerBuilder> to allow the
|
||||||
server to perform any special logic it wants to perform when a client sets the logging level. However, the
|
server to perform any special logic it wants to perform when a client sets the logging level. However, the
|
||||||
SDK already takes care of setting the <xref:ModelContextProtocol.Server.McpServer.LoggingLevel> in the <xref:ModelContextProtocol.Server.McpServer>, so most servers will not need to
|
SDK already takes care of setting the <xref:ModelContextProtocol.Server.McpServer.LoggingLevel> in the <xref:ModelContextProtocol.Server.McpServer>, so most servers don't need to
|
||||||
implement this.
|
implement this.
|
||||||
|
|
||||||
MCP Servers using the MCP C# SDK can obtain an [ILoggerProvider](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.iloggerprovider) from the <xref:ModelContextProtocol.Server.McpServer.AsClientLoggerProvider> method on <xref:ModelContextProtocol.Server.McpServer>,
|
MCP Servers using the MCP C# SDK can obtain an [ILoggerProvider](https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.iloggerprovider) from the <xref:ModelContextProtocol.Server.McpServer.AsClientLoggerProvider> method on <xref:ModelContextProtocol.Server.McpServer>,
|
||||||
|
|
@ -74,7 +74,7 @@ to send all log messages or none—this is not specified in the protocol. So
|
||||||
sets a logging level to ensure it receives the desired log messages and only those messages.
|
sets a logging level to ensure it receives the desired log messages and only those messages.
|
||||||
|
|
||||||
The `loggingLevel` set by the client is an MCP logging level.
|
The `loggingLevel` set by the client is an MCP logging level.
|
||||||
See the [Logging Levels](#logging-levels) section above for the mapping between MCP and .NET logging levels.
|
For the mapping between MCP and .NET logging levels, see the [Logging Levels](#logging-levels) section.
|
||||||
|
|
||||||
[!code-csharp[](samples/client/Program.cs?name=snippet_LoggingLevel)]
|
[!code-csharp[](samples/client/Program.cs?name=snippet_LoggingLevel)]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ uid: mrtr
|
||||||
|
|
||||||
<!-- mlc-disable-next-line -->
|
<!-- mlc-disable-next-line -->
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> MRTR is part of the **`2026-07-28`** revision of the MCP specification ([SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)). The wire format and API surface may change before the revision is ratified. See the [Experimental APIs](../../experimental.md) documentation for details on working with experimental APIs.
|
> MRTR is part of the **`2026-07-28`** revision of the MCP specification ([SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)). The wire format and API surface might change before the revision is ratified. For details on working with experimental APIs, see [Experimental APIs](../../experimental.md).
|
||||||
|
|
||||||
Multi Round-Trip Requests (MRTR) let a server tool request input from the client — such as [elicitation](xref:elicitation), [sampling](xref:sampling), or [roots](xref:roots) — as part of a single tool call, without requiring a separate server-to-client JSON-RPC request for each interaction. Instead of returning a final result, the server returns an **incomplete result** containing one or more input requests. The client fulfills those requests and retries the original tool call with the responses attached.
|
Multi Round-Trip Requests (MRTR) let a server tool request input from the client — such as [elicitation](xref:elicitation), [sampling](xref:sampling), or [roots](xref:roots) — as part of a single tool call, without requiring a separate server-to-client JSON-RPC request for each interaction. Instead of returning a final result, the server returns an **incomplete result** containing one or more input requests. The client fulfills those requests and retries the original tool call with the responses attached.
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ MRTR is useful when:
|
||||||
|
|
||||||
1. The client calls a tool on the server via `tools/call`.
|
1. The client calls a tool on the server via `tools/call`.
|
||||||
2. The server tool determines it needs client input and returns an `InputRequiredResult` containing `inputRequests` and/or `requestState`.
|
2. The server tool determines it needs client input and returns an `InputRequiredResult` containing `inputRequests` and/or `requestState`.
|
||||||
3. The client resolves each input request (for example by prompting the user for elicitation, calling an LLM for sampling, or listing its roots).
|
3. The client resolves each input request (for example, by prompting the user for elicitation, calling an LLM for sampling, or listing its roots).
|
||||||
4. The client retries the original `tools/call` with `inputResponses` (keyed to the input requests) and `requestState` echoed back.
|
4. The client retries the original `tools/call` with `inputResponses` (keyed to the input requests) and `requestState` echoed back.
|
||||||
5. The server processes the responses and either returns a final result or another `InputRequiredResult` for additional rounds.
|
5. The server processes the responses and either returns a final result or another `InputRequiredResult` for additional rounds.
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ var clientOptions = new McpClientOptions
|
||||||
|
|
||||||
Under `2026-07-28`, MRTR is the recommended way to obtain client input from a server handler. The spec removes the legacy server-to-client `elicitation/create`, `sampling/createMessage`, and `roots/list` request methods, so any code that needs to work on a `2026-07-28` Streamable HTTP server (where Streamable HTTP no longer supports sessions) must use `InputRequiredException` rather than <xref:ModelContextProtocol.Server.McpServer.ElicitAsync*>, <xref:ModelContextProtocol.Server.McpServer.SampleAsync*>, or <xref:ModelContextProtocol.Server.McpServer.RequestRootsAsync*>. The legacy methods still work on stateful sessions — that's how stdio servers keep working on `2026-07-28` today — but they throw `InvalidOperationException("X is not supported in stateless mode.")` on any stateless session, current or `2026-07-28`.
|
Under `2026-07-28`, MRTR is the recommended way to obtain client input from a server handler. The spec removes the legacy server-to-client `elicitation/create`, `sampling/createMessage`, and `roots/list` request methods, so any code that needs to work on a `2026-07-28` Streamable HTTP server (where Streamable HTTP no longer supports sessions) must use `InputRequiredException` rather than <xref:ModelContextProtocol.Server.McpServer.ElicitAsync*>, <xref:ModelContextProtocol.Server.McpServer.SampleAsync*>, or <xref:ModelContextProtocol.Server.McpServer.RequestRootsAsync*>. The legacy methods still work on stateful sessions — that's how stdio servers keep working on `2026-07-28` today — but they throw `InvalidOperationException("X is not supported in stateless mode.")` on any stateless session, current or `2026-07-28`.
|
||||||
|
|
||||||
Under the current protocol revision (`2025-06-18` and earlier), `InputRequiredException` is still supported in stateful sessions via a backward-compatibility resolver — see [Compatibility](#compatibility) below.
|
Under the current protocol revision (`2025-06-18` and earlier), `InputRequiredException` is still supported in stateful sessions via a backward-compatibility resolver — see the [Compatibility](#compatibility) section.
|
||||||
|
|
||||||
## Authoring an MRTR tool
|
## Authoring an MRTR tool
|
||||||
|
|
||||||
|
|
@ -57,7 +57,7 @@ A tool participates in MRTR by throwing <xref:ModelContextProtocol.Protocol.Inpu
|
||||||
|
|
||||||
### Checking MRTR support
|
### Checking MRTR support
|
||||||
|
|
||||||
Tools should check <xref:ModelContextProtocol.Server.McpServer.IsMrtrSupported> before throwing `InputRequiredException`. It returns `true` when either:
|
Tools should check <xref:ModelContextProtocol.Server.McpServer.IsMrtrSupported> before throwing `InputRequiredException`. The property returns `true` when either:
|
||||||
|
|
||||||
- The negotiated protocol revision is `2026-07-28` (MRTR is native), or
|
- The negotiated protocol revision is `2026-07-28` (MRTR is native), or
|
||||||
- The session is stateful under the current protocol (the SDK can resolve input requests via legacy JSON-RPC and retry the handler).
|
- The session is stateful under the current protocol (the SDK can resolve input requests via legacy JSON-RPC and retry the handler).
|
||||||
|
|
@ -136,9 +136,11 @@ When the client retries a tool call, the retry data is available on the request
|
||||||
|
|
||||||
Use <xref:ModelContextProtocol.Protocol.InputResponse.Deserialize*> with the `JsonTypeInfo<T>` matching the response type. The expected type follows from the matching <xref:ModelContextProtocol.Protocol.InputRequest.Method> in the original `inputRequests` map — there is no on-the-wire discriminator.
|
Use <xref:ModelContextProtocol.Protocol.InputResponse.Deserialize*> with the `JsonTypeInfo<T>` matching the response type. The expected type follows from the matching <xref:ModelContextProtocol.Protocol.InputRequest.Method> in the original `inputRequests` map — there is no on-the-wire discriminator.
|
||||||
|
|
||||||
- Elicitation — `response.Deserialize(InputResponse.ElicitResultJsonTypeInfo)`
|
| Input | Deserialize call |
|
||||||
- Sampling — `response.Deserialize(InputResponse.CreateMessageResultJsonTypeInfo)`
|
|-------------|-----------------------------------------------------------------------|
|
||||||
- Roots list — `response.Deserialize(InputResponse.ListRootsResultJsonTypeInfo)`
|
| Elicitation | `response.Deserialize(InputResponse.ElicitResultJsonTypeInfo)` |
|
||||||
|
| Sampling | `response.Deserialize(InputResponse.CreateMessageResultJsonTypeInfo)` |
|
||||||
|
| Roots list | `response.Deserialize(InputResponse.ListRootsResultJsonTypeInfo)` |
|
||||||
|
|
||||||
### Load shedding with requestState-only responses
|
### Load shedding with requestState-only responses
|
||||||
|
|
||||||
|
|
@ -267,10 +269,10 @@ if (!server.IsMrtrSupported)
|
||||||
|
|
||||||
The SDK supports `InputRequiredException` across two protocol revisions and two session modes:
|
The SDK supports `InputRequiredException` across two protocol revisions and two session modes:
|
||||||
|
|
||||||
| Negotiated protocol | Session mode | Behavior |
|
| Negotiated protocol | Session mode | Behavior |
|
||||||
|---|---|---|
|
|---------------------|--------------|-------------------------------------------------------------------------|
|
||||||
| `2026-07-28` | Stateful | Native MRTR — `InputRequiredResult` is serialized directly to the wire. |
|
| `2026-07-28` | Stateful | Native MRTR — `InputRequiredResult` is serialized directly to the wire. |
|
||||||
| `2026-07-28` | Stateless | Native MRTR — `InputRequiredResult` is serialized directly to the wire. No server-side handler state needed. |
|
| `2026-07-28` | Stateless | Native MRTR — `InputRequiredResult` is serialized directly to the wire. No server-side handler state needed. |
|
||||||
| Current (`2025-06-18` and earlier) | Stateful | Backward-compatibility resolver — the SDK sends standard `elicitation/create` / `sampling/createMessage` / `roots/list` JSON-RPC requests to the client, collects the responses, and retries the handler with `inputResponses` populated. Up to 10 retry rounds. |
|
| Current (`2025-06-18` and earlier) | Stateful | Backward-compatibility resolver — the SDK sends standard `elicitation/create` / `sampling/createMessage` / `roots/list` JSON-RPC requests to the client, collects the responses, and retries the handler with `inputResponses` populated. Up to 10 retry rounds. |
|
||||||
| Current (`2025-06-18` and earlier) | Stateless | **Not supported** — `InputRequiredException` raises an `McpException`. The client doesn't speak MRTR, and the server can't resolve input requests via JSON-RPC without a persistent session. |
|
| Current (`2025-06-18` and earlier) | Stateless | **Not supported** — `InputRequiredException` raises an `McpException`. The client doesn't speak MRTR, and the server can't resolve input requests via JSON-RPC without a persistent session. |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,18 @@ uid: pagination
|
||||||
|
|
||||||
## Pagination
|
## Pagination
|
||||||
|
|
||||||
MCP uses [cursor-based pagination] for all list operations that may return large result sets.
|
MCP uses [cursor-based pagination] for all list operations that can return large result sets.
|
||||||
|
|
||||||
[cursor-based pagination]: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination
|
[cursor-based pagination]: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
Instead of offset-based pagination (page 1, page 2, etc.), MCP uses opaque cursor tokens. Each paginated response may include a `NextCursor` value. If present, pass it in the next request to retrieve the next page of results.
|
Instead of offset-based pagination (page 1, page 2, etc.), MCP uses opaque cursor tokens. Each paginated response might include a `NextCursor` value. If present, pass it in the next request to retrieve the next page of results.
|
||||||
|
|
||||||
Two levels of API are provided for paginated operations:
|
Two levels of API are provided for paginated operations:
|
||||||
|
|
||||||
1. **Convenience methods** (e.g., `ListToolsAsync()` returning `IList<T>`) that automatically handle pagination and return all results.
|
1. **Convenience methods** (for example, `ListToolsAsync()` returning `IList<T>`) that automatically handle pagination and return all results.
|
||||||
2. **Raw methods** (e.g., `ListToolsAsync(ListToolsRequestParams)` returning the result type directly) that provide direct control over pagination.
|
2. **Raw methods** (for example, `ListToolsAsync(ListToolsRequestParams)` returning the result type directly) that provide direct control over pagination.
|
||||||
|
|
||||||
### Automatic pagination
|
### Automatic pagination
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ The Model Context Protocol (MCP) supports [progress tracking] for long-running o
|
||||||
|
|
||||||
[progress tracking]: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress
|
[progress tracking]: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress
|
||||||
|
|
||||||
Typically progress tracking is supported by server tools that perform operations that take a significant amount of time to complete, such as image generation or complex calculations.
|
Typically, progress tracking is supported by server tools that perform operations that take a significant amount of time to complete, such as image generation or complex calculations.
|
||||||
However, progress tracking is defined in the MCP specification as a general feature that can be implemented for any request that's handled by either a server or a client.
|
However, progress tracking is defined in the MCP specification as a general feature that can be implemented for any request that's handled by either a server or a client.
|
||||||
This project illustrates the common case of a server tool that performs a long-running operation and sends progress updates to the client.
|
This project illustrates the common case of a server tool that performs a long-running operation and sends progress updates to the client.
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ await using var handler = mcpClient.RegisterNotificationHandler(NotificationMeth
|
||||||
|
|
||||||
The second way is to pass a [`Progress<T>`](https://learn.microsoft.com/dotnet/api/system.progress-1) instance to the tool method. `Progress<T>` is a standard .NET type that provides a way to receive progress updates.
|
The second way is to pass a [`Progress<T>`](https://learn.microsoft.com/dotnet/api/system.progress-1) instance to the tool method. `Progress<T>` is a standard .NET type that provides a way to receive progress updates.
|
||||||
For the purposes of MCP progress notifications, `T` should be <xref:ModelContextProtocol.ProgressNotificationValue>.
|
For the purposes of MCP progress notifications, `T` should be <xref:ModelContextProtocol.ProgressNotificationValue>.
|
||||||
The MCP C# SDK will automatically handle progress notifications and report them through the `Progress<T>` instance.
|
The MCP C# SDK automatically handles progress notifications and reports them through the `Progress<T>` instance.
|
||||||
This notification handler will only receive progress updates for the specific request that was made,
|
This notification handler will only receive progress updates for the specific request that was made,
|
||||||
rather than all progress notifications from the server.
|
rather than all progress notifications from the server.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ MCP [prompts] allow servers to expose reusable prompt templates to clients. Prom
|
||||||
|
|
||||||
[prompts]: https://modelcontextprotocol.io/specification/2025-11-25/server/prompts
|
[prompts]: https://modelcontextprotocol.io/specification/2025-11-25/server/prompts
|
||||||
|
|
||||||
This document covers implementing prompts on the server, consuming them from the client, rich content types, and change notifications.
|
This document covers implementing prompts on the server, consuming prompts from the client, rich content types, and change notifications.
|
||||||
|
|
||||||
### Defining prompts on the server
|
### Defining prompts on the server
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ builder.Services.AddMcpServer()
|
||||||
|
|
||||||
### Rich content in prompts
|
### Rich content in prompts
|
||||||
|
|
||||||
Prompt messages can contain more than just text. For text and image content, use `ChatMessage` from Microsoft.Extensions.AI. `DataContent` is automatically mapped to the appropriate MCP content block: image MIME types become <xref:ModelContextProtocol.Protocol.ImageContentBlock>, audio MIME types become <xref:ModelContextProtocol.Protocol.AudioContentBlock>, and all other MIME types become <xref:ModelContextProtocol.Protocol.EmbeddedResourceBlock> with binary resource contents. For text embedded resources specifically, use <xref:ModelContextProtocol.Protocol.PromptMessage> directly.
|
Prompt messages can contain more than just text. For text and image content, use [`ChatMessage`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.ai.chatmessage) from Microsoft.Extensions.AI. `DataContent` is automatically mapped to the appropriate MCP content block: image MIME types become <xref:ModelContextProtocol.Protocol.ImageContentBlock>, audio MIME types become <xref:ModelContextProtocol.Protocol.AudioContentBlock>, and all other MIME types become <xref:ModelContextProtocol.Protocol.EmbeddedResourceBlock> with binary resource contents. For text-embedded resources specifically, use <xref:ModelContextProtocol.Protocol.PromptMessage> directly.
|
||||||
|
|
||||||
#### Image content
|
#### Image content
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ MCP [resources] allow servers to expose data and content to clients. Resources r
|
||||||
|
|
||||||
[resources]: https://modelcontextprotocol.io/specification/2025-11-25/server/resources
|
[resources]: https://modelcontextprotocol.io/specification/2025-11-25/server/resources
|
||||||
|
|
||||||
This document covers implementing resources on the server, consuming them from the client, resource templates, subscriptions, and change notifications.
|
This document covers implementing resources on the server, consuming resources from the client, resource templates, subscriptions, and change notifications.
|
||||||
|
|
||||||
### Defining resources on the server
|
### Defining resources on the server
|
||||||
|
|
||||||
|
|
@ -216,7 +216,7 @@ builder.Services.AddMcpServer()
|
||||||
{
|
{
|
||||||
if (ctx.Params.Uri is { } uri)
|
if (ctx.Params.Uri is { } uri)
|
||||||
{
|
{
|
||||||
// Track the subscription (e.g., in a concurrent dictionary)
|
// Track the subscription (for example, in a concurrent dictionary)
|
||||||
subscriptions[ctx.Server.SessionId].TryAdd(uri, 0);
|
subscriptions[ctx.Server.SessionId].TryAdd(uri, 0);
|
||||||
}
|
}
|
||||||
return new EmptyResult();
|
return new EmptyResult();
|
||||||
|
|
@ -244,7 +244,7 @@ await server.SendNotificationAsync(
|
||||||
|
|
||||||
### Resource list change notifications
|
### Resource list change notifications
|
||||||
|
|
||||||
When the set of available resources changes (resources added or removed), the server notifies clients:
|
When the set of available resources changes (resources added or removed), the server notifies clients.
|
||||||
|
|
||||||
#### Sending notifications from the server
|
#### Sending notifications from the server
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,4 +142,4 @@ public static string ListRootsWithMrtr(
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> See [Multi Round-Trip Requests (MRTR)](xref:mrtr) for the full protocol details, including load shedding, multiple round trips, and the compatibility matrix.
|
> For the full protocol details, including load shedding, multiple round trips, and the compatibility matrix, see [Multi Round-Trip Requests (MRTR)](xref:mrtr).
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ MCP [sampling] allows servers to request LLM completions from the client. This e
|
||||||
|
|
||||||
### Server: requesting a completion
|
### Server: requesting a completion
|
||||||
|
|
||||||
Inject <xref:ModelContextProtocol.Server.McpServer> into a tool method and use the <xref:ModelContextProtocol.Server.McpServer.AsSamplingChatClient*> extension method to get an <xref:Microsoft.Extensions.AI.IChatClient> that sends requests through the connected client:
|
To get an <xref:Microsoft.Extensions.AI.IChatClient> that sends requests through the connected client, inject <xref:ModelContextProtocol.Server.McpServer> into a tool method and use the <xref:ModelContextProtocol.Server.McpServer.AsSamplingChatClient*> extension method:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
[McpServerTool(Name = "SummarizeContent"), Description("Summarizes the given text")]
|
[McpServerTool(Name = "SummarizeContent"), Description("Summarizes the given text")]
|
||||||
|
|
@ -171,4 +171,4 @@ public static string SampleWithMrtr(
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> See [Multi Round-Trip Requests (MRTR)](xref:mrtr) for the full protocol details, including load shedding, multiple round trips, and the compatibility matrix.
|
> For the full protocol details, including load shedding, multiple round trips, and the compatibility matrix, see [Multi Round-Trip Requests (MRTR)](xref:mrtr).
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ The `Stateless` property is the single most important setting for forward-proofi
|
||||||
|
|
||||||
The `2026-07-28` protocol revision goes further than `Stateless = true`: it removes the `initialize` handshake (SEP-2575) and the `Mcp-Session-Id` header (SEP-2567) from the wire format entirely. Clients bootstrap by sending `server/discover` instead, and every request carries the negotiated protocol version in the `MCP-Protocol-Version` HTTP header (HTTP transport) or the `_meta.io.modelcontextprotocol/protocolVersion` JSON-RPC field (every transport).
|
The `2026-07-28` protocol revision goes further than `Stateless = true`: it removes the `initialize` handshake (SEP-2575) and the `Mcp-Session-Id` header (SEP-2567) from the wire format entirely. Clients bootstrap by sending `server/discover` instead, and every request carries the negotiated protocol version in the `MCP-Protocol-Version` HTTP header (HTTP transport) or the `_meta.io.modelcontextprotocol/protocolVersion` JSON-RPC field (every transport).
|
||||||
|
|
||||||
**Server side.** With `Stateless = true` (the default), the SDK already meets `2026-07-28` on the wire. Any HTTP POST that arrives with the `2026-07-28` `MCP-Protocol-Version` header is routed through the stateless path automatically — no session is created, no `Mcp-Session-Id` is returned, and the `GET` and `DELETE` endpoints are not mapped. Clients that still send `initialize` on the same endpoint continue to work in stateless mode for the lifetime of that single POST. With `Stateless = false`, the server still creates HTTP sessions when the client speaks `2025-11-25` or earlier — but a `2026-07-28` request on a stateful server is refused with a `-32022 UnsupportedProtocolVersion` error, so a dual-path client downgrades to the `initialize` handshake and obtains a session. If a `2026-07-28` request carries an `Mcp-Session-Id`, the server ignores the header and still does not echo or mint a session ID for that request.
|
**Server side.** With `Stateless = true` (the default), the SDK already meets `2026-07-28` on the wire. Any HTTP `POST` that arrives with the `2026-07-28` `MCP-Protocol-Version` header is routed through the stateless path automatically — no session is created, no `Mcp-Session-Id` is returned, and the `GET` and `DELETE` endpoints aren't mapped. Clients that still send `initialize` on the same endpoint continue to work in stateless mode for the lifetime of that single POST. With `Stateless = false`, the server still creates HTTP sessions when the client speaks `2025-11-25` or earlier — but a `2026-07-28` request on a stateful server is refused with a `-32022 UnsupportedProtocolVersion` error, so a dual-path client downgrades to the `initialize` handshake and obtains a session. If a `2026-07-28` request carries an `Mcp-Session-Id`, the server ignores the header and still does not echo or mint a session ID for that request.
|
||||||
|
|
||||||
**Stateful options marked obsolete.** Because Streamable HTTP no longer supports sessions starting with the `2026-07-28` revision, the stateful-only knobs on <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions> — `IdleTimeout`, `MaxIdleSessionCount`, `EventStreamStore`, `SessionMigrationHandler`, and `PerSessionExecutionContext` — are now marked `[Obsolete]` with diagnostic `MCP9006` to signal that they only apply to initialize-handshake back-compat. You can still set them — the warning is informational — and they continue to govern stateful behavior for initialize-capable clients.
|
**Stateful options marked obsolete.** Because Streamable HTTP no longer supports sessions starting with the `2026-07-28` revision, the stateful-only knobs on <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions> — `IdleTimeout`, `MaxIdleSessionCount`, `EventStreamStore`, `SessionMigrationHandler`, and `PerSessionExecutionContext` — are now marked `[Obsolete]` with diagnostic `MCP9006` to signal that they only apply to initialize-handshake back-compat. You can still set them — the warning is informational — and they continue to govern stateful behavior for initialize-capable clients.
|
||||||
|
|
||||||
|
|
@ -64,7 +64,7 @@ var clientOptions = new McpClientOptions
|
||||||
|
|
||||||
### Migrating from legacy SSE
|
### Migrating from legacy SSE
|
||||||
|
|
||||||
If your clients connect to a `/sse` endpoint (e.g., `https://my-server.example.com/sse`), they are using the [legacy SSE transport](#legacy-sse-transport) — regardless of any `Stateless` or session settings on the server. The `/sse` and `/message` endpoints are now **disabled by default** (<xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> is `false` and marked `[Obsolete]` with diagnostic `MCP9004`). Upgrading the server SDK without updating clients will break SSE connections.
|
If your clients connect to a `/sse` endpoint (for example, `https://my-server.example.com/sse`), they are using the [legacy SSE transport](#legacy-sse-transport) — regardless of any `Stateless` or session settings on the server. The `/sse` and `/message` endpoints are now **disabled by default** (<xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> is `false` and marked `[Obsolete]` with diagnostic `MCP9004`). Upgrading the server SDK without updating clients will break SSE connections.
|
||||||
|
|
||||||
**Client-side migration.** Change the client `Endpoint` from the `/sse` path to the root MCP endpoint — the same URL your server passes to `MapMcp()`. For example:
|
**Client-side migration.** Change the client `Endpoint` from the `/sse` path to the root MCP endpoint — the same URL your server passes to `MapMcp()`. For example:
|
||||||
|
|
||||||
|
|
@ -110,16 +110,16 @@ When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless>
|
||||||
- <xref:ModelContextProtocol.McpSession.SessionId> is `null`, and the `Mcp-Session-Id` header is not sent or expected
|
- <xref:ModelContextProtocol.McpSession.SessionId> is `null`, and the `Mcp-Session-Id` header is not sent or expected
|
||||||
- Each HTTP request creates a fresh server context — no state carries over between requests
|
- Each HTTP request creates a fresh server context — no state carries over between requests
|
||||||
- <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))
|
- <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))
|
||||||
- The `GET` and `DELETE` MCP endpoints are not mapped, and [legacy SSE endpoints](#legacy-sse-transport) (`/sse` and `/message`) are always disabled in stateless mode — clients that only support the legacy SSE transport cannot connect
|
- The `GET` and `DELETE` MCP endpoints are not mapped, and [legacy SSE endpoints](#legacy-sse-transport) (`/sse` and `/message`) are always disabled in stateless mode — clients that only support the legacy SSE transport can't connect
|
||||||
- **Server-to-client requests are disabled**, including:
|
- **Server-to-client requests are disabled**, including:
|
||||||
- [Sampling](xref:sampling) (`SampleAsync`)
|
- [Sampling](xref:sampling) (`SampleAsync`)
|
||||||
- [Elicitation](xref:elicitation) (`ElicitAsync`)
|
- [Elicitation](xref:elicitation) (`ElicitAsync`)
|
||||||
- [Roots](xref:roots) (`RequestRootsAsync`)
|
- [Roots](xref:roots) (`RequestRootsAsync`)
|
||||||
- Ping — the server cannot ping the client to verify connectivity
|
- Ping — the server can't ping the client to verify connectivity
|
||||||
|
|
||||||
[MRTR](xref:mrtr) brings elicitation (and the deprecated sampling and roots) to stateless mode when both client and server speak `2026-07-28` — see [Stateless alternatives for server-to-client interactions](#stateless-alternatives-for-server-to-client-interactions).
|
[MRTR](xref:mrtr) brings elicitation (and the deprecated sampling and roots) to stateless mode when both client and server speak `2026-07-28` — see [Stateless alternatives for server-to-client interactions](#stateless-alternatives-for-server-to-client-interactions).
|
||||||
- **[Unsolicited](#how-streamable-http-delivers-messages) server-to-client notifications** (e.g., resource update notifications, logging messages) are not supported. Every notification must be part of a direct response to a client POST request — see [How Streamable HTTP delivers messages](#how-streamable-http-delivers-messages) for why.
|
- **[Unsolicited](#how-streamable-http-delivers-messages) server-to-client notifications** (for example, resource update notifications, logging messages) are not supported. Every notification must be part of a direct response to a client `POST` request — to understand why, see [How Streamable HTTP delivers messages](#how-streamable-http-delivers-messages).
|
||||||
- **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.
|
- **No concurrent client isolation.** Every request is independent — the server can't distinguish between two agents calling the same tool simultaneously, and there is no mechanism to maintain separate state per client.
|
||||||
- **No state reset on reconnect.** Stateless servers have no concept of "the previous connection." There is no session to close and no fresh session to start. If your server holds any external state, you must manage cleanup through other means.
|
- **No state reset on reconnect.** Stateless servers have no concept of "the previous connection." There is no session to close and no fresh session to start. If your server holds any external state, you must manage cleanup through other means.
|
||||||
- [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.
|
- [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.
|
||||||
|
|
||||||
|
|
@ -135,7 +135,7 @@ Use stateless mode when your server:
|
||||||
- Needs to scale horizontally behind a load balancer without session affinity
|
- Needs to scale horizontally behind a load balancer without session affinity
|
||||||
- Is deployed to serverless environments (Azure Functions, AWS Lambda, etc.)
|
- Is deployed to serverless environments (Azure Functions, AWS Lambda, etc.)
|
||||||
|
|
||||||
Most MCP servers fall into this category. Tools that call APIs, query databases, process data, or return computed results are all natural fits for stateless mode. See [Forward and backward compatibility](#forward-and-backward-compatibility) for guidance on choosing between stateless and stateful mode.
|
Most MCP servers fall into this category. Tools that call APIs, query databases, process data, or return computed results are all natural fits for stateless mode. For guidance on choosing between stateless and stateful mode, see [Forward and backward compatibility](#forward-and-backward-compatibility).
|
||||||
|
|
||||||
### Stateless alternatives for server-to-client interactions
|
### Stateless alternatives for server-to-client interactions
|
||||||
|
|
||||||
|
|
@ -148,9 +148,9 @@ This means servers that need user confirmation ([elicitation](xref:elicitation))
|
||||||
When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> is `false`, the server assigns an `Mcp-Session-Id` to each client during the `initialize` handshake when the client speaks the `2025-11-25` (or earlier) protocol revision. The client must include this header in all subsequent requests. The server maintains an in-memory session for each connected client, enabling:
|
When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> is `false`, the server assigns an `Mcp-Session-Id` to each client during the `initialize` handshake when the client speaks the `2025-11-25` (or earlier) protocol revision. The client must include this header in all subsequent requests. The server maintains an in-memory session for each connected client, enabling:
|
||||||
|
|
||||||
- Server-to-client requests (sampling, elicitation, roots) via an open HTTP response stream
|
- Server-to-client requests (sampling, elicitation, roots) via an open HTTP response stream
|
||||||
- [Unsolicited notifications](#how-streamable-http-delivers-messages) (resource updates, logging messages) via the GET stream
|
- [Unsolicited notifications](#how-streamable-http-delivers-messages) (resource updates, logging messages) via the `GET` stream
|
||||||
- Resource subscriptions
|
- Resource subscriptions
|
||||||
- Session-scoped state (e.g., `RunSessionHandler`, state that persists across multiple requests within a session)
|
- Session-scoped state (for example, `RunSessionHandler`, state that persists across multiple requests within a session)
|
||||||
|
|
||||||
### When to use stateful mode
|
### When to use stateful mode
|
||||||
|
|
||||||
|
|
@ -164,12 +164,12 @@ Use stateful mode when your server needs one or more of:
|
||||||
- **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.
|
- **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.
|
||||||
- **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.
|
- **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.
|
||||||
|
|
||||||
The [deployment considerations](#deployment-considerations) 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 less of a concern, and sessions provide the richest feature set.
|
The [deployment considerations](#deployment-considerations) section lists 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 less of a concern, and sessions provide the richest feature set.
|
||||||
|
|
||||||
## Comparison
|
## Comparison
|
||||||
|
|
||||||
| Consideration | Stateless | Stateful |
|
| Consideration | Stateless | Stateful |
|
||||||
|---|---|---|
|
|---------------|-----------|----------|
|
||||||
| **Deployment** | Any topology — load balancer, serverless, multi-instance | Requires session affinity (sticky sessions) |
|
| **Deployment** | Any topology — load balancer, serverless, multi-instance | Requires session affinity (sticky sessions) |
|
||||||
| **Scaling** | Horizontal scaling without constraints | Limited by session-affinity routing |
|
| **Scaling** | Horizontal scaling without constraints | Limited by session-affinity routing |
|
||||||
| **Server restarts** | No impact — each request is independent | All sessions lost; clients must reinitialize |
|
| **Server restarts** | No impact — each request is independent | All sessions lost; clients must reinitialize |
|
||||||
|
|
@ -177,7 +177,7 @@ The [deployment considerations](#deployment-considerations) below are real conce
|
||||||
| **Server-to-client requests** | Available via [MRTR](xref:mrtr) (`2026-07-28`-only) for elicitation, plus deprecated sampling and roots | Supported (elicitation; deprecated sampling and roots) |
|
| **Server-to-client requests** | Available via [MRTR](xref:mrtr) (`2026-07-28`-only) for elicitation, plus deprecated sampling and roots | Supported (elicitation; deprecated sampling and roots) |
|
||||||
| **[Unsolicited notifications](#how-streamable-http-delivers-messages)** | Not supported | Supported (resource updates, logging) |
|
| **[Unsolicited notifications](#how-streamable-http-delivers-messages)** | Not supported | Supported (resource updates, logging) |
|
||||||
| **Resource subscriptions** | Not supported | Supported |
|
| **Resource subscriptions** | Not supported | Supported |
|
||||||
| **Client compatibility** | Works with all Streamable HTTP clients | Also supports legacy SSE-only clients via <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> (disabled by default), but some Streamable HTTP clients [may not send `Mcp-Session-Id` correctly](#deployment-considerations) |
|
| **Client compatibility** | Works with all Streamable HTTP clients | Also supports legacy SSE-only clients via <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> (disabled by default), but some Streamable HTTP clients [might not send `Mcp-Session-Id` correctly](#deployment-considerations) |
|
||||||
| **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 |
|
| **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 |
|
||||||
| **Concurrent client isolation** | No distinction between clients — all requests are independent | Each client gets its own session with isolated state |
|
| **Concurrent client isolation** | No distinction between clients — all requests are independent | Each client gets its own session with isolated state |
|
||||||
| **State reset on reconnect** | No concept of reconnection — every request stands alone | Client reconnection starts a new session with a clean slate |
|
| **State reset on reconnect** | No concept of reconnection — every request stands alone | Client reconnection starts a new session with a clean slate |
|
||||||
|
|
@ -191,28 +191,28 @@ The [deployment considerations](#deployment-considerations) below are real conce
|
||||||
|
|
||||||
Understanding how messages flow between client and server over HTTP is key to understanding why sessions exist and when you can avoid them.
|
Understanding how messages flow between client and server over HTTP is key to understanding why sessions exist and when you can avoid them.
|
||||||
|
|
||||||
**POST response streams (solicited messages).** Every JSON-RPC request from the client arrives as an HTTP POST. The server holds the POST response body open as a [Server-Sent Events (SSE)](https://html.spec.whatwg.org/multipage/server-sent-events.html) stream and writes messages back to it: the JSON-RPC response, any intermediate messages the handler produces (progress notifications, log messages), and — critically — any **server-to-client requests** the handler makes during execution, such as sampling, elicitation, or roots requests. This is a **solicited** interaction: the client's POST request solicited the server's response, and the server writes everything related to that request into the same HTTP response body. The POST response completes when the final JSON-RPC response is sent.
|
**POST response streams (solicited messages).** Every JSON-RPC request from the client arrives as an HTTP POST. The server holds the `POST` response body open as a [Server-Sent Events (SSE)](https://html.spec.whatwg.org/multipage/server-sent-events.html) stream and writes messages back to it: the JSON-RPC response, any intermediate messages the handler produces (progress notifications, log messages), and — critically — any **server-to-client requests** the handler makes during execution, such as sampling, elicitation, or roots requests. This is a **solicited** interaction: the client's `POST` request solicited the server's response, and the server writes everything related to that request into the same HTTP response body. The `POST` response completes when the final JSON-RPC response is sent.
|
||||||
|
|
||||||
**The GET stream (unsolicited messages).** The client can optionally open a long-lived HTTP GET request to the same MCP endpoint. This stream is the **only** channel for **unsolicited** messages — notifications or server-to-client requests that the server initiates _outside the context of any active request handler_. For example:
|
**The `GET` stream (unsolicited messages).** The client can optionally open a long-lived HTTP `GET` request to the same MCP endpoint. This stream is the **only** channel for **unsolicited** messages — notifications or server-to-client requests that the server initiates _outside the context of any active request handler_. For example:
|
||||||
|
|
||||||
- A resource-changed notification fired by a background file watcher
|
- A resource-changed notification fired by a background file watcher
|
||||||
- A log message emitted asynchronously after all request handlers have returned
|
- A log message emitted asynchronously after all request handlers have returned
|
||||||
- A server-to-client request that isn't triggered by a tool call
|
- A server-to-client request that isn't triggered by a tool call
|
||||||
|
|
||||||
These messages are "unsolicited" because no client POST solicited them. There is no POST response body to write them to — because outside of POST requests that solicit the server 1:1 with a JSON-RPC request, there is simply no HTTP response body stream available. The GET stream fills this gap.
|
These messages are "unsolicited" because no client `POST` solicited them. There is no `POST` response body to write them to — because outside of `POST` requests that solicit the server 1:1 with a JSON-RPC request, there is simply no HTTP response body stream available. The `GET` stream fills this gap.
|
||||||
|
|
||||||
**No GET stream = messages silently dropped.** Clients are not required to open a GET stream. If the client hasn't opened one, the server has no delivery path for unsolicited messages and silently drops them. This is by design in the [Streamable HTTP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) — unsolicited messages are best-effort.
|
**No `GET` stream = messages silently dropped.** Clients are not required to open a `GET` stream. If the client hasn't opened one, the server has no delivery path for unsolicited messages and silently drops them. This is by design in the [Streamable HTTP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) — unsolicited messages are best-effort.
|
||||||
|
|
||||||
**Why stateless mode can't support unsolicited messages.** In stateless mode, the GET endpoint is not mapped at all. Every message the server sends must be part of a POST response — there is no other HTTP response body to write to. This is also why server-to-client requests (sampling, elicitation, roots) are disabled: the server could initiate a request down the POST response stream during a handler, but the client's response to that request would arrive as a _new_ POST — which in stateless mode creates a completely independent server context with no connection to the original handler. The server has no way to correlate the client's reply with the handler that asked the question. Sessions solve this by keeping the handler alive across multiple HTTP round-trips within the same in-memory session.
|
**Why stateless mode can't support unsolicited messages.** In stateless mode, the `GET` endpoint is not mapped at all. Every message the server sends must be part of a `POST` response — there is no other HTTP response body to write to. This is also why server-to-client requests (sampling, elicitation, roots) are disabled: the server could initiate a request down the `POST` response stream during a handler, but the client's response to that request would arrive as a _new_ `POST` — which in stateless mode creates a completely independent server context with no connection to the original handler. The server has no way to correlate the client's reply with the handler that asked the question. Sessions solve this by keeping the handler alive across multiple HTTP round-trips within the same in-memory session.
|
||||||
|
|
||||||
#### Session lifecycle
|
#### Session lifecycle
|
||||||
|
|
||||||
A session begins when a client sends an `initialize` JSON-RPC request without an `Mcp-Session-Id` header. The server:
|
A session begins when a client sends an `initialize` JSON-RPC request without an `Mcp-Session-Id` header. The server:
|
||||||
|
|
||||||
1. Creates a new session with a unique session ID
|
1. Creates a new session with a unique session ID.
|
||||||
2. Calls <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.ConfigureSessionOptions> (if configured) to customize the session's `McpServerOptions`
|
2. Calls <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.ConfigureSessionOptions> (if configured) to customize the session's `McpServerOptions`.
|
||||||
3. Starts the MCP server for the session
|
3. Starts the MCP server for the session.
|
||||||
4. Returns the session ID in the `Mcp-Session-Id` response header along with the `InitializeResult`
|
4. Returns the session ID in the `Mcp-Session-Id` response header along with the `InitializeResult`.
|
||||||
|
|
||||||
All subsequent requests from the client must include this session ID.
|
All subsequent requests from the client must include this session ID.
|
||||||
|
|
||||||
|
|
@ -220,20 +220,20 @@ All subsequent requests from the client must include this session ID.
|
||||||
|
|
||||||
The server tracks the last activity time for each Streamable HTTP session. Activity is recorded when:
|
The server tracks the last activity time for each Streamable HTTP session. Activity is recorded when:
|
||||||
|
|
||||||
- A request arrives for the session (POST or GET)
|
- A request arrives for the session (`POST` or `GET`)
|
||||||
- A response is sent for the session
|
- A response is sent for the session
|
||||||
|
|
||||||
#### Idle timeout
|
#### Idle timeout
|
||||||
|
|
||||||
Streamable HTTP sessions that have no activity for the duration of <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.IdleTimeout> (default: **2 hours**) are automatically closed. The idle timeout is checked in the background every 5 seconds.
|
Streamable HTTP sessions that have no activity for the duration of <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.IdleTimeout> (default: **2 hours**) are automatically closed. The idle timeout is checked in the background every 5 seconds.
|
||||||
|
|
||||||
A client can keep its session alive by maintaining any open HTTP request (e.g., a long-running POST with a streamed response or an open `GET` for unsolicited messages). Sessions with active requests are never considered idle.
|
A client can keep its session alive by maintaining any open HTTP request (for example, a long-running `POST` with a streamed response or an open `GET` for unsolicited messages). Sessions with active requests are never considered idle.
|
||||||
|
|
||||||
When a session times out:
|
When a session times out:
|
||||||
|
|
||||||
- The session's `McpServer` is disposed
|
- The session's `McpServer` is disposed
|
||||||
- Any pending requests receive cancellation
|
- Any pending requests receive cancellation
|
||||||
- A client trying to use the expired session ID receives a `404 Session not found` error and should start a new session
|
- A client that tries to use the expired session ID receives a `404 Session not found` error and should start a new session
|
||||||
|
|
||||||
You can disable idle timeout by setting it to `Timeout.InfiniteTimeSpan`, though this is not recommended for production deployments.
|
You can disable idle timeout by setting it to `Timeout.InfiniteTimeSpan`, though this is not recommended for production deployments.
|
||||||
|
|
||||||
|
|
@ -251,10 +251,10 @@ Sessions with any active HTTP request don't count toward this limit.
|
||||||
|
|
||||||
Streamable HTTP sessions can be terminated by:
|
Streamable HTTP sessions can be terminated by:
|
||||||
|
|
||||||
- **Client DELETE request**: The client sends an HTTP `DELETE` to the session endpoint with its `Mcp-Session-Id`
|
- **Client DELETE request**: The client sends an HTTP `DELETE` to the session endpoint with its `Mcp-Session-Id`.
|
||||||
- **Idle timeout**: The session exceeds the idle timeout without activity
|
- **Idle timeout**: The session exceeds the idle timeout without activity.
|
||||||
- **Max idle count**: The server exceeds its maximum idle session count and prunes the oldest sessions
|
- **Max idle count**: The server exceeds its maximum idle session count and prunes the oldest sessions.
|
||||||
- **Server shutdown**: All sessions are disposed when the server shuts down
|
- **Server shutdown**: All sessions are disposed when the server shuts down.
|
||||||
|
|
||||||
#### Deployment considerations
|
#### Deployment considerations
|
||||||
|
|
||||||
|
|
@ -264,11 +264,11 @@ Stateful sessions introduce several challenges for production, internet-facing s
|
||||||
|
|
||||||
**Memory consumption.** Each session consumes memory on the server for the lifetime of the session. The default idle timeout is **2 hours**, and the default maximum idle session count is **10,000**. A server with many concurrent clients can accumulate significant memory usage. Monitor your idle session count and tune <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.IdleTimeout> and <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.MaxIdleSessionCount> to match your workload.
|
**Memory consumption.** Each session consumes memory on the server for the lifetime of the session. The default idle timeout is **2 hours**, and the default maximum idle session count is **10,000**. A server with many concurrent clients can accumulate significant memory usage. Monitor your idle session count and tune <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.IdleTimeout> and <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.MaxIdleSessionCount> to match your workload.
|
||||||
|
|
||||||
**Server restarts lose all sessions.** Sessions are stored in memory by default. When the server restarts (for deployments, crashes, or scaling events), all sessions are lost. Clients must reinitialize their sessions, which some clients may not handle gracefully. You can mitigate this with <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.SessionMigrationHandler>, but this adds complexity. See [Session migration](#session-migration) for details.
|
**Server restarts lose all sessions.** Sessions are stored in memory by default. When the server restarts (for deployments, crashes, or scaling events), all sessions are lost. Clients must reinitialize their sessions, which some clients might not handle gracefully. You can mitigate this with <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.SessionMigrationHandler>, but this adds complexity. See [Session migration](#session-migration) for details.
|
||||||
|
|
||||||
**Clients that don't send Mcp-Session-Id.** Some MCP clients may not send the `Mcp-Session-Id` header on every request. When this happens, the server responds with an error: `"Bad Request: A new session can only be created by an initialize request."` This can happen after a server restart, when a client loses its session ID, or when a client simply doesn't support sessions. If you see this error, consider whether your server actually needs sessions — and if not, switch to stateless mode.
|
**Clients that don't send Mcp-Session-Id.** Some MCP clients might not send the `Mcp-Session-Id` header on every request. When this happens, the server responds with an error: `"Bad Request: A new session can only be created by an initialize request."` This can happen after a server restart, when a client loses its session ID, or when a client simply doesn't support sessions. If you see this error, consider whether your server actually needs sessions — and if not, switch to stateless mode.
|
||||||
|
|
||||||
**No built-in backpressure on advanced features.** By default, each JSON-RPC request holds its HTTP POST open until the handler responds — providing natural HTTP/2 backpressure. However, advanced features like <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EventStreamStore> and [Tasks](xref:tasks) can decouple handler execution from the HTTP request, removing this protection. See [Request backpressure](#request-backpressure) for details and mitigations.
|
**No built-in backpressure on advanced features.** By default, each JSON-RPC request holds its HTTP `POST` open until the handler responds — providing natural HTTP/2 backpressure. However, advanced features like <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EventStreamStore> and [Tasks](xref:tasks) can decouple handler execution from the HTTP request, removing this protection. See [Request backpressure](#request-backpressure) for details and mitigations.
|
||||||
|
|
||||||
### stdio transport
|
### stdio transport
|
||||||
|
|
||||||
|
|
@ -288,10 +288,10 @@ The SDK's MCP client (<xref:ModelContextProtocol.Client.McpClient>) participates
|
||||||
|
|
||||||
When you call <xref:ModelContextProtocol.Client.McpClient.CreateAsync*>, the client:
|
When you call <xref:ModelContextProtocol.Client.McpClient.CreateAsync*>, the client:
|
||||||
|
|
||||||
1. Connects to the server via the configured transport
|
1. Connects to the server via the configured transport.
|
||||||
2. Sends an `initialize` JSON-RPC request (without an `Mcp-Session-Id` header)
|
2. Sends an `initialize` JSON-RPC request (without an `Mcp-Session-Id` header).
|
||||||
3. Receives the server's `InitializeResult` — if the response includes an `Mcp-Session-Id` header, the client stores it
|
3. Receives the server's `InitializeResult` — if the response includes an `Mcp-Session-Id` header, the client stores it.
|
||||||
4. Automatically includes the session ID in all subsequent requests (POST, GET, DELETE)
|
4. Automatically includes the session ID in all subsequent requests (POST, GET, DELETE).
|
||||||
|
|
||||||
This is entirely automatic — you don't need to manage the session ID yourself. The <xref:ModelContextProtocol.McpSession.SessionId> property exposes the current session ID (or `null` for transports that don't support sessions, like stdio).
|
This is entirely automatic — you don't need to manage the session ID yourself. The <xref:ModelContextProtocol.McpSession.SessionId> property exposes the current session ID (or `null` for transports that don't support sessions, like stdio).
|
||||||
|
|
||||||
|
|
@ -299,9 +299,9 @@ This is entirely automatic — you don't need to manage the session ID yourself.
|
||||||
|
|
||||||
The server can terminate a session at any time — due to idle timeout, max session count exceeded, explicit shutdown, or any server-side policy. When this happens, subsequent requests with that session ID receive HTTP `404`. The client detects this and:
|
The server can terminate a session at any time — due to idle timeout, max session count exceeded, explicit shutdown, or any server-side policy. When this happens, subsequent requests with that session ID receive HTTP `404`. The client detects this and:
|
||||||
|
|
||||||
1. Wraps the failure in a <xref:ModelContextProtocol.Client.ClientTransportClosedException> with <xref:ModelContextProtocol.Client.HttpClientCompletionDetails> containing the HTTP status code
|
1. Wraps the failure in a <xref:ModelContextProtocol.Client.ClientTransportClosedException> with <xref:ModelContextProtocol.Client.HttpClientCompletionDetails> containing the HTTP status code.
|
||||||
2. Cancels all in-flight operations
|
2. Cancels all in-flight operations.
|
||||||
3. Completes the <xref:ModelContextProtocol.Client.McpClient.Completion> task
|
3. Completes the <xref:ModelContextProtocol.Client.McpClient.Completion> task.
|
||||||
|
|
||||||
**There is no automatic reconnection after session expiry.** Your application must handle this. You can either create a fresh session with <xref:ModelContextProtocol.Client.McpClient.CreateAsync*>, or attempt to resume the existing session with <xref:ModelContextProtocol.Client.McpClient.ResumeSessionAsync*> if the server supports it.
|
**There is no automatic reconnection after session expiry.** Your application must handle this. You can either create a fresh session with <xref:ModelContextProtocol.Client.McpClient.CreateAsync*>, or attempt to resume the existing session with <xref:ModelContextProtocol.Client.McpClient.ResumeSessionAsync*> if the server supports it.
|
||||||
|
|
||||||
|
|
@ -338,7 +338,7 @@ async Task<McpClient> ConnectWithRetryAsync(
|
||||||
|
|
||||||
#### Stream reconnection
|
#### Stream reconnection
|
||||||
|
|
||||||
The Streamable HTTP client automatically reconnects its SSE event stream when the connection drops. This only applies to **stateful sessions** — the GET event stream is how the server sends unsolicited messages to the client, and it requires an active session. Stream reconnection is separate from session expiry: reconnection recovers the event stream within an existing session, while the example above handles creating a new session after the server has terminated the old one.
|
The Streamable HTTP client automatically reconnects its SSE event stream when the connection drops. This only applies to **stateful sessions** — the `GET` event stream is how the server sends unsolicited messages to the client, and it requires an active session. Stream reconnection is separate from session expiry: reconnection recovers the event stream within an existing session, while the previous example handles creating a new session after the server has terminated the old one.
|
||||||
|
|
||||||
If the server has an [event store](#session-resumability) configured, the client sends `Last-Event-ID` on reconnection so the server can replay missed events. See [Transports](xref:transports) for details on reconnection intervals and retry limits (<xref:ModelContextProtocol.Client.HttpClientTransportOptions.MaxReconnectionAttempts>, <xref:ModelContextProtocol.Client.HttpClientTransportOptions.DefaultReconnectionInterval>). If all reconnection attempts are exhausted, the transport closes and `McpClient.Completion` resolves.
|
If the server has an [event store](#session-resumability) configured, the client sends `Last-Event-ID` on reconnection so the server can replay missed events. See [Transports](xref:transports) for details on reconnection intervals and retry limits (<xref:ModelContextProtocol.Client.HttpClientTransportOptions.MaxReconnectionAttempts>, <xref:ModelContextProtocol.Client.HttpClientTransportOptions.DefaultReconnectionInterval>). If all reconnection attempts are exhausted, the transport closes and `McpClient.Completion` resolves.
|
||||||
|
|
||||||
|
|
@ -352,19 +352,19 @@ If the server is still tracking the session (or supports [session migration](#se
|
||||||
- <xref:ModelContextProtocol.Client.McpClient.ServerInstructions> (optional)
|
- <xref:ModelContextProtocol.Client.McpClient.ServerInstructions> (optional)
|
||||||
- <xref:ModelContextProtocol.McpSession.NegotiatedProtocolVersion> (optional)
|
- <xref:ModelContextProtocol.McpSession.NegotiatedProtocolVersion> (optional)
|
||||||
|
|
||||||
See the [Resuming sessions](xref:transports#resuming-sessions) section in the Transports guide for a code example.
|
For a code example, see the [Resuming sessions](xref:transports#resuming-sessions) section in the Transports guide.
|
||||||
|
|
||||||
Session resumption is useful when:
|
Session resumption is useful when:
|
||||||
|
|
||||||
- The client process restarts but the server session is still alive
|
- The client process restarts but the server session is still alive.
|
||||||
- A transient network failure disconnects the client but the server hasn't timed out the session
|
- A transient network failure disconnects the client but the server hasn't timed out the session.
|
||||||
- You want to hand off a session between different parts of your application
|
- You want to hand off a session between different parts of your application.
|
||||||
|
|
||||||
#### Terminating a session
|
#### Terminating a session
|
||||||
|
|
||||||
When you dispose an `McpClient` (via `await using` or explicit `DisposeAsync`), the client sends an HTTP `DELETE` request to the session endpoint with the `Mcp-Session-Id` header. This tells the server to clean up the session immediately rather than waiting for the idle timeout.
|
When you dispose an `McpClient` (via `await using` or explicit `DisposeAsync`), the client sends an HTTP `DELETE` request to the session endpoint with the `Mcp-Session-Id` header. This tells the server to clean up the session immediately rather than waiting for the idle timeout.
|
||||||
|
|
||||||
The <xref:ModelContextProtocol.Client.HttpClientTransportOptions.OwnsSession> property (default: `true`) controls this behavior. Set it to `false` when you're creating a transport purely to bootstrap session information (e.g., reading capabilities) without intending to own the session's lifetime.
|
The <xref:ModelContextProtocol.Client.HttpClientTransportOptions.OwnsSession> property (default: `true`) controls this behavior. Set it to `false` when you're creating a transport purely to bootstrap session information (for example, reading capabilities) without intending to own the session's lifetime.
|
||||||
|
|
||||||
### Client transport options
|
### Client transport options
|
||||||
|
|
||||||
|
|
@ -373,9 +373,9 @@ The following <xref:ModelContextProtocol.Client.HttpClientTransportOptions> prop
|
||||||
| Property | Default | Description |
|
| Property | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| <xref:ModelContextProtocol.Client.HttpClientTransportOptions.KnownSessionId> | `null` | Pre-existing session ID for use with <xref:ModelContextProtocol.Client.McpClient.ResumeSessionAsync*>. When set, the client includes this session ID immediately and starts listening for unsolicited messages. |
|
| <xref:ModelContextProtocol.Client.HttpClientTransportOptions.KnownSessionId> | `null` | Pre-existing session ID for use with <xref:ModelContextProtocol.Client.McpClient.ResumeSessionAsync*>. When set, the client includes this session ID immediately and starts listening for unsolicited messages. |
|
||||||
| <xref:ModelContextProtocol.Client.HttpClientTransportOptions.EnableStandaloneGetStream> | `true` | Opens the standalone GET stream for unsolicited messages. Set to `false` to skip it; POST request/response streaming still works. |
|
| <xref:ModelContextProtocol.Client.HttpClientTransportOptions.EnableStandaloneGetStream> | `true` | Opens the standalone `GET` stream for unsolicited messages. Set to `false` to skip it; `POST` request/response streaming still works. |
|
||||||
| <xref:ModelContextProtocol.Client.HttpClientTransportOptions.OwnsSession> | `true` | Whether to send a DELETE request when the client is disposed. Set to `false` when you don't want disposal to terminate the server session. |
|
| <xref:ModelContextProtocol.Client.HttpClientTransportOptions.OwnsSession> | `true` | Whether to send a `DELETE` request when the client is disposed. Set to `false` when you don't want disposal to terminate the server session. |
|
||||||
| <xref:ModelContextProtocol.Client.HttpClientTransportOptions.AdditionalHeaders> | `null` | Custom headers included in all requests (e.g., for authentication). These are sent alongside the automatic `Mcp-Session-Id` header. |
|
| <xref:ModelContextProtocol.Client.HttpClientTransportOptions.AdditionalHeaders> | `null` | Custom headers included in all requests (for example, for authentication). These are sent alongside the automatic `Mcp-Session-Id` header. |
|
||||||
|
|
||||||
For transport-level options like reconnection intervals and transport mode, see [Transports](xref:transports).
|
For transport-level options like reconnection intervals and transport mode, see [Transports](xref:transports).
|
||||||
|
|
||||||
|
|
@ -447,7 +447,7 @@ options.ConfigureSessionOptions = async (httpContext, mcpServerOptions, cancella
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
See the [AspNetCoreMcpPerSessionTools](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/AspNetCoreMcpPerSessionTools) sample for a complete example that filters tools based on route parameters.
|
For a complete example that filters tools based on route parameters, see the [AspNetCoreMcpPerSessionTools](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/AspNetCoreMcpPerSessionTools) sample.
|
||||||
|
|
||||||
#### Per-request configuration in stateless mode
|
#### Per-request configuration in stateless mode
|
||||||
|
|
||||||
|
|
@ -469,7 +469,7 @@ builder.Services.AddMcpServer()
|
||||||
})
|
})
|
||||||
.WithTools<DefaultTools>();
|
.WithTools<DefaultTools>();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Security and user binding
|
### Security and user binding
|
||||||
|
|
||||||
#### User binding
|
#### User binding
|
||||||
|
|
@ -478,13 +478,13 @@ When authentication is configured, the server automatically binds sessions to th
|
||||||
|
|
||||||
##### How it works
|
##### How it works
|
||||||
|
|
||||||
1. When a session is created, the server captures the authenticated user's identity from `HttpContext.User`
|
1. When a session is created, the server captures the authenticated user's identity from `HttpContext.User`.
|
||||||
2. The server extracts a user ID claim in priority order:
|
2. The server extracts a user ID claim in priority order:
|
||||||
- `ClaimTypes.NameIdentifier` (`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier`)
|
- `ClaimTypes.NameIdentifier` (`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier`)
|
||||||
- `"sub"` (OpenID Connect subject claim)
|
- `"sub"` (OpenID Connect subject claim)
|
||||||
- `ClaimTypes.Upn` (`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn`)
|
- `ClaimTypes.Upn` (`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn`)
|
||||||
3. On each subsequent request, the server validates that the current user matches the session's original user
|
3. On each subsequent request, the server validates that the current user matches the session's original user.
|
||||||
4. If there's a mismatch, the server responds with `403 Forbidden`
|
4. If there's a mismatch, the server responds with `403 Forbidden`.
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
|
|
@ -498,9 +498,9 @@ In stateful mode, the server's <xref:ModelContextProtocol.Server.McpServer.Servi
|
||||||
|
|
||||||
This means:
|
This means:
|
||||||
|
|
||||||
- **Scoped services** are created fresh for each handler invocation and disposed when the handler completes
|
- **Scoped services** are created fresh for each handler invocation and disposed when the handler completes.
|
||||||
- **Singleton services** resolve from the application container as usual
|
- **Singleton services** resolve from the application container as usual.
|
||||||
- **Transient services** create a new instance per resolution, as usual
|
- **Transient services** create a new instance per resolution, as usual.
|
||||||
|
|
||||||
### Stateless HTTP
|
### Stateless HTTP
|
||||||
|
|
||||||
|
|
@ -508,8 +508,8 @@ In stateless mode, the server uses ASP.NET Core's per-request `HttpContext.Reque
|
||||||
|
|
||||||
This means:
|
This means:
|
||||||
|
|
||||||
- **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
|
- **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.
|
||||||
- The DI lifetime model is identical to a standard ASP.NET Core controller or minimal API endpoint
|
- The DI lifetime model is identical to a standard ASP.NET Core controller or minimal API endpoint.
|
||||||
|
|
||||||
### stdio
|
### stdio
|
||||||
|
|
||||||
|
|
@ -536,11 +536,11 @@ await using McpServer server = McpServer.Create(
|
||||||
|
|
||||||
### DI scope summary
|
### DI scope summary
|
||||||
|
|
||||||
| Mode | Service provider | ScopeRequests | Handler scope |
|
| Mode | Service provider | ScopeRequests | Handler scope |
|
||||||
|------|-----------------|---------------|---------------|
|
|--------------------|-------------------------------|------------------|----------------------------------|
|
||||||
| **Stateful HTTP** | Application services | `true` (default) | New scope per handler invocation |
|
| **Stateful HTTP** | Application services | `true` (default) | New scope per handler invocation |
|
||||||
| **Stateless HTTP** | `HttpContext.RequestServices` | `false` (forced) | Shared HTTP request scope |
|
| **Stateless HTTP** | `HttpContext.RequestServices` | `false` (forced) | Shared HTTP request scope |
|
||||||
| **stdio** | Application services | `true` (default, configurable) | New scope per handler invocation |
|
| **stdio** | Application services | `true` (default, configurable) | New scope per handler invocation |
|
||||||
| **McpServer.Create** | Caller-provided | Caller-controlled | Depends on `ScopeRequests` and whether the provider is already scoped |
|
| **McpServer.Create** | Caller-provided | Caller-controlled | Depends on `ScopeRequests` and whether the provider is already scoped |
|
||||||
|
|
||||||
## Cancellation and disposal
|
## Cancellation and disposal
|
||||||
|
|
@ -552,42 +552,42 @@ Every tool, prompt, and resource handler can receive a `CancellationToken`. The
|
||||||
| Mode | Token source | Cancelled when |
|
| Mode | Token source | Cancelled when |
|
||||||
|------|-------------|----------------|
|
|------|-------------|----------------|
|
||||||
| **Stateless HTTP** | `HttpContext.RequestAborted` | Client disconnects, or ASP.NET Core shuts down. Identical to a standard minimal API or controller action. |
|
| **Stateless HTTP** | `HttpContext.RequestAborted` | Client disconnects, or ASP.NET Core shuts down. Identical to a standard minimal API or controller action. |
|
||||||
| **Stateful Streamable HTTP** | Linked token: HTTP request + application shutdown + session disposal | Client disconnects, `ApplicationStopping` fires, or the session is terminated (idle timeout, DELETE, max idle count). |
|
| **Stateful Streamable HTTP** | Linked token: HTTP request + application shutdown + session disposal | Client disconnects, `ApplicationStopping` fires, or the session is terminated (idle timeout, `DELETE`, max idle count). |
|
||||||
| **SSE (legacy)** | Linked token: GET request + application shutdown | Client disconnects the SSE stream, or `ApplicationStopping` fires. The entire session terminates with the GET stream. |
|
| **SSE (legacy)** | Linked token: `GET` request + application shutdown | Client disconnects the SSE stream, or `ApplicationStopping` fires. The entire session terminates with the `GET` stream. |
|
||||||
| **stdio** | Token passed to `McpServer.RunAsync()` | stdin EOF (client process exits), or the token is cancelled (e.g., host shutdown via Ctrl+C). |
|
| **stdio** | Token passed to `McpServer.RunAsync()` | stdin EOF (client process exits), or the token is cancelled (for example, host shutdown via <kbd>Ctrl+C</kbd>). |
|
||||||
|
|
||||||
Stateless mode has the simplest cancellation story: the handler's `CancellationToken` is `HttpContext.RequestAborted` — the same token any ASP.NET Core endpoint receives. No additional tokens, linked sources, or session-level lifecycle to reason about.
|
Stateless mode has the simplest cancellation story: the handler's `CancellationToken` is `HttpContext.RequestAborted` — the same token any ASP.NET Core endpoint receives. No additional tokens, linked sources, or session-level lifecycle to reason about.
|
||||||
|
|
||||||
### Client-initiated cancellation
|
### Client-initiated cancellation
|
||||||
|
|
||||||
In stateful modes (Streamable HTTP, SSE, stdio), a client can cancel a specific in-flight request by sending a [`notifications/cancelled`](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation) notification with the request ID. The SDK looks up the running handler and cancels its `CancellationToken`. This may result in an `OperationCanceledException` if the handler is awaiting a cancellation-aware operation when the token is cancelled.
|
In stateful modes (Streamable HTTP, SSE, stdio), a client can cancel a specific in-flight request by sending a [`notifications/cancelled`](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation) notification with the request ID. The SDK looks up the running handler and cancels its `CancellationToken`. This can result in an `OperationCanceledException` if the handler is awaiting a cancellation-aware operation when the token is cancelled.
|
||||||
|
|
||||||
- Invalid or unknown request IDs are silently ignored
|
- Invalid or unknown request IDs are silently ignored.
|
||||||
- In stateless mode, there is no persistent session to receive the notification on, so client-initiated cancellation does not apply
|
- In stateless mode, there is no persistent session to receive the notification on, so client-initiated cancellation does not apply.
|
||||||
- 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.
|
- 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.
|
||||||
|
|
||||||
### Server and session disposal
|
### Server and session disposal
|
||||||
|
|
||||||
When an `McpServer` is disposed — whether due to session termination, transport closure, or application shutdown — the SDK **awaits all in-flight handlers** before `DisposeAsync()` returns. This means:
|
When an `McpServer` is disposed — whether due to session termination, transport closure, or application shutdown — the SDK **awaits all in-flight handlers** before `DisposeAsync()` returns. This means:
|
||||||
|
|
||||||
- Handlers have an opportunity to complete cleanup (e.g., flushing writes, releasing locks)
|
- Handlers have an opportunity to complete cleanup (for example, flushing writes, releasing locks).
|
||||||
- Scoped services created for the handler are disposed after the handler completes
|
- Scoped services created for the handler are disposed after the handler completes.
|
||||||
- The SDK logs each handler's completion at `Information` level, including elapsed time
|
- The SDK logs each handler's completion at `Information` level, including elapsed time.
|
||||||
|
|
||||||
#### Graceful shutdown in ASP.NET Core
|
#### Graceful shutdown in ASP.NET Core
|
||||||
|
|
||||||
When `ApplicationStopping` fires (e.g., `SIGTERM`, `Ctrl+C`, `app.StopAsync()`), the SDK immediately cancels active SSE and GET streams so that connected clients don't block shutdown. In-flight POST request handlers continue running and are awaited before the server finishes disposing. The total shutdown time is bounded by ASP.NET Core's `HostOptions.ShutdownTimeout` (default: **30 seconds**). In practice, the SDK completes shutdown well within this limit.
|
When `ApplicationStopping` fires (for example, `SIGTERM`, <kbd>Ctrl+C</kbd>, `app.StopAsync()`), the SDK immediately cancels active SSE and `GET` streams so that connected clients don't block shutdown. In-flight `POST` request handlers continue running and are awaited before the server finishes disposing. The total shutdown time is bounded by ASP.NET Core's `HostOptions.ShutdownTimeout` (default: **30 seconds**). In practice, the SDK completes shutdown well within this limit.
|
||||||
|
|
||||||
For stateless servers, shutdown is even simpler: each request is independent, so there are no long-lived sessions to drain — just standard ASP.NET Core request completion.
|
For stateless servers, shutdown is even simpler: each request is independent, so there are no long-lived sessions to drain — just standard ASP.NET Core request completion.
|
||||||
|
|
||||||
#### stdio process lifecycle
|
#### stdio process lifecycle
|
||||||
|
|
||||||
- **Graceful shutdown** (stdin EOF, `SIGTERM`, `Ctrl+C`): The transport closes, in-flight handlers are awaited, and `McpServer.DisposeAsync()` runs normally.
|
- **Graceful shutdown** (stdin EOF, `SIGTERM`, <kbd>Ctrl+C</kbd>): The transport closes, in-flight handlers are awaited, and `McpServer.DisposeAsync()` runs normally.
|
||||||
- **Process kill** (`SIGKILL`): No cleanup occurs. Handlers are interrupted mid-execution, and no disposal code runs. This is inherent to process-level termination and not specific to the SDK.
|
- **Process kill** (`SIGKILL`): No cleanup occurs. Handlers are interrupted mid-execution, and no disposal code runs. This is inherent to process-level termination and not specific to the SDK.
|
||||||
|
|
||||||
### Stateless per-request logging
|
### Stateless per-request logging
|
||||||
|
|
||||||
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.
|
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 entries are typically invisible at default log levels but might 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.
|
||||||
|
|
||||||
## Tasks and session modes
|
## Tasks and session modes
|
||||||
|
|
||||||
|
|
@ -603,9 +603,9 @@ In stateless mode, there is no `SessionId`, so the task store does not apply ses
|
||||||
|
|
||||||
In stateful mode, the `IMcpTaskStore` receives the session's `SessionId` on every operation: `CreateTaskAsync`, `GetTaskAsync`, `ListTasksAsync`, `CancelTaskAsync`, etc. The built-in <xref:ModelContextProtocol.Extensions.Tasks.InMemoryMcpTaskStore> enforces session isolation: tasks created in one session cannot be accessed from another.
|
In stateful mode, the `IMcpTaskStore` receives the session's `SessionId` on every operation: `CreateTaskAsync`, `GetTaskAsync`, `ListTasksAsync`, `CancelTaskAsync`, etc. The built-in <xref:ModelContextProtocol.Extensions.Tasks.InMemoryMcpTaskStore> enforces session isolation: tasks created in one session cannot be accessed from another.
|
||||||
|
|
||||||
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.Extensions.Tasks.IMcpTaskStore> backed by an external store. See [Implementing a custom task store](xref:tasks#implementing-a-custom-task-store) for guidance.
|
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.Extensions.Tasks.IMcpTaskStore> backed by an external store. For guidance, see [Implementing a custom task store](xref:tasks#implementing-a-custom-task-store).
|
||||||
|
|
||||||
### Task cancellation vs request cancellation
|
### Task cancellation vs. request cancellation
|
||||||
|
|
||||||
The MCP specification defines two distinct cancellation mechanisms:
|
The MCP specification defines two distinct cancellation mechanisms:
|
||||||
|
|
||||||
|
|
@ -616,17 +616,17 @@ For task-augmented requests, the specification [requires](https://modelcontextpr
|
||||||
|
|
||||||
## Request backpressure
|
## Request backpressure
|
||||||
|
|
||||||
How well the server is protected against a flood of concurrent requests depends on the session mode and which advanced features are enabled. **In the default configuration, stateful and stateless modes provide identical HTTP-level backpressure** — both hold the POST response open while the handler runs, so HTTP/2's `MaxStreamsPerConnection` (default: **100**) naturally limits concurrent handlers per connection. The unbounded cases (legacy SSE, `EventStreamStore`, Tasks) are all **opt-in** advanced features.
|
How well the server is protected against a flood of concurrent requests depends on the session mode and which advanced features are enabled. **In the default configuration, stateful and stateless modes provide identical HTTP-level backpressure** — both hold the `POST` response open while the handler runs, so HTTP/2's `MaxStreamsPerConnection` (default: **100**) naturally limits concurrent handlers per connection. The unbounded cases (legacy SSE, `EventStreamStore`, Tasks) are all **opt-in** advanced features.
|
||||||
|
|
||||||
### Default stateful mode (no EventStreamStore, no tasks)
|
### Default stateful mode (no EventStreamStore, no tasks)
|
||||||
|
|
||||||
In the default configuration, each JSON-RPC request holds its POST response open until the handler produces a result. The POST response body is an SSE stream that carries the JSON-RPC response, and the server awaits the handler's completion before closing it. This means:
|
In the default configuration, each JSON-RPC request holds its `POST` response open until the handler produces a result. The `POST` response body is an SSE stream that carries the JSON-RPC response, and the server awaits the handler's completion before closing it. This means:
|
||||||
|
|
||||||
- Each in-flight handler occupies one HTTP/2 stream
|
- Each in-flight handler occupies one HTTP/2 stream
|
||||||
- The HTTP server's `MaxStreamsPerConnection` (default: **100** in Kestrel) limits concurrent handlers per connection
|
- The HTTP server's `MaxStreamsPerConnection` (default: **100** in Kestrel) limits concurrent handlers per connection
|
||||||
- This is the same backpressure model as **gRPC unary calls** — one request occupies one stream until the response is sent
|
- This is the same backpressure model as **gRPC unary calls** — one request occupies one stream until the response is sent
|
||||||
|
|
||||||
One difference from gRPC: handler cancellation tokens are linked to the **session** lifetime, not `HttpContext.RequestAborted`. If a client disconnects from a POST mid-flight, the handler continues running until it completes or the session is terminated. But the client has freed a stream slot, so it can submit a new request — meaning the server could accumulate up to `MaxStreamsPerConnection` handlers that outlive their original connections. In practice this is bounded and comparable to how gRPC handlers behave when the client cancels an RPC.
|
One difference from gRPC: handler cancellation tokens are linked to the **session** lifetime, not `HttpContext.RequestAborted`. If a client disconnects from a `POST` mid-flight, the handler continues running until it completes or the session is terminated. But the client has freed a stream slot, so it can submit a new request — meaning the server could accumulate up to `MaxStreamsPerConnection` handlers that outlive their original connections. In practice this is bounded and comparable to how gRPC handlers behave when the client cancels an RPC.
|
||||||
|
|
||||||
For comparison, ASP.NET Core SignalR limits concurrent hub invocations per client to **1** by default (`MaximumParallelInvocationsPerClient`). Default stateful MCP is less restrictive but still bounded by HTTP/2 stream limits.
|
For comparison, ASP.NET Core SignalR limits concurrent hub invocations per client to **1** by default (`MaximumParallelInvocationsPerClient`). Default stateful MCP is less restrictive but still bounded by HTTP/2 stream limits.
|
||||||
|
|
||||||
|
|
@ -634,19 +634,19 @@ For comparison, ASP.NET Core SignalR limits concurrent hub invocations per clien
|
||||||
|
|
||||||
Legacy SSE endpoints are [disabled by default](#legacy-sse-transport) and must be explicitly enabled via <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse>. This is the primary reason they are disabled — the SSE transport has no built-in HTTP-level backpressure.
|
Legacy SSE endpoints are [disabled by default](#legacy-sse-transport) and must be explicitly enabled via <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse>. This is the primary reason they are disabled — the SSE transport has no built-in HTTP-level backpressure.
|
||||||
|
|
||||||
The legacy SSE transport separates the request and response channels: clients POST JSON-RPC messages to `/message` and receive responses through a long-lived GET SSE stream on `/sse`. The POST endpoint returns **202 Accepted immediately** after queuing the message — it does not wait for the handler to complete. This means there is **no HTTP-level backpressure** on handler concurrency, because each POST frees its connection immediately regardless of how long the handler runs.
|
The legacy SSE transport separates the request and response channels: clients `POST` JSON-RPC messages to `/message` and receive responses through a long-lived `GET` SSE stream on `/sse`. The `POST` endpoint returns **202 Accepted immediately** after queuing the message — it does not wait for the handler to complete. This means there is **no HTTP-level backpressure** on handler concurrency, because each `POST` frees its connection immediately regardless of how long the handler runs.
|
||||||
|
|
||||||
Internally, handlers are dispatched with the same fire-and-forget pattern as Streamable HTTP (`_ = ProcessMessageAsync()`). A client can send unlimited POST requests to `/message` while keeping the GET stream open, and each one spawns a concurrent handler with no built-in limit.
|
Internally, handlers are dispatched with the same fire-and-forget pattern as Streamable HTTP (`_ = ProcessMessageAsync()`). A client can send unlimited `POST` requests to `/message` while keeping the `GET` stream open, and each one spawns a concurrent handler with no built-in limit.
|
||||||
|
|
||||||
The GET stream does provide **session lifetime bounds**: handler cancellation tokens are linked to the GET request's `HttpContext.RequestAborted`, so when the client disconnects the SSE stream, all in-flight handlers are cancelled. This is similar to SignalR's connection-bound lifetime model — but unlike SignalR, there is no per-client concurrency limit like `MaximumParallelInvocationsPerClient`. The GET stream provides cleanup on disconnect, not rate-limiting during the connection.
|
The `GET` stream does provide **session lifetime bounds**: handler cancellation tokens are linked to the `GET` request's `HttpContext.RequestAborted`, so when the client disconnects the SSE stream, all in-flight handlers are cancelled. This is similar to SignalR's connection-bound lifetime model — but unlike SignalR, there is no per-client concurrency limit like `MaximumParallelInvocationsPerClient`. The `GET` stream provides cleanup on disconnect, not rate-limiting during the connection.
|
||||||
|
|
||||||
### With EventStreamStore
|
### With EventStreamStore
|
||||||
|
|
||||||
<xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EventStreamStore> is an advanced API that enables session resumability — storing SSE events so clients can reconnect and replay missed messages using the `Last-Event-ID` header. When configured, handlers gain the ability to call `EnablePollingAsync()`, which closes the POST response early and switches the client to polling mode.
|
<xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EventStreamStore> is an advanced API that enables session resumability — storing SSE events so clients can reconnect and replay missed messages using the `Last-Event-ID` header. When configured, handlers gain the ability to call `EnablePollingAsync()`, which closes the `POST` response early and switches the client to polling mode.
|
||||||
|
|
||||||
When a handler calls `EnablePollingAsync()`:
|
When a handler calls `EnablePollingAsync()`:
|
||||||
|
|
||||||
- The POST response completes **before the handler finishes**
|
- The `POST` response completes **before the handler finishes**
|
||||||
- The handler continues running in the background, decoupled from any HTTP request
|
- The handler continues running in the background, decoupled from any HTTP request
|
||||||
- The client's HTTP/2 stream slot is freed, allowing it to submit more requests
|
- The client's HTTP/2 stream slot is freed, allowing it to submit more requests
|
||||||
- **HTTP-level backpressure no longer applies** — there is no built-in limit on how many concurrent handlers can accumulate
|
- **HTTP-level backpressure no longer applies** — there is no built-in limit on how many concurrent handlers can accumulate
|
||||||
|
|
@ -655,11 +655,11 @@ The `EventStreamStore` itself has TTL-based limits (default: 2-hour event expira
|
||||||
|
|
||||||
### With tasks (experimental)
|
### With tasks (experimental)
|
||||||
|
|
||||||
[Tasks](xref:tasks) are an experimental feature that enables a "call-now, fetch-later" pattern for long-running tool calls. When a client sends a task-augmented `tools/call` request, the server creates a task record in the <xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore>, starts the tool handler as a fire-and-forget background task, and returns the task ID immediately, so the POST response completes **before the handler starts its real work**.
|
[Tasks](xref:tasks) are an experimental feature that enables a "call-now, fetch-later" pattern for long-running tool calls. When a client sends a task-augmented `tools/call` request, the server creates a task record in the <xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore>, starts the tool handler as a fire-and-forget background task, and returns the task ID immediately, so the `POST` response completes **before the handler starts its real work**.
|
||||||
|
|
||||||
This means:
|
This means:
|
||||||
|
|
||||||
- **No HTTP-level backpressure on task handlers** — each POST returns almost immediately, freeing the stream slot
|
- **No HTTP-level backpressure on task handlers** — each `POST` returns almost immediately, freeing the stream slot
|
||||||
- A client can rapidly submit many task-augmented requests, each spawning a background handler with no concurrency limit
|
- A client can rapidly submit many task-augmented requests, each spawning a background handler with no concurrency limit
|
||||||
- Task cleanup is governed by TTL (time-to-live), not by handler completion or session termination
|
- Task cleanup is governed by TTL (time-to-live), not by handler completion or session termination
|
||||||
|
|
||||||
|
|
@ -669,15 +669,15 @@ For servers using the built-in automatic task handlers without external work dis
|
||||||
|
|
||||||
### Stateless mode
|
### Stateless mode
|
||||||
|
|
||||||
Stateless mode provides the same HTTP-level backpressure as default stateful mode. In both modes, each POST is held open until the handler responds. The one difference is cancellation: in stateless mode, the handler's `CancellationToken` is `HttpContext.RequestAborted`, so if a client disconnects mid-flight, the handler is cancelled immediately — identical to a standard ASP.NET Core minimal API or controller action. In default stateful mode, the handler's token is session-scoped, so a disconnected client's handler continues running until it completes or the session is terminated (see [Handler cancellation tokens](#handler-cancellation-tokens) above).
|
Stateless mode provides the same HTTP-level backpressure as default stateful mode. In both modes, each `POST` is held open until the handler responds. The one difference is cancellation: in stateless mode, the handler's `CancellationToken` is `HttpContext.RequestAborted`, so if a client disconnects mid-flight, the handler is cancelled immediately — identical to a standard ASP.NET Core minimal API or controller action. In default stateful mode, the handler's token is session-scoped, so a disconnected client's handler continues running until it completes or the session is terminated (see the [Handler cancellation tokens](#handler-cancellation-tokens) section).
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
| Configuration | POST held open? | Backpressure mechanism | Concurrent handler limit per connection |
|
| Configuration | `POST` held open? | Backpressure mechanism | Concurrent handler limit per connection |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **Stateless** | Yes (handler = request) | HTTP/2 streams, server timeouts | `MaxStreamsPerConnection` (default: 100) |
|
| **Stateless** | Yes (handler = request) | HTTP/2 streams, server timeouts | `MaxStreamsPerConnection` (default: 100) |
|
||||||
| **Stateful (default)** | Yes (until handler responds) | HTTP/2 streams, server timeouts | `MaxStreamsPerConnection` (default: 100) |
|
| **Stateful (default)** | Yes (until handler responds) | HTTP/2 streams, server timeouts | `MaxStreamsPerConnection` (default: 100) |
|
||||||
| **SSE (legacy — opt-in)** | No (returns 202 Accepted) | None built-in; GET stream provides cleanup | Unbounded — apply rate limiting |
|
| **SSE (legacy — opt-in)** | No (returns 202 Accepted) | None built-in; `GET` stream provides cleanup | Unbounded — apply rate limiting |
|
||||||
| **Stateful + EventStreamStore** | No (if `EnablePollingAsync()` called) | None built-in | Unbounded — apply rate limiting |
|
| **Stateful + EventStreamStore** | No (if `EnablePollingAsync()` called) | None built-in | Unbounded — apply rate limiting |
|
||||||
| **Stateful + Tasks** | No (returns task ID immediately) | None built-in | Unbounded — apply rate limiting |
|
| **Stateful + Tasks** | No (returns task ID immediately) | None built-in | Unbounded — apply rate limiting |
|
||||||
|
|
||||||
|
|
@ -722,7 +722,7 @@ app.MapMcp().AddEndpointFilter(async (context, next) =>
|
||||||
|
|
||||||
<!-- mlc-disable-next-line -->
|
<!-- mlc-disable-next-line -->
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The `AllowNewSessionForNonInitializeRequests` AppContext switch (`ModelContextProtocol.AspNetCore.AllowNewSessionForNonInitializeRequests`) is a back-compat escape hatch that allows creating new sessions from non-initialize POST requests that arrive without an `Mcp-Session-Id` header. When enabled, the server creates a **brand-new session** for each such request rather than rejecting it — the response still carries the `Mcp-Session-Id` header with the new session's ID. This is **non-compliant with the [Streamable HTTP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http)**, which requires that only `initialize` requests create sessions. Use it only as a temporary workaround for clients that don't implement the session protocol correctly.
|
> The `AllowNewSessionForNonInitializeRequests` AppContext switch (`ModelContextProtocol.AspNetCore.AllowNewSessionForNonInitializeRequests`) is a back-compat escape hatch that allows creating new sessions from non-initialize `POST` requests that arrive without an `Mcp-Session-Id` header. When enabled, the server creates a **brand-new session** for each such request rather than rejecting it — the response still carries the `Mcp-Session-Id` header with the new session's ID. This is **non-compliant with the [Streamable HTTP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http)**, which requires that only `initialize` requests create sessions. Use it only as a temporary workaround for clients that don't implement the session protocol correctly.
|
||||||
|
|
||||||
### Other activity tags
|
### Other activity tags
|
||||||
|
|
||||||
|
|
@ -732,10 +732,10 @@ Other tags include `mcp.method.name`, `mcp.protocol.version`, `jsonrpc.request.i
|
||||||
|
|
||||||
The SDK records histograms under the `Experimental.ModelContextProtocol` meter:
|
The SDK records histograms under the `Experimental.ModelContextProtocol` meter:
|
||||||
|
|
||||||
| Metric | Description |
|
| Metric | Description |
|
||||||
|--------|-------------|
|
|---------------------------------|-----------------------------------------------------|
|
||||||
| `mcp.server.session.duration` | Duration of the MCP session on the server |
|
| `mcp.server.session.duration` | Duration of the MCP session on the server |
|
||||||
| `mcp.client.session.duration` | Duration of the MCP session on the client |
|
| `mcp.client.session.duration` | Duration of the MCP session on the client |
|
||||||
| `mcp.server.operation.duration` | Duration of each request/notification on the server |
|
| `mcp.server.operation.duration` | Duration of each request/notification on the server |
|
||||||
| `mcp.client.operation.duration` | Duration of each request/notification on the client |
|
| `mcp.client.operation.duration` | Duration of each request/notification on the client |
|
||||||
|
|
||||||
|
|
@ -746,26 +746,26 @@ In stateless mode, each HTTP request is its own "session", so `mcp.server.sessio
|
||||||
The legacy [SSE (Server-Sent Events)](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) transport is also supported by `MapMcp()` and always uses stateful mode. Legacy SSE endpoints (`/sse` and `/message`) are **disabled by default** due to [backpressure concerns](#request-backpressure). To enable them, set <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> to `true` — this property is marked `[Obsolete]` with a diagnostic warning (`MCP9004`) to signal that it should only be used when you need to support legacy SSE-only clients and understand the backpressure implications. Alternatively, set the `ModelContextProtocol.AspNetCore.EnableLegacySse` [AppContext switch](https://learn.microsoft.com/dotnet/api/system.appcontext) to `true`.
|
The legacy [SSE (Server-Sent Events)](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) transport is also supported by `MapMcp()` and always uses stateful mode. Legacy SSE endpoints (`/sse` and `/message`) are **disabled by default** due to [backpressure concerns](#request-backpressure). To enable them, set <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> to `true` — this property is marked `[Obsolete]` with a diagnostic warning (`MCP9004`) to signal that it should only be used when you need to support legacy SSE-only clients and understand the backpressure implications. Alternatively, set the `ModelContextProtocol.AspNetCore.EnableLegacySse` [AppContext switch](https://learn.microsoft.com/dotnet/api/system.appcontext) to `true`.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Setting `EnableLegacySse = true` while `Stateless = true` throws an `InvalidOperationException` at startup, because SSE requires in-memory session state shared between the GET and POST requests.
|
> Setting `EnableLegacySse = true` while `Stateless = true` throws an `InvalidOperationException` at startup, because SSE requires in-memory session state shared between the `GET` and `POST` requests.
|
||||||
|
|
||||||
### How SSE sessions work
|
### How SSE sessions work
|
||||||
|
|
||||||
1. The client connects to the `/sse` endpoint with a GET request
|
1. The client connects to the `/sse` endpoint with a `GET` request.
|
||||||
2. The server generates a session ID and sends a `/message?sessionId={id}` URL as the first SSE event
|
2. The server generates a session ID and sends a `/message?sessionId={id}` URL as the first SSE event.
|
||||||
3. The client sends JSON-RPC messages as POST requests to that `/message?sessionId={id}` URL
|
3. The client sends JSON-RPC messages as `POST` requests to that `/message?sessionId={id}` URL.
|
||||||
4. The server streams responses and unsolicited messages back over the open SSE GET stream
|
4. The server streams responses and unsolicited messages back over the open SSE `GET` stream.
|
||||||
|
|
||||||
Unlike Streamable HTTP which uses the `Mcp-Session-Id` header, legacy SSE passes the session ID as a query string parameter on the `/message` endpoint.
|
Unlike Streamable HTTP, which uses the `Mcp-Session-Id` header, legacy SSE passes the session ID as a query string parameter on the `/message` endpoint.
|
||||||
|
|
||||||
### Session lifetime
|
### Session lifetime
|
||||||
|
|
||||||
SSE session lifetime is tied directly to the GET SSE stream. When the client disconnects (detected via `HttpContext.RequestAborted`), or the server shuts down (via `IHostApplicationLifetime.ApplicationStopping`), the session is immediately removed. There is no idle timeout or maximum idle session count for SSE sessions — the session exists exactly as long as the SSE connection is open.
|
SSE session lifetime is tied directly to the `GET` SSE stream. When the client disconnects (detected via `HttpContext.RequestAborted`), or the server shuts down (via `IHostApplicationLifetime.ApplicationStopping`), the session is immediately removed. There is no idle timeout or maximum idle session count for SSE sessions — the session exists exactly as long as the SSE connection is open.
|
||||||
|
|
||||||
This makes SSE sessions behave similarly to [stdio](#stdio-transport): the session is implicit in the connection lifetime, and disconnection is the only termination mechanism.
|
This makes SSE sessions behave similarly to [stdio](#stdio-transport): the session is implicit in the connection lifetime, and disconnection is the only termination mechanism.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
<xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.ConfigureSessionOptions> and <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.RunSessionHandler> both work with SSE sessions. They are called during the `/sse` GET request handler, and services resolve from the GET request's `HttpContext.RequestServices`. [User binding](#user-binding) also works — the authenticated user is captured from the GET request and verified on each POST to `/message`.
|
<xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.ConfigureSessionOptions> and <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.RunSessionHandler> both work with SSE sessions. They are called during the `/sse` `GET` request handler, and services resolve from the `GET` request's `HttpContext.RequestServices`. [User binding](#user-binding) also works — the authenticated user is captured from the `GET` request and verified on each `POST` to `/message`.
|
||||||
|
|
||||||
## Advanced features
|
## Advanced features
|
||||||
|
|
||||||
|
|
@ -791,9 +791,9 @@ builder.Services.AddSingleton<ISessionMigrationHandler, MySessionMigrationHandle
|
||||||
|
|
||||||
Implementations should:
|
Implementations should:
|
||||||
|
|
||||||
- Validate that the request is authorized (check `HttpContext.User`)
|
- Validate that the request is authorized (check `HttpContext.User`).
|
||||||
- Reconstruct the session state from external storage (database, distributed cache, etc.)
|
- Reconstruct the session state from external storage (database, distributed cache, etc.).
|
||||||
- Return `McpServerOptions` pre-populated with `KnownClientInfo` and `KnownClientCapabilities` to skip re-initialization
|
- Return `McpServerOptions` pre-populated with `KnownClientInfo` and `KnownClientCapabilities` to skip re-initialization.
|
||||||
|
|
||||||
Session migration adds significant complexity. Consider whether stateless mode is a better fit for your deployment scenario.
|
Session migration adds significant complexity. Consider whether stateless mode is a better fit for your deployment scenario.
|
||||||
|
|
||||||
|
|
@ -817,4 +817,4 @@ When configured:
|
||||||
- Events are stored for later replay
|
- Events are stored for later replay
|
||||||
- When a client reconnects with `Last-Event-ID`, missed events are replayed before new events are sent
|
- When a client reconnects with `Last-Event-ID`, missed events are replayed before new events are sent
|
||||||
|
|
||||||
This is useful for clients that may experience transient network issues. Without an event store, clients that disconnect and reconnect may miss events that were sent while they were disconnected.
|
This is useful for clients that might experience transient network issues. Without an event store, clients that disconnect and reconnect might miss events that were sent while they were disconnected.
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ and the client polls for status, optionally exchanging additional input along th
|
||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
A client opts into tasks on a per-request basis by including the `io.modelcontextprotocol/tasks`
|
A client opts into tasks on a per-request basis by including the `io.modelcontextprotocol/tasks`
|
||||||
extension key in the request's `_meta`. When that opt-in is present, the server **may** respond
|
extension key in the request's `_meta`. When that opt-in is present, the server **might** respond
|
||||||
with a <xref:ModelContextProtocol.Extensions.Tasks.CreateTaskResult> instead of the standard result
|
with a <xref:ModelContextProtocol.Extensions.Tasks.CreateTaskResult> instead of the standard result
|
||||||
(e.g., <xref:ModelContextProtocol.Protocol.CallToolResult>). The client then polls `tasks/get`
|
(for example, <xref:ModelContextProtocol.Protocol.CallToolResult>). The client then polls `tasks/get`
|
||||||
until the task reaches a terminal state.
|
until the task reaches a terminal state.
|
||||||
|
|
||||||
Per the SEP, the server **must not** return `CreateTaskResult` for a request that did not include
|
Per the SEP, the server **must not** return `CreateTaskResult` for a request that did not include
|
||||||
|
|
@ -81,7 +81,7 @@ When tasks are enabled with `WithTasks` the SDK automatically:
|
||||||
|
|
||||||
For production scenarios that need durability, session isolation, multi-process routing, or
|
For production scenarios that need durability, session isolation, multi-process routing, or
|
||||||
TTL-based cleanup, implement <xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore> yourself
|
TTL-based cleanup, implement <xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore> yourself
|
||||||
(see [Implementing a custom task store](#implementing-a-custom-task-store) below).
|
(see the [Implementing a custom task store](#implementing-a-custom-task-store) section).
|
||||||
|
|
||||||
#### Returning a task from a tool handler
|
#### Returning a task from a tool handler
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ options.Handlers.CallToolWithAlternateHandler = async (context, ct) =>
|
||||||
|
|
||||||
> This low-level handler is mutually exclusive with `WithTasks`. When a store is configured, the
|
> This low-level handler is mutually exclusive with `WithTasks`. When a store is configured, the
|
||||||
> SDK does the wrapping for you and throws `InvalidOperationException` if the alternate handler also
|
> SDK does the wrapping for you and throws `InvalidOperationException` if the alternate handler also
|
||||||
> returns an alternate. Use one mechanism or the other. When you return a task this way you are also
|
> returns an alternate. Use one mechanism or the other. When you return a task this way, you're also
|
||||||
> responsible for serving `tasks/get`, `tasks/update`, and `tasks/cancel`, which the store provides
|
> responsible for serving `tasks/get`, `tasks/update`, and `tasks/cancel`, which the store provides
|
||||||
> automatically.
|
> automatically.
|
||||||
|
|
||||||
|
|
@ -202,7 +202,7 @@ if (raw.IsTask)
|
||||||
|
|
||||||
`CallToolWithPollingAsync` includes a safety net for misbehaving servers: if the task stays in
|
`CallToolWithPollingAsync` includes a safety net for misbehaving servers: if the task stays in
|
||||||
<xref:ModelContextProtocol.Extensions.Tasks.McpTaskStatus.InputRequired> across many consecutive polls
|
<xref:ModelContextProtocol.Extensions.Tasks.McpTaskStatus.InputRequired> across many consecutive polls
|
||||||
without exposing any new input request keys (i.e. every previously requested input has already
|
without exposing any new input request keys (that is, every previously requested input has already
|
||||||
been resolved by the client and yet the server keeps returning `InputRequired`), the client
|
been resolved by the client and yet the server keeps returning `InputRequired`), the client
|
||||||
gives up, issues a best-effort `tasks/cancel`, and throws
|
gives up, issues a best-effort `tasks/cancel`, and throws
|
||||||
<xref:ModelContextProtocol.McpException>. This guards against a server that never transitions
|
<xref:ModelContextProtocol.McpException>. This guards against a server that never transitions
|
||||||
|
|
@ -245,7 +245,7 @@ Per SEP-2663:
|
||||||
Implement <xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore> for production scenarios. Key
|
Implement <xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore> for production scenarios. Key
|
||||||
requirements drawn from the SEP and the SDK contract:
|
requirements drawn from the SEP and the SDK contract:
|
||||||
|
|
||||||
1. **Thread safety** — every method may be called concurrently.
|
1. **Thread safety** — every method can be called concurrently.
|
||||||
2. **Idempotent terminal transitions** —
|
2. **Idempotent terminal transitions** —
|
||||||
<xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore.SetCompletedAsync*>,
|
<xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore.SetCompletedAsync*>,
|
||||||
<xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore.SetFailedAsync*>, and
|
<xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore.SetFailedAsync*>, and
|
||||||
|
|
@ -264,7 +264,7 @@ requirements drawn from the SEP and the SDK contract:
|
||||||
task is durably persisted, so that a subsequent
|
task is durably persisted, so that a subsequent
|
||||||
<xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore.GetTaskAsync*> with the returned task ID
|
<xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore.GetTaskAsync*> with the returned task ID
|
||||||
resolves immediately — even from a different process or node. Stores backed by
|
resolves immediately — even from a different process or node. Stores backed by
|
||||||
eventually-consistent storage must wait for the write to become visible (quorum
|
eventually consistent storage must wait for the write to become visible (quorum
|
||||||
acknowledgement, write-through, etc.) before returning. Required by SEP-2663 §306.
|
acknowledgement, write-through, etc.) before returning. Required by SEP-2663 §306.
|
||||||
5. **Singleton under stateless HTTP** — when the server runs in stateless mode (each request
|
5. **Singleton under stateless HTTP** — when the server runs in stateless mode (each request
|
||||||
spins up a fresh server instance), the same `IMcpTaskStore` instance must be shared across
|
spins up a fresh server instance), the same `IMcpTaskStore` instance must be shared across
|
||||||
|
|
@ -337,14 +337,14 @@ In the built-in SDK pipeline, when a task is wrapped by a configured `TaskStore`
|
||||||
|
|
||||||
<xref:ModelContextProtocol.Extensions.Tasks.InMemoryMcpTaskStore> uses immutable record snapshots with
|
<xref:ModelContextProtocol.Extensions.Tasks.InMemoryMcpTaskStore> uses immutable record snapshots with
|
||||||
compare-and-swap updates for lock-free thread safety. `InputRequests` and `InputResponses` are
|
compare-and-swap updates for lock-free thread safety. `InputRequests` and `InputResponses` are
|
||||||
exposed as `ImmutableDictionary<,>` so observers cannot mutate internal state.
|
exposed as `ImmutableDictionary<,>` so observers can't mutate internal state.
|
||||||
|
|
||||||
#### Capability bypass inside a task scope
|
#### Capability bypass inside a task scope
|
||||||
|
|
||||||
When `server.ElicitAsync`/`server.SampleAsync`/`server.RequestRootsAsync` execute inside a task
|
When `server.ElicitAsync`/`server.SampleAsync`/`server.RequestRootsAsync` execute inside a task
|
||||||
scope, the SDK intentionally skips the normal client-capability negotiation checks
|
scope, the SDK intentionally skips the normal client-capability negotiation checks
|
||||||
(`ThrowIfElicitationUnsupported`, etc.). The tasks extension itself is the negotiated capability:
|
(`ThrowIfElicitationUnsupported`, etc.). The tasks extension itself is the negotiated capability:
|
||||||
the client opted in by including the extension marker in the originating request, so it is
|
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`.
|
responsible for handling — or rejecting — the input requests surfaced through `tasks/get`.
|
||||||
|
|
||||||
### Known limitations
|
### Known limitations
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue