Default draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567) (#1610)
This commit is contained in:
parent
3368bb7349
commit
6c323e783b
119 changed files with 5704 additions and 712 deletions
|
|
@ -172,10 +172,10 @@ Here's an example implementation of how a console application might handle elici
|
|||
|
||||
### Multi Round-Trip Requests (MRTR)
|
||||
|
||||
[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `DRAFT-2026-v1`. Under the draft protocol, the server-to-client `elicitation/create` request method is removed; the recommended way to ask the user for input from a server handler is to throw <xref:ModelContextProtocol.Protocol.InputRequiredException> and let the SDK emit an <xref:ModelContextProtocol.Protocol.InputRequiredResult> on the wire.
|
||||
[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `2026-07-28`. Under the draft protocol, the server-to-client `elicitation/create` request method is removed; the recommended way to ask the user for input from a server handler is to throw <xref:ModelContextProtocol.Protocol.InputRequiredException> and let the SDK emit an <xref:ModelContextProtocol.Protocol.InputRequiredResult> on the wire.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `ElicitAsync` throws `InvalidOperationException("Elicitation is not supported in stateless mode.")` whenever the server is running stateless — which includes every Streamable HTTP server under `DRAFT-2026-v1` once that revision is forced to stateless-only in a future PR. Stdio servers and current-protocol stateful Streamable HTTP servers continue to work via the legacy server-to-client `elicitation/create` request flow. For code that needs to run on stateless servers — including all `DRAFT-2026-v1` Streamable HTTP servers going forward — throw `InputRequiredException` from your handler instead. It works under both protocols and both session modes.
|
||||
> `ElicitAsync` throws `InvalidOperationException("Elicitation is not supported in stateless mode.")` whenever the server is running stateless — which includes every Streamable HTTP server under `2026-07-28` once that revision is forced to stateless-only in a future PR. Stdio servers and current-protocol stateful Streamable HTTP servers continue to work via the legacy server-to-client `elicitation/create` request flow. For code that needs to run on stateless servers — including all `2026-07-28` Streamable HTTP servers going forward — throw `InputRequiredException` from your handler instead. It works under both protocols and both session modes.
|
||||
|
||||
For example:
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ public static string ElicitWithMrtr(
|
|||
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "This tool requires MRTR support (DRAFT-2026-v1, or a stateful current-protocol session).";
|
||||
return "This tool requires MRTR support (2026-07-28, or a stateful current-protocol session).";
|
||||
}
|
||||
|
||||
// First call — request user input
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ uid: mrtr
|
|||
|
||||
<!-- mlc-disable-next-line -->
|
||||
> [!WARNING]
|
||||
> MRTR is part of the **`DRAFT-2026-v1`** 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 may change before the revision is ratified. See the [Experimental APIs](../../experimental.md) documentation for details on working with experimental APIs.
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -33,13 +33,12 @@ MRTR is useful when:
|
|||
|
||||
## Opting in
|
||||
|
||||
MRTR activates when both peers negotiate protocol revision **`DRAFT-2026-v1`** during `initialize`. The C# SDK opts in by listing `DRAFT-2026-v1` as a supported protocol version on the client; servers automatically accept it when offered. No experimental flags are required.
|
||||
MRTR activates when both peers negotiate protocol revision **`2026-07-28`**. The C# SDK client prefers the draft revision by default — it probes with `server/discover` and falls back to a legacy `initialize` handshake only when the server doesn't support draft. Servers accept the draft automatically when a client offers it. No experimental flags are required; pinning `ProtocolVersion` to a legacy revision opts back out.
|
||||
|
||||
```csharp
|
||||
// Client
|
||||
// Client — the SDK prefers the 2026-07-28 draft (and therefore MRTR) by default.
|
||||
var clientOptions = new McpClientOptions
|
||||
{
|
||||
ProtocolVersion = "DRAFT-2026-v1",
|
||||
Handlers = new McpClientHandlers
|
||||
{
|
||||
ElicitationHandler = HandleElicitationAsync,
|
||||
|
|
@ -48,7 +47,7 @@ var clientOptions = new McpClientOptions
|
|||
};
|
||||
```
|
||||
|
||||
Under `DRAFT-2026-v1`, 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 `DRAFT-2026-v1` Streamable HTTP server (which will be stateless-only in a future revision) 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 under draft today — but they throw `InvalidOperationException("X is not supported in stateless mode.")` on any stateless session, current or draft.
|
||||
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 (which is stateless-only under the draft revision) 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 under draft today — but they throw `InvalidOperationException("X is not supported in stateless mode.")` on any stateless session, current or draft.
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -60,7 +59,7 @@ A tool participates in MRTR by throwing <xref:ModelContextProtocol.Protocol.Inpu
|
|||
|
||||
Tools should check <xref:ModelContextProtocol.Server.McpServer.IsMrtrSupported> before throwing `InputRequiredException`. It returns `true` when either:
|
||||
|
||||
- The negotiated protocol revision is `DRAFT-2026-v1` (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).
|
||||
|
||||
```csharp
|
||||
|
|
@ -71,7 +70,7 @@ public static string MyTool(
|
|||
{
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "This tool requires a client that negotiates DRAFT-2026-v1, "
|
||||
return "This tool requires a client that negotiates 2026-07-28, "
|
||||
+ "or a stateful current-protocol session.";
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +257,7 @@ When MRTR is not supported, you can provide domain-specific guidance:
|
|||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "This tool requires interactive input. To use it:\n"
|
||||
+ "1. Connect with a client that negotiates MCP protocol revision DRAFT-2026-v1, or\n"
|
||||
+ "1. Connect with a client that negotiates MCP protocol revision 2026-07-28, or\n"
|
||||
+ "2. Use a stateful current-protocol session so the server can resolve the input requests for you.\n"
|
||||
+ "\nStateless current-protocol sessions cannot resolve MRTR input requests.";
|
||||
}
|
||||
|
|
@ -270,22 +269,18 @@ The SDK supports `InputRequiredException` across two protocol revisions and two
|
|||
|
||||
| Negotiated protocol | Session mode | Behavior |
|
||||
|---|---|---|
|
||||
| `DRAFT-2026-v1` | Stateful | Native MRTR — `InputRequiredResult` is serialized directly to the wire. |
|
||||
| `DRAFT-2026-v1` | Stateless | Native MRTR — `InputRequiredResult` is serialized directly to the wire. No server-side handler state needed. |
|
||||
| `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. |
|
||||
| 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. |
|
||||
|
||||
> [!NOTE]
|
||||
> The backcompat resolver is intentionally limited to 10 retry rounds. Tools that need more rounds should require `DRAFT-2026-v1` (check `IsMrtrSupported`).
|
||||
> The backcompat resolver is intentionally limited to 10 retry rounds. Tools that need more rounds should require `2026-07-28` (check `IsMrtrSupported`).
|
||||
|
||||
### Why `ElicitAsync` / `SampleAsync` / `RequestRootsAsync` throw on stateless servers
|
||||
|
||||
`ElicitAsync` / `SampleAsync` / `RequestRootsAsync` issue a JSON-RPC request to the client and wait for the response on the same session. Stateless servers don't have a persistent session to wait on, so the SDK fails fast with `InvalidOperationException("X is not supported in stateless mode.")` (the check is `McpServer.ClientCapabilities is null`, which is the SDK's proxy for stateless).
|
||||
|
||||
Under the current protocol revision (`2025-06-18` and earlier), stdio and stateful Streamable HTTP keep `ClientCapabilities` populated, so the legacy methods work normally and remain the recommended way to do one-shot client interactions. Under `DRAFT-2026-v1`, the spec removes those request methods from Streamable HTTP entirely; the SDK still allows the legacy methods on draft stdio sessions because stdio is implicitly single-process / stateful and the client handler is wired up regardless of negotiated revision. `InputRequiredException` is the way to write tools that work on every supported configuration.
|
||||
Under the current protocol revision (`2025-06-18` and earlier), stdio and stateful Streamable HTTP keep `ClientCapabilities` populated, so the legacy methods work normally and remain the recommended way to do one-shot client interactions. Under `2026-07-28`, the spec removes those request methods from Streamable HTTP entirely; the SDK still allows the legacy methods on draft stdio sessions because stdio is implicitly single-process / stateful and the client handler is wired up regardless of negotiated revision. `InputRequiredException` is the way to write tools that work on every supported configuration.
|
||||
|
||||
### Future direction
|
||||
|
||||
The `DRAFT-2026-v1` revision is moving toward a stateless-only model: `Mcp-Session-Id` is being removed, and Streamable HTTP servers will run statelessly by default under the draft revision. When that lands, the `Stateful` row for `DRAFT-2026-v1` in the compatibility matrix above collapses into the `Stateless` row (Streamable HTTP under draft becomes stateless-only), and `InputRequiredException` becomes uniformly required for non-stdio servers. The current-protocol resolver path will remain for backward compatibility with older clients and stateful servers.
|
||||
|
||||
This work is a follow-up to the present PR.
|
||||
Because `2026-07-28` removes `Mcp-Session-Id` (SEP-2567) and the `initialize` handshake (SEP-2575), Streamable HTTP runs statelessly whenever a client speaks the draft. The `Stateful` row for `2026-07-28` in the compatibility matrix above therefore applies only to stdio — a server explicitly set to `Stateless = false` still serves draft requests sessionlessly and creates a legacy session only when an older client falls back to `initialize`.
|
||||
|
|
|
|||
|
|
@ -106,10 +106,10 @@ server.RegisterNotificationHandler(
|
|||
|
||||
### Multi Round-Trip Requests (MRTR)
|
||||
|
||||
[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `DRAFT-2026-v1`. Under the draft protocol, the server-to-client `roots/list` request method is removed; the recommended way to ask the client for its roots from a server handler is to throw <xref:ModelContextProtocol.Protocol.InputRequiredException> and let the SDK emit an <xref:ModelContextProtocol.Protocol.InputRequiredResult> on the wire.
|
||||
[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `2026-07-28`. Under the draft protocol, the server-to-client `roots/list` request method is removed; the recommended way to ask the client for its roots from a server handler is to throw <xref:ModelContextProtocol.Protocol.InputRequiredException> and let the SDK emit an <xref:ModelContextProtocol.Protocol.InputRequiredResult> on the wire.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `RequestRootsAsync` throws `InvalidOperationException("Roots are not supported in stateless mode.")` whenever the server is running stateless — which includes every Streamable HTTP server under `DRAFT-2026-v1` once that revision is forced to stateless-only in a future PR. Stdio servers and current-protocol stateful Streamable HTTP servers continue to work via the legacy server-to-client `roots/list` request flow. For code that needs to run on stateless servers — including all `DRAFT-2026-v1` Streamable HTTP servers going forward — throw `InputRequiredException` from your handler instead. It works under both protocols and both session modes.
|
||||
> `RequestRootsAsync` throws `InvalidOperationException("Roots are not supported in stateless mode.")` whenever the server is running stateless — which includes every Streamable HTTP server under `2026-07-28` once that revision is forced to stateless-only in a future PR. Stdio servers and current-protocol stateful Streamable HTTP servers continue to work via the legacy server-to-client `roots/list` request flow. For code that needs to run on stateless servers — including all `2026-07-28` Streamable HTTP servers going forward — throw `InputRequiredException` from your handler instead. It works under both protocols and both session modes.
|
||||
|
||||
For example:
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ public static string ListRootsWithMrtr(
|
|||
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "This tool requires MRTR support (DRAFT-2026-v1, or a stateful current-protocol session).";
|
||||
return "This tool requires MRTR support (2026-07-28, or a stateful current-protocol session).";
|
||||
}
|
||||
|
||||
// First call — request the client's root list
|
||||
|
|
|
|||
|
|
@ -123,10 +123,10 @@ Sampling requires the client to advertise the `sampling` capability. This is han
|
|||
|
||||
### Multi Round-Trip Requests (MRTR)
|
||||
|
||||
[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `DRAFT-2026-v1`. Under the draft protocol, the server-to-client `sampling/createMessage` request method is removed; the recommended way to ask the client to sample from a server handler is to throw <xref:ModelContextProtocol.Protocol.InputRequiredException> and let the SDK emit an <xref:ModelContextProtocol.Protocol.InputRequiredResult> on the wire.
|
||||
[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `2026-07-28`. Under the draft protocol, the server-to-client `sampling/createMessage` request method is removed; the recommended way to ask the client to sample from a server handler is to throw <xref:ModelContextProtocol.Protocol.InputRequiredException> and let the SDK emit an <xref:ModelContextProtocol.Protocol.InputRequiredResult> on the wire.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `SampleAsync` and `AsSamplingChatClient` throw `InvalidOperationException("Sampling is not supported in stateless mode.")` whenever the server is running stateless — which includes every Streamable HTTP server under `DRAFT-2026-v1` once that revision is forced to stateless-only in a future PR. Stdio servers and current-protocol stateful Streamable HTTP servers continue to work via the legacy server-to-client `sampling/createMessage` request flow. For code that needs to run on stateless servers — including all `DRAFT-2026-v1` Streamable HTTP servers going forward — throw `InputRequiredException` from your handler instead. It works under both protocols and both session modes.
|
||||
> `SampleAsync` and `AsSamplingChatClient` throw `InvalidOperationException("Sampling is not supported in stateless mode.")` whenever the server is running stateless — which includes every Streamable HTTP server under `2026-07-28` once that revision is forced to stateless-only in a future PR. Stdio servers and current-protocol stateful Streamable HTTP servers continue to work via the legacy server-to-client `sampling/createMessage` request flow. For code that needs to run on stateless servers — including all `2026-07-28` Streamable HTTP servers going forward — throw `InputRequiredException` from your handler instead. It works under both protocols and both session modes.
|
||||
|
||||
For example:
|
||||
|
||||
|
|
@ -146,7 +146,7 @@ public static string SampleWithMrtr(
|
|||
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "This tool requires MRTR support (DRAFT-2026-v1, or a stateful current-protocol session).";
|
||||
return "This tool requires MRTR support (2026-07-28, or a stateful current-protocol session).";
|
||||
}
|
||||
|
||||
// First call — request LLM completion from the client
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ uid: stateless
|
|||
|
||||
# Stateless and stateful mode
|
||||
|
||||
The MCP [Streamable HTTP transport] uses an `Mcp-Session-Id` HTTP header to associate multiple requests with a single logical session. However, **we recommend most servers disable sessions entirely by setting <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> to `true`**. Stateless mode avoids the complexity, memory overhead, and deployment constraints that come with sessions. Sessions are only necessary when the server needs to send requests _to_ the client, push [unsolicited notifications](#how-streamable-http-delivers-messages), or maintain per-client state across requests.
|
||||
The MCP [Streamable HTTP transport] uses an `Mcp-Session-Id` HTTP header to associate multiple requests with a single logical session. However, **we recommend most servers disable sessions entirely by setting <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> to `true`**. Stateless mode avoids the complexity, memory overhead, and deployment constraints that come with sessions. Sessions are only necessary when the server needs to push [unsolicited notifications](#how-streamable-http-delivers-messages), maintain per-client state across requests, or send requests _to_ clients that don't support [MRTR](xref:mrtr).
|
||||
|
||||
When sessions are enabled (the current C# SDK default), the server creates and tracks an in-memory session for each client, while the client automatically includes the session ID in subsequent requests. The [MCP specification requires](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) that clients use sessions when a server's `initialize` response includes an `Mcp-Session-Id` header — this is not optional for the client. Session expiry detection and reconnection are the responsibility of the application using the client SDK (see [Client-side session behavior](#client-side-session-behavior)).
|
||||
When sessions are enabled (`Stateless = false`), the server creates and tracks an in-memory session for each client, while the client automatically includes the session ID in subsequent requests. The [MCP specification requires](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) that clients use sessions when a server's `initialize` response includes an `Mcp-Session-Id` header — this is not optional for the client. Session expiry detection and reconnection are the responsibility of the application using the client SDK (see [Client-side session behavior](#client-side-session-behavior)).
|
||||
|
||||
[Streamable HTTP transport]: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http
|
||||
|
||||
**Quick guide — which mode should I use?**
|
||||
|
||||
- Does your server need to send requests _to_ the client (sampling, elicitation, roots)? → **Use stateful.**
|
||||
- Does your server need to send requests _to_ the client (elicitation, sampling, roots) and can't rely on clients supporting [MRTR](xref:mrtr)? → **Use stateful.**
|
||||
- Does your server send [unsolicited notifications](#how-streamable-http-delivers-messages) or support resource subscriptions? → **Use stateful.**
|
||||
- Do you need to support clients that only speak the [legacy SSE transport](#legacy-sse-transport)? → **Use stateful** with <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> (disabled by default due to [backpressure concerns](#request-backpressure)).
|
||||
- Does your server manage per-client state that concurrent agents must not share (isolated environments, parallel workspaces)? → **Use stateful.**
|
||||
|
|
@ -24,19 +24,44 @@ When sessions are enabled (the current C# SDK default), the server creates and t
|
|||
|
||||
<!-- mlc-disable-next-line -->
|
||||
> [!NOTE]
|
||||
> **Why isn't stateless the C# SDK default?** Stateful mode remains the default for backward compatibility and because it is the only HTTP mode with full feature parity with [stdio](xref:transports) (server-to-client requests, unsolicited notifications, subscriptions). Stateless is the recommended choice when you don't need those features — see [Forward and backward compatibility](#forward-and-backward-compatibility) for guidance on choosing an explicit setting.
|
||||
> **Why is stateless now the default?** Earlier versions of the SDK defaulted to stateful for back-compat with the `2025-11-25` (and older) protocol revisions, which require the `Mcp-Session-Id` header. The `2026-07-28` draft revision removes that header (SEP-2567) and the `initialize` handshake (SEP-2575) entirely, so the SDK now defaults <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> to `true` to match the new wire format. You can still opt back into sessions with `Stateless = false` for [unsolicited notifications](#how-streamable-http-delivers-messages), resource subscriptions, per-client isolation, or server-to-client requests against clients that don't support [MRTR](xref:mrtr) — see [Stateful mode (sessions)](#stateful-mode-sessions).
|
||||
|
||||
## Forward and backward compatibility
|
||||
|
||||
The `Stateless` property is the single most important setting for forward-proofing your MCP server. The current C# SDK default is `Stateless = false` (sessions enabled), but **we expect this default to change** once mechanisms like [MRTR](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) bring server-to-client interactions (sampling, elicitation, roots) to stateless mode. We recommend every server set `Stateless` explicitly rather than relying on the default:
|
||||
The `Stateless` property is the single most important setting for forward-proofing your MCP server. The default is now `Stateless = true` (sessions disabled), which is the forward-compatible setting for the `2026-07-28` draft revision and beyond. Stateless servers still respond to legacy clients on `2025-11-25` and earlier — the SDK keeps the `initialize` + `Mcp-Session-Id` handshake available for those clients — but they cannot use the session-dependent features ([unsolicited notifications](#how-streamable-http-delivers-messages), resource subscriptions, per-client isolation). Server-to-client requests are the exception: [elicitation](xref:elicitation) — and the now-deprecated [sampling](xref:sampling) and [roots](xref:roots) — can run statelessly through [MRTR](xref:mrtr), though MRTR requires the unratified `2026-07-28` draft and is far less widely supported than session-based requests. We recommend every server set `Stateless` explicitly rather than relying on the default:
|
||||
|
||||
- **`Stateless = true`** — the best forward-compatible choice. Your server opts out of sessions entirely. No matter how the SDK default changes in the future, your behavior stays the same. If you don't need [unsolicited notifications](#how-streamable-http-delivers-messages), server-to-client requests, or session-scoped state, this is the setting to use today.
|
||||
- **`Stateless = true`** — the current default and the forward-compatible choice. Your server opts out of sessions entirely and the `Mcp-Session-Id` header is never sent or honored. The `2026-07-28` draft revision drops the `initialize` handshake and `Mcp-Session-Id` from the wire format entirely, so this is the only configuration that lets the server respond to draft clients without falling back to legacy handling. If you don't need [unsolicited notifications](#how-streamable-http-delivers-messages), server-to-client requests, or session-scoped state, this is the setting to use today.
|
||||
|
||||
- **`Stateless = false`** — the right choice when your server depends on sessions for features like sampling, elicitation, roots, unsolicited notifications, or per-client isolation. Setting this explicitly protects your server from a future default change. The [MCP specification requires](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) that clients use sessions when a server's `initialize` response includes an `Mcp-Session-Id` header, so compliant clients will always honor your server's session. Once [MRTR](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) or a similar mechanism is available, you may be able to migrate server-to-client interactions to stateless mode and drop sessions entirely — but until then, explicit `Stateless = false` is the safe choice. See [Stateless alternatives for server-to-client interactions](#stateless-alternatives-for-server-to-client-interactions) for more on MRTR.
|
||||
- **`Stateless = false`** — the right choice when your server depends on sessions for [unsolicited notifications](#how-streamable-http-delivers-messages), resource subscriptions, or per-client isolation, none of which work without a session. Setting this explicitly protects your server from a future default change, and the [MCP specification requires](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) that clients use sessions when a server's `initialize` response includes an `Mcp-Session-Id` header, so compliant clients always honor your server's session. Server-to-client requests no longer force a session: [elicitation](xref:elicitation) — and the now-deprecated [sampling](xref:sampling) and [roots](xref:roots) — can run statelessly through [MRTR](xref:mrtr) (see [Stateless alternatives for server-to-client interactions](#stateless-alternatives-for-server-to-client-interactions)). MRTR is only as available as the unratified `2026-07-28` draft, however, so keep a session if you need server-to-client requests against clients that don't speak the draft. Note that even with `Stateless = false`, draft requests are still served sessionlessly because the protocol forbids the session header — the stateful path activates only when a client falls back to a legacy revision.
|
||||
|
||||
<!-- mlc-disable-next-line -->
|
||||
> [!TIP]
|
||||
> If you're not sure which to pick, start with `Stateless = true`. You can switch to `Stateless = false` later if you discover you need server-to-client requests or unsolicited notifications. Either way, setting the property explicitly means your server's behavior won't silently change when the SDK default is updated.
|
||||
> If you're not sure which to pick, leave the default (`Stateless = true`). You can switch to `Stateless = false` later if you discover you need unsolicited notifications or resource subscriptions. Either way, setting the property explicitly means your server's behavior won't silently change when the SDK default is updated.
|
||||
|
||||
### The 2026-07-28 draft revision
|
||||
|
||||
The `2026-07-28` draft 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 the draft on the wire. Any HTTP POST that arrives with the draft `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. Legacy 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 falls back to legacy session creation when the client speaks `2025-11-25` or earlier — but a sessionless draft request on a stateful server is refused with a `-32004 UnsupportedProtocolVersion` error, so a dual-era client downgrades to the legacy `initialize` handshake and obtains a session. A draft request that carries an `Mcp-Session-Id` is always rejected, since the draft revision has no session concept.
|
||||
|
||||
**Stateful options marked obsolete.** Because the draft revision is unconditionally sessionless, 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 legacy-protocol back-compat. You can still set them — the warning is informational — and they continue to govern stateful behavior for legacy clients.
|
||||
|
||||
**Client side — automatic fallback.** Clients automatically probe the draft revision first and fall back to the `initialize` handshake when the server doesn't support it:
|
||||
|
||||
- **HTTP**: the client sends its first request with the draft `MCP-Protocol-Version` header. If the server returns HTTP `400` with anything other than a structured `-32004` / `-32003` / `-32001` JSON-RPC error, the client switches to the legacy `initialize` flow on the same endpoint.
|
||||
- **stdio**: the client sends a `server/discover` probe with a 5-second timeout. A `DiscoverResult` confirms the draft revision; a `-32004` error with a `supported` payload triggers a retry at the highest mutually-supported version; anything else — including a timeout — falls back to legacy `initialize` on the same stdin/stdout. The SDK does not relaunch the server process.
|
||||
|
||||
The era is cached per <xref:ModelContextProtocol.Client.IClientTransport> instance, so the probe cost is paid only on the first connect.
|
||||
|
||||
**Opting out of fallback.** Set <xref:ModelContextProtocol.Client.McpClientOptions.MinProtocolVersion> to <xref:ModelContextProtocol.McpSession.DraftProtocolVersion> when you want the client to refuse to fall back. The connect call throws an <xref:ModelContextProtocol.McpException> instead of silently degrading. This is useful for strict-modern production code and for tests that need to assert draft-only behavior.
|
||||
|
||||
```csharp
|
||||
var clientOptions = new McpClientOptions
|
||||
{
|
||||
ProtocolVersion = McpSession.DraftProtocolVersion,
|
||||
MinProtocolVersion = McpSession.DraftProtocolVersion,
|
||||
};
|
||||
```
|
||||
|
||||
### Migrating from legacy SSE
|
||||
|
||||
|
|
@ -93,7 +118,7 @@ When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless>
|
|||
- [Roots](xref:roots) (`RequestRootsAsync`)
|
||||
- Ping — the server cannot ping the client to verify connectivity
|
||||
|
||||
The proposed [MRTR mechanism](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) is designed to bring these capabilities to stateless mode, but it is not yet available.
|
||||
[MRTR](xref:mrtr) brings elicitation (and the deprecated sampling and roots) to stateless mode when both client and server speak the `2026-07-28` draft — 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.
|
||||
- **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 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.
|
||||
|
|
@ -115,17 +140,13 @@ Most MCP servers fall into this category. Tools that call APIs, query databases,
|
|||
|
||||
### Stateless alternatives for server-to-client interactions
|
||||
|
||||
<!-- mlc-disable-next-line -->
|
||||
> [!NOTE]
|
||||
> Multi Round-Trip Requests (MRTR) is a proposed experimental feature that is not yet available. See PR [#1458](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) for the reference implementation and specification proposal.
|
||||
The traditional approach to server-to-client interactions (elicitation, sampling, roots) requires sessions because the server must hold an open connection to send JSON-RPC requests back to the client. [Multi Round-Trip Requests (MRTR)](xref:mrtr) is a sessionless alternative — instead of sending a request, the server returns an **incomplete result** that tells the client what input is needed. The client fulfills the requests and retries the tool call with the responses attached.
|
||||
|
||||
The traditional approach to server-to-client interactions (elicitation, sampling, roots) requires sessions because the server must hold an open connection to send JSON-RPC requests back to the client. [Multi Round-Trip Requests (MRTR)](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) is a proposed alternative that works with stateless servers by inverting the communication model — instead of sending a request, the server returns an **incomplete result** that tells the client what input is needed. The client fulfills the requests and retries the tool call with the responses attached.
|
||||
|
||||
This means servers that need user confirmation, LLM reasoning, or other client input can still run in stateless mode when both sides support MRTR.
|
||||
This means servers that need user confirmation ([elicitation](xref:elicitation)) — or the now-deprecated (SEP-2577) [sampling](xref:sampling) and [roots](xref:roots) — can run in stateless mode when both sides support MRTR. Because MRTR rides on the unratified `2026-07-28` draft, it is far less broadly supported than session-based requests; keep a session if you must interact with clients that don't speak the draft.
|
||||
|
||||
## Stateful mode (sessions)
|
||||
|
||||
When <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> is `false` (the default), the server assigns an `Mcp-Session-Id` to each client during the `initialize` handshake. 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
|
||||
- [Unsolicited notifications](#how-streamable-http-delivers-messages) (resource updates, logging messages) via the GET stream
|
||||
|
|
@ -154,7 +175,7 @@ The [deployment considerations](#deployment-considerations) below are real conce
|
|||
| **Scaling** | Horizontal scaling without constraints | Limited by session-affinity routing |
|
||||
| **Server restarts** | No impact — each request is independent | All sessions lost; clients must reinitialize |
|
||||
| **Memory** | Per-request only | Per-session (default: up to 10,000 sessions × 2 hours) |
|
||||
| **Server-to-client requests** | Not supported (see [MRTR proposal](https://github.com/modelcontextprotocol/csharp-sdk/pull/1458) for a stateless alternative) | Supported (sampling, elicitation, roots) |
|
||||
| **Server-to-client requests** | Available via [MRTR](xref:mrtr) (draft-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) |
|
||||
| **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) |
|
||||
|
|
@ -396,14 +417,16 @@ builder.Services.AddMcpServer()
|
|||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> | `bool` | `false` | Enables stateless mode. No sessions, no `Mcp-Session-Id` header, no server-to-client requests. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.IdleTimeout> | `TimeSpan` | 2 hours | Duration of inactivity before a session is closed. Checked every 5 seconds. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.MaxIdleSessionCount> | `int` | 10,000 | Maximum idle sessions before the oldest are forcibly terminated. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.ConfigureSessionOptions> | `Func<HttpContext, McpServerOptions, CancellationToken, Task>?` | `null` | Per-session callback to customize `McpServerOptions` with access to `HttpContext`. In stateless mode, this runs on every HTTP request. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> | `bool` | `true` | Enables stateless mode. No sessions, no `Mcp-Session-Id` header, no server-to-client requests on the legacy protocol. Required by the `2026-07-28` draft revision. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.IdleTimeout> | `TimeSpan` | 2 hours | _Stateful only (`MCP9006`)._ Duration of inactivity before a session is closed. Checked every 5 seconds. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.MaxIdleSessionCount> | `int` | 10,000 | _Stateful only (`MCP9006`)._ Maximum idle sessions before the oldest are forcibly terminated. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.ConfigureSessionOptions> | `Func<HttpContext, McpServerOptions, CancellationToken, Task>?` | `null` | Per-session callback to customize `McpServerOptions` with access to `HttpContext`. In stateless mode (including all draft-revision requests), this runs on every HTTP request. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.RunSessionHandler> | `Func<HttpContext, McpServer, CancellationToken, Task>?` | `null` | *(Experimental)* Custom session lifecycle handler. Consider `ConfigureSessionOptions` instead. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.SessionMigrationHandler> | `ISessionMigrationHandler?` | `null` | Enables cross-instance session migration. Can also be registered in DI. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EventStreamStore> | `ISseEventStreamStore?` | `null` | Stores SSE events for session resumability via `Last-Event-ID`. Can also be registered in DI. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.PerSessionExecutionContext> | `bool` | `false` | Uses a single `ExecutionContext` for the entire session instead of per-request. Enables session-scoped `AsyncLocal<T>` values but prevents `IHttpContextAccessor` from working in handlers. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.SessionMigrationHandler> | `ISessionMigrationHandler?` | `null` | _Stateful only (`MCP9006`)._ Enables cross-instance session migration. Can also be registered in DI. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EventStreamStore> | `ISseEventStreamStore?` | `null` | _Stateful only (`MCP9006`)._ Stores SSE events for session resumability via `Last-Event-ID`. Can also be registered in DI. |
|
||||
| <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.PerSessionExecutionContext> | `bool` | `false` | _Stateful only (`MCP9006`)._ Uses a single `ExecutionContext` for the entire session instead of per-request. Enables session-scoped `AsyncLocal<T>` values but prevents `IHttpContextAccessor` from working in handlers. |
|
||||
|
||||
The properties marked _Stateful only_ above carry diagnostic [`MCP9006`](xref:list-of-diagnostics#obsolete-apis) because they have no effect when the request is served sessionlessly (every draft-revision request, plus every request on a server with `Stateless = true`). They remain available as back-compat knobs for the legacy stateful Streamable HTTP path.
|
||||
|
||||
### ConfigureSessionOptions
|
||||
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ Rules and constraints:
|
|||
- The header name must contain only visible ASCII characters (0x21–0x7E) excluding colon (`:`).
|
||||
- Values containing non-ASCII characters, control characters, or leading/trailing whitespace are Base64-encoded using the `=?base64?{value}?=` wrapper.
|
||||
- Header names must be case-insensitively unique within the tool's input schema.
|
||||
- Header validation is enforced only for protocol versions that support the HTTP Standardization feature (currently `DRAFT-2026-v1` and later).
|
||||
- Header validation is enforced only for protocol versions that support the HTTP Standardization feature (currently `2026-07-28` and later).
|
||||
|
||||
### Pre-loading tool definitions on the client
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
---
|
||||
uid: list-of-diagnostics
|
||||
---
|
||||
|
||||
# List of Diagnostics Produced by MCP C# SDK
|
||||
|
||||
This document provides information about each of the diagnostics produced by the MCP C# SDK analyzers and source generators.
|
||||
|
|
@ -40,3 +44,4 @@ When APIs are marked as obsolete, a diagnostic is emitted to warn users that the
|
|||
| `MCP9003` | In place | The `RequestContext<TParams>(McpServer, JsonRpcRequest)` constructor is obsolete. Use the overload that accepts a `parameters` argument: `RequestContext<TParams>(McpServer, JsonRpcRequest, TParams)`. |
|
||||
| `MCP9004` | In place | <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> opts into the legacy SSE transport which has no built-in HTTP-level backpressure. Use Streamable HTTP instead. See [Stateless — Legacy SSE transport](xref:stateless#legacy-sse-transport) for details. |
|
||||
| `MCP9005` | In place | The Roots, Sampling, and Logging features are deprecated as of specification version 2026-07-28 and may be removed in a future version. See SEP-2577 for more information. |
|
||||
| `MCP9006` | In place | The stateful Streamable HTTP configuration knobs on <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions> — `EventStreamStore`, `SessionMigrationHandler`, `PerSessionExecutionContext`, `IdleTimeout`, and `MaxIdleSessionCount` — only apply when `Stateless = false`. The draft protocol revision (`2026-07-28`) is sessionless, and the SDK now defaults `Stateless` to `true`. These knobs remain available for back-compat with the legacy stateful Streamable HTTP transport but new code should target the stateless path. |
|
||||
|
|
|
|||
28
package-lock.json
generated
28
package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "csharp-sdk",
|
||||
"name": "halter73-expert-train",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/conformance": "0.1.16",
|
||||
"@modelcontextprotocol/conformance": "0.2.0-alpha.2",
|
||||
"@modelcontextprotocol/server-everything": "2026.1.26",
|
||||
"@modelcontextprotocol/server-memory": "2026.1.26"
|
||||
}
|
||||
|
|
@ -23,18 +23,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/conformance": {
|
||||
"version": "0.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/conformance/-/conformance-0.1.16.tgz",
|
||||
"integrity": "sha512-GI7qiN0r39/MH2srVUR3AXaEN0YLCro20lIBbnvc1frBhszenxvUifBuTzxeVQVagILfBzCIcnungUOma8OrgA==",
|
||||
"version": "0.2.0-alpha.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/conformance/-/conformance-0.2.0-alpha.2.tgz",
|
||||
"integrity": "sha512-/8bde9d0mfsvgd9IwQgNIl1AS9uNOp/+ZG+2nNRWXtPs6xrz/cNp4ObBMmGY9kP8dkDaF3bvjtC/2Hj8TStMRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"commander": "^14.0.2",
|
||||
"eventsource-parser": "^3.0.6",
|
||||
"eventsource-parser": "^3.0.8",
|
||||
"express": "^5.1.0",
|
||||
"jose": "^6.1.2",
|
||||
"undici": "^7.19.0",
|
||||
"undici": "^7.25.0",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
|
|
@ -602,9 +602,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
|
||||
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
|
@ -1428,9 +1428,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
|
||||
"integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz",
|
||||
"integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"private": true,
|
||||
"description": "Pinned npm dependencies for MCP C# SDK integration and conformance tests",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/conformance": "0.1.16",
|
||||
"@modelcontextprotocol/conformance": "0.2.0-alpha.2",
|
||||
"@modelcontextprotocol/server-everything": "2026.1.26",
|
||||
"@modelcontextprotocol/server-memory": "2026.1.26"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ internal static class McpHttpHeaders
|
|||
/// The associated helpers perform exact ordinal matches against this single value rather
|
||||
/// than any ordered comparison.
|
||||
/// </remarks>
|
||||
public const string DraftProtocolVersion = "DRAFT-2026-v1";
|
||||
public const string DraftProtocolVersion = "2026-07-28";
|
||||
|
||||
/// <summary>The session identifier header.</summary>
|
||||
public const string SessionId = "Mcp-Session-Id";
|
||||
|
|
|
|||
|
|
@ -42,4 +42,8 @@ internal static class Obsoletions
|
|||
public const string DeprecatedRoots_Message = "The Roots feature is deprecated as of specification version 2026-07-28 and may be removed in a future version. See SEP-2577 for more information.";
|
||||
public const string DeprecatedSampling_Message = "The Sampling feature is deprecated as of specification version 2026-07-28 and may be removed in a future version. See SEP-2577 for more information.";
|
||||
public const string DeprecatedLogging_Message = "The Logging feature is deprecated as of specification version 2026-07-28 and may be removed in a future version. See SEP-2577 for more information.";
|
||||
|
||||
public const string LegacyStatefulHttp_DiagnosticId = "MCP9006";
|
||||
public const string LegacyStatefulHttp_Message = "Stateful Streamable HTTP mode is a back-compat-only escape hatch for legacy clients. Set HttpServerTransportOptions.Stateless = true (the default as of the 2026-07-28 protocol revision) for new code. See SEP-2567.";
|
||||
public const string LegacyStatefulHttp_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#obsolete-apis";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ using ModelContextProtocol.Server;
|
|||
|
||||
namespace ModelContextProtocol.AspNetCore;
|
||||
|
||||
#pragma warning disable MCP9006 // This type only exists to configure the obsolete legacy resumability store.
|
||||
|
||||
/// <summary>
|
||||
/// Configures <see cref="DistributedCacheEventStreamStoreOptions"/> by resolving
|
||||
/// the <see cref="IDistributedCache"/> from DI when not explicitly set.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ using ModelContextProtocol.Server;
|
|||
|
||||
namespace ModelContextProtocol.AspNetCore;
|
||||
|
||||
#pragma warning disable MCP9006 // This type only exists to validate the obsolete legacy resumability store options.
|
||||
|
||||
/// <summary>
|
||||
/// Validates that <see cref="DistributedCacheEventStreamStoreOptions.Cache"/> is set.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ public static class HttpMcpServerBuilderExtensions
|
|||
/// set the <see cref="DistributedCacheEventStreamStoreOptions.Cache"/> property in the <paramref name="configureOptions"/> callback.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Obsolete(ModelContextProtocol.Obsoletions.LegacyStatefulHttp_Message, DiagnosticId = ModelContextProtocol.Obsoletions.LegacyStatefulHttp_DiagnosticId, UrlFormat = ModelContextProtocol.Obsoletions.LegacyStatefulHttp_Url)]
|
||||
#pragma warning disable MCP9006 // The method is itself obsolete and intentionally wires up the legacy resumability store.
|
||||
public static IMcpServerBuilder WithDistributedCacheEventStreamStore(this IMcpServerBuilder builder, Action<DistributedCacheEventStreamStoreOptions>? configureOptions = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
|
@ -99,4 +101,5 @@ public static class HttpMcpServerBuilderExtensions
|
|||
|
||||
return builder;
|
||||
}
|
||||
#pragma warning restore MCP9006
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ public class HttpServerTransportOptions
|
|||
/// allowing for load balancing without session affinity.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> if the server runs in a stateless mode; <see langword="false"/> if the server tracks state between requests. The default is <see langword="false"/>.
|
||||
/// <see langword="true"/> if the server runs in a stateless mode; <see langword="false"/> if the server tracks state between requests.
|
||||
/// The default is <see langword="true"/> as of the <c>2026-07-28</c> draft protocol revision (SEP-2567);
|
||||
/// set to <see langword="false"/> only when you need to support legacy clients that rely on session affinity.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// If <see langword="true"/>, <see cref="McpSession.SessionId"/> will be null, and the "MCP-Session-Id" header will not be used,
|
||||
|
|
@ -58,8 +60,16 @@ public class HttpServerTransportOptions
|
|||
/// Unsolicited server-to-client messages and all server-to-client requests are also unsupported, because any responses
|
||||
/// might arrive at another ASP.NET Core application process.
|
||||
/// Client sampling, elicitation, and roots capabilities are also disabled in stateless mode, because the server cannot make requests.
|
||||
/// <para>
|
||||
/// The <c>2026-07-28</c> draft protocol revision is sessionless and removes <c>Mcp-Session-Id</c> entirely
|
||||
/// (SEP-2567), so over HTTP draft requests are only ever served when <see langword="true"/>. When this
|
||||
/// property is <see langword="false"/>, a sessionless draft request is refused with a
|
||||
/// <c>-32004 UnsupportedProtocolVersion</c> error so that a dual-era client downgrades to the legacy
|
||||
/// <c>initialize</c> handshake and obtains the session that the server was configured to provide. A draft
|
||||
/// request that carries an <c>Mcp-Session-Id</c> is always rejected, regardless of this property's value.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool Stateless { get; set; }
|
||||
public bool Stateless { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether the server maps legacy SSE endpoints (<c>/sse</c> and <c>/message</c>)
|
||||
|
|
@ -112,6 +122,7 @@ public class HttpServerTransportOptions
|
|||
/// If this property is not set, the server will attempt to resolve an <see cref="ISseEventStreamStore"/> from DI.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Obsolete(Obsoletions.LegacyStatefulHttp_Message, DiagnosticId = Obsoletions.LegacyStatefulHttp_DiagnosticId, UrlFormat = Obsoletions.LegacyStatefulHttp_Url)]
|
||||
public ISseEventStreamStore? EventStreamStore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -128,6 +139,7 @@ public class HttpServerTransportOptions
|
|||
/// If this property is not set, the server will attempt to resolve an <see cref="ISessionMigrationHandler"/> from DI.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Obsolete(Obsoletions.LegacyStatefulHttp_Message, DiagnosticId = Obsoletions.LegacyStatefulHttp_DiagnosticId, UrlFormat = Obsoletions.LegacyStatefulHttp_Url)]
|
||||
public ISessionMigrationHandler? SessionMigrationHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -144,6 +156,7 @@ public class HttpServerTransportOptions
|
|||
/// Enabling a per-session <see cref="ExecutionContext"/> can be useful for setting <see cref="AsyncLocal{T}"/> variables
|
||||
/// that persist for the entire session, but it prevents you from using IHttpContextAccessor in handlers.
|
||||
/// </remarks>
|
||||
[Obsolete(Obsoletions.LegacyStatefulHttp_Message, DiagnosticId = Obsoletions.LegacyStatefulHttp_DiagnosticId, UrlFormat = Obsoletions.LegacyStatefulHttp_Url)]
|
||||
public bool PerSessionExecutionContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -162,6 +175,7 @@ public class HttpServerTransportOptions
|
|||
/// tied to the open GET <c>/sse</c> request, and they are removed immediately when the client disconnects.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Obsolete(Obsoletions.LegacyStatefulHttp_Message, DiagnosticId = Obsoletions.LegacyStatefulHttp_DiagnosticId, UrlFormat = Obsoletions.LegacyStatefulHttp_Url)]
|
||||
public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromHours(2);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -182,6 +196,7 @@ public class HttpServerTransportOptions
|
|||
/// exactly as long as the SSE connection is open.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Obsolete(Obsoletions.LegacyStatefulHttp_Message, DiagnosticId = Obsoletions.LegacyStatefulHttp_DiagnosticId, UrlFormat = Obsoletions.LegacyStatefulHttp_Url)]
|
||||
public int MaxIdleSessionCount { get; set; } = 10_000;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ internal sealed class HttpServerTransportOptionsSetup(IServiceProvider servicePr
|
|||
{
|
||||
public void Configure(HttpServerTransportOptions options)
|
||||
{
|
||||
#pragma warning disable MCP9006 // Stateful Streamable HTTP options are obsolete but still wired up internally.
|
||||
options.EventStreamStore ??= serviceProvider.GetService<ISseEventStreamStore>();
|
||||
options.SessionMigrationHandler ??= serviceProvider.GetService<ISessionMigrationHandler>();
|
||||
#pragma warning restore MCP9006
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
|
@ -18,12 +18,14 @@ internal sealed partial class IdleTrackingBackgroundService : BackgroundService
|
|||
ILogger<IdleTrackingBackgroundService> logger)
|
||||
{
|
||||
// Still run loop given infinite IdleTimeout to enforce the MaxIdleSessionCount and assist graceful shutdown.
|
||||
#pragma warning disable MCP9006 // Stateful Streamable HTTP options are obsolete but still wired up internally.
|
||||
if (options.Value.IdleTimeout != Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(options.Value.IdleTimeout, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(options.Value.MaxIdleSessionCount, 0);
|
||||
#pragma warning restore MCP9006
|
||||
|
||||
_sessions = sessions;
|
||||
_options = options;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ using System.Text.Json.Serialization.Metadata;
|
|||
|
||||
namespace ModelContextProtocol.AspNetCore;
|
||||
|
||||
#pragma warning disable MCP9006 // These extensions only operate on the obsolete legacy resumability reader.
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="ISseEventStreamReader"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -17,9 +17,11 @@ internal sealed partial class StatefulSessionManager(
|
|||
private readonly ConcurrentDictionary<string, StreamableHttpSession> _sessions = new(StringComparer.Ordinal);
|
||||
|
||||
private readonly TimeProvider _timeProvider = httpServerTransportOptions.Value.TimeProvider;
|
||||
#pragma warning disable MCP9006 // Stateful Streamable HTTP options are obsolete but still wired up internally.
|
||||
private readonly TimeSpan _idleTimeout = httpServerTransportOptions.Value.IdleTimeout;
|
||||
private readonly long _idleTimeoutTicks = GetIdleTimeoutInTimestampTicks(httpServerTransportOptions.Value.IdleTimeout, httpServerTransportOptions.Value.TimeProvider);
|
||||
private readonly int _maxIdleSessionCount = httpServerTransportOptions.Value.MaxIdleSessionCount;
|
||||
#pragma warning restore MCP9006
|
||||
|
||||
private readonly object _idlePruningLock = new();
|
||||
private readonly List<long> _idleTimestamps = [];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Buffers;
|
||||
using System.Buffers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
|
@ -12,6 +12,7 @@ using System.Collections.Concurrent;
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace ModelContextProtocol.AspNetCore;
|
||||
|
|
@ -39,9 +40,17 @@ internal sealed class StreamableHttpHandler(
|
|||
"2025-03-26",
|
||||
"2025-06-18",
|
||||
"2025-11-25",
|
||||
"DRAFT-2026-v1",
|
||||
McpHttpHeaders.DraftProtocolVersion,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The supported protocol versions excluding the draft revision. Used when refusing a sessionless
|
||||
/// draft request on a stateful (Stateless = false) server so a dual-era client falls back to a
|
||||
/// legacy initialize handshake instead of retrying the draft version.
|
||||
/// </summary>
|
||||
private static readonly string[] s_supportedProtocolVersionsExcludingDraft =
|
||||
[.. s_supportedProtocolVersions.Where(static v => !string.Equals(v, McpHttpHeaders.DraftProtocolVersion, StringComparison.Ordinal))];
|
||||
|
||||
private static readonly JsonTypeInfo<JsonRpcMessage> s_messageTypeInfo = GetRequiredJsonTypeInfo<JsonRpcMessage>();
|
||||
private static readonly JsonTypeInfo<JsonRpcError> s_errorTypeInfo = GetRequiredJsonTypeInfo<JsonRpcError>();
|
||||
|
||||
|
|
@ -54,9 +63,9 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
public async Task HandlePostRequestAsync(HttpContext context)
|
||||
{
|
||||
if (!ValidateProtocolVersionHeader(context, out var errorMessage))
|
||||
if (!ValidateProtocolVersionHeader(context, out var protocolVersionError))
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
|
||||
await WriteJsonRpcErrorDetailAsync(context, protocolVersionError, StatusCodes.Status400BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -73,16 +82,31 @@ internal sealed class StreamableHttpHandler(
|
|||
return;
|
||||
}
|
||||
|
||||
var message = await ReadJsonRpcMessageAsync(context);
|
||||
JsonRpcMessage? message;
|
||||
try
|
||||
{
|
||||
message = await ReadJsonRpcMessageAsync(context);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// The POST body was not a well-formed JSON-RPC message (malformed JSON, or a request whose
|
||||
// id was explicitly null, which MCP forbids). Surface a conformant JSON-RPC error response
|
||||
// with a null id rather than letting the exception bubble up as an opaque 500.
|
||||
await WriteJsonRpcErrorAsync(context,
|
||||
"Bad Request: The POST body did not contain a valid JSON-RPC message.",
|
||||
StatusCodes.Status400BadRequest, (int)McpErrorCode.InvalidRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message is null)
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context,
|
||||
"Bad Request: The POST body did not contain a valid JSON-RPC message.",
|
||||
StatusCodes.Status400BadRequest);
|
||||
StatusCodes.Status400BadRequest, (int)McpErrorCode.InvalidRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateMcpHeaders(context, message, mcpServerOptionsSnapshot.Value.ToolCollection, out errorMessage))
|
||||
if (!ValidateMcpHeaders(context, message, mcpServerOptionsSnapshot.Value.ToolCollection, out var errorMessage))
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context, errorMessage, StatusCodes.Status400BadRequest, (int)McpErrorCode.HeaderMismatch);
|
||||
return;
|
||||
|
|
@ -108,9 +132,23 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
public async Task HandleGetRequestAsync(HttpContext context)
|
||||
{
|
||||
if (!ValidateProtocolVersionHeader(context, out var errorMessage))
|
||||
if (!ValidateProtocolVersionHeader(context, out var protocolVersionError))
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
|
||||
await WriteJsonRpcErrorDetailAsync(context, protocolVersionError, StatusCodes.Status400BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
|
||||
|
||||
// Under the draft protocol revision the standalone HTTP GET endpoint for unsolicited
|
||||
// server-to-client messages is removed (SEP-2575); clients use subscriptions/listen (POST)
|
||||
// instead. The draft is also sessionless (SEP-2567), so a draft GET is invalid whether or
|
||||
// not it carries an Mcp-Session-Id.
|
||||
if (IsDraftProtocolRequest(context))
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context,
|
||||
"Bad Request: The GET endpoint is not supported by the draft protocol revision. Use subscriptions/listen via POST instead.",
|
||||
StatusCodes.Status400BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +160,6 @@ internal sealed class StreamableHttpHandler(
|
|||
return;
|
||||
}
|
||||
|
||||
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
|
||||
var session = await GetSessionAsync(context, sessionId);
|
||||
if (session is null)
|
||||
{
|
||||
|
|
@ -203,7 +240,9 @@ internal sealed class StreamableHttpHandler(
|
|||
}
|
||||
}
|
||||
|
||||
#pragma warning disable MCP9006 // Stateful Streamable HTTP resumability types are obsolete but still wired up internally.
|
||||
private static async Task HandleResumePostResponseStreamAsync(HttpContext context, ISseEventStreamReader eventStreamReader)
|
||||
#pragma warning restore MCP9006
|
||||
{
|
||||
InitializeSseResponse(context);
|
||||
await eventStreamReader.CopyToAsync(context.Response.Body, context.RequestAborted);
|
||||
|
|
@ -211,13 +250,24 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
public async Task HandleDeleteRequestAsync(HttpContext context)
|
||||
{
|
||||
if (!ValidateProtocolVersionHeader(context, out var errorMessage))
|
||||
if (!ValidateProtocolVersionHeader(context, out var protocolVersionError))
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
|
||||
await WriteJsonRpcErrorDetailAsync(context, protocolVersionError, StatusCodes.Status400BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
|
||||
|
||||
// Under the draft revision there are no sessions to terminate (SEP-2567). A draft DELETE is
|
||||
// invalid whether or not it carries an Mcp-Session-Id.
|
||||
if (IsDraftProtocolRequest(context))
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context,
|
||||
"Bad Request: The DELETE endpoint is not supported by the draft protocol revision (the draft protocol is sessionless).",
|
||||
StatusCodes.Status400BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(sessionId) || !sessionManager.TryGetValue(sessionId, out var session))
|
||||
{
|
||||
return;
|
||||
|
|
@ -280,10 +330,12 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
private async ValueTask<StreamableHttpSession?> TryMigrateSessionAsync(HttpContext context, string sessionId)
|
||||
{
|
||||
#pragma warning disable MCP9006 // Stateful Streamable HTTP options are obsolete but still wired up internally.
|
||||
if (HttpServerTransportOptions.SessionMigrationHandler is not { } handler)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
#pragma warning restore MCP9006
|
||||
|
||||
var migrationLock = _migrationLocks.GetOrAdd(sessionId, static _ => new SemaphoreSlim(1, 1));
|
||||
await migrationLock.WaitAsync(context.RequestAborted);
|
||||
|
|
@ -319,6 +371,34 @@ internal sealed class StreamableHttpHandler(
|
|||
private async ValueTask<StreamableHttpSession?> GetOrCreateSessionAsync(HttpContext context, JsonRpcMessage message)
|
||||
{
|
||||
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
|
||||
bool isDraftRequest = IsDraftProtocolRequest(context);
|
||||
|
||||
// Under the draft protocol revision, the draft is sessionless: SEP-2567 removes the
|
||||
// Mcp-Session-Id header (and the session concept) and SEP-2575 removes the initialize
|
||||
// handshake. So over HTTP, draft <=> sessionless, with no exceptions:
|
||||
if (isDraftRequest)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
// A draft request carrying an Mcp-Session-Id is non-conformant (SEP-2567).
|
||||
await WriteJsonRpcErrorAsync(context,
|
||||
"Bad Request: Mcp-Session-Id is not supported under the draft protocol revision; the draft protocol is sessionless (SEP-2567).",
|
||||
StatusCodes.Status400BadRequest);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (HttpServerTransportOptions.Stateless)
|
||||
{
|
||||
// The default (stateless) HTTP transport serves sessionless draft requests natively.
|
||||
return await StartNewSessionAsync(context);
|
||||
}
|
||||
|
||||
// The author explicitly opted into sessions (Stateless = false), which the draft revision
|
||||
// cannot provide. Refuse the draft version so a dual-era client falls back to the legacy
|
||||
// initialize handshake and gets the session it asked for (SEP-2575 fallback semantics).
|
||||
await WriteUnsupportedDraftVersionErrorAsync(context);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
|
|
@ -350,14 +430,28 @@ internal sealed class StreamableHttpHandler(
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> when the request declares the draft protocol revision via
|
||||
/// the <c>MCP-Protocol-Version</c> header. Draft requests are always sessionless and do not perform
|
||||
/// the legacy <c>initialize</c> handshake (SEP-2575 + SEP-2567).
|
||||
/// </summary>
|
||||
private static bool IsDraftProtocolRequest(HttpContext context)
|
||||
{
|
||||
var protocolVersionHeader = context.Request.Headers[McpProtocolVersionHeaderName].ToString();
|
||||
return string.Equals(protocolVersionHeader, McpHttpHeaders.DraftProtocolVersion, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private async ValueTask<StreamableHttpSession> StartNewSessionAsync(HttpContext context)
|
||||
{
|
||||
string sessionId;
|
||||
StreamableHttpServerTransport transport;
|
||||
|
||||
if (!HttpServerTransportOptions.Stateless)
|
||||
bool isStateless = HttpServerTransportOptions.Stateless;
|
||||
|
||||
if (!isStateless)
|
||||
{
|
||||
sessionId = MakeNewSessionId();
|
||||
#pragma warning disable MCP9006 // Stateful Streamable HTTP options are obsolete but still wired up internally.
|
||||
transport = new(loggerFactory)
|
||||
{
|
||||
SessionId = sessionId,
|
||||
|
|
@ -367,12 +461,13 @@ internal sealed class StreamableHttpHandler(
|
|||
? (initParams, ct) => handler.OnSessionInitializedAsync(context, sessionId, initParams, ct)
|
||||
: null,
|
||||
};
|
||||
#pragma warning restore MCP9006
|
||||
|
||||
context.Response.Headers[McpSessionIdHeaderName] = sessionId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// In stateless mode, each request is independent. Don't set any session ID on the transport.
|
||||
// In stateless mode (legacy or draft), each request is independent. Don't set any session ID on the transport.
|
||||
// If in the future we support resuming stateless requests, we should populate
|
||||
// the event stream store and retry interval here as well.
|
||||
sessionId = "";
|
||||
|
|
@ -393,11 +488,13 @@ internal sealed class StreamableHttpHandler(
|
|||
{
|
||||
var mcpServerServices = applicationServices;
|
||||
var mcpServerOptions = mcpServerOptionsSnapshot.Value;
|
||||
if (HttpServerTransportOptions.Stateless || HttpServerTransportOptions.ConfigureSessionOptions is not null || configureOptions is not null)
|
||||
bool effectivelyStateless = HttpServerTransportOptions.Stateless;
|
||||
|
||||
if (effectivelyStateless || HttpServerTransportOptions.ConfigureSessionOptions is not null || configureOptions is not null)
|
||||
{
|
||||
mcpServerOptions = mcpServerOptionsFactory.Create(Options.DefaultName);
|
||||
|
||||
if (HttpServerTransportOptions.Stateless)
|
||||
if (effectivelyStateless)
|
||||
{
|
||||
// The session does not outlive the request in stateless mode.
|
||||
mcpServerServices = context.RequestServices;
|
||||
|
|
@ -434,8 +531,10 @@ internal sealed class StreamableHttpHandler(
|
|||
var transport = new StreamableHttpServerTransport(loggerFactory)
|
||||
{
|
||||
SessionId = sessionId,
|
||||
#pragma warning disable MCP9006 // Stateful Streamable HTTP options are obsolete but still wired up internally.
|
||||
FlowExecutionContextFromRequests = !HttpServerTransportOptions.PerSessionExecutionContext,
|
||||
EventStreamStore = HttpServerTransportOptions.EventStreamStore,
|
||||
#pragma warning restore MCP9006
|
||||
};
|
||||
|
||||
// Initialize the transport with the migrated session's init params.
|
||||
|
|
@ -450,9 +549,11 @@ internal sealed class StreamableHttpHandler(
|
|||
});
|
||||
}
|
||||
|
||||
#pragma warning disable MCP9006 // Stateful Streamable HTTP resumability types are obsolete but still wired up internally.
|
||||
private async ValueTask<ISseEventStreamReader?> GetEventStreamReaderAsync(HttpContext context, string lastEventId)
|
||||
{
|
||||
if (HttpServerTransportOptions.EventStreamStore is not { } eventStreamStore)
|
||||
#pragma warning restore MCP9006
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context,
|
||||
"Bad Request: This server does not support resuming streams.",
|
||||
|
|
@ -559,22 +660,60 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
/// <summary>
|
||||
/// Validates the MCP-Protocol-Version header if present. A missing header is allowed for backwards compatibility,
|
||||
/// but an invalid or unsupported value must be rejected with 400 Bad Request per the MCP spec.
|
||||
/// but an invalid or unsupported value must be rejected with 400 Bad Request per the MCP spec. Per SEP-2575, the
|
||||
/// rejection uses the <see cref="McpErrorCode.UnsupportedProtocolVersion"/> error code with a data payload
|
||||
/// listing the server's supported versions so the client can select a fallback.
|
||||
/// </summary>
|
||||
private static bool ValidateProtocolVersionHeader(HttpContext context, out string? errorMessage)
|
||||
private static bool ValidateProtocolVersionHeader(HttpContext context, [NotNullWhen(false)] out JsonRpcErrorDetail? errorDetail)
|
||||
{
|
||||
var protocolVersionHeader = context.Request.Headers[McpProtocolVersionHeaderName].ToString();
|
||||
if (!string.IsNullOrEmpty(protocolVersionHeader) &&
|
||||
!s_supportedProtocolVersions.Contains(protocolVersionHeader))
|
||||
{
|
||||
errorMessage = $"Bad Request: The MCP-Protocol-Version header value '{protocolVersionHeader}' is not supported.";
|
||||
errorDetail = new JsonRpcErrorDetail
|
||||
{
|
||||
Code = (int)McpErrorCode.UnsupportedProtocolVersion,
|
||||
Message = $"Bad Request: The MCP-Protocol-Version header value '{protocolVersionHeader}' is not supported.",
|
||||
Data = JsonSerializer.SerializeToNode(
|
||||
new UnsupportedProtocolVersionErrorData
|
||||
{
|
||||
Supported = [.. s_supportedProtocolVersions],
|
||||
Requested = protocolVersionHeader,
|
||||
},
|
||||
GetRequiredJsonTypeInfo<UnsupportedProtocolVersionErrorData>()),
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = null;
|
||||
errorDetail = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refuses a sessionless draft request on a stateful (Stateless = false) server. The draft revision
|
||||
/// is sessionless (SEP-2567) and cannot honor the author's opt-in to sessions, so we return
|
||||
/// <see cref="McpErrorCode.UnsupportedProtocolVersion"/> with a supported-versions list that excludes
|
||||
/// the draft. A dual-era client then falls back to the legacy initialize handshake (SEP-2575).
|
||||
/// </summary>
|
||||
private static Task WriteUnsupportedDraftVersionErrorAsync(HttpContext context)
|
||||
{
|
||||
var errorDetail = new JsonRpcErrorDetail
|
||||
{
|
||||
Code = (int)McpErrorCode.UnsupportedProtocolVersion,
|
||||
Message = $"Bad Request: The draft protocol revision '{McpHttpHeaders.DraftProtocolVersion}' is sessionless and is not supported when the server is configured with sessions (HttpServerTransportOptions.Stateless = false). " +
|
||||
"Use the initialize handshake with a supported non-draft protocol version instead.",
|
||||
Data = JsonSerializer.SerializeToNode(
|
||||
new UnsupportedProtocolVersionErrorData
|
||||
{
|
||||
Supported = s_supportedProtocolVersionsExcludingDraft,
|
||||
Requested = McpHttpHeaders.DraftProtocolVersion,
|
||||
},
|
||||
GetRequiredJsonTypeInfo<UnsupportedProtocolVersionErrorData>()),
|
||||
};
|
||||
|
||||
return WriteJsonRpcErrorDetailAsync(context, errorDetail, StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates standard MCP request headers (Mcp-Method, Mcp-Name) and custom parameter headers
|
||||
/// (Mcp-Param-*) against the JSON-RPC request body.
|
||||
|
|
@ -640,6 +779,23 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
var mcpNameInHeader = context.Request.Headers[McpHttpHeaders.Name].ToString().Trim();
|
||||
|
||||
// Per SEP-2243, non-ASCII Mcp-Name values MUST be Base64-encoded using the
|
||||
// "=?base64?...?=" wrapper. Reject raw values containing characters outside the valid
|
||||
// HTTP header value range, then decode so the comparison below is against the
|
||||
// decoded value (mirrors the Mcp-Param-* validation in ValidateCustomParamHeaders).
|
||||
if (!IsValidHeaderValue(mcpNameInHeader))
|
||||
{
|
||||
errorMessage = "Header mismatch: Mcp-Name header contains invalid characters.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var decodedMcpNameInHeader = McpHeaderEncoder.DecodeValue(mcpNameInHeader);
|
||||
if (decodedMcpNameInHeader is null)
|
||||
{
|
||||
errorMessage = "Header mismatch: Mcp-Name header contains invalid Base64 encoding.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract the params and name value from the body based on the method, if present.
|
||||
var bodyParams = message switch
|
||||
{
|
||||
|
|
@ -656,7 +812,7 @@ internal sealed class StreamableHttpHandler(
|
|||
};
|
||||
|
||||
// Check that the header value matches the body value if the body value is present.
|
||||
if (!string.Equals(mcpNameInHeader, mcpNameInBody, StringComparison.Ordinal))
|
||||
if (!string.Equals(decodedMcpNameInHeader, mcpNameInBody, StringComparison.Ordinal))
|
||||
{
|
||||
errorMessage = $"Header mismatch: Mcp-Name header value '{mcpNameInHeader}' does not match body value '{mcpNameInBody}'.";
|
||||
return false;
|
||||
|
|
@ -997,6 +1153,12 @@ internal sealed class StreamableHttpHandler(
|
|||
return SafeIntegerParse.NotNumeric;
|
||||
}
|
||||
|
||||
private static Task WriteJsonRpcErrorDetailAsync(HttpContext context, JsonRpcErrorDetail detail, int statusCode)
|
||||
{
|
||||
var jsonRpcError = new JsonRpcError { Error = detail };
|
||||
return Results.Json(jsonRpcError, s_errorTypeInfo, statusCode: statusCode).ExecuteAsync(context);
|
||||
}
|
||||
|
||||
private static bool MatchesApplicationJsonMediaType(MediaTypeHeaderValue acceptHeaderValue)
|
||||
=> acceptHeaderValue.MatchesMediaType("application/json");
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue