diff --git a/docs/concepts/elicitation/elicitation.md b/docs/concepts/elicitation/elicitation.md index 003723e39..60530d85d 100644 --- a/docs/concepts/elicitation/elicitation.md +++ b/docs/concepts/elicitation/elicitation.md @@ -172,7 +172,7 @@ 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 `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 and let the SDK emit an on the wire. +[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `2026-07-28`. In that revision, 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 and let the SDK emit an 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 `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. diff --git a/docs/concepts/mrtr/mrtr.md b/docs/concepts/mrtr/mrtr.md index 2168a300a..5044e6d51 100644 --- a/docs/concepts/mrtr/mrtr.md +++ b/docs/concepts/mrtr/mrtr.md @@ -33,10 +33,10 @@ MRTR is useful when: ## Opting in -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. +MRTR activates when both peers negotiate protocol revision **`2026-07-28`**. The C# SDK client prefers `2026-07-28` by default — it probes with `server/discover` and falls back to a legacy `initialize` handshake only when the server doesn't support it. Servers accept `2026-07-28` automatically when a client offers it. No experimental flags are required; pinning `ProtocolVersion` to a legacy revision opts back out. ```csharp -// Client — the SDK prefers the 2026-07-28 draft (and therefore MRTR) by default. +// Client — the SDK prefers 2026-07-28 (and therefore MRTR) by default. var clientOptions = new McpClientOptions { Handlers = new McpClientHandlers @@ -47,7 +47,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 (which is stateless-only under the draft revision) must use `InputRequiredException` rather than , , or . 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 (where Streamable HTTP no longer supports sessions) must use `InputRequiredException` rather than , , or . 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. @@ -281,6 +281,6 @@ The SDK supports `InputRequiredException` across two protocol revisions and two `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 `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. +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 `2026-07-28` 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. -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`. +Because `2026-07-28` removes `Mcp-Session-Id` (SEP-2567) and the `initialize` handshake (SEP-2575), Streamable HTTP runs statelessly whenever a client speaks `2026-07-28`. 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 `2026-07-28` requests without a session and creates a legacy session only when an older client falls back to `initialize`. diff --git a/docs/concepts/roots/roots.md b/docs/concepts/roots/roots.md index 220887fae..bb0218f97 100644 --- a/docs/concepts/roots/roots.md +++ b/docs/concepts/roots/roots.md @@ -106,7 +106,7 @@ server.RegisterNotificationHandler( ### Multi Round-Trip Requests (MRTR) -[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 and let the SDK emit an on the wire. +[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `2026-07-28`. In that revision, 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 and let the SDK emit an 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 `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. diff --git a/docs/concepts/sampling/sampling.md b/docs/concepts/sampling/sampling.md index 1dd0b90ec..825acfb0a 100644 --- a/docs/concepts/sampling/sampling.md +++ b/docs/concepts/sampling/sampling.md @@ -123,7 +123,7 @@ 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 `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 and let the SDK emit an on the wire. +[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `2026-07-28`. In that revision, 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 and let the SDK emit an 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 `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. diff --git a/docs/concepts/stateless/stateless.md b/docs/concepts/stateless/stateless.md index 861a1cd6b..6750443df 100644 --- a/docs/concepts/stateless/stateless.md +++ b/docs/concepts/stateless/stateless.md @@ -24,42 +24,41 @@ When sessions are enabled (`Stateless = false`), the server creates and tracks a > [!NOTE] -> **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 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). +> **Why is stateless now the default?** Earlier versions of the SDK defaulted to stateful, but not because the `2025-11-25` (and older) protocol revisions ever required a server to use the `Mcp-Session-Id` header. They didn't. The original SSE transport could only operate statefully, and keeping Streamable HTTP stateful by default let server-to-client requests (elicitation, sampling, roots) keep working on `2025-11-25` the way they always had. A client was required to echo a server-assigned `Mcp-Session-Id` on later requests, but whether to assign one was always the server's choice. The `2026-07-28` protocol revision removes the header (SEP-2567) and the `initialize` handshake (SEP-2575) from the wire format entirely, and server-to-client requests now run through [MRTR](xref:mrtr), so the SDK now defaults 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 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: +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` protocol 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` revision 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 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 = 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` protocol 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 `2026-07-28` 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 [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. +- **`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` revision, however, so keep a session if you need server-to-client requests against clients that don't speak it. Note that even with `Stateless = false`, a `2026-07-28` request is still served without a session because the protocol has no session header; the stateful path activates only when a client falls back to a legacy revision. > [!TIP] > 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 protocol 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). +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 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. +**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. 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 `2026-07-28` request (which carries no session) on a stateful server is refused with a `-32022 UnsupportedProtocolVersion` error, so a dual-era client downgrades to the legacy `initialize` handshake and obtains a session. A `2026-07-28` request that carries an `Mcp-Session-Id` is always rejected, since the revision has no session concept. -**Stateful options marked obsolete.** Because the draft revision is unconditionally sessionless, the stateful-only knobs on — `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. +**Stateful options marked obsolete.** Because Streamable HTTP no longer supports sessions starting with the `2026-07-28` revision, the stateful-only knobs on — `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: +**Client side — automatic fallback.** Clients automatically probe `2026-07-28` 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. +- **HTTP**: the client sends its first request with the `2026-07-28` `MCP-Protocol-Version` header. If the server returns HTTP `400` with anything other than a structured `-32022` / `-32021` / `-32020` 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 `2026-07-28`; a `-32022` 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 instance, so the probe cost is paid only on the first connect. -**Opting out of fallback.** Set to when you want the client to refuse to fall back. The connect call throws an instead of silently degrading. This is useful for strict-modern production code and for tests that need to assert draft-only behavior. +**Opting out of fallback.** Pin to `2026-07-28` when you want the client to refuse to fall back. A non-null `ProtocolVersion` is also treated as the minimum, so the connect call throws an instead of silently degrading to a legacy revision. This is useful for strict-modern production code and for tests that need to assert `2026-07-28`-only behavior. To try several versions yourself, leave `ProtocolVersion` unset (the default) or retry the connection with a different value. ```csharp var clientOptions = new McpClientOptions { - ProtocolVersion = McpSession.DraftProtocolVersion, - MinProtocolVersion = McpSession.DraftProtocolVersion, + ProtocolVersion = "2026-07-28", }; ``` @@ -118,7 +117,7 @@ When - [Roots](xref:roots) (`RequestRootsAsync`) - Ping — the server cannot 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 the `2026-07-28` draft — 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. - **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. @@ -140,9 +139,9 @@ Most MCP servers fall into this category. Tools that call APIs, query databases, ### Stateless alternatives for server-to-client interactions -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)](xref:mrtr) is a stateless 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. -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. +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` revision, it is far less broadly supported than session-based requests; keep a session if you must interact with clients that don't speak it. ## Stateful mode (sessions) @@ -175,7 +174,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** | Available via [MRTR](xref:mrtr) (draft-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) | | **Resource subscriptions** | Not supported | Supported | | **Client compatibility** | Works with all Streamable HTTP clients | Also supports legacy SSE-only clients via (disabled by default), but some Streamable HTTP clients [may not send `Mcp-Session-Id` correctly](#deployment-considerations) | @@ -417,16 +416,16 @@ builder.Services.AddMcpServer() | Property | Type | Default | Description | |----------|------|---------|-------------| -| | `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. | +| | `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` protocol revision. | | | `TimeSpan` | 2 hours | _Stateful only (`MCP9006`)._ Duration of inactivity before a session is closed. Checked every 5 seconds. | | | `int` | 10,000 | _Stateful only (`MCP9006`)._ Maximum idle sessions before the oldest are forcibly terminated. | -| | `Func?` | `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. | +| | `Func?` | `null` | Per-session callback to customize `McpServerOptions` with access to `HttpContext`. In stateless mode (including all `2026-07-28` requests), this runs on every HTTP request. | | | `Func?` | `null` | *(Experimental)* Custom session lifecycle handler. Consider `ConfigureSessionOptions` instead. | | | `ISessionMigrationHandler?` | `null` | _Stateful only (`MCP9006`)._ Enables cross-instance session migration. Can also be registered in DI. | | | `ISseEventStreamStore?` | `null` | _Stateful only (`MCP9006`)._ Stores SSE events for session resumability via `Last-Event-ID`. Can also be registered in DI. | | | `bool` | `false` | _Stateful only (`MCP9006`)._ Uses a single `ExecutionContext` for the entire session instead of per-request. Enables session-scoped `AsyncLocal` 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. +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 without a session (every `2026-07-28` 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 diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index 9bf6c982c..1f5e3fcd4 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -44,4 +44,4 @@ When APIs are marked as obsolete, a diagnostic is emitted to warn users that the | `MCP9003` | In place | The `RequestContext(McpServer, JsonRpcRequest)` constructor is obsolete. Use the overload that accepts a `parameters` argument: `RequestContext(McpServer, JsonRpcRequest, TParams)`. | | `MCP9004` | In place | opts into the legacy SSE transport which has no built-in HTTP-level backpressure. Use Streamable HTTP instead. See [Stateless — Legacy SSE transport](xref:stateless#legacy-sse-transport) for details. | | `MCP9005` | In place | The Roots, Sampling, and Logging features are deprecated as of specification version 2026-07-28 and may be removed in a future version. See SEP-2577 for more information. | -| `MCP9006` | In place | The stateful Streamable HTTP configuration knobs on — `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. | +| `MCP9006` | In place | The stateful Streamable HTTP configuration knobs on — `EventStreamStore`, `SessionMigrationHandler`, `PerSessionExecutionContext`, `IdleTimeout`, and `MaxIdleSessionCount` — only apply when `Stateless = false`. Starting with the `2026-07-28` protocol revision, Streamable HTTP no longer supports sessions, and the SDK now defaults `Stateless` to `true`. These knobs remain available for back-compat with the legacy stateful Streamable HTTP transport but new code should target the stateless path. | diff --git a/package-lock.json b/package-lock.json index 77ce83884..fc400cc42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "@modelcontextprotocol/conformance": "0.2.0-alpha.2", + "@modelcontextprotocol/conformance": "0.2.0-alpha.5", "@modelcontextprotocol/server-everything": "2026.1.26", "@modelcontextprotocol/server-memory": "2026.1.26" } @@ -23,9 +23,9 @@ } }, "node_modules/@modelcontextprotocol/conformance": { - "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==", + "version": "0.2.0-alpha.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/conformance/-/conformance-0.2.0-alpha.5.tgz", + "integrity": "sha512-sYxNHKk/m7Vx0/XxKulbcmgqz7wH2tXIge9u1G1CzpluaZHTci966m5dw7wGyQKx7IR3/V+/hAAGXc8ZqBMoyw==", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/package.json b/package.json index 21d33001c..3dbb9fba1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "description": "Pinned npm dependencies for MCP C# SDK integration and conformance tests", "dependencies": { - "@modelcontextprotocol/conformance": "0.2.0-alpha.2", + "@modelcontextprotocol/conformance": "0.2.0-alpha.5", "@modelcontextprotocol/server-everything": "2026.1.26", "@modelcontextprotocol/server-memory": "2026.1.26" } diff --git a/src/Common/McpHttpHeaders.cs b/src/Common/McpHttpHeaders.cs index ae5c84d6f..31800e2fa 100644 --- a/src/Common/McpHttpHeaders.cs +++ b/src/Common/McpHttpHeaders.cs @@ -10,27 +10,23 @@ namespace ModelContextProtocol.Protocol; internal static class McpHttpHeaders { /// - /// The draft MCP protocol version string used to gate behaviors that are only enabled - /// for clients negotiating the in-progress draft specification. + /// The 2026-07-28 MCP protocol revision (SEP-2575 + SEP-2567). It removed the initialize + /// handshake and Mcp-Session-Id, so Streamable HTTP no longer has sessions; it also enabled + /// MRTR (SEP-2322) and made the standard MCP request headers (Mcp-Method, Mcp-Name) + /// required. Behaviors that began at this revision are gated by ordinal-comparing the per-request + /// version against it (see ), so it underpins the more + /// semantically named helpers. It is also the latest revision this SDK supports, so clients prefer it + /// by default. /// - /// - /// Behaviors currently gated on this version include: - /// - /// - /// Requiring the standard MCP request headers (Mcp-Method and Mcp-Name) - /// on Streamable HTTP POST requests; servers treat missing headers as errors only when - /// the client's MCP-Protocol-Version header matches this value. - /// - /// - /// Reporting unresolvable resource URIs from resources/read with the standard - /// JSON-RPC (-32602) code rather than the - /// legacy (-32002) code. - /// - /// - /// The associated helpers perform exact ordinal matches against this single value rather - /// than any ordered comparison. - /// - public const string DraftProtocolVersion = "2026-07-28"; + public const string July2026ProtocolVersion = "2026-07-28"; + + /// + /// The 2025-11-25 MCP protocol revision: the latest revision that still supports Streamable HTTP + /// sessions (the initialize handshake and Mcp-Session-Id); newer revisions remove them. + /// It is the default version for the legacy initialize and session-resume code paths, and the + /// version the server advertises when a peer requests an unsupported version on the legacy handshake. + /// + public const string November2025ProtocolVersion = "2025-11-25"; /// The session identifier header. public const string SessionId = "Mcp-Session-Id"; @@ -76,18 +72,20 @@ internal static class McpHttpHeaders internal const string ToolContextKey = "Mcp.Tool"; /// - /// Protocol versions that require standard MCP request headers (Mcp-Method, Mcp-Name). + /// Returns if the given protocol version is + /// or later, the revision that removed the initialize handshake and Streamable HTTP sessions. + /// Protocol versions are ISO-8601 dates, so an ordinal comparison orders them chronologically. /// - private static readonly HashSet s_versionsWithStandardHeaders = new(StringComparer.Ordinal) - { - DraftProtocolVersion, - }; + internal static bool IsJuly2026OrLaterProtocolVersion(string? protocolVersion) + => !string.IsNullOrEmpty(protocolVersion) + && StringComparer.Ordinal.Compare(protocolVersion, July2026ProtocolVersion) >= 0; /// - /// Returns if the given protocol version requires standard MCP request headers. + /// Returns if the given protocol version requires standard MCP request headers + /// (Mcp-Method, Mcp-Name). /// public static bool SupportsStandardHeaders(string? protocolVersion) - => !string.IsNullOrEmpty(protocolVersion) && s_versionsWithStandardHeaders.Contains(protocolVersion!); + => IsJuly2026OrLaterProtocolVersion(protocolVersion); /// /// Returns if the negotiated protocol version reports unresolvable @@ -95,5 +93,5 @@ public static bool SupportsStandardHeaders(string? protocolVersion) /// rather than the legacy (-32002). /// internal static bool UseInvalidParamsForMissingResource(string? protocolVersion) - => string.Equals(protocolVersion, DraftProtocolVersion, StringComparison.Ordinal); + => IsJuly2026OrLaterProtocolVersion(protocolVersion); } diff --git a/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs b/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs index b24bb6f17..e0c8a8826 100644 --- a/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs +++ b/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using ModelContextProtocol.Server; namespace ModelContextProtocol.AspNetCore; @@ -51,7 +51,7 @@ public class HttpServerTransportOptions /// /// /// if the server runs in a stateless mode; if the server tracks state between requests. - /// The default is as of the 2026-07-28 draft protocol revision (SEP-2567); + /// The default is as of the 2026-07-28 protocol revision (SEP-2567); /// set to only when you need to support legacy clients that rely on session affinity. /// /// @@ -61,12 +61,13 @@ public class HttpServerTransportOptions /// 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. /// - /// The 2026-07-28 draft protocol revision is sessionless and removes Mcp-Session-Id entirely - /// (SEP-2567), so over HTTP draft requests are only ever served when . When this - /// property is , a sessionless draft request is refused with a - /// -32004 UnsupportedProtocolVersion error so that a dual-era client downgrades to the legacy - /// initialize handshake and obtains the session that the server was configured to provide. A draft - /// request that carries an Mcp-Session-Id is always rejected, regardless of this property's value. + /// Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions: + /// the revision removed Mcp-Session-Id (SEP-2567), so over HTTP its requests are only ever served + /// when this property is . When it is , such a request is + /// refused with a -32022 UnsupportedProtocolVersion error so that a dual-era client downgrades to + /// the legacy initialize handshake and obtains the session the server was configured to provide. + /// A request that carries an Mcp-Session-Id is always rejected by the 2026-07-28 and later + /// revisions, regardless of this property's value. /// /// public bool Stateless { get; set; } = true; diff --git a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs index 0b1a9df60..3224ceda8 100644 --- a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs +++ b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs @@ -39,17 +39,17 @@ internal sealed class StreamableHttpHandler( "2024-11-05", "2025-03-26", "2025-06-18", - "2025-11-25", - McpHttpHeaders.DraftProtocolVersion, + McpHttpHeaders.November2025ProtocolVersion, + McpHttpHeaders.July2026ProtocolVersion, ]; /// - /// 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. + /// The supported protocol versions that still allow Streamable HTTP sessions (excluding 2026-07-28 and + /// later). Used when refusing a 2026-07-28 request on a stateful (Stateless = false) server so a dual-era + /// client falls back to a legacy initialize handshake instead of retrying the 2026-07-28 version. /// - private static readonly string[] s_supportedProtocolVersionsExcludingDraft = - [.. s_supportedProtocolVersions.Where(static v => !string.Equals(v, McpHttpHeaders.DraftProtocolVersion, StringComparison.Ordinal))]; + private static readonly string[] s_sessionSupportingProtocolVersions = + [.. s_supportedProtocolVersions.Where(static v => !McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(v))]; private static readonly JsonTypeInfo s_messageTypeInfo = GetRequiredJsonTypeInfo(); private static readonly JsonTypeInfo s_errorTypeInfo = GetRequiredJsonTypeInfo(); @@ -140,14 +140,13 @@ public async Task HandleGetRequestAsync(HttpContext context) 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)) + // The 2026-07-28 revision (SEP-2575) removes the standalone HTTP GET endpoint for unsolicited + // server-to-client messages; clients use subscriptions/listen (POST) instead. Because Streamable HTTP + // no longer has sessions (SEP-2567), the GET is invalid whether or not it carries an Mcp-Session-Id. + if (IsJuly2026OrLaterProtocol(context)) { await WriteJsonRpcErrorAsync(context, - "Bad Request: The GET endpoint is not supported by the draft protocol revision. Use subscriptions/listen via POST instead.", + "Bad Request: The GET endpoint is not supported by the 2026-07-28 and later protocol revisions. Use subscriptions/listen via POST instead.", StatusCodes.Status400BadRequest); return; } @@ -258,12 +257,12 @@ public async Task HandleDeleteRequestAsync(HttpContext context) 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)) + // Starting with the 2026-07-28 revision, Streamable HTTP has no sessions to terminate (SEP-2567), + // so the DELETE is invalid whether or not it carries an Mcp-Session-Id. + if (IsJuly2026OrLaterProtocol(context)) { await WriteJsonRpcErrorAsync(context, - "Bad Request: The DELETE endpoint is not supported by the draft protocol revision (the draft protocol is sessionless).", + "Bad Request: The DELETE endpoint is not supported by the 2026-07-28 and later protocol revisions.", StatusCodes.Status400BadRequest); return; } @@ -371,33 +370,31 @@ await WriteJsonRpcErrorAsync(context, private async ValueTask 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) + // The 2026-07-28 revision removes the Mcp-Session-Id header and the session concept (SEP-2567) + // and the initialize handshake (SEP-2575), so over HTTP it never has a session, with no exceptions: + if (IsJuly2026OrLaterProtocol(context)) { if (!string.IsNullOrEmpty(sessionId)) { - // A draft request carrying an Mcp-Session-Id is non-conformant (SEP-2567). + // A request carrying an Mcp-Session-Id is non-conformant under the 2026-07-28 revision (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).", + "Bad Request: Mcp-Session-Id is not supported by the 2026-07-28 and later protocol revisions (SEP-2567).", StatusCodes.Status400BadRequest); return null; } - if (HttpServerTransportOptions.Stateless) + 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 2026-07-28 + // revision cannot provide. Refuse it so a dual-era client falls back to the legacy + // initialize handshake and gets the session it asked for (SEP-2575 fallback semantics). + await WriteUnsupportedProtocolVersionErrorAsync(context); + return null; } - // 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; + // The default (stateless) HTTP transport serves these requests natively. + return await StartNewSessionAsync(context); } if (string.IsNullOrEmpty(sessionId)) @@ -431,14 +428,15 @@ await WriteJsonRpcErrorAsync(context, } /// - /// Returns when the request declares the draft protocol revision via - /// the MCP-Protocol-Version header. Draft requests are always sessionless and do not perform - /// the legacy initialize handshake (SEP-2575 + SEP-2567). + /// Returns when the request's MCP-Protocol-Version header declares a + /// revision that operates without sessions, so the server must serve it statelessly. Such requests + /// never carry an Mcp-Session-Id and never perform the legacy initialize handshake + /// (SEP-2575 + SEP-2567). /// - private static bool IsDraftProtocolRequest(HttpContext context) + private static bool IsJuly2026OrLaterProtocol(HttpContext context) { var protocolVersionHeader = context.Request.Headers[McpProtocolVersionHeaderName].ToString(); - return string.Equals(protocolVersionHeader, McpHttpHeaders.DraftProtocolVersion, StringComparison.Ordinal); + return McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(protocolVersionHeader); } private async ValueTask StartNewSessionAsync(HttpContext context) @@ -446,9 +444,7 @@ private async ValueTask StartNewSessionAsync(HttpContext string sessionId; StreamableHttpServerTransport transport; - bool isStateless = HttpServerTransportOptions.Stateless; - - if (!isStateless) + if (!HttpServerTransportOptions.Stateless) { sessionId = MakeNewSessionId(); #pragma warning disable MCP9006 // Stateful Streamable HTTP options are obsolete but still wired up internally. @@ -467,7 +463,7 @@ private async ValueTask StartNewSessionAsync(HttpContext } else { - // In stateless mode (legacy or draft), each request is independent. Don't set any session ID on the transport. + // In stateless mode, 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 = ""; @@ -488,13 +484,12 @@ private async ValueTask CreateSessionAsync( { var mcpServerServices = applicationServices; var mcpServerOptions = mcpServerOptionsSnapshot.Value; - bool effectivelyStateless = HttpServerTransportOptions.Stateless; - if (effectivelyStateless || HttpServerTransportOptions.ConfigureSessionOptions is not null || configureOptions is not null) + if (HttpServerTransportOptions.Stateless || HttpServerTransportOptions.ConfigureSessionOptions is not null || configureOptions is not null) { mcpServerOptions = mcpServerOptionsFactory.Create(Options.DefaultName); - if (effectivelyStateless) + if (HttpServerTransportOptions.Stateless) { // The session does not outlive the request in stateless mode. mcpServerServices = context.RequestServices; @@ -690,23 +685,25 @@ private static bool ValidateProtocolVersionHeader(HttpContext context, [NotNullW } /// - /// 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 - /// with a supported-versions list that excludes - /// the draft. A dual-era client then falls back to the legacy initialize handshake (SEP-2575). + /// Refuses a 2026-07-28 (or later) request on a stateful (Stateless = false) server. Starting with that + /// revision, Streamable HTTP no longer has sessions (SEP-2567), so it cannot honor the author's opt-in to + /// sessions; we return with a supported-versions list + /// that excludes 2026-07-28 and later. A dual-era client then falls back to the legacy initialize handshake + /// (SEP-2575). /// - private static Task WriteUnsupportedDraftVersionErrorAsync(HttpContext context) + private static Task WriteUnsupportedProtocolVersionErrorAsync(HttpContext context) { + var requestedProtocolVersion = context.Request.Headers[McpProtocolVersionHeaderName].ToString(); 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.", + Message = $"Bad Request: Starting with protocol version '{McpHttpHeaders.July2026ProtocolVersion}', Streamable HTTP does not support sessions and is not supported when the server is configured with sessions enabled (HttpServerTransportOptions.Stateless = false). " + + "Use the initialize handshake with a protocol version that still supports sessions instead.", Data = JsonSerializer.SerializeToNode( new UnsupportedProtocolVersionErrorData { - Supported = s_supportedProtocolVersionsExcludingDraft, - Requested = McpHttpHeaders.DraftProtocolVersion, + Supported = s_sessionSupportingProtocolVersions, + Requested = requestedProtocolVersion, }, GetRequiredJsonTypeInfo()), }; diff --git a/src/ModelContextProtocol.Core/Client/AutoDetectingClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/AutoDetectingClientSessionTransport.cs index 3247dd8a7..015d4048d 100644 --- a/src/ModelContextProtocol.Core/Client/AutoDetectingClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/AutoDetectingClientSessionTransport.cs @@ -76,9 +76,9 @@ private async Task InitializeAsync(JsonRpcMessage message, CancellationToken can } else if (await StreamableHttpClientSessionTransport.TryReadJsonRpcErrorAsync(response, cancellationToken).ConfigureAwait(false) is { } parsedError) { - // A JSON-RPC error envelope in the body means the peer IS a Streamable HTTP server - // — it just rejected our specific request (e.g., -32004 UnsupportedProtocolVersion, - // -32003 MissingRequiredClientCapability, -32001 HeaderMismatch, or any other + // A JSON-RPC error envelope in the body means the peer IS a Streamable HTTP server. + // It just rejected our specific request (e.g., -32022 UnsupportedProtocolVersion, + // -32021 MissingRequiredClientCapability, -32020 HeaderMismatch, or any other // application-level error). Don't fall back to SSE — that would mask the real signal // and surface a misleading "session id required" error from the SSE GET path. // Adopt the Streamable HTTP transport and throw the structured exception so the diff --git a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs index 218cd55d0..bfe81d19b 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs @@ -1174,10 +1174,10 @@ public async ValueTask> CallToolRawAsync( { Name = requestParams.Name, Arguments = requestParams.Arguments, - // The SEP-2663 Tasks extension is draft-only. On a legacy session, send a plain tools/call + // The SEP-2663 Tasks extension requires the 2026-07-28 or later protocol revision. On an older session, send a plain tools/call // (no task capability envelope) so the server returns a direct CallToolResult and never // creates a task. - Meta = IsDraftProtocol() ? GetMetaWithTaskCapability(requestParams.Meta) : requestParams.Meta, + Meta = IsJuly2026OrLaterProtocol() ? GetMetaWithTaskCapability(requestParams.Meta) : requestParams.Meta, }; JsonRpcRequest jsonRpcRequest = new() @@ -1405,19 +1405,18 @@ private static JsonObject GetMetaWithTaskCapability(JsonObject? existingMeta) } /// - /// Throws when the negotiated protocol version is not the draft revision. The SEP-2663 Tasks - /// extension is draft-only, and a task id only ever exists when the session negotiated draft, so - /// invoking tasks/get, tasks/update, or tasks/cancel on a legacy session is a - /// programming error rather than a recoverable protocol condition. + /// Throws when the negotiated protocol version does not support the SEP-2663 Tasks extension. Tasks + /// require the 2026-07-28 or later protocol revision, and a task id only ever exists when the session + /// negotiated such a revision, so invoking tasks/get, tasks/update, or tasks/cancel + /// on an older session is a programming error rather than a recoverable protocol condition. /// private void ThrowIfTasksNotSupported(string operationName) { - if (!IsDraftProtocol()) + if (!IsJuly2026OrLaterProtocol()) { throw new InvalidOperationException( - $"'{operationName}' requires the draft protocol revision ('{DraftProtocolVersion}'). " + - $"The negotiated protocol version is '{NegotiatedProtocolVersion ?? "(none)"}'. " + - "The Tasks extension is only available under the draft revision."); + $"'{operationName}' requires a newer protocol revision that supports tasks. " + + $"The negotiated protocol version is '{NegotiatedProtocolVersion ?? "(none)"}'."); } } } diff --git a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs index eb3ced9a4..eb2fedc85 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs @@ -289,21 +289,21 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) try { - // The draft protocol revision (SEP-2575) is the default: there is no initialize + // The 2026-07-28 revision (SEP-2575) is the default: there is no initialize // handshake. Instead, the client calls server/discover to learn the server's // capabilities and then begins sending normal RPCs that carry protocolVersion / // clientInfo / clientCapabilities in their per-request _meta. A null ProtocolVersion - // prefers the draft revision and automatically falls back to the legacy initialize - // handshake when the server doesn't support it. The legacy branch below runs only - // when the caller explicitly pins a non-draft version (opting out of draft). - if (_options.ProtocolVersion is null || _options.ProtocolVersion == McpSessionHandler.DraftProtocolVersion) + // prefers the 2026-07-28 revision and automatically falls back to the legacy initialize + // handshake when the server doesn't support it. The legacy branch below runs only when + // the caller explicitly pins a version that still supports Streamable HTTP sessions (opting out of the default). + if (_options.ProtocolVersion is null || McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(_options.ProtocolVersion)) { - string draftVersion = McpSessionHandler.DraftProtocolVersion; + string preferredVersion = _options.ProtocolVersion ?? McpHttpHeaders.July2026ProtocolVersion; - // Eagerly set the negotiated version so InjectDraftMetaIfNeeded recognizes us as - // a draft client when SendRequestAsync is invoked for server/discover. - _negotiatedProtocolVersion = draftVersion; - _sessionHandler.NegotiatedProtocolVersion = draftVersion; + // Eagerly set the negotiated version so InjectRequestMetaIfNeeded recognizes us as being + // on the 2026-07-28 revision when SendRequestAsync is invoked for server/discover. + _negotiatedProtocolVersion = preferredVersion; + _sessionHandler.NegotiatedProtocolVersion = preferredVersion; DiscoverResult? discoverResult = null; bool fallbackToLegacy = false; @@ -331,7 +331,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) } catch (UnsupportedProtocolVersionException ex) { - // Spec-recognized modern-server signal: -32004 with data.supported[]. The server is + // Spec-recognized modern-server signal: -32022 with data.supported[]. The server is // modern but doesn't speak our preferred version. Retry with a mutually supported // version from data.supported[] instead of falling back to legacy initialize. fallbackToLegacy = true; @@ -339,13 +339,13 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) } catch (MissingRequiredClientCapabilityException) { - // Spec-recognized modern-server signal: -32003. The server is modern but rejected + // Spec-recognized modern-server signal: -32021. The server is modern but rejected // our capability set. Surface as-is (no fallback): the user must add capabilities. throw; } catch (McpProtocolException ex) when (ex.ErrorCode == McpErrorCode.HeaderMismatch) { - // Spec-recognized modern-server signal: -32001. The server is modern but rejected + // Spec-recognized modern-server signal: -32020. The server is modern but rejected // our request envelope (e.g., the MCP-Protocol-Version HTTP header didn't match // the body _meta.io.modelcontextprotocol/protocolVersion). Surface as-is (no // fallback): falling back to legacy initialize wouldn't fix a malformed envelope. @@ -353,14 +353,14 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) } catch (McpProtocolException) { - // Per spec PR #2844, the fallback MUST NOT be keyed to a single error code — - // any non-modern JSON-RPC error from the probe indicates a legacy server. + // Per spec PR #2844, the fallback MUST NOT be keyed to a single error code. + // Any non-modern JSON-RPC error from the probe indicates a legacy server. // Common causes include MethodNotFound from a server that has no // server/discover handler, InvalidParams from a server confused by the // SEP-2575 _meta envelope, ParseError from a server that can't handle our // payload shape, or any other transport-defined error. The three modern-server - // signals (-32004 UnsupportedProtocolVersion, -32003 - // MissingRequiredClientCapability, -32001 HeaderMismatch) are caught above and + // signals (-32022 UnsupportedProtocolVersion, -32021 + // MissingRequiredClientCapability, -32020 HeaderMismatch) are caught above and // never reach here. fallbackToLegacy = true; } @@ -371,10 +371,10 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) fallbackToLegacy = true; } - if (discoverResult is not null && !discoverResult.SupportedVersions.Contains(draftVersion)) + if (discoverResult is not null && !discoverResult.SupportedVersions.Contains(preferredVersion)) { // Server is reachable and supports server/discover, but doesn't support the - // experimental version. Fall back to legacy initialize with the highest + // 2026-07-28 version. Fall back to legacy initialize with the highest // mutually-supported version from supportedVersions[]. fallbackToLegacy = true; serverSupportedVersions = discoverResult.SupportedVersions; @@ -390,15 +390,17 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) .Where(McpSessionHandler.SupportedProtocolVersions.Contains) .OrderByDescending(v => v, StringComparer.Ordinal) .FirstOrDefault() - ?? McpSessionHandler.LatestProtocolVersion; + ?? McpHttpHeaders.November2025ProtocolVersion; - // Honor MinProtocolVersion: refuse to fall back below the configured minimum. - // String.Compare is the spec's prescribed ordering for ISO-8601 date-based versions. - if (_options.MinProtocolVersion is { } minVersion && - StringComparer.Ordinal.Compare(fallbackVersion, minVersion) < 0) + // A non-null ProtocolVersion is also the minimum: refuse to fall back below the + // explicitly requested version. String.Compare is the spec's prescribed ordering + // for ISO-8601 date-based versions. + if (_options.ProtocolVersion is { } pinnedVersion && + StringComparer.Ordinal.Compare(fallbackVersion, pinnedVersion) < 0) { throw new McpException( - $"Server does not support the configured minimum protocol version '{minVersion}'. " + + $"The server does not support the requested protocol version '{pinnedVersion}'. " + + "Leave McpClientOptions.ProtocolVersion unset to allow automatic fallback to an older version. " + (serverSupportedVersions is null ? "The server appears to be a legacy server that requires the deprecated initialize handshake." : $"Server-supported versions: {string.Join(", ", serverSupportedVersions)}.")); @@ -423,9 +425,9 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) else { // Legacy initialize handshake. Reached only when the caller explicitly pinned a - // non-draft ProtocolVersion (opting out of the draft default), so + // ProtocolVersion that still supports Streamable HTTP sessions (opting out of the default), so // _options.ProtocolVersion is non-null here. - string requestProtocol = _options.ProtocolVersion ?? McpSessionHandler.LatestProtocolVersion; + string requestProtocol = _options.ProtocolVersion ?? McpHttpHeaders.November2025ProtocolVersion; await PerformLegacyInitializeAsync(requestProtocol, initializationCts.Token).ConfigureAwait(false); } } @@ -475,13 +477,13 @@ private async Task PerformLegacyInitializeAsync(string requestProtocol, Cancella _serverInfo = initializeResponse.ServerInfo; _serverInstructions = initializeResponse.Instructions; - // When the user explicitly pinned a legacy (non-draft) protocol version, the server MUST - // respect it. When the user pinned the draft version but we fell back (e.g., legacy server - // rejected server/discover), or when no version was pinned, accept any supported response. - // This is the spec-mandated behavior: a draft client must be able to downgrade to whatever - // legacy version the server advertises. + // When the user explicitly pinned a version that supports Streamable HTTP sessions, the server MUST respect it. + // When the user pinned the 2026-07-28 version but we fell back (e.g., legacy server rejected + // server/discover), or when no version was pinned, accept any supported response. This is the + // spec-mandated behavior: a 2026-07-28 client must be able to downgrade to whatever + // version the server advertises. bool isResponseProtocolValid; - if (_options.ProtocolVersion is { } optionsProtocol && optionsProtocol != McpSessionHandler.DraftProtocolVersion) + if (_options.ProtocolVersion is { } optionsProtocol && !McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(optionsProtocol)) { isResponseProtocolValid = optionsProtocol == initializeResponse.ProtocolVersion; } @@ -495,15 +497,6 @@ private async Task PerformLegacyInitializeAsync(string requestProtocol, Cancella throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}"); } - // If the user set a MinProtocolVersion, also enforce it against the negotiated response - // (the server could have downgraded further than the version we asked for). - if (_options.MinProtocolVersion is { } minVersion && - StringComparer.Ordinal.Compare(initializeResponse.ProtocolVersion, minVersion) < 0) - { - throw new McpException( - $"Server negotiated protocol version '{initializeResponse.ProtocolVersion}' is below the configured minimum '{minVersion}'."); - } - _negotiatedProtocolVersion = initializeResponse.ProtocolVersion; _sessionHandler.NegotiatedProtocolVersion = _negotiatedProtocolVersion; @@ -531,7 +524,7 @@ internal void ResumeSession(ResumeClientSessionOptions resumeOptions) _serverInstructions = resumeOptions.ServerInstructions; _negotiatedProtocolVersion = resumeOptions.NegotiatedProtocolVersion ?? _options.ProtocolVersion - ?? McpSessionHandler.LatestProtocolVersion; + ?? McpHttpHeaders.November2025ProtocolVersion; // Update session handler with the negotiated protocol version for telemetry _sessionHandler.NegotiatedProtocolVersion = _negotiatedProtocolVersion; @@ -626,7 +619,7 @@ request.Params is System.Text.Json.Nodes.JsonObject paramsObjForHeaders && const int maxRetries = 10; - InjectDraftMetaIfNeeded(request); + InjectRequestMetaIfNeeded(request); for (int attempt = 0; attempt <= maxRetries; attempt++) { @@ -665,7 +658,7 @@ request.Params is System.Text.Json.Nodes.JsonObject paramsObjForHeaders && } request = new JsonRpcRequest { Method = request.Method, Params = paramsObj, Context = request.Context }; - InjectDraftMetaIfNeeded(request); + InjectRequestMetaIfNeeded(request); } else if (inputRequiredResult.RequestState is not null) { @@ -675,7 +668,7 @@ request.Params is System.Text.Json.Nodes.JsonObject paramsObjForHeaders && paramsObj.Remove("inputResponses"); request = new JsonRpcRequest { Method = request.Method, Params = paramsObj, Context = request.Context }; - InjectDraftMetaIfNeeded(request); + InjectRequestMetaIfNeeded(request); } else { @@ -695,25 +688,25 @@ request.Params is System.Text.Json.Nodes.JsonObject paramsObjForHeaders && } /// - /// Injects the draft-protocol per-request _meta fields (protocol version, client info, - /// client capabilities) into the request when this client is using the draft protocol revision - /// (SEP-2575). No-op for legacy clients. + /// Injects the 2026-07-28 protocol's per-request _meta fields (protocol version, client info, + /// client capabilities) into the request when this client negotiated the 2026-07-28 or later revision + /// (SEP-2575). No-op on a legacy session. /// - private void InjectDraftMetaIfNeeded(JsonRpcRequest request) + private void InjectRequestMetaIfNeeded(JsonRpcRequest request) { - if (!IsDraftProtocol()) + if (!IsJuly2026OrLaterProtocol()) { return; } - // Initialize is never sent under the draft revision, but guard defensively in case a caller + // Initialize is never sent on a 2026-07-28 session, but guard defensively in case a caller // routes it through here (e.g., during back-compat fallback negotiation). if (request.Method == RequestMethods.Initialize) { return; } - McpSessionHandler.InjectDraftMeta( + McpSessionHandler.InjectRequestMeta( request, _negotiatedProtocolVersion!, _options.ClientInfo ?? DefaultImplementation, @@ -755,7 +748,7 @@ public override async ValueTask DisposeAsync() /// Logs a warning if the session negotiated MRTR but the server sent a legacy JSON-RPC request. private void WarnIfLegacyRequestOnMrtrSession(string method) { - if (IsDraftProtocol()) + if (IsJuly2026OrLaterProtocol()) { LogLegacyRequestOnMrtrSession(_endpointName, method); } @@ -764,7 +757,7 @@ private void WarnIfLegacyRequestOnMrtrSession(string method) /// Logs a warning if the session did not negotiate MRTR but the server sent an InputRequiredResult. private void WarnIfInputRequiredResultOnNonMrtrSession(string method) { - if (!IsDraftProtocol()) + if (!IsJuly2026OrLaterProtocol()) { LogInputRequiredResultOnNonMrtrSession(_endpointName, method, _negotiatedProtocolVersion); } diff --git a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs index 4e9a7acce..aaf3cefa6 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs @@ -52,52 +52,23 @@ public sealed class McpClientOptions /// /// /// - /// When non-, this version is requested from the server. Setting it to a - /// legacy (non-draft) version such as 2025-11-25 opts out of the draft revision and forces - /// the initialize handshake; the handshake then fails if the server's negotiated version - /// does not match. + /// When (the default), the client prefers the latest revision (2026-07-28), + /// which removed the initialize handshake and Streamable HTTP sessions. It probes with + /// server/discover and automatically falls back to the legacy initialize handshake, + /// downgrading to any version the server advertises, when the server does not support that revision. /// /// - /// When (the default), the client prefers the draft revision - /// (): it probes with server/discover and - /// automatically falls back to a legacy initialize handshake, downgrading to any version - /// the server advertises, when the server does not support the draft revision. + /// When non-, this value is both the requested version and the minimum the client + /// will accept: the client requests exactly this version and refuses to downgrade below it, throwing an + /// instead of falling back. Setting it to 2026-07-28 therefore disables + /// the automatic legacy-server fallback, and setting it to a version that still supports Streamable HTTP + /// sessions, such as 2025-11-25, forces the initialize handshake and fails if the server + /// negotiates a different version. To try more than one version, leave this unset for automatic fallback + /// or retry the connection with a different value. /// /// public string? ProtocolVersion { get; set; } - /// - /// Gets or sets the minimum protocol version the client will accept during version negotiation. - /// - /// - /// - /// When negotiating with a server that advertises multiple supported versions, or when falling back - /// to a legacy server, the client will refuse any version older than this minimum and surface an - /// instead. - /// - /// - /// This is useful when the client requires features (such as the draft revision's removal of the - /// initialize handshake or Mcp-Session-Id) that are not available in older protocol - /// revisions. Because the client already prefers the draft revision by default, setting this to - /// disables the automatic legacy-server fallback - /// that otherwise switches to the initialize handshake. - /// - /// - /// If (the default), the client falls back to any version the server - /// advertises, including legacy versions such as 2025-11-25. - /// - /// - /// - /// // The draft revision is already the default; pin the minimum to refuse the legacy fallback. - /// var clientOptions = new McpClientOptions - /// { - /// MinProtocolVersion = McpSession.DraftProtocolVersion, - /// }; - /// - /// - /// - public string? MinProtocolVersion { get; set; } - /// /// Gets or sets a timeout for the client-server initialization handshake sequence. /// @@ -128,10 +99,10 @@ public sealed class McpClientOptions /// /// /// - /// This timeout only has an effect when the client prefers the draft protocol revision — that is, - /// when is (the default) or - /// . In that mode the client first probes the server - /// with a server/discover request. A legacy server that predates the draft revision may + /// This timeout only has an effect when the client prefers the 2026-07-28 protocol revision, that is, + /// when is (the default) or 2026-07-28. + /// In that mode the client first probes the server with a + /// server/discover request. A legacy server that predates the 2026-07-28 revision may /// silently drop the unknown method, so the probe is bounded by this timeout; when it elapses the /// client concludes the server is legacy and falls back to the initialize handshake on the /// same connection. When the caller pins a legacy , no probe is issued @@ -140,7 +111,7 @@ public sealed class McpClientOptions /// /// The default is intentionally short so that dual-era clients fall back quickly against legacy /// servers. Increase it for high-latency environments (for example, cold-start serverless peers or - /// satellite links) where a short probe could trigger the legacy fallback before a draft-capable + /// satellite links) where a short probe could trigger the legacy fallback before a server on the new revision /// server has had a chance to respond. The probe is always also bounded by /// , which governs the overall connect budget: if this value is /// greater than or equal to , the probe is effectively bounded by diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index c02d5eaea..26ffdce65 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -67,19 +67,19 @@ public override async Task SendMessageAsync(JsonRpcMessage message, Cancellation // Per spec PR #2844 (HTTP backwards compatibility), a 400 Bad Request that carries a // JSON-RPC error envelope means the peer is signalling something application-level about // our request. Surface ANY JSON-RPC error on a 400 as McpProtocolException so the - // connect-time logic can react — for example, the three modern draft-protocol error codes - // (-32004 UnsupportedProtocolVersion, -32003 MissingRequiredClientCapability, - // -32001 HeaderMismatch) lead to typed exceptions, while other codes (e.g. -32600 from - // legacy servers that don't understand the draft _meta envelope) become generic + // connect-time logic can react. For example, the three modern protocol error codes + // (-32022 UnsupportedProtocolVersion, -32021 MissingRequiredClientCapability, + // -32020 HeaderMismatch) lead to typed exceptions, while other codes (e.g. -32600 from + // legacy servers that don't understand the SEP-2575 _meta envelope) become generic // McpProtocolException instances and trigger the fallback-to-legacy-initialize path. // Other status codes (401 auth, 403 forbidden, 404 session-not-found, 5xx server) continue // to surface as HttpRequestException to preserve back-compat with transport-layer behaviors. - // The three modern draft-protocol error codes are also surfaced for non-400 status codes - // for robustness — servers occasionally emit them with 4xx codes other than 400. + // The three modern protocol error codes are also surfaced for non-400 status codes + // for robustness. Servers occasionally emit them with 4xx codes other than 400. if (!response.IsSuccessStatusCode && await TryReadJsonRpcErrorAsync(response, cancellationToken).ConfigureAwait(false) is { } parsedError && (response.StatusCode == HttpStatusCode.BadRequest || - IsModernDraftErrorCode((McpErrorCode)parsedError.Error.Code))) + IsModernProtocolErrorCode((McpErrorCode)parsedError.Error.Code))) { throw McpSessionHandler.CreateRemoteProtocolExceptionFromError(parsedError); } @@ -87,7 +87,7 @@ await TryReadJsonRpcErrorAsync(response, cancellationToken).ConfigureAwait(false await response.EnsureSuccessStatusCodeWithResponseBodyAsync(cancellationToken).ConfigureAwait(false); } - private static bool IsModernDraftErrorCode(McpErrorCode code) => + private static bool IsModernProtocolErrorCode(McpErrorCode code) => code is McpErrorCode.UnsupportedProtocolVersion or McpErrorCode.MissingRequiredClientCapability or McpErrorCode.HeaderMismatch; @@ -144,9 +144,9 @@ internal async Task SendHttpRequestAsync(JsonRpcMessage mes LogTransportSendingMessageSensitive(message); - // Under the draft protocol revision (SEP-2575), every request carries its protocol version in + // Under the 2026-07-28 or later protocol revision (SEP-2575), every request carries its protocol version in // _meta/io.modelcontextprotocol/protocolVersion (and the matching MCP-Protocol-Version HTTP - // header). Pick the value off the message so the first draft request (server/discover) can + // header). Pick the value off the message so the first request (server/discover) can // include the header even before we've recorded a negotiated version from an initialize reply. var protocolVersionForRequest = ExtractProtocolVersionFromMeta(message) ?? _negotiatedProtocolVersion; @@ -229,7 +229,7 @@ internal async Task SendHttpRequestAsync(JsonRpcMessage mes } else if (rpcRequest.Method == RequestMethods.ServerDiscover && rpcResponseOrError is JsonRpcResponse) { - // Under the draft protocol revision (SEP-2575), server/discover replaces the initialize + // Under the 2026-07-28 or later protocol revision (SEP-2575), server/discover replaces the initialize // handshake. The transport caches the protocol version from the outgoing request's _meta // so subsequent requests carry the matching MCP-Protocol-Version header without re-parsing. _negotiatedProtocolVersion ??= ExtractProtocolVersionFromMeta(message); @@ -240,7 +240,7 @@ internal async Task SendHttpRequestAsync(JsonRpcMessage mes /// /// Reads the protocol version from a request's _meta/io.modelcontextprotocol/protocolVersion field, - /// introduced by the draft protocol revision (SEP-2575). Returns for messages that + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). Returns for messages that /// don't have that field. /// private static string? ExtractProtocolVersionFromMeta(JsonRpcMessage message) diff --git a/src/ModelContextProtocol.Core/McpErrorCode.cs b/src/ModelContextProtocol.Core/McpErrorCode.cs index 74f9110bb..65c662a1a 100644 --- a/src/ModelContextProtocol.Core/McpErrorCode.cs +++ b/src/ModelContextProtocol.Core/McpErrorCode.cs @@ -23,7 +23,7 @@ public enum McpErrorCode /// This error code is in the JSON-RPC implementation-defined server error range (-32000 to -32099). /// /// - HeaderMismatch = -32001, + HeaderMismatch = -32020, /// /// Indicates that the requested resource could not be found. @@ -49,24 +49,24 @@ public enum McpErrorCode /// /// /// - /// Introduced by the draft protocol revision (SEP-2575). The error data MUST include a + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). The error data MUST include a /// requiredCapabilities object describing the capabilities the server requires from the client /// to process the request. For HTTP, the response status code is 400 Bad Request. /// /// - MissingRequiredClientCapability = -32003, + MissingRequiredClientCapability = -32021, /// /// Indicates that the request's declared protocol version is not supported by the server. /// /// /// - /// Introduced by the draft protocol revision (SEP-2575). The error data MUST include a + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). The error data MUST include a /// supported array of protocol version strings the server supports and the original /// requested protocol version. For HTTP, the response status code is 400 Bad Request. /// /// - UnsupportedProtocolVersion = -32004, + UnsupportedProtocolVersion = -32022, /// /// Indicates that URL-mode elicitation is required to complete the requested operation. diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs index 32a68104b..da1b898dd 100644 --- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs +++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs @@ -89,7 +89,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element) // matching JSON Schema 2020-12: a schema may be either a JSON object (the usual form // with keywords like "type", "properties", etc.) or a boolean (`true` matches anything, // `false` matches nothing). Stricter keyword-level validation is intentionally not - // performed. Pre-2026-06-30 clients still receive the legacy wrapped wire shape — that + // performed. Pre-2026-07-28 clients still receive the legacy wrapped wire shape — that // wiring lives in AIFunctionMcpServerTool.CreateStructuredResponse and McpServerImpl's // listToolsHandler. internal static bool IsValidToolOutputSchema(JsonElement element) => diff --git a/src/ModelContextProtocol.Core/McpSession.cs b/src/ModelContextProtocol.Core/McpSession.cs index 4804e1ad3..e1bd0844b 100644 --- a/src/ModelContextProtocol.Core/McpSession.cs +++ b/src/ModelContextProtocol.Core/McpSession.cs @@ -28,27 +28,6 @@ namespace ModelContextProtocol; /// public abstract partial class McpSession : IAsyncDisposable { - /// The latest stable protocol revision this SDK supports. - /// - /// Set or - /// to this value to explicitly pin to the current stable revision instead of accepting whatever - /// the runtime negotiates. - /// - public const string LatestProtocolVersion = McpSessionHandler.LatestProtocolVersion; - - /// The in-progress draft protocol revision this SDK supports. - /// - /// The draft revision removes the initialize handshake (SEP-2575) and the - /// Mcp-Session-Id header (SEP-2567), so it is sessionless on the wire and over HTTP is only - /// served when the server is stateless. A stateful (HttpServerTransportOptions.Stateless = false) - /// server refuses a sessionless draft request so that a dual-era client downgrades to the legacy - /// initialize flow. Clients prefer this revision by default and automatically fall back to the - /// legacy flow when the server does not support it; pin - /// to a legacy version to opt out, or set to this - /// value to keep the draft preference while refusing the legacy fallback. - /// - public const string DraftProtocolVersion = McpSessionHandler.DraftProtocolVersion; - /// Gets an identifier associated with the current MCP session. /// /// Typically populated in transports supporting multiple sessions, such as Streamable HTTP or SSE. @@ -67,15 +46,16 @@ public abstract partial class McpSession : IAsyncDisposable public abstract string? NegotiatedProtocolVersion { get; } /// - /// Gets a value indicating whether the negotiated protocol version is the draft revision - /// (, which carries SEP-2575 + SEP-2567 + MRTR). + /// Gets a value indicating whether the negotiated protocol version is 2026-07-28 or later: the + /// revision that removed the initialize handshake (SEP-2575) and Mcp-Session-Id (SEP-2567) + /// and enabled MRTR (SEP-2322). /// /// /// Returns when no version has been negotiated yet. This is the shared - /// definition of "is this peer speaking the draft revision" used by both the client and server. + /// definition of "is this peer speaking the 2026-07-28 or later revision" used by both the client and server. /// - internal bool IsDraftProtocol() => - NegotiatedProtocolVersion == DraftProtocolVersion; + internal bool IsJuly2026OrLaterProtocol() => + McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(NegotiatedProtocolVersion); /// /// Sends a JSON-RPC request to the connected session and waits for a response. diff --git a/src/ModelContextProtocol.Core/McpSessionHandler.cs b/src/ModelContextProtocol.Core/McpSessionHandler.cs index 1a33fe670..a0122c9a4 100644 --- a/src/ModelContextProtocol.Core/McpSessionHandler.cs +++ b/src/ModelContextProtocol.Core/McpSessionHandler.cs @@ -28,22 +28,9 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable private static readonly Histogram s_serverOperationDuration = Diagnostics.CreateDurationHistogram( "mcp.server.operation.duration", "MCP request or notification duration as observed on the receiver from the time it was received until the result or ack is sent."); - /// The latest version of the protocol supported by this implementation. - internal const string LatestProtocolVersion = "2025-11-25"; - /// - /// The draft protocol version (SEP-2575 + SEP-2567) that removes the initialize handshake - /// and Mcp-Session-Id and enables MRTR (Multi Round-Trip Requests) per SEP-2322. - /// Clients prefer this revision by default and fall back to the legacy initialize handshake - /// when the server does not support it; pin to a - /// legacy version to opt out. Servers remain reactive: with - /// left they honor whichever - /// supported revision each peer requests, so a single server serves both draft and legacy clients. - /// - internal const string DraftProtocolVersion = "2026-07-28"; - - /// - /// All protocol versions supported by this implementation. + /// All protocol versions supported by this implementation. The version constants live on + /// so the shared source file is the single source of truth. /// Keep in sync with s_supportedProtocolVersions in StreamableHttpHandler. /// internal static readonly string[] SupportedProtocolVersions = @@ -51,8 +38,8 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable "2024-11-05", "2025-03-26", "2025-06-18", - LatestProtocolVersion, - DraftProtocolVersion, + McpHttpHeaders.November2025ProtocolVersion, + McpHttpHeaders.July2026ProtocolVersion, ]; /// @@ -66,7 +53,7 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable /// internal static bool SupportsPrimingEvent(string? protocolVersion) { - const string MinResumabilityProtocolVersion = "2025-11-25"; + const string MinResumabilityProtocolVersion = McpHttpHeaders.November2025ProtocolVersion; if (protocolVersion is null) { @@ -82,22 +69,19 @@ internal static bool SupportsPrimingEvent(string? protocolVersion) /// /// The negotiated protocol version, or null if /// negotiation has not completed. - /// true if the version is "2026-06-30" or later (including the - /// in-flight "DRAFT-2026-06-v1", since 'D' > '2' ordinally); false - /// otherwise. A false return signals that the wire emission boundary must apply - /// the {"result": <value>} envelope expected by clients on protocol versions - /// that pre-date SEP-2106's widening of outputSchema to any JSON Schema 2020-12 - /// document. + /// true if the version is the 2026-07-28 revision or later, which is where + /// SEP-2106 widened outputSchema to any JSON Schema 2020-12 document; false otherwise. + /// A false return signals that the wire emission boundary must apply the + /// {"result": <value>} envelope expected by clients on protocol versions that pre-date + /// SEP-2106. internal static bool SupportsNaturalOutputSchemas(string? protocolVersion) { - const string MinNaturalOutputSchemasProtocolVersion = "2026-06-30"; - if (protocolVersion is null) { return false; } - return string.Compare(protocolVersion, MinNaturalOutputSchemasProtocolVersion, StringComparison.Ordinal) >= 0; + return string.Compare(protocolVersion, McpHttpHeaders.July2026ProtocolVersion, StringComparison.Ordinal) >= 0; } private readonly bool _isServer; @@ -169,17 +153,16 @@ public McpSessionHandler( _outgoingMessageFilter = outgoingMessageFilter ?? (next => next); _logger = logger; - // ping was removed in the draft protocol revision (SEP-2575). Under draft, return - // MethodNotFound; under legacy, the per-spec behavior is to always answer with PingResult. - // Liveness on draft sessions belongs to transport- and request-level timeouts, not a - // dedicated MCP RPC. + // ping was removed in the 2026-07-28 protocol revision (SEP-2575). On the 2026-07-28 or later version, + // return MethodNotFound; on an older version, the per-spec behavior is to always answer + // with PingResult. Liveness on those requests belongs to transport- and request-level + // timeouts, not a dedicated MCP RPC. _requestHandlers.Set( RequestMethods.Ping, (request, jsonRpcRequest, cancellationToken) => { string? perRequestVersion = jsonRpcRequest?.Context?.ProtocolVersion ?? NegotiatedProtocolVersion; - if (perRequestVersion is not null && - StringComparer.Ordinal.Compare(perRequestVersion, DraftProtocolVersion) >= 0) + if (McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(perRequestVersion)) { throw new McpProtocolException( $"Method '{RequestMethods.Ping}' is not available on protocol version '{perRequestVersion}'.", @@ -426,7 +409,7 @@ private static async Task GetCompletionDetailsAsync(Tas private async Task HandleMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken) { - // Project the draft-protocol per-request _meta fields onto the message context before any + // Project the 2026-07-28 protocol's per-request _meta fields onto the message context before any // filters run so they (and downstream handlers) can read client info / capabilities / // protocol version / log level without re-parsing. if (_isServer && message is JsonRpcRequest incomingRequest) @@ -570,7 +553,7 @@ await SendMessageAsync(new JsonRpcResponse } /// - /// Reads the draft-protocol per-request _meta fields off the request and projects them onto + /// Reads the 2026-07-28 protocol's per-request _meta fields off the request and projects them onto /// so they're available without re-parsing throughout the pipeline. /// /// @@ -599,8 +582,8 @@ internal static void PopulateContextFromMeta(JsonRpcRequest request) { // If a transport-level header (e.g., the Streamable HTTP MCP-Protocol-Version header) already // populated this, validate the body _meta matches per SEP-2575. A disagreement is reported with - // -32001 HeaderMismatch (the same code used for the Mcp-Method/Mcp-Name header-vs-body checks), - // which conformant draft clients recognize as a modern-server signal and surface as-is rather + // -32020 HeaderMismatch (the same code used for the Mcp-Method/Mcp-Name header-vs-body checks), + // which conformant 2026-07-28 clients recognize as a modern-server signal and surface as-is rather // than mistaking it for a legacy server and falling back to the initialize handshake. if (context.ProtocolVersion is { } existing && !string.Equals(existing, protocolVersionValue, StringComparison.Ordinal)) { @@ -629,16 +612,16 @@ internal static void PopulateContextFromMeta(JsonRpcRequest request) } /// - /// Injects the draft-protocol per-request _meta fields into an outgoing request. + /// Injects the 2026-07-28 protocol's per-request _meta fields into an outgoing request. /// Protocol version and client info overwrite any existing values; client capabilities are merged /// so per-request capability opt-ins already present in the envelope are preserved. /// /// - /// Used by in draft mode to carry protocol version, client info, and - /// client capabilities on every outgoing request (replacing what the legacy initialize handshake - /// previously negotiated once). + /// Used by on a 2026-07-28 or later session to carry protocol version, client + /// info, and client capabilities on every outgoing request (replacing what the legacy + /// initialize handshake previously negotiated once). /// - internal static void InjectDraftMeta( + internal static void InjectRequestMeta( JsonRpcRequest request, string protocolVersion, Implementation clientInfo, diff --git a/src/ModelContextProtocol.Core/MissingRequiredClientCapabilityException.cs b/src/ModelContextProtocol.Core/MissingRequiredClientCapabilityException.cs index aca6d4902..9beaa7a51 100644 --- a/src/ModelContextProtocol.Core/MissingRequiredClientCapabilityException.cs +++ b/src/ModelContextProtocol.Core/MissingRequiredClientCapabilityException.cs @@ -10,9 +10,9 @@ namespace ModelContextProtocol; /// in the request's per-request _meta/io.modelcontextprotocol/clientCapabilities field. /// /// -/// Introduced by the draft protocol revision (SEP-2575). Servers throw this exception when a handler cannot +/// Introduced by the 2026-07-28 protocol revision (SEP-2575). Servers throw this exception when a handler cannot /// proceed because the client did not declare a required capability for the request. The exception is converted -/// to a JSON-RPC error response with code (-32003) +/// to a JSON-RPC error response with code (-32021) /// and a payload. /// public sealed class MissingRequiredClientCapabilityException : McpProtocolException diff --git a/src/ModelContextProtocol.Core/Protocol/DiscoverRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/DiscoverRequestParams.cs index e9a343f46..198fb4311 100644 --- a/src/ModelContextProtocol.Core/Protocol/DiscoverRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/DiscoverRequestParams.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Protocol; /// The discover RPC takes no payload of its own. Per-request metadata /// (protocol version, client info, client capabilities) flows through the /// inherited property under the -/// io.modelcontextprotocol/* keys defined by the draft protocol revision (SEP-2575). +/// io.modelcontextprotocol/* keys defined by the 2026-07-28 protocol revision (SEP-2575). /// /// public sealed class DiscoverRequestParams : RequestParams diff --git a/src/ModelContextProtocol.Core/Protocol/DiscoverResult.cs b/src/ModelContextProtocol.Core/Protocol/DiscoverResult.cs index 7a4e75453..5ec348c06 100644 --- a/src/ModelContextProtocol.Core/Protocol/DiscoverResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/DiscoverResult.cs @@ -7,7 +7,7 @@ namespace ModelContextProtocol.Protocol; /// /// /// -/// Introduced by the draft protocol revision (SEP-2575) as the canonical way for a client +/// Introduced by the 2026-07-28 protocol revision (SEP-2575) as the canonical way for a client /// to learn what a server supports without performing the legacy initialize handshake. /// /// @@ -47,8 +47,9 @@ public sealed class DiscoverResult : Result, ICacheableResult /// /// /// Spec PR #2855 makes ttlMs a required field on . The - /// server emits a safe default (, i.e. immediately stale) on - /// draft sessions when the application has not set an explicit value, preserving today's + /// server emits a safe default (, i.e. immediately stale) for the + /// 2026-07-28 and later protocol revisions when the application has not set an explicit value, + /// preserving today's /// "do not cache" behavior while satisfying the wire requirement. /// [JsonPropertyName("ttlMs")] @@ -58,7 +59,8 @@ public sealed class DiscoverResult : Result, ICacheableResult /// /// /// Spec PR #2855 makes cacheScope a required field on . The - /// server emits a safe default () on draft sessions + /// server emits a safe default () for the 2026-07-28 and + /// later protocol revisions /// when the application has not set an explicit value. /// [JsonPropertyName("cacheScope")] diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageContext.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageContext.cs index b804c288a..28d7774cb 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageContext.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageContext.cs @@ -91,7 +91,7 @@ public sealed class JsonRpcMessageContext /// _meta/io.modelcontextprotocol/clientInfo field. /// /// - /// Introduced by the draft protocol revision (SEP-2575). When the request was made under the draft revision, + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). When the request was made under the 2026-07-28 or later revision, /// the server uses this in lieu of the value previously captured during the initialize handshake. /// public Implementation? ClientInfo { get; set; } @@ -101,7 +101,7 @@ public sealed class JsonRpcMessageContext /// _meta/io.modelcontextprotocol/clientCapabilities field. /// /// - /// Introduced by the draft protocol revision (SEP-2575). Per the spec, the server MUST NOT infer client + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). Per the spec, the server MUST NOT infer client /// capabilities from previous requests; the authoritative value is the one declared on each request. /// public ClientCapabilities? ClientCapabilities { get; set; } @@ -111,7 +111,7 @@ public sealed class JsonRpcMessageContext /// _meta/io.modelcontextprotocol/logLevel field. /// /// - /// Introduced by the draft protocol revision (SEP-2575). Replaces the legacy + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). Replaces the legacy /// RPC. When absent, the server MUST NOT emit log notifications /// for the request. /// diff --git a/src/ModelContextProtocol.Core/Protocol/MetaKeys.cs b/src/ModelContextProtocol.Core/Protocol/MetaKeys.cs index 495f71553..4d9139953 100644 --- a/src/ModelContextProtocol.Core/Protocol/MetaKeys.cs +++ b/src/ModelContextProtocol.Core/Protocol/MetaKeys.cs @@ -9,9 +9,9 @@ public static class MetaKeys /// The metadata key used to carry the MCP protocol version in a request's _meta field. /// /// - /// Introduced by the draft protocol revision (SEP-2575). For HTTP transports, the value MUST - /// match the MCP-Protocol-Version header. Servers reject mismatched versions with - /// . + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). For HTTP transports, the value MUST + /// match the MCP-Protocol-Version header. Servers reject a header/body mismatch with + /// . /// public const string ProtocolVersion = "io.modelcontextprotocol/protocolVersion"; @@ -19,7 +19,7 @@ public static class MetaKeys /// The metadata key used to identify the client software in a request's _meta field. /// /// - /// Introduced by the draft protocol revision (SEP-2575). Carries an + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). Carries an /// describing the client; replaces the clientInfo previously sent only with initialize. /// public const string ClientInfo = "io.modelcontextprotocol/clientInfo"; @@ -28,7 +28,7 @@ public static class MetaKeys /// The metadata key used to declare client capabilities in a request's _meta field. /// /// - /// Introduced by the draft protocol revision (SEP-2575). Carries a + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). Carries a /// describing what optional features the client supports for this specific request. Servers MUST NOT /// infer capabilities from previous requests. /// @@ -38,7 +38,7 @@ public static class MetaKeys /// The metadata key used to specify the desired log level for a request's resulting log notifications. /// /// - /// Introduced by the draft protocol revision (SEP-2575). Carries a . + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). Carries a . /// Replaces the legacy RPC. When absent, the server /// MUST NOT send log notifications for the request. /// @@ -49,7 +49,7 @@ public static class MetaKeys /// subscription. /// /// - /// Introduced by the draft protocol revision (SEP-2575). Allows clients to demultiplex notifications + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). Allows clients to demultiplex notifications /// belonging to different subscriptions on a shared channel (especially STDIO). /// public const string SubscriptionId = "io.modelcontextprotocol/subscriptionId"; diff --git a/src/ModelContextProtocol.Core/Protocol/MissingRequiredClientCapabilityErrorData.cs b/src/ModelContextProtocol.Core/Protocol/MissingRequiredClientCapabilityErrorData.cs index 8370aaf9a..8e5991f26 100644 --- a/src/ModelContextProtocol.Core/Protocol/MissingRequiredClientCapabilityErrorData.cs +++ b/src/ModelContextProtocol.Core/Protocol/MissingRequiredClientCapabilityErrorData.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol; /// Represents the payload for the JSON-RPC error. /// /// -/// Introduced by the draft protocol revision (SEP-2575). When a server cannot fulfill a request because +/// Introduced by the 2026-07-28 protocol revision (SEP-2575). When a server cannot fulfill a request because /// the client did not declare a required capability in its per-request /// _meta/io.modelcontextprotocol/clientCapabilities field, it MUST return this error so clients /// know which capabilities to advertise on a retry. diff --git a/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs b/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs index 39fe0780f..0bd3348f8 100644 --- a/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs +++ b/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs @@ -159,7 +159,7 @@ public static class NotificationMethods /// response stream to indicate which notification types the server agreed to deliver. /// /// - /// Introduced by the draft protocol revision (SEP-2575). The notification's params mirror the shape + /// Introduced by the 2026-07-28 protocol revision (SEP-2575). The notification's params mirror the shape /// of the requested notifications and include only the entries the server actually supports. /// public const string SubscriptionsAcknowledgedNotification = "notifications/subscriptions/acknowledged"; diff --git a/src/ModelContextProtocol.Core/Protocol/RequestMethods.cs b/src/ModelContextProtocol.Core/Protocol/RequestMethods.cs index 028a5629d..a2884efba 100644 --- a/src/ModelContextProtocol.Core/Protocol/RequestMethods.cs +++ b/src/ModelContextProtocol.Core/Protocol/RequestMethods.cs @@ -158,7 +158,7 @@ public static class RequestMethods /// /// /// - /// This RPC is introduced in the draft protocol revision (SEP-2575) as the canonical way for a client + /// This RPC is introduced in the 2026-07-28 protocol revision (SEP-2575) as the canonical way for a client /// to learn what a server supports without performing the legacy initialize handshake. /// /// @@ -166,7 +166,7 @@ public static class RequestMethods /// information, and optional usage instructions. /// /// - /// Servers SHOULD implement this method. Legacy clients MAY ignore it. Draft-revision clients + /// Servers SHOULD implement this method. Legacy clients MAY ignore it. Clients on the 2026-07-28 revision /// typically call this once during connection establishment. /// /// @@ -178,7 +178,7 @@ public static class RequestMethods /// /// /// - /// This RPC is introduced in the draft protocol revision (SEP-2575) and replaces the unsolicited + /// This RPC is introduced in the 2026-07-28 protocol revision (SEP-2575) and replaces the unsolicited /// HTTP GET endpoint and the legacy / /// request methods. /// diff --git a/src/ModelContextProtocol.Core/Protocol/SubscriptionsAcknowledgedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/SubscriptionsAcknowledgedNotificationParams.cs index f4212b2b7..577cd3566 100644 --- a/src/ModelContextProtocol.Core/Protocol/SubscriptionsAcknowledgedNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/SubscriptionsAcknowledgedNotificationParams.cs @@ -7,7 +7,7 @@ namespace ModelContextProtocol.Protocol; /// /// /// -/// Introduced by the draft protocol revision (SEP-2575). This notification is the first message on a +/// Introduced by the 2026-07-28 protocol revision (SEP-2575). This notification is the first message on a /// response stream and informs the client which /// subset of requested notification types the server has agreed to deliver. /// diff --git a/src/ModelContextProtocol.Core/Protocol/SubscriptionsListenRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/SubscriptionsListenRequestParams.cs index a81d45669..ac6c38c5b 100644 --- a/src/ModelContextProtocol.Core/Protocol/SubscriptionsListenRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/SubscriptionsListenRequestParams.cs @@ -7,7 +7,7 @@ namespace ModelContextProtocol.Protocol; /// /// /// -/// Introduced by the draft protocol revision (SEP-2575). The client uses this request to open a +/// Introduced by the 2026-07-28 protocol revision (SEP-2575). The client uses this request to open a /// long-lived channel for receiving notifications outside the context of a specific request. /// /// diff --git a/src/ModelContextProtocol.Core/Protocol/UnsupportedProtocolVersionErrorData.cs b/src/ModelContextProtocol.Core/Protocol/UnsupportedProtocolVersionErrorData.cs index ac394db90..ab684e6cf 100644 --- a/src/ModelContextProtocol.Core/Protocol/UnsupportedProtocolVersionErrorData.cs +++ b/src/ModelContextProtocol.Core/Protocol/UnsupportedProtocolVersionErrorData.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol; /// Represents the payload for the JSON-RPC error. /// /// -/// Introduced by the draft protocol revision (SEP-2575). When a server receives a request whose +/// Introduced by the 2026-07-28 protocol revision (SEP-2575). When a server receives a request whose /// declared protocol version it does not implement, it MUST return this error so clients can /// fall back to a mutually supported version. /// diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index 6fad7766b..82b6ceb9d 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Protocol; using System.ComponentModel; @@ -236,8 +236,8 @@ private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider /// /// Returns a clone whose is rewritten /// into the wire shape required by clients on protocol versions older than - /// "2026-06-30". Those versions require outputSchema.type == "object"; - /// SEP-2106 (negotiated at "2026-06-30" and later) widens that to any JSON + /// "2026-07-28". Those versions require outputSchema.type == "object"; + /// SEP-2106 (negotiated at "2026-07-28" and later) widens that to any JSON /// Schema 2020-12 document. To stay compatible, non-object schemas are wrapped in /// {"type":"object","properties":{"result":<schema>}} and the /// type:["object","null"] array form is normalized to plain "object" @@ -510,7 +510,7 @@ schema.ValueKind is not JsonValueKind.Object || // Per SEP-2106, any valid JSON Schema document is acceptable for outputSchema — // arrays, primitives, compositions, and nullable types pass through unchanged. // Explicit OutputSchema takes precedence over AIFunction's return schema. - // Back-compat for pre-2026-06-30 clients is applied at the wire emission sites + // Back-compat for pre-2026-07-28 clients is applied at the wire emission sites // (CreateStructuredResponse for tools/call, listToolsHandler for tools/list). if (toolCreateOptions.OutputSchema is { } explicitSchema) { @@ -531,7 +531,7 @@ schema.ValueKind is not JsonValueKind.Object || /// is neither plain object-typed (type:"object") nor the /// type:["object","null"] array form. Used by /// to decide whether to apply the envelope when emitting to a client that negotiated a - /// protocol version older than "2026-06-30" (those versions pre-date SEP-2106's + /// protocol version older than "2026-07-28" (those versions pre-date SEP-2106's /// allowance of non-object output schemas). The inner type:["object","null"] /// check is hoisted into a named bool to keep the surrounding control flow free of /// empty branches. @@ -565,11 +565,11 @@ schemaNode is JsonObject objSchema && /// /// Transforms into the wire shape required by clients - /// on protocol versions older than "2026-06-30": non-object schemas are wrapped + /// on protocol versions older than "2026-07-28": non-object schemas are wrapped /// in {"type":"object","properties":{"result":<schema>},"required":["result"]}, /// the type:["object","null"] array form is normalized to plain "object", /// and plain object-typed schemas pass through unchanged. SEP-2106 clients - /// ("2026-06-30"+) see the natural schema and never need this transform. + /// ("2026-07-28"+) see the natural schema and never need this transform. /// Dispatches on so the wrap decision lives /// in one place. /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 2cc5d4621..f6eb0d60e 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -107,7 +107,7 @@ public McpServerImpl(ITransport transport, McpServerOptions options, ILoggerFact // A stateful session can push unsolicited list-changed notifications, so subscribe to the // collection change events. A stateless HTTP server cannot send unsolicited notifications, so // instead suppress the listChanged capability it would otherwise advertise. - if (IsStatefulSession()) + if (HasStatefulTransport()) { Register(ServerOptions.ToolCollection, NotificationMethods.ToolListChangedNotification); Register(ServerOptions.PromptCollection, NotificationMethods.PromptListChangedNotification); @@ -134,9 +134,9 @@ void Register(McpServerPrimitiveCollection? collection, ServerCapabilities.Resources.ListChanged = null; } - // And initialize the session. The built-in draft state-sync filter runs ahead of any - // user-supplied incoming filters; see PrependDraftStateSyncFilter for what it records and why. - var incomingMessageFilter = PrependDraftStateSyncFilter(BuildMessageFilterPipeline(options.Filters.Message.IncomingFilters)); + // And initialize the session. The built-in meta-reading filter runs ahead of any + // user-supplied incoming filters; see PrependMetaReadingFilter for what it records and why. + var incomingMessageFilter = PrependMetaReadingFilter(BuildMessageFilterPipeline(options.Filters.Message.IncomingFilters)); var outgoingMessageFilter = BuildMessageFilterPipeline(options.Filters.Message.OutgoingFilters); _sessionHandler = new McpSessionHandler( @@ -158,13 +158,13 @@ void Register(McpServerPrimitiveCollection? collection, /// version, before delegating to the user-supplied incoming filters. /// /// - /// Under the draft protocol revision (SEP-2575) there is no initialize handshake, so these values + /// Under the 2026-07-28 protocol revision (SEP-2575) there is no initialize handshake, so these values /// MUST be populated per-request. For legacy clients the per-request values are absent and the built-in /// filter is a no-op (the values were captured during the initialize handler). /// - private JsonRpcMessageFilter PrependDraftStateSyncFilter(JsonRpcMessageFilter inner) + private JsonRpcMessageFilter PrependMetaReadingFilter(JsonRpcMessageFilter inner) { - JsonRpcMessageFilter draftStateSync = next => async (message, cancellationToken) => + JsonRpcMessageFilter metaReadingFilter = next => async (message, cancellationToken) => { if (message is JsonRpcRequest { Method: not RequestMethods.Initialize } request && request.Context is { } context) { @@ -174,7 +174,7 @@ private JsonRpcMessageFilter PrependDraftStateSyncFilter(JsonRpcMessageFilter in { // Per SEP-2575, the server MUST reject any request whose per-request // _meta/io.modelcontextprotocol/protocolVersion is not one of its supported versions - // with an UnsupportedProtocolVersionError (-32004) carrying the supported list. + // with an UnsupportedProtocolVersionError (-32022) carrying the supported list. if (!McpSessionHandler.SupportedProtocolVersions.Contains(protocolVersion)) { throw new UnsupportedProtocolVersionException( @@ -185,12 +185,12 @@ private JsonRpcMessageFilter PrependDraftStateSyncFilter(JsonRpcMessageFilter in SetNegotiatedProtocolVersion(protocolVersion); } - if (context.ClientCapabilities is { } clientCapabilities && IsDraftProtocol() && IsStatefulSession()) + if (context.ClientCapabilities is { } clientCapabilities && IsJuly2026OrLaterProtocol() && HasStatefulTransport()) { - // Under the draft revision the per-request _meta envelope carries the client's FULL - // capabilities (SEP-2575), so a plain overwrite is correct. The IsDraftProtocol() gate + // Under the 2026-07-28 revision the per-request _meta envelope carries the client's FULL + // capabilities (SEP-2575), so a plain overwrite is correct. The IsJuly2026OrLaterProtocol() gate // makes any legacy per-request envelope a no-op (legacy capabilities stay as the - // initialize handshake established them); the IsStatefulSession() gate keeps + // initialize handshake established them); the HasStatefulTransport() gate keeps // _clientCapabilities null under StreamableHttpServerTransport { Stateless = true } // (where the same server instance handles every request, so persisting per-request // capability state would both leak across requests and break the StatelessServerTests @@ -216,7 +216,7 @@ private JsonRpcMessageFilter PrependDraftStateSyncFilter(JsonRpcMessageFilter in await next(message, cancellationToken).ConfigureAwait(false); }; - return next => draftStateSync(inner(next)); + return next => metaReadingFilter(inner(next)); } /// @@ -368,12 +368,12 @@ private void ConfigureInitialize(McpServerOptions options) protocolVersion ??= request?.ProtocolVersion is string clientProtocolVersion && McpSessionHandler.SupportedProtocolVersions.Contains(clientProtocolVersion) ? clientProtocolVersion : - McpSessionHandler.LatestProtocolVersion; + McpHttpHeaders.November2025ProtocolVersion; // The legacy initialize handshake is authoritative: it may supersede a protocol version - // a prior draft server/discover probe established on the same connection (the dual-era + // a prior server/discover probe established on the same connection (the dual-era // fallback path a permissive client takes against an unknown server). Unlike the - // per-request draft version - which SetNegotiatedProtocolVersion locks once negotiated - + // per-request 2026-07-28 version - which SetNegotiatedProtocolVersion locks once negotiated - // initialize force-sets the version. _negotiatedProtocolVersion = protocolVersion; _sessionHandler.NegotiatedProtocolVersion = protocolVersion; @@ -391,7 +391,7 @@ private void ConfigureInitialize(McpServerOptions options) } /// - /// Registers the server/discover request handler introduced by the draft protocol revision (SEP-2575). + /// Registers the server/discover request handler introduced by the 2026-07-28 protocol revision (SEP-2575). /// /// /// The handler is registered unconditionally so legacy clients can probe it too. It returns the server's @@ -421,7 +421,7 @@ private void ConfigureDiscover(McpServerOptions options) } /// - /// Registers the subscriptions/listen request handler introduced by the draft protocol revision (SEP-2575). + /// Registers the subscriptions/listen request handler introduced by the 2026-07-28 protocol revision (SEP-2575). /// /// /// @@ -449,7 +449,7 @@ private void ConfigureSubscriptions(McpServerOptions options) // request granting no notifications and complete immediately. This runs after protocol // negotiation, so it is not a legacy-server signal and never triggers a client fallback to the // initialize handshake. - if (!IsStatefulSession()) + if (!HasStatefulTransport()) { var statelessSubscription = new ActiveSubscription( jsonRpcRequest.Id, @@ -521,16 +521,16 @@ private sealed record ActiveSubscription(RequestId Id, SubscriptionsListenNotifi /// /// /// Pre-SEP-2575 clients do not open subscriptions/listen streams, so they keep receiving a single - /// session-wide broadcast. Draft clients instead receive only the change notifications they explicitly + /// session-wide broadcast. Clients on the 2026-07-28 or later revision instead receive only the change notifications they explicitly /// requested, each routed back over the originating subscription stream and tagged with its id; the server - /// MUST NOT send a draft client notification types it never subscribed to. + /// MUST NOT send such a client notification types it never subscribed to. /// private async Task SendListChangedNotificationAsync(string notificationMethod) { // Legacy clients never open a subscriptions/listen stream, so they keep the session-wide broadcast. - // subscriptions/listen is a SEP-2575 draft feature, so draft clients instead get a fan-out limited - // to the notification types they explicitly subscribed to. - if (!IsDraftProtocol()) + // subscriptions/listen is a SEP-2575 feature, so clients on the 2026-07-28 or later revision instead get + // a fan-out limited to the notification types they explicitly subscribed to. + if (!IsJuly2026OrLaterProtocol()) { await this.SendNotificationAsync(notificationMethod).ConfigureAwait(false); return; @@ -809,12 +809,12 @@ private void ConfigureTasks(McpServerOptions options) updateTaskHandler ??= (static async (request, _) => throw new McpProtocolException($"Unknown task: '{request.Params?.TaskId}'", McpErrorCode.InvalidParams)); cancelTaskHandler ??= (static async (request, _) => throw new McpProtocolException($"Unknown task: '{request.Params?.TaskId}'", McpErrorCode.InvalidParams)); - // The tasks/* methods do not exist before the draft revision (SEP-2663). Reject them with + // The tasks/* methods do not exist before the 2026-07-28 revision (SEP-2663). Reject them with // MethodNotFound when the request was negotiated under a legacy protocol version. The handlers - // stay registered so a dual-era server still serves them for draft requests. - getTaskHandler = GateTaskMethodToDraft(getTaskHandler, RequestMethods.TasksGet); - updateTaskHandler = GateTaskMethodToDraft(updateTaskHandler, RequestMethods.TasksUpdate); - cancelTaskHandler = GateTaskMethodToDraft(cancelTaskHandler, RequestMethods.TasksCancel); + // stay registered so a dual-era server still serves them for 2026-07-28 requests. + getTaskHandler = GateTaskMethodToJuly2026OrLaterProtocol(getTaskHandler, RequestMethods.TasksGet); + updateTaskHandler = GateTaskMethodToJuly2026OrLaterProtocol(updateTaskHandler, RequestMethods.TasksUpdate); + cancelTaskHandler = GateTaskMethodToJuly2026OrLaterProtocol(cancelTaskHandler, RequestMethods.TasksCancel); // Advertise tasks extension in server capabilities. ServerCapabilities.Extensions ??= new Dictionary(); @@ -841,17 +841,17 @@ private void ConfigureTasks(McpServerOptions options) /// /// Wraps a tasks/* request handler so it throws unless the - /// request was negotiated under the draft revision. The tasks extension (SEP-2663) only interoperates - /// under draft, and these methods don't exist on legacy peers. + /// request was negotiated under the 2026-07-28 or later revision. The tasks extension (SEP-2663) only + /// interoperates on the 2026-07-28 revision, and these methods don't exist on older peers. /// - private McpRequestHandler GateTaskMethodToDraft( + private McpRequestHandler GateTaskMethodToJuly2026OrLaterProtocol( McpRequestHandler inner, string method) => (request, cancellationToken) => { - if (!IsDraftProtocolRequest(request.JsonRpcRequest)) + if (!IsJuly2026OrLaterProtocolRequest(request.JsonRpcRequest)) { throw new McpProtocolException( - $"The method '{method}' requires the draft protocol revision ('{DraftProtocolVersion}'); " + + $"The method '{method}' requires a newer protocol revision that supports tasks; " + $"the negotiated protocol version is '{NegotiatedProtocolVersion ?? "(none)"}'.", McpErrorCode.MethodNotFound); } @@ -1176,11 +1176,11 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) if (request.Params?.Cursor is null) { // SEP-2106 wire shaping: clients on protocol versions older than - // 2026-06-30 require outputSchema.type == "object", so the natural + // 2026-07-28 require outputSchema.type == "object", so the natural // schema is reshaped before emission (type:["object","null"] normalized // to "object", any other non-object schema wrapped in // {"type":"object","properties":{"result":}}). Clients on - // 2026-06-30+ receive the natural JSON Schema 2020-12 document stored + // 2026-07-28+ receive the natural JSON Schema 2020-12 document stored // on Tool.OutputSchema. Only AIFunctionMcpServerTool tools go through // reshaping; custom McpServerTool subclasses build their Tool directly // and pass through unchanged at every protocol version. @@ -1260,12 +1260,12 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) var innerTaskHandler = callToolWithTaskHandler; callToolWithTaskHandler = async (request, cancellationToken) => { - // The SEP-2663 Tasks extension is draft-only: the task wire shapes we ship do not + // The SEP-2663 Tasks extension requires the 2026-07-28 or later revision: the task wire shapes we ship do not // interoperate with legacy (<= 2025-11-25) peers. Only materialize a task when the - // request was negotiated under the draft revision AND the client opted in; otherwise + // request was negotiated under the 2026-07-28 or later revision AND the client opted in; otherwise // run the inner handler and return the direct result (best-effort downgrade, which also // defends against a non-conformant legacy client that forges the opt-in envelope). - if (IsDraftProtocolRequest(request.JsonRpcRequest) && HasTaskExtensionOptIn(request.Params?.Meta)) + if (IsJuly2026OrLaterProtocolRequest(request.JsonRpcRequest) && HasTaskExtensionOptIn(request.Params?.Meta)) { var taskInfo = await taskStore.CreateTaskAsync(cancellationToken).ConfigureAwait(false); var taskId = taskInfo.TaskId; @@ -1770,12 +1770,13 @@ internal static LoggingLevel ToLoggingLevel(LogLevel level) => }; /// - /// Checks whether the negotiated protocol version enables MRTR per SEP-2322 (2026-07-28). MRTR rides on - /// the draft revision, so this is the MRTR-meaning alias of - - /// use it at the input-required/handler-suspension sites where the intent is "the client understands - /// " rather than "the peer speaks the draft revision". + /// Checks whether the negotiated protocol version enables MRTR per SEP-2322 (first available in the + /// 2026-07-28 revision). MRTR rides on the 2026-07-28 revision, so this is the MRTR-meaning alias of + /// - use it at the input-required/handler-suspension + /// sites where the intent is "the client understands " rather than + /// "the peer speaks the 2026-07-28 or later revision". /// - internal bool ClientSupportsMrtr() => IsDraftProtocol(); + internal bool ClientSupportsMrtr() => IsJuly2026OrLaterProtocol(); /// /// Returns when the session is stateful - the same server instance handles @@ -1784,23 +1785,21 @@ internal static LoggingLevel ToLoggingLevel(LogLevel level) => /// elicitation/create / sampling/createMessage / roots/list to the client and /// retry the handler with the responses. /// - internal bool IsStatefulSession() => + internal bool HasStatefulTransport() => _sessionTransport is not StreamableHttpServerTransport { Stateless: true }; /// - /// Returns when the given request was negotiated under the draft protocol + /// Returns when the given request was negotiated under the 2026-07-28 or later protocol /// revision, derived from the per-request _meta/MCP-Protocol-Version value (so it works - /// for sessionless draft over stateless HTTP) and falling back to the session-negotiated version. - /// Used to gate the SEP-2663 Tasks extension, which only interoperates under the draft revision. + /// for requests over stateless HTTP) and falling back to the session-negotiated version. + /// Used to gate the SEP-2663 Tasks extension, which only interoperates on the 2026-07-28 revision. /// - private bool IsDraftProtocolRequest(JsonRpcRequest? request) => - string.Equals( - request?.Context?.ProtocolVersion ?? NegotiatedProtocolVersion, - DraftProtocolVersion, - StringComparison.Ordinal); + private bool IsJuly2026OrLaterProtocolRequest(JsonRpcRequest? request) => + McpHttpHeaders.IsJuly2026OrLaterProtocolVersion( + request?.Context?.ProtocolVersion ?? NegotiatedProtocolVersion); /// - public override bool IsMrtrSupported => ClientSupportsMrtr() || IsStatefulSession(); + public override bool IsMrtrSupported => ClientSupportsMrtr() || HasStatefulTransport(); /// /// Invokes a handler and catches to convert it to an @@ -1834,7 +1833,7 @@ private bool IsDraftProtocolRequest(JsonRpcRequest? request) => // In stateless mode without MRTR, the server can't resolve input requests via // JSON-RPC (no persistent session for server-to-client requests), and the client // won't recognize the InputRequiredResult. This is the one unsupported configuration. - if (!IsStatefulSession()) + if (!HasStatefulTransport()) { throw new McpException( "A tool handler returned an incomplete result, but the server is stateless and the client does not support MRTR. " + @@ -2056,7 +2055,7 @@ private void WrapHandlerWithMrtr(string method) // For all other cases - legacy clients, stateless sessions - fall through to the // exception-based path, which transparently resolves InputRequiredException via // legacy JSON-RPC requests when the client doesn't speak MRTR. - if (!ClientSupportsMrtr() || !IsStatefulSession()) + if (!ClientSupportsMrtr() || !HasStatefulTransport()) { return await InvokeWithInputRequiredResultHandlingAsync(originalHandler, request, cancellationToken).ConfigureAwait(false); } diff --git a/src/ModelContextProtocol.Core/UnsupportedProtocolVersionException.cs b/src/ModelContextProtocol.Core/UnsupportedProtocolVersionException.cs index fc37e05cd..72ba8906d 100644 --- a/src/ModelContextProtocol.Core/UnsupportedProtocolVersionException.cs +++ b/src/ModelContextProtocol.Core/UnsupportedProtocolVersionException.cs @@ -9,10 +9,10 @@ namespace ModelContextProtocol; /// Represents an exception used to signal that a request's declared protocol version is not supported by the server. /// /// -/// Introduced by the draft protocol revision (SEP-2575). Servers throw this exception when they cannot process +/// Introduced by the 2026-07-28 protocol revision (SEP-2575). Servers throw this exception when they cannot process /// a request because the per-request _meta/io.modelcontextprotocol/protocolVersion (or the equivalent /// transport-level header) names a version the server does not implement. The exception is converted to a -/// JSON-RPC error response with code (-32004) and +/// JSON-RPC error response with code (-32022) and /// a payload. /// public sealed class UnsupportedProtocolVersionException : McpProtocolException diff --git a/tests/Common/Utils/NodeHelpers.cs b/tests/Common/Utils/NodeHelpers.cs index b549bdd76..45ecdcde2 100644 --- a/tests/Common/Utils/NodeHelpers.cs +++ b/tests/Common/Utils/NodeHelpers.cs @@ -2,7 +2,6 @@ using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; -using ModelContextProtocol.Protocol; namespace ModelContextProtocol.Tests.Utils; @@ -85,7 +84,7 @@ public static void EnsureNpmDependenciesInstalled() /// When (the default) and the MCP_CONFORMANCE_PROTOCOL_VERSION /// environment variable is set, a "--spec-version <value>" argument is appended. /// Pass for scenarios that pin their own spec version (e.g. the - /// draft-only caching scenario) to avoid a conflicting duplicate flag. + /// caching scenario specific to the 2026-07-28 protocol) to avoid a conflicting duplicate flag. /// /// A configured ProcessStartInfo for running the binary. public static ProcessStartInfo ConformanceTestStartInfo(string arguments, bool appendProtocolVersionFromEnv = true) @@ -188,12 +187,9 @@ public static bool IsNodeInstalled() /// the pinned version in package.json) means this also returns /// when a newer private build has been installed locally via /// npm install --no-save <path-to-conformance>. - /// Additionally requires that the installed conformance package emits the draft wire - /// version this SDK speaks — see . /// public static bool HasSep2243Scenarios() - => HasInstalledConformanceVersionAtLeast(new Version(0, 2, 0)) - && HasMatchingDraftWireVersion(); + => HasInstalledConformanceVersionAtLeast(new Version(0, 2, 0)); /// /// Checks whether the SEP-2549 "caching" conformance scenario (added in conformance @@ -202,47 +198,9 @@ public static bool HasSep2243Scenarios() /// Reading the installed version (rather than the pinned version in package.json) means /// this also returns when a newer private build has been installed /// locally via npm install --no-save <path-to-conformance>. - /// Additionally requires that the installed conformance package emits the draft wire - /// version this SDK speaks — see . /// public static bool HasCachingScenario() - => HasInstalledConformanceVersionAtLeast(new Version(0, 2, 0)) - && HasMatchingDraftWireVersion(); - - /// - /// Returns when the installed conformance package's bundled - /// dist emits the same draft protocol version string as this SDK - /// (). Used to suppress draft-only - /// conformance scenarios when the published conformance binary is still pinned to a - /// stale wire string (for example, conformance 0.2.0-alpha.2 ships - /// "DRAFT-2026-v1" while this SDK speaks "2026-07-28"). - /// - /// - /// This check is a pragmatic alternative to inspecting the conformance package's - /// internal constants: the bundled dist/index.js is minified so we can't grep - /// the constant name, but the literal version string survives bundling and is unique - /// enough to be a reliable signal. - /// - public static bool HasMatchingDraftWireVersion() - { - try - { - var repoRoot = FindRepoRoot(); - var distPath = Path.Combine( - repoRoot, "node_modules", "@modelcontextprotocol", "conformance", "dist", "index.js"); - if (!File.Exists(distPath)) - { - return false; - } - - var bundled = File.ReadAllText(distPath); - return bundled.Contains(McpHttpHeaders.DraftProtocolVersion, StringComparison.Ordinal); - } - catch - { - return false; - } - } + => HasInstalledConformanceVersionAtLeast(new Version(0, 2, 0)); /// /// Returns when the conformance package installed in node_modules @@ -425,12 +383,9 @@ private static bool ConformanceOutputIndicatesSuccess(string output) /// Reading the installed version (rather than the pinned version in package.json) means /// this also returns when a newer private build has been installed /// locally via npm install --no-save <path-to-conformance>. - /// Additionally requires that the installed conformance package emits the draft wire - /// version this SDK speaks — see . /// public static bool HasMrtrScenarios() - => HasInstalledConformanceVersionAtLeast(new Version(0, 2, 0)) - && HasMatchingDraftWireVersion(); + => HasInstalledConformanceVersionAtLeast(new Version(0, 2, 0)); private static ProcessStartInfo NpmStartInfo(string arguments, string workingDirectory) { diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/CachingConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/CachingConformanceTests.cs index 27cba0d40..38e503258 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/CachingConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/CachingConformanceTests.cs @@ -4,8 +4,9 @@ namespace ModelContextProtocol.ConformanceTests; /// -/// A ConformanceServer instance started in the SEP-2575 stateless lifecycle, which the draft -/// SEP-2549 "caching" conformance scenario requires. Started on demand (so it is not bound +/// A ConformanceServer instance started in the SEP-2575 stateless lifecycle, which the +/// SEP-2549 "caching" conformance scenario (new in the 2026-07-28 protocol revision) +/// requires. Started on demand (so it is not bound /// when the caching test is skipped) and torn down via . Uses a /// distinct port range from the stateful ConformanceServerFixture (3001/3002/3003) so /// the two can run in parallel without TCP conflicts. @@ -105,13 +106,9 @@ public async ValueTask DisposeAsync() /// (tools/list, prompts/list, resources/list, resources/templates/list, resources/read). /// /// -/// The scenario is draft-only (introduced in spec wire version 2026-07-28) and uses the -/// stateless lifecycle. It is gated on the installed conformance package version (>= 0.2.0) -/// AND on the installed package emitting the draft wire string this SDK speaks (so it stays -/// skipped under conformance 0.2.0-alpha.2 which still ships the placeholder -/// DRAFT-2026-v1). It activates automatically once a conformance package emitting -/// 2026-07-28 is installed (e.g. via -/// npm install --no-save <path-to-conformance>). The stateless server is +/// The scenario was introduced in spec wire version 2026-07-28 and uses the stateless lifecycle. +/// It is gated on the installed conformance +/// package version (>= 0.2.0). The stateless server is /// started only after the gates pass, so a skipped run binds no port. /// public class CachingConformanceTests(ITestOutputHelper output) @@ -126,7 +123,7 @@ public async Task RunCachingConformanceTest() await using var server = await StatelessConformanceServer.StartAsync(TestContext.Current.CancellationToken); - // The caching scenario only exists in the draft spec, so pin the spec version + // The caching scenario only exists in the 2026-07-28 protocol revision, so pin the spec version // explicitly (and suppress the MCP_CONFORMANCE_PROTOCOL_VERSION override to avoid a // conflicting duplicate --spec-version flag). var result = await NodeHelpers.RunServerConformanceAsync( diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs index f389ffbba..2990b3d83 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs @@ -65,8 +65,11 @@ public async Task RunConformanceTest(string scenario) // HTTP Standardization (SEP-2243) [Theory(Skip = "SEP-2243 conformance scenarios not available (requires conformance package >= 0.2.0).", SkipUnless = nameof(HasSep2243Scenarios))] [InlineData("http-standard-headers")] - [InlineData("http-custom-headers")] [InlineData("http-invalid-tool-headers")] + // Commented out: the upstream scenario annotates a "number"-typed parameter with x-mcp-header, + // which SEP-2243 forbids, so the client rejects the tool and sends no Mcp-Param-* headers, + // failing every positive check. Re-enable once a conformant conformance package ships (#1655). + // [InlineData("http-custom-headers")] public async Task RunConformanceTest_Sep2243(string scenario) { // Run the conformance test suite diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpHeaderConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpHeaderConformanceTests.cs index db751af6b..4da16a3eb 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpHeaderConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpHeaderConformanceTests.cs @@ -114,7 +114,7 @@ private static McpServerTool CreateUnionHeaderTestTool() public async Task Server_AcceptsUnionIntegerCanonicalForm() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // Union-typed (["integer","null"]) parameter: header carries canonical "42" while the body // carries the decimal form 42.0. The server must treat the union type as integer and match. @@ -135,7 +135,7 @@ public async Task Server_AcceptsUnionIntegerCanonicalForm() public async Task Server_RejectsUnionIntegerOutsideSafeRange() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); var callJson = CallTool("union_test", """{"priority":9007199254740993}"""); @@ -154,7 +154,7 @@ public async Task Server_RejectsUnionIntegerOutsideSafeRange() public async Task Server_AcceptsExponentBodyMatchingDecimalHeader() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // Body carries the integer in exponent form (1e2 = 100); header carries the decimal "100". var callJson = CallTool("header_test", """{"region":"test","priority":1e2,"verbose":false,"emptyVal":""}"""); @@ -177,7 +177,7 @@ public async Task Server_AcceptsExponentBodyMatchingDecimalHeader() public async Task Server_AcceptsWhitespaceAroundMcpNameHeaderValue() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // Per SEP-2243: servers MUST accept extra whitespace around header values // and compare the trimmed value to the request body. @@ -201,7 +201,7 @@ public async Task Server_AcceptsWhitespaceAroundMcpNameHeaderValue() public async Task Server_AcceptsWhitespaceAroundMcpMethodHeaderValue() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // Per SEP-2243: servers MUST accept extra whitespace around header values var callJson = CallTool("header_test", """{"region":"us-west1","priority":42,"verbose":false,"emptyVal":""}"""); @@ -224,7 +224,7 @@ public async Task Server_AcceptsWhitespaceAroundMcpMethodHeaderValue() public async Task Server_ValidatesEmptyStringHeaderValue_AgainstBodyValue() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // Send a tools/call with an empty string param that has an x-mcp-header. // The header should be present with an empty value, matching the body's empty string. @@ -248,7 +248,7 @@ public async Task Server_ValidatesEmptyStringHeaderValue_AgainstBodyValue() public async Task Server_RejectsHeaderMismatch_WhenEmptyHeaderDoesNotMatchBody() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // Send a tools/call where the body has a non-empty value but the header is empty var callJson = CallTool("header_test", """{"region":"us-west1","priority":42,"verbose":false,"emptyVal":"some-value"}"""); @@ -271,7 +271,7 @@ public async Task Server_RejectsHeaderMismatch_WhenEmptyHeaderDoesNotMatchBody() public async Task Server_AcceptsBase64EncodedHeaderWithControlChars() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // Encode a value with a newline control character using Base64 var valueWithNewline = "line1\nline2"; @@ -297,7 +297,7 @@ public async Task Server_AcceptsBase64EncodedHeaderWithControlChars() public async Task Server_AcceptsMaxSafeIntegerWithFullPrecision() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // The maximum safe integer (2^53 - 1) must be accepted, and compared exactly without // losing precision through a double conversion. @@ -326,7 +326,7 @@ public async Task Server_AcceptsMaxSafeIntegerWithFullPrecision() public async Task Server_RejectsIntegerOutsideSafeRange(string outOfRangeValue) { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // Per SEP-2243 integer values MUST be within the JavaScript safe integer range. // A matching header and body that are both outside the range must still be rejected. @@ -355,7 +355,7 @@ public async Task Server_RejectsIntegerOutsideSafeRange(string outOfRangeValue) public async Task Server_AcceptsNumericEquivalentHeaderValues(string headerValue, string bodyValue) { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // bodyValue is inserted as a raw JSON numeric literal so that forms such as "42.0" and // "4.2e1" are preserved in the body exactly as another SDK might serialize them. @@ -382,7 +382,7 @@ public async Task Server_AcceptsNumericEquivalentHeaderValues(string headerValue public async Task Server_RejectsNonIntegerValue_EvenWhenHeaderAndBodyMatch(string nonIntegerValue) { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // For an integer-typed parameter a non-whole numeric value is invalid and must be rejected // even when the header and body strings are byte-for-byte identical (it must not slip through @@ -407,7 +407,7 @@ public async Task Server_RejectsNonIntegerValue_EvenWhenHeaderAndBodyMatch(strin public async Task Server_RejectsNonNumericMismatch_ForIntegerParam() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // Header says "99" but body says priority:42 — must reject even with numeric comparison var callJson = CallTool("header_test", """{"region":"test","priority":42,"verbose":false,"emptyVal":""}"""); @@ -427,17 +427,17 @@ public async Task Server_RejectsNonNumericMismatch_ForIntegerParam() } [Fact] - public async Task Server_SkipsHeaderValidation_ForNonDraftVersion() + public async Task Server_SkipsHeaderValidation_ForLegacyVersion() { await StartAsync(); - await InitializeWithNonDraftVersionAsync(); + await InitializeWithLegacyVersionAsync(); - // With non-draft version, Mcp-Param-* headers are NOT validated even if mismatched + // With the legacy version, Mcp-Param-* headers are NOT validated even if mismatched var callJson = CallTool("header_test", """{"region":"us-west1","priority":42,"verbose":false,"emptyVal":""}"""); using var request = new HttpRequestMessage(HttpMethod.Post, ""); request.Content = new StringContent(callJson, Encoding.UTF8, "application/json"); - // Send the WRONG header value — this should still succeed because version is non-draft + // Send the WRONG header value. This should still succeed because the version is legacy. request.Headers.Add("MCP-Protocol-Version", "2025-11-25"); request.Headers.Add("Mcp-Method", "tools/call"); request.Headers.Add("Mcp-Name", "header_test"); @@ -451,7 +451,7 @@ public async Task Server_SkipsHeaderValidation_ForNonDraftVersion() public async Task Server_RejectsInvalidUtf8EncodedHeaderValue() { await StartAsync(); - await InitializeWithDraftVersionAsync(); + await InitializeWithJuly2026ProtocolVersionAsync(); // Create a separate HttpClient that sends raw UTF-8 bytes in Mcp-* headers // instead of properly base64-encoding non-ASCII values. @@ -570,32 +570,32 @@ public void SupportsStandardHeaders_CorrectlyGatesVersions(string? version, bool #region Helpers - private async Task InitializeWithDraftVersionAsync() + private async Task InitializeWithJuly2026ProtocolVersionAsync() { HttpClient.DefaultRequestHeaders.Remove("mcp-session-id"); using var request = new HttpRequestMessage(HttpMethod.Post, ""); - request.Content = JsonContent(InitializeRequestDraft); + request.Content = JsonContent(InitializeRequestJuly2026Protocol); request.Headers.Add("MCP-Protocol-Version", "2026-07-28"); request.Headers.Add("Mcp-Method", "initialize"); using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - // Draft protocol revision (SEP-2567) is sessionless: the server does not return a + // Starting with the 2026-07-28 protocol revision (SEP-2567), Streamable HTTP does not return a // mcp-session-id header. Subsequent requests carry MCP-Protocol-Version=2026-07-28 - // to route through the sessionless path. + // so each one is handled independently. } - private async Task InitializeWithNonDraftVersionAsync() + private async Task InitializeWithLegacyVersionAsync() { HttpClient.DefaultRequestHeaders.Remove("mcp-session-id"); using var response = await HttpClient.PostAsync("", JsonContent(InitializeRequest), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - // Server is stateless by default (SEP-2567), so initializing with the non-draft protocol does not return - // a mcp-session-id header. Subsequent requests are independent, just like the draft path. + // Server is stateless by default (SEP-2567), so initializing with the legacy protocol does not return + // a mcp-session-id header. Subsequent requests are independent, just like requests on the 2026-07-28 revision. } private static StringContent JsonContent(string json) => new(json, Encoding.UTF8, "application/json"); @@ -614,7 +614,7 @@ private string CallTool(string toolName, string arguments = "{}") {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"TestClient","version":"1.0"}}} """; - private static string InitializeRequestDraft => """ + private static string InitializeRequestJuly2026Protocol => """ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"TestClient","version":"1.0"}}} """; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index 5fd4d49e4..abd3823f2 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -34,8 +34,8 @@ public async Task ConnectAndPing_Sse_TestServer() // Arrange // Act - // ping was removed in the draft revision (SEP-2575), so pin to the latest stable protocol - // version to keep exercising the legacy ping RPC. Draft liveness relies on the transport. + // ping was removed in the 2026-07-28 protocol revision (SEP-2575), so pin to the latest stable + // protocol version to keep exercising the legacy ping RPC. On the 2026-07-28 protocol, liveness relies on the transport. await using var client = await GetClientAsync(new McpClientOptions { ProtocolVersion = "2025-11-25" }); await client.PingAsync(cancellationToken: TestContext.Current.CancellationToken); @@ -49,8 +49,8 @@ public async Task Connect_TestServer_ShouldProvideServerFields() // Arrange // Act - // Stateful Streamable HTTP only provisions a session ID under the legacy handshake; the draft - // revision is sessionless. Pin to the latest stable version to keep covering session-ID provisioning. + // Stateful Streamable HTTP only provisions a session ID under the legacy handshake. Starting with the + // 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions. Pin to the latest stable version to keep covering session-ID provisioning. await using var client = await GetClientAsync(new McpClientOptions { ProtocolVersion = "2025-11-25" }); // Assert diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/DraftHttpFallbackTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/July2026ProtocolHttpFallbackTests.cs similarity index 85% rename from tests/ModelContextProtocol.AspNetCore.Tests/DraftHttpFallbackTests.cs rename to tests/ModelContextProtocol.AspNetCore.Tests/July2026ProtocolHttpFallbackTests.cs index 55401c15a..02bfba288 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/DraftHttpFallbackTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/July2026ProtocolHttpFallbackTests.cs @@ -14,10 +14,10 @@ namespace ModelContextProtocol.AspNetCore.Tests; /// -/// Regression tests for the draft-protocol-to-legacy fallback path over Streamable HTTP. These +/// Regression tests for the 2026-07-28-to-legacy fallback path over Streamable HTTP. These /// hand-craft minimal HTTP servers that mimic real-world peer behavior (e.g. Python's /// simple-streamablehttp-stateless returns a JSON-RPC error envelope in a 400 body -/// on a draft probe; vanilla Go does the same on POST /) so the client's HTTP-fallback +/// on a 2026-07-28 probe; vanilla Go does the same on POST /) so the client's HTTP-fallback /// logic can be exercised in isolation without the cross-SDK harness. /// /// @@ -27,10 +27,10 @@ namespace ModelContextProtocol.AspNetCore.Tests; /// /// /// -/// only surfaced the three modern draft -/// error codes (-32004, -32003, -32001) as ; +/// only surfaced the three error codes +/// introduced by the 2026-07-28 revision (-32022, -32021, -32020) as ; /// any other JSON-RPC error code in a 400 body (e.g. -32600 from a legacy server -/// that doesn't understand the draft _meta envelope) threw +/// that doesn't understand the 2026-07-28 _meta envelope) threw /// and bypassed the connect-time fallback logic. Per spec PR #2844, the fallback must trigger /// on ANY non-modern JSON-RPC error in a 400 body. /// @@ -43,7 +43,7 @@ namespace ModelContextProtocol.AspNetCore.Tests; /// /// /// -public class DraftHttpFallbackTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable +public class July2026ProtocolHttpFallbackTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable { private WebApplication? _app; @@ -84,13 +84,13 @@ private static async Task WriteJsonRpcErrorAsync(HttpContext context, HttpStatus } /// - /// Mimics Python's simple-streamablehttp-stateless on a draft probe: returns + /// Mimics Python's simple-streamablehttp-stateless on a 2026-07-28 probe: returns /// 400 + JSON-RPC -32600 ("Bad Request: Unsupported protocol version") for the /// initial server/discover, then performs a normal legacy initialize handshake /// when the client falls back. /// [Fact] - public async Task DraftClient_AgainstLegacyHttpServer_FallsBack_To_Initialize_When_400_Contains_JsonRpcError() + public async Task Client_AgainstLegacyHttpServer_FallsBack_To_Initialize_When_400_Contains_JsonRpcError() { var ct = TestContext.Current.CancellationToken; @@ -107,11 +107,11 @@ await StartServerAsync(async context => return; } - // Draft probe: simulate a legacy server that rejects the unknown protocol version with + // 2026-07-28 probe: simulate a legacy server that rejects the unknown protocol version with // a -32600 envelope (matches Python's wire shape verified in cross-SDK testing). if (request.Method == RequestMethods.ServerDiscover) { - await WriteJsonRpcErrorAsync(context, HttpStatusCode.BadRequest, code: -32600, message: "Bad Request: Unsupported protocol version: draft"); + await WriteJsonRpcErrorAsync(context, HttpStatusCode.BadRequest, code: -32600, message: "Bad Request: Unsupported protocol version: 2026-07-28"); return; } @@ -157,10 +157,9 @@ await StartServerAsync(async context => Endpoint = new("http://localhost:5000/mcp"), }, HttpClient, LoggerFactory); - await using var client = await McpClient.CreateAsync(transport, new McpClientOptions - { - ProtocolVersion = McpSession.DraftProtocolVersion, - }, loggerFactory: LoggerFactory, cancellationToken: ct); + // Default options prefer 2026-07-28 but allow automatic fallback to a legacy server. + await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(), + loggerFactory: LoggerFactory, cancellationToken: ct); Assert.Equal("2025-06-18", client.NegotiatedProtocolVersion); @@ -170,12 +169,12 @@ await StartServerAsync(async context => } /// - /// Mimics vanilla Go: returns 400 + JSON-RPC -32004 with - /// data.supported[] on a draft probe so the client retries legacy + /// Mimics vanilla Go: returns 400 + JSON-RPC -32022 with + /// data.supported[] on a 2026-07-28 probe so the client retries legacy /// initialize with one of the advertised versions. /// [Fact] - public async Task DraftClient_OnUnsupportedProtocolVersion_AdoptsStreamableHttp_NoSseFallback() + public async Task Client_OnUnsupportedProtocolVersion_AdoptsStreamableHttp_NoSseFallback() { var ct = TestContext.Current.CancellationToken; @@ -194,12 +193,12 @@ await StartServerAsync(async context => if (request.Method == RequestMethods.ServerDiscover) { - // -32004 with the spec-shaped data: client should retry with one of supported[]. + // -32022 with the spec-shaped data: client should retry with one of supported[]. // Use the typed payload type so the source-generated serializer can handle it. var data = JsonSerializer.SerializeToNode(new UnsupportedProtocolVersionErrorData { Supported = new List { "2025-11-25" }, - Requested = "draft", + Requested = "2026-07-28", }, GetJsonTypeInfo()); var rpcError = new JsonRpcError @@ -245,20 +244,19 @@ await StartServerAsync(async context => Endpoint = new("http://localhost:5000/mcp"), }, HttpClient, LoggerFactory); - await using var client = await McpClient.CreateAsync(transport, new McpClientOptions - { - ProtocolVersion = McpSession.DraftProtocolVersion, - }, loggerFactory: LoggerFactory, cancellationToken: ct); + // Default options prefer 2026-07-28 but allow automatic fallback to a legacy server. + await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(), + loggerFactory: LoggerFactory, cancellationToken: ct); Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion); } /// - /// A 400 with a JSON-RPC -32001 HeaderMismatch envelope must be surfaced to the - /// caller (no legacy fallback) — falling back wouldn't fix a malformed envelope. + /// A 400 with a JSON-RPC -32020 HeaderMismatch envelope must be surfaced to the + /// caller (no legacy fallback). Falling back wouldn't fix a malformed envelope. /// [Fact] - public async Task DraftClient_OnHeaderMismatch_400_Surfaces_McpProtocolException_NoFallback() + public async Task Client_OnHeaderMismatch_400_Surfaces_McpProtocolException_NoFallback() { var ct = TestContext.Current.CancellationToken; bool initializeReceived = false; @@ -295,7 +293,7 @@ await WriteJsonRpcErrorAsync(context, HttpStatusCode.BadRequest, { await using var client = await McpClient.CreateAsync(transport, new McpClientOptions { - ProtocolVersion = McpSession.DraftProtocolVersion, + ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion, }, loggerFactory: LoggerFactory, cancellationToken: ct); }); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/DraftHttpHandlerTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/July2026ProtocolHttpHandlerTests.cs similarity index 72% rename from tests/ModelContextProtocol.AspNetCore.Tests/DraftHttpHandlerTests.cs rename to tests/ModelContextProtocol.AspNetCore.Tests/July2026ProtocolHttpHandlerTests.cs index b6f3352ae..9cce9b0db 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/DraftHttpHandlerTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/July2026ProtocolHttpHandlerTests.cs @@ -9,26 +9,24 @@ namespace ModelContextProtocol.AspNetCore.Tests; /// -/// HTTP-level tests for the draft protocol revision (SEP-2575 + SEP-2567): verify that the server -/// suppresses the Mcp-Session-Id header for draft requests and returns structured +/// HTTP-level tests for the 2026-07-28 protocol revision (SEP-2575 + SEP-2567): verify that the server +/// suppresses the Mcp-Session-Id header for those requests and returns structured /// errors instead of plain 400s. /// -public class DraftHttpHandlerTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable +public class July2026ProtocolHttpHandlerTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable { - private const string DraftVersion = McpHttpHeaders.DraftProtocolVersion; - private WebApplication? _app; private async Task StartAsync(bool stateless = false) { Builder.Services.AddMcpServer(options => { - options.ServerInfo = new Implementation { Name = nameof(DraftHttpHandlerTests), Version = "1" }; + options.ServerInfo = new Implementation { Name = nameof(July2026ProtocolHttpHandlerTests), Version = "1" }; }).WithHttpTransport(options => { - // Stateless = false maps the GET/DELETE endpoints and opts the author into sessions, which the - // draft revision cannot honor (so sessionless draft requests are refused). Stateless = true (the - // default) serves sessionless draft natively. + // Stateless = false maps the GET/DELETE endpoints and opts the author into sessions. Starting with + // the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions, so such a request is + // refused on a session-enabled server. Stateless = true (the default) serves them natively. options.Stateless = stateless; }); @@ -50,33 +48,33 @@ public async ValueTask DisposeAsync() } [Fact] - public async Task DraftRequest_OnStatelessServer_Succeeds_WithoutMcpSessionIdHeader() + public async Task Request_OnStatelessServer_Succeeds_WithoutMcpSessionIdHeader() { await StartAsync(stateless: true); - HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion); + HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpHttpHeaders.July2026ProtocolVersion); HttpClient.DefaultRequestHeaders.Add("Mcp-Method", "server/discover"); - // On a stateless server, sessionless draft server/discover succeeds without creating a session. + // On a stateless server, server/discover succeeds without creating a session. var content = new StringContent( """{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}""", Encoding.UTF8, "application/json"); using var response = await HttpClient.PostAsync("", content, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.False(response.Headers.Contains("Mcp-Session-Id"), "Draft responses must not include Mcp-Session-Id"); + Assert.False(response.Headers.Contains("Mcp-Session-Id"), "Responses on the 2026-07-28 revision must not include Mcp-Session-Id"); } [Fact] - public async Task DraftRequest_OnStatefulServer_IsRefused_WithUnsupportedProtocolVersionError() + public async Task Request_OnStatefulServer_IsRefused_WithUnsupportedProtocolVersionError() { - // The draft revision is sessionless (SEP-2567), so it cannot honor a server configured with - // sessions (Stateless = false). The server refuses the draft version with - // UnsupportedProtocolVersion (excluding draft from Supported) so a dual-era client falls back + // Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions (SEP-2567), + // so the server cannot honor it when configured with sessions (Stateless = false). The server refuses that + // version with UnsupportedProtocolVersion (excluding it from Supported) so a dual-era client falls back // to the legacy initialize handshake. await StartAsync(stateless: false); - HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion); + HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpHttpHeaders.July2026ProtocolVersion); HttpClient.DefaultRequestHeaders.Add("Mcp-Method", "server/discover"); var content = new StringContent( @@ -95,10 +93,10 @@ public async Task DraftRequest_OnStatefulServer_IsRefused_WithUnsupportedProtoco var dataElement = (JsonElement)rpcError.Error.Data!; var errorData = dataElement.Deserialize(McpJsonUtilities.DefaultOptions); Assert.NotNull(errorData); - Assert.Equal(DraftVersion, errorData.Requested); - // The draft version is excluded from Supported so the client downgrades to a legacy version. + Assert.Equal(McpHttpHeaders.July2026ProtocolVersion, errorData.Requested); + // The 2026-07-28 protocol version is excluded from Supported so the client downgrades to a legacy version. Assert.NotEmpty(errorData.Supported); - Assert.DoesNotContain(DraftVersion, errorData.Supported); + Assert.DoesNotContain(McpHttpHeaders.July2026ProtocolVersion, errorData.Supported); } [Fact] @@ -130,13 +128,14 @@ public async Task RequestWithUnsupportedProtocolVersion_Returns_UnsupportedProto } [Fact] - public async Task DraftRequest_WithMcpSessionIdHeader_IsRejected() + public async Task Request_WithMcpSessionIdHeader_IsRejected() { - // The draft revision is sessionless (SEP-2567): a draft request carrying an Mcp-Session-Id is - // non-conformant and is rejected with 400 regardless of the Stateless setting. + // Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions (SEP-2567): + // a request carrying an Mcp-Session-Id is non-conformant and is rejected with 400 regardless of the + // Stateless setting. await StartAsync(); - HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion); + HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpHttpHeaders.July2026ProtocolVersion); HttpClient.DefaultRequestHeaders.Add("Mcp-Method", "server/discover"); HttpClient.DefaultRequestHeaders.Add("Mcp-Session-Id", "non-existent-session-id"); @@ -149,11 +148,11 @@ public async Task DraftRequest_WithMcpSessionIdHeader_IsRejected() } [Fact] - public async Task DraftGet_WithoutSessionId_IsRejected() + public async Task Get_WithoutSessionId_IsRejected() { await StartAsync(); - HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion); + HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpHttpHeaders.July2026ProtocolVersion); using var response = await HttpClient.GetAsync("", TestContext.Current.CancellationToken); @@ -161,11 +160,11 @@ public async Task DraftGet_WithoutSessionId_IsRejected() } [Fact] - public async Task DraftGet_WithSessionId_IsRejected() + public async Task Get_WithSessionId_IsRejected() { await StartAsync(); - HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion); + HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpHttpHeaders.July2026ProtocolVersion); HttpClient.DefaultRequestHeaders.Add("Mcp-Session-Id", "non-existent-session-id"); using var response = await HttpClient.GetAsync("", TestContext.Current.CancellationToken); @@ -174,11 +173,11 @@ public async Task DraftGet_WithSessionId_IsRejected() } [Fact] - public async Task DraftDelete_WithoutSessionId_IsRejected() + public async Task Delete_WithoutSessionId_IsRejected() { await StartAsync(); - HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion); + HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpHttpHeaders.July2026ProtocolVersion); using var response = await HttpClient.DeleteAsync("", TestContext.Current.CancellationToken); @@ -186,11 +185,11 @@ public async Task DraftDelete_WithoutSessionId_IsRejected() } [Fact] - public async Task DraftDelete_WithSessionId_IsRejected() + public async Task Delete_WithSessionId_IsRejected() { await StartAsync(); - HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion); + HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpHttpHeaders.July2026ProtocolVersion); HttpClient.DefaultRequestHeaders.Add("Mcp-Session-Id", "non-existent-session-id"); using var response = await HttpClient.DeleteAsync("", TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/DraftStatefulFallbackTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/July2026ProtocolStatefulFallbackTests.cs similarity index 76% rename from tests/ModelContextProtocol.AspNetCore.Tests/DraftStatefulFallbackTests.cs rename to tests/ModelContextProtocol.AspNetCore.Tests/July2026ProtocolStatefulFallbackTests.cs index 18815fd00..ef2d3f6aa 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/DraftStatefulFallbackTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/July2026ProtocolStatefulFallbackTests.cs @@ -10,15 +10,15 @@ namespace ModelContextProtocol.AspNetCore.Tests; /// -/// End-to-end coverage for a default (draft-first) client connecting to a real C# Streamable HTTP +/// End-to-end coverage for a default (2026-07-28-first) client connecting to a real C# Streamable HTTP /// server that deliberately opted into sessions ( -/// is false). Draft is sessionless (SEP-2567 / SEP-2575), so the server refuses the -/// sessionless draft probe with -32004 UnsupportedProtocolVersion. The client must then -/// auto-downgrade to the legacy initialize handshake, obtain the stateful session the server -/// author opted into, and continue to work — including a server→client elicitation round-trip +/// is false). Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports +/// sessions (SEP-2567 / SEP-2575), so the server refuses the probe with -32022 UnsupportedProtocolVersion. +/// The client must then auto-downgrade to the legacy initialize handshake, obtain the stateful session +/// the server author opted into, and continue to work, including a server→client elicitation round-trip /// resolved over the stateful session via the legacy backcompat resolver. /// -public class DraftStatefulFallbackTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable +public class July2026ProtocolStatefulFallbackTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable { private WebApplication? _app; @@ -38,7 +38,7 @@ public async ValueTask DisposeAsync() private static async Task GreetViaElicit(McpServer server, CancellationToken cancellationToken) { // Server→client round-trip: only works when the session is stateful, which is exactly what - // the legacy fallback re-establishes for the draft-first client. + // the legacy fallback re-establishes for the 2026-07-28-first client. var elicitResult = await server.ElicitAsync(new ElicitRequestParams { Message = "What is your name?", @@ -56,10 +56,10 @@ private async Task StartStatefulServerAsync() { Builder.Services.AddMcpServer(options => { - options.ServerInfo = new Implementation { Name = nameof(DraftStatefulFallbackTests), Version = "1" }; + options.ServerInfo = new Implementation { Name = nameof(July2026ProtocolStatefulFallbackTests), Version = "1" }; }) - // Stateless = false is a deliberate opt-in to sessions. Draft can never be served - // statefully, so the server refuses the sessionless draft probe and the client downgrades. + // Stateless = false is a deliberate opt-in to sessions. Starting with the 2026-07-28 protocol revision, + // Streamable HTTP can never be served statefully, so the server refuses the probe and the client downgrades. .WithHttpTransport(options => options.Stateless = false) .WithTools([McpServerTool.Create(Greet), McpServerTool.Create(GreetViaElicit)]); @@ -76,7 +76,7 @@ private async Task ConnectDefaultClientAsync(Action TransportMode = HttpTransportMode.StreamableHttp, }, HttpClient, LoggerFactory); - // Default options: ProtocolVersion is null, which now prefers the draft revision and probes + // Default options: ProtocolVersion is null, which now prefers the 2026-07-28 protocol revision and probes // with server/discover before falling back to a legacy initialize handshake. var clientOptions = new McpClientOptions(); configureClient?.Invoke(clientOptions); @@ -84,13 +84,13 @@ private async Task ConnectDefaultClientAsync(Action } [Fact] - public async Task DefaultDraftClient_AgainstStatefulServer_DowngradesToLegacy_AndToolsWork() + public async Task DefaultClient_AgainstStatefulServer_DowngradesToLegacy_AndToolsWork() { await StartStatefulServerAsync(); await using var client = await ConnectDefaultClientAsync(); - // The sessionless draft probe was refused (-32004), so the client downgraded to legacy. + // The 2026-07-28 probe was refused (-32022), so the client downgraded to legacy. Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion); var result = await client.CallToolAsync("greet", @@ -102,7 +102,7 @@ public async Task DefaultDraftClient_AgainstStatefulServer_DowngradesToLegacy_An } [Fact] - public async Task DefaultDraftClient_AgainstStatefulServer_ServerToClientElicitation_RoundTrips() + public async Task DefaultClient_AgainstStatefulServer_ServerToClientElicitation_RoundTrips() { await StartStatefulServerAsync(); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs index dcd1bcf50..889a7daab 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs @@ -486,8 +486,8 @@ public async Task EnablePollingAsync_ThrowsInvalidOperationException_WhenNoEvent await app.StartAsync(TestContext.Current.CancellationToken); - // Polling via an event-stream store is a stateful-session feature. Under draft, Streamable HTTP - // is sessionless, so pin to the latest stable version to keep exercising the stateful path. + // Polling via an event-stream store is a stateful-session feature. Starting with the 2026-07-28 + // protocol revision, Streamable HTTP no longer supports sessions, so pin to the latest stable version to keep exercising the stateful path. await using var mcpClient = await ConnectAsync(configureClient: options => options.ProtocolVersion = "2025-11-25"); await mcpClient.CallToolAsync("polling_tool", cancellationToken: TestContext.Current.CancellationToken); @@ -541,7 +541,7 @@ public async Task AdditionalHeaders_AreSent_InPostAndDeleteRequests() }; // DELETE requests are only sent when there's a session ID to delete - a legacy stateful - // behavior. Under draft, Streamable HTTP is sessionless. Pin to the latest stable version. + // behavior. Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions. Pin to the latest stable version. await using var mcpClient = await ConnectAsync(transportOptions: transportOptions, configureClient: options => options.ProtocolVersion = "2025-11-25"); // Do a tool call to ensure there's more than just the initialize request @@ -722,7 +722,7 @@ public async Task Client_CanReconnect_AfterSessionExpiry() // Connect the first client and verify it works. // Server-side session expiry and reconnect rely on session IDs, a legacy stateful behavior. - // Under draft, Streamable HTTP is sessionless. Pin both clients to the latest stable version. + // Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions. Pin both clients to the latest stable version. var client1 = await ConnectAsync(configureClient: options => options.ProtocolVersion = "2025-11-25"); var originalSessionId = client1.SessionId; Assert.NotNull(originalSessionId); @@ -797,7 +797,7 @@ public async Task EndpointFilter_CanReadSessionId_BeforeAndAfterHandler() await app.StartAsync(TestContext.Current.CancellationToken); // The stateful (else) branch below asserts session-ID behavior, which only exists under the - // legacy handshake; the draft revision is sessionless. Pin legacy only for the stateful variant. + // legacy handshake. Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions. Pin legacy only for the stateful variant. await using var client = await ConnectAsync(configureClient: options => { if (!Stateless) diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.Mrtr.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.Mrtr.cs index e5c5a123f..3d8abb0f1 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.Mrtr.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.Mrtr.cs @@ -10,12 +10,12 @@ namespace ModelContextProtocol.AspNetCore.Tests; public abstract partial class MapMcpTests { - // Draft is sessionless (SEP-2567): the Streamable HTTP handler refuses a sessionless draft request - // when the server opted into sessions (Stateless = false), so a draft-pinned client downgrades to - // legacy instead of negotiating 2026-07-28. These draft MRTR tests therefore can't run on the - // stateful Streamable HTTP fixture; the same coverage runs on the stateless and legacy-SSE fixtures. - private const string DraftStatefulStreamableHttpSkipReason = - "Draft is sessionless (SEP-2567); stateful Streamable HTTP refuses sessionless draft. Covered by the stateless and SSE fixtures."; + // Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions (SEP-2567): + // the handler refuses a request when the server opted into sessions (Stateless = false), so a client pinned + // to that revision downgrades to legacy instead of negotiating 2026-07-28. These MRTR tests therefore can't + // run on the stateful Streamable HTTP fixture; the same coverage runs on the stateless and legacy-SSE fixtures. + private const string July2026StatefulStreamableHttpSkipReason = + "Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions (SEP-2567); stateful Streamable HTTP refuses it. Covered by the stateless and SSE fixtures."; private ServerMessageTracker ConfigureServer(params Delegate[] tools) { @@ -25,7 +25,7 @@ private ServerMessageTracker ConfigureServer(params Delegate[] tools) options.ServerInfo = new Implementation { Name = "MrtrTestServer", Version = "1" }; // Do not pin a protocol version - let it be negotiated based on what the client requests. // 2026-07-28 is in SupportedProtocolVersions, so an opt-in client gets it; others get - // the latest non-draft. + // the latest legacy version. messageTracker.AddFilters(options.Filters.Message); }) .WithHttpTransport(ConfigureStateless) @@ -40,8 +40,8 @@ private Task ConnectExperimentalAsync() => options.ProtocolVersion = "2026-07-28"; }); - // The default client now negotiates draft (2026-07-28). The legacy JSON-RPC MRTR back-compat - // resolver only applies to legacy clients, so pin these to the latest non-draft version. + // The default client now negotiates the 2026-07-28 protocol revision. The legacy + // JSON-RPC MRTR back-compat resolver only applies to legacy clients, so pin these to the latest legacy version. private Task ConnectLegacyAsync() => ConnectAsync(configureClient: options => { @@ -169,16 +169,16 @@ private static async Task MrtrMixed(McpServer server, RequestContext configureClient = experimentalClient ? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2026-07-28"; } - // ProtocolVersion null now defaults to draft, so pin the legacy client explicitly to keep dual-era coverage. + // ProtocolVersion null now defaults to the 2026-07-28 protocol revision, so pin the legacy client explicitly to keep dual-era coverage. : options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2025-11-25"; }; // The await-style portion of this tool calls server.SampleAsync/ElicitAsync on round 3. @@ -289,10 +289,10 @@ public async Task Mrtr_ParallelAwaits(bool experimentalClient) // Parallel awaits work with regular JSON-RPC but fail with MRTR because // MrtrContext only supports one exchange at a time (TrySetResult gate). Assert.SkipWhen(Stateless, "Await-style API requires handler suspension (stateful only)."); - // Under the draft protocol revision (SEP-2567), the server is implicitly stateless for draft - // clients, so parallel-await MRTR can't reach its concurrency gate. Skip the experimental-client + // Starting with the 2026-07-28 protocol revision (SEP-2567), the server is implicitly stateless for + // clients on that revision, so parallel-await MRTR can't reach its concurrency gate. Skip the experimental-client // case for the same reason as Mrtr_MixedExceptionAndAwaitStyle. - Assert.SkipWhen(experimentalClient, "Await-style MRTR requires session affinity; draft protocol revision (SEP-2567) is sessionless."); + Assert.SkipWhen(experimentalClient, "Await-style MRTR requires session affinity; starting with the 2026-07-28 protocol revision (SEP-2567) Streamable HTTP no longer supports sessions."); ConfigureServer(MrtrParallelAwait); await using var app = Builder.Build(); @@ -301,7 +301,7 @@ public async Task Mrtr_ParallelAwaits(bool experimentalClient) Action configureClient = experimentalClient ? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2026-07-28"; } - // ProtocolVersion null now defaults to draft, so pin the legacy client explicitly to keep dual-era coverage. + // ProtocolVersion null now defaults to the 2026-07-28 protocol revision, so pin the legacy client explicitly to keep dual-era coverage. : options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2025-11-25"; }; await using var client = await ConnectAsync(configureClient: configureClient); @@ -357,7 +357,7 @@ private static string MrtrElicit(RequestContext context) [Fact] public async Task Mrtr_Roots_CompletesViaMrtr() { - Assert.SkipWhen(UseStreamableHttp && !Stateless, DraftStatefulStreamableHttpSkipReason); + Assert.SkipWhen(UseStreamableHttp && !Stateless, July2026StatefulStreamableHttpSkipReason); var messageTracker = ConfigureServer( [McpServerTool(Name = "mrtr-roots")] (RequestContext context) => @@ -435,7 +435,7 @@ private static string MrtrMulti(RequestContext context) [InlineData(false)] public async Task Mrtr_MultiRoundTrip_Completes(bool experimentalClient) { - Assert.SkipWhen(experimentalClient && UseStreamableHttp && !Stateless, DraftStatefulStreamableHttpSkipReason); + Assert.SkipWhen(experimentalClient && UseStreamableHttp && !Stateless, July2026StatefulStreamableHttpSkipReason); var messageTracker = ConfigureServer(MrtrMulti); await using var app = Builder.Build(); @@ -445,7 +445,7 @@ public async Task Mrtr_MultiRoundTrip_Completes(bool experimentalClient) // Configure client - experimental or default based on parameter. Action configureClient = experimentalClient ? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2026-07-28"; } - // ProtocolVersion null now defaults to draft, so pin the legacy client explicitly to keep dual-era coverage. + // ProtocolVersion null now defaults to the 2026-07-28 protocol revision, so pin the legacy client explicitly to keep dual-era coverage. : options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2025-11-25"; }; await using var client = await ConnectAsync(configureClient: configureClient); @@ -484,7 +484,7 @@ public async Task Mrtr_MultiRoundTrip_Completes(bool experimentalClient) [InlineData(false)] public async Task Mrtr_IsMrtrSupported(bool experimentalClient) { - Assert.SkipWhen(experimentalClient && UseStreamableHttp && !Stateless, DraftStatefulStreamableHttpSkipReason); + Assert.SkipWhen(experimentalClient && UseStreamableHttp && !Stateless, July2026StatefulStreamableHttpSkipReason); ConfigureServer([McpServerTool(Name = "mrtr-check")] (McpServer server) => server.IsMrtrSupported.ToString()); await using var app = Builder.Build(); @@ -494,7 +494,7 @@ public async Task Mrtr_IsMrtrSupported(bool experimentalClient) // Configure client - experimental or default based on parameter. Action configureClient = experimentalClient ? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2026-07-28"; } - // ProtocolVersion null now defaults to draft, so pin the legacy client explicitly to keep dual-era coverage. + // ProtocolVersion null now defaults to the 2026-07-28 protocol revision, so pin the legacy client explicitly to keep dual-era coverage. : options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2025-11-25"; }; await using var client = await ConnectAsync(configureClient: configureClient); Assert.Equal(experimentalClient ? "2026-07-28" : "2025-11-25", client.NegotiatedProtocolVersion); @@ -550,7 +550,7 @@ private static string MrtrConcurrentThree(RequestContext [Fact] public async Task Mrtr_ConcurrentThreeInputs_ResolvedSimultaneously() { - Assert.SkipWhen(UseStreamableHttp && !Stateless, DraftStatefulStreamableHttpSkipReason); + Assert.SkipWhen(UseStreamableHttp && !Stateless, July2026StatefulStreamableHttpSkipReason); var messageTracker = ConfigureServer(MrtrConcurrentThree); await using var app = Builder.Build(); @@ -604,7 +604,7 @@ public async Task Mrtr_ConcurrentThreeInputs_ResolvedSimultaneously() [Fact] public async Task Mrtr_LoadShedding_RequestStateOnly_CompletesViaMrtr() { - Assert.SkipWhen(UseStreamableHttp && !Stateless, DraftStatefulStreamableHttpSkipReason); + Assert.SkipWhen(UseStreamableHttp && !Stateless, July2026StatefulStreamableHttpSkipReason); var messageTracker = ConfigureServer( [McpServerTool(Name = "mrtr-loadshed")] (RequestContext context) => diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs index b269c9951..ef6832101 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs @@ -111,8 +111,8 @@ public async Task Messages_FromNewUser_AreRejected() await app.StartAsync(TestContext.Current.CancellationToken); - // Session-scoped user validation across requests is a legacy stateful-session behavior; the - // draft revision is sessionless. Pin to the latest stable version to keep covering it. + // Session-scoped user validation across requests is a legacy stateful-session behavior. Starting with the + // 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions. Pin to the latest stable version to keep covering it. var httpRequestException = await Assert.ThrowsAsync( () => ConnectAsync(configureClient: options => options.ProtocolVersion = "2025-11-25")); Assert.Equal(HttpStatusCode.Forbidden, httpRequestException.StatusCode); @@ -163,8 +163,8 @@ public async Task Sampling_DoesNotCloseStreamPrematurely() await using var mcpClient = await ConnectAsync(configureClient: options => { // Server->client sampling over the open response stream is a stateful-session behavior. - // Under draft, Streamable HTTP is forced sessionless, so the implicit-MRTR suspend path - // doesn't apply over HTTP (draft sampling is covered by the stdio MRTR tests). Pin legacy. + // Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions, so the implicit-MRTR suspend path + // doesn't apply over HTTP (this sampling path is covered by the stdio MRTR tests). Pin legacy. options.ProtocolVersion = "2025-11-25"; options.Handlers.SamplingHandler = async (parameters, _, _) => { @@ -326,9 +326,9 @@ await client.CallToolAsync("echo_with_user_name", new Dictionary { ["message"] = "hi" }, cancellationToken: TestContext.Current.CancellationToken); - // The client now defaults to the draft revision, whose handshake is server/discover + // The client now defaults to the 2026-07-28 protocol revision, whose handshake is server/discover // rather than the legacy initialize request. On the stateful Streamable HTTP fixture the - // sessionless draft request is refused, so the client downgrades to the legacy initialize. + // request is refused, so the client downgrades to the legacy initialize. var expectedHandshakeMethod = UseStreamableHttp && !Stateless ? RequestMethods.Initialize : RequestMethods.ServerDiscover; @@ -387,7 +387,7 @@ public async Task OutgoingFilter_SeesResponsesAndRequests() await using var client = await ConnectAsync(configureClient: opts => { // Server-originated sampling requests and the initialize response are legacy stateful - // behaviors; the draft revision routes sampling through MRTR and drops initialize. + // behaviors; the 2026-07-28 protocol revision routes sampling through MRTR and drops initialize. opts.ProtocolVersion = "2025-11-25"; opts.Capabilities = clientOptions.Capabilities; opts.Handlers = clientOptions.Handlers; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MrtrProtocolTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MrtrProtocolTests.cs index 15c415683..68076e292 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MrtrProtocolTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MrtrProtocolTests.cs @@ -13,10 +13,10 @@ namespace ModelContextProtocol.AspNetCore.Tests; /// -/// Protocol-level tests for Multi Round-Trip Requests (MRTR) over the draft revision. -/// Under the draft protocol (SEP-2575 + SEP-2567) Streamable HTTP is sessionless, so these tests -/// drive the default server with raw, sessionless -/// draft JSON-RPC requests (no initialize, no Mcp-Session-Id) and verify the explicit +/// Protocol-level tests for Multi Round-Trip Requests (MRTR) over the 2026-07-28 protocol revision. +/// Under that revision (SEP-2575 + SEP-2567) Streamable HTTP no longer supports sessions, so these tests +/// drive the default server with raw +/// JSON-RPC requests (no initialize, no Mcp-Session-Id) and verify the explicit /// MRTR structure, retry with inputResponses, and error handling. /// Stateful-session MRTR behaviors (implicit handler suspension, disposal cancellation) are covered /// over stdio by MrtrHandlerLifecycleTests, and unknown-session rejection by @@ -24,7 +24,6 @@ namespace ModelContextProtocol.AspNetCore.Tests; /// public class MrtrProtocolTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable { - private const string DraftVersion = McpHttpHeaders.DraftProtocolVersion; private WebApplication? _app; @@ -143,12 +142,12 @@ static CallToolResult (RequestContext context) => _app.MapMcp(); await _app.StartAsync(TestContext.Current.CancellationToken); - // Drive the server with sessionless draft requests: every request carries the draft + // Drive the server with raw requests: every request carries the 2026-07-28 protocol // MCP-Protocol-Version header and (via PostJsonRpcAsync) the SEP-2243 Mcp-Method/Mcp-Name // headers. No initialize handshake and no Mcp-Session-Id. HttpClient.DefaultRequestHeaders.Accept.Add(new("application/json")); HttpClient.DefaultRequestHeaders.Accept.Add(new("text/event-stream")); - HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion); + HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpHttpHeaders.July2026ProtocolVersion); } public async ValueTask DisposeAsync() @@ -303,7 +302,7 @@ static string (RequestContext context) => HttpClient.DefaultRequestHeaders.Accept.Add(new("application/json")); HttpClient.DefaultRequestHeaders.Accept.Add(new("text/event-stream")); - // Initialize with the current (non-draft) protocol so the server's backcompat resolver runs. + // Initialize with the current legacy protocol so the server's backcompat resolver runs. var initJson = """ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{"roots":{}},"clientInfo":{"name":"BackcompatTestClient","version":"1.0.0"}}} """; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/RawHttpConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/RawHttpConformanceTests.cs index 4ec8ba4d9..050bd428b 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/RawHttpConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/RawHttpConformanceTests.cs @@ -15,11 +15,10 @@ namespace ModelContextProtocol.AspNetCore.Tests; /// Wire-format conformance tests for the Streamable HTTP server driven directly via , /// without going through . These hand-craft HTTP /// requests and assert the exact status codes / response bodies the server emits for the SEP-2575 + -/// SEP-2567 (sessionless, no-initialize) draft revision. +/// SEP-2567 2026-07-28 protocol revision. /// public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable { - private const string DraftVersion = McpHttpHeaders.DraftProtocolVersion; private const string ProtocolVersionHeader = "MCP-Protocol-Version"; private WebApplication? _app; @@ -83,22 +82,22 @@ private static async Task ReadJsonResponseAsync(HttpResponseMessage re return JsonNode.Parse(body)!; } - private static string DraftMetaFragment(string protocolVersion = DraftVersion) => + private static string July2026ProtocolMetaFragment(string protocolVersion = McpHttpHeaders.July2026ProtocolVersion) => @"""_meta"":{""io.modelcontextprotocol/protocolVersion"":""" + protocolVersion + @""",""io.modelcontextprotocol/clientInfo"":{""name"":""raw"",""version"":""1.0""}," + @"""io.modelcontextprotocol/clientCapabilities"":{}}"; [Fact] - public async Task DraftToolsCall_WithFullMeta_Succeeds_200() + public async Task July2026ToolsCall_WithFullMeta_Succeeds_200() { await StartAsync(); var body = @"{""jsonrpc"":""2.0"",""id"":1,""method"":""tools/call"",""params"":{""name"":""echo"",""arguments"":{""text"":""hi""}," + - DraftMetaFragment() + "}}"; + July2026ProtocolMetaFragment() + "}}"; using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) }; - request.Headers.Add(ProtocolVersionHeader, DraftVersion); + request.Headers.Add(ProtocolVersionHeader, McpHttpHeaders.July2026ProtocolVersion); request.Headers.Add("Mcp-Method", "tools/call"); request.Headers.Add("Mcp-Name", "echo"); using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken); @@ -107,7 +106,8 @@ public async Task DraftToolsCall_WithFullMeta_Succeeds_200() var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken); Assert.Equal("echo:hi", json["result"]!["content"]![0]!["text"]!.GetValue()); - // Per SEP-2567 draft is sessionless: server MUST NOT issue a Mcp-Session-Id. + // Per SEP-2567, starting with the 2026-07-28 protocol revision Streamable HTTP no longer + // supports sessions: the server MUST NOT issue a Mcp-Session-Id. Assert.False(response.Headers.Contains("mcp-session-id")); } @@ -116,17 +116,17 @@ public async Task ServerDiscover_RawPost_ReturnsDiscoverResult() { await StartAsync(); - var body = @"{""jsonrpc"":""2.0"",""id"":1,""method"":""server/discover"",""params"":{" + DraftMetaFragment() + "}}"; + var body = @"{""jsonrpc"":""2.0"",""id"":1,""method"":""server/discover"",""params"":{" + July2026ProtocolMetaFragment() + "}}"; using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) }; - request.Headers.Add(ProtocolVersionHeader, DraftVersion); + request.Headers.Add(ProtocolVersionHeader, McpHttpHeaders.July2026ProtocolVersion); request.Headers.Add("Mcp-Method", "server/discover"); using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken); var supported = json["result"]!["supportedVersions"]!.AsArray().Select(n => n!.GetValue()).ToList(); - Assert.Contains(DraftVersion, supported); + Assert.Contains(McpHttpHeaders.July2026ProtocolVersion, supported); // Spec PR #2855 makes ttlMs and cacheScope required on DiscoverResult; the server emits the // safest defaults (immediately stale, not shareable) when the application hasn't customized. @@ -136,13 +136,13 @@ public async Task ServerDiscover_RawPost_ReturnsDiscoverResult() } [Fact] - public async Task DraftPost_WithUnsupportedProtocolVersionHeader_Returns400_With_Minus32004() + public async Task July2026Post_WithUnsupportedProtocolVersionHeader_Returns400_With_Minus32022() { await StartAsync(); var body = @"{""jsonrpc"":""2.0"",""id"":1,""method"":""tools/call"",""params"":{""name"":""echo"",""arguments"":{""text"":""x""}," + - DraftMetaFragment("9999-99-99") + "}}"; + July2026ProtocolMetaFragment("9999-99-99") + "}}"; using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) }; request.Headers.Add(ProtocolVersionHeader, "9999-99-99"); @@ -150,7 +150,7 @@ public async Task DraftPost_WithUnsupportedProtocolVersionHeader_Returns400_With request.Headers.Add("Mcp-Name", "echo"); using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken); - // Per spec/streamable-http.mdx the server MUST return 400 Bad Request with -32004 and a data payload + // Per spec/streamable-http.mdx the server MUST return 400 Bad Request with -32022 and a data payload // listing the supported versions. The dual-era client uses this to switch versions without fallback. Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken); @@ -160,25 +160,25 @@ public async Task DraftPost_WithUnsupportedProtocolVersionHeader_Returns400_With Assert.NotNull(data); Assert.Equal("9999-99-99", data!["requested"]!.GetValue()); var supported = data["supported"]!.AsArray().Select(n => n!.GetValue()).ToList(); - Assert.Contains(DraftVersion, supported); + Assert.Contains(McpHttpHeaders.July2026ProtocolVersion, supported); } [Fact] - public async Task DraftPost_ProtocolVersionHeaderMetaMismatch_ReturnsHeaderMismatch_Minus32001() + public async Task July2026Post_ProtocolVersionHeaderMetaMismatch_ReturnsHeaderMismatch_Minus32020() { await StartAsync(); - // The MCP-Protocol-Version header declares the draft revision, but the per-request _meta declares a + // The MCP-Protocol-Version header declares the 2026-07-28 protocol revision, but the per-request _meta declares a // different (still individually supported) version. Per SEP-2575 the server MUST reject the - // disagreement. It uses -32001 HeaderMismatch (the same code as the Mcp-Method/Mcp-Name header-vs-body - // checks) so a conformant draft client surfaces the error instead of mistaking the modern server for a + // disagreement. It uses -32020 HeaderMismatch (the same code as the Mcp-Method/Mcp-Name header-vs-body + // checks) so a conformant client on this revision surfaces the error instead of mistaking the modern server for a // legacy one and falling back to the initialize handshake. var body = @"{""jsonrpc"":""2.0"",""id"":1,""method"":""server/discover"",""params"":{" + - DraftMetaFragment("2025-11-25") + "}}"; + July2026ProtocolMetaFragment("2025-11-25") + "}}"; using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) }; - request.Headers.Add(ProtocolVersionHeader, DraftVersion); + request.Headers.Add(ProtocolVersionHeader, McpHttpHeaders.July2026ProtocolVersion); request.Headers.Add("Mcp-Method", "server/discover"); using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/RequestAbortCancellationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/RequestAbortCancellationTests.cs index 501d698a6..807daaefe 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/RequestAbortCancellationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/RequestAbortCancellationTests.cs @@ -13,7 +13,7 @@ namespace ModelContextProtocol.AspNetCore.Tests; /// Verifies that aborting an HTTP request flows cancellation into the running request handler's /// . /// -/// Under the draft protocol revision (SEP-2575 + SEP-2567) the HTTP request lifetime is the +/// Starting with the 2026-07-28 protocol revision (SEP-2575 + SEP-2567) the HTTP request lifetime is the /// request lifetime: there are no sessions, so a dropped connection is equivalent to cancelling the /// in-flight request. The same holds for legacy stateless mode, where each request is independent and /// outlived by nothing. These tests pin that behavior so a tool's fires @@ -22,7 +22,6 @@ namespace ModelContextProtocol.AspNetCore.Tests; /// public class RequestAbortCancellationTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable { - private const string DraftVersion = McpHttpHeaders.DraftProtocolVersion; private WebApplication? _app; @@ -84,13 +83,14 @@ public async ValueTask DisposeAsync() } [Fact] - public async Task DraftSessionlessRequest_AbortFlowsCancellationToToolHandler() + public async Task July2026Request_AbortFlowsCancellationToToolHandler() { - // Draft is sessionless (SEP-2567) and is served natively only on a stateless server; a - // Stateless=false server refuses sessionless draft so dual-era clients fall back to initialize. + // Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions (SEP-2567) and is + // served natively only on a stateless server; a Stateless=false server refuses these requests so dual-era + // clients fall back to initialize. await StartAsync(stateless: true); - using var request = CreateBlockingToolRequest(draft: true); + using var request = CreateBlockingToolRequest(july2026Protocol: true); await AssertAbortCancelsToolAsync(request); } @@ -100,19 +100,19 @@ public async Task StatelessRequest_AbortFlowsCancellationToToolHandler() { await StartAsync(stateless: true); - using var request = CreateBlockingToolRequest(draft: false); + using var request = CreateBlockingToolRequest(july2026Protocol: false); await AssertAbortCancelsToolAsync(request); } - private static HttpRequestMessage CreateBlockingToolRequest(bool draft) + private static HttpRequestMessage CreateBlockingToolRequest(bool july2026Protocol) { - // Draft tools/call requires the SEP-2243 Mcp-Method/Mcp-Name headers and the per-request _meta + // A 2026-07-28 tools/call requires the SEP-2243 Mcp-Method/Mcp-Name headers and the per-request _meta // (protocol version, client info, capabilities) that replaces the initialize handshake (SEP-2567). - var body = draft + var body = july2026Protocol ? """ - {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"blockingTool","_meta":{"io.modelcontextprotocol/protocolVersion":"DRAFT_VERSION","io.modelcontextprotocol/clientInfo":{"name":"raw","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}} - """.Replace("DRAFT_VERSION", DraftVersion) + {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"blockingTool","_meta":{"io.modelcontextprotocol/protocolVersion":"PROTOCOL_VERSION","io.modelcontextprotocol/clientInfo":{"name":"raw","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}} + """.Replace("PROTOCOL_VERSION", McpHttpHeaders.July2026ProtocolVersion) : """{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"blockingTool"}}"""; var request = new HttpRequestMessage(HttpMethod.Post, "") @@ -123,9 +123,9 @@ private static HttpRequestMessage CreateBlockingToolRequest(bool draft) request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); - if (draft) + if (july2026Protocol) { - request.Headers.Add("MCP-Protocol-Version", DraftVersion); + request.Headers.Add("MCP-Protocol-Version", McpHttpHeaders.July2026ProtocolVersion); request.Headers.Add("Mcp-Method", "tools/call"); request.Headers.Add("Mcp-Name", "blockingTool"); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ResumabilityIntegrationTestsBase.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ResumabilityIntegrationTestsBase.cs index c79207c2f..29e69483e 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ResumabilityIntegrationTestsBase.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ResumabilityIntegrationTestsBase.cs @@ -517,9 +517,9 @@ protected async Task ConnectClientAsync() TransportMode = HttpTransportMode.StreamableHttp, }, HttpClient, LoggerFactory); - // Resumability (Last-Event-ID) and Mcp-Session-Id are removed in the draft revision - // (SEP-2567). Pin the client to the latest stable version so it negotiates the stateful, - // resumable legacy handshake instead of the sessionless draft default. + // Resumability (Last-Event-ID) and Mcp-Session-Id are removed in the 2026-07-28 protocol + // revision (SEP-2567). Pin the client to the latest stable version so it negotiates the stateful, + // resumable legacy handshake instead of the 2026-07-28 default. return await McpClient.CreateAsync(transport, new McpClientOptions { ProtocolVersion = "2025-11-25" }, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs index c0990737e..82fb9c020 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs @@ -88,13 +88,46 @@ public async ValueTask DisposeAsync() } } +/// +/// Shared fixture that starts a single stateless ConformanceServer for the +/// SEP-2322 MRTR scenarios in . Those scenarios negotiate the +/// 2026-07-28 revision, which is served only on a stateless server, so they +/// cannot reuse the stateful . Reusing one server across all +/// the MRTR theory rows avoids the TCP TIME_WAIT conflicts that per-test restarts on a single port +/// cause on Windows. Uses a dedicated port range (303x) so it runs in parallel with the stateful +/// fixture (300x), the caching server (301x), and the SEP-2243 servers (302x) without colliding. +/// +public sealed class StatelessMrtrConformanceServerFixture : IAsyncLifetime +{ + private StatelessConformanceServer? _server; + + public string ServerUrl => _server?.ServerUrl + ?? throw new InvalidOperationException("The stateless conformance server has not been started."); + + public async ValueTask InitializeAsync() + { + _server = await StatelessConformanceServer.StartAsync(CancellationToken.None, basePort: 3031); + } + + public async ValueTask DisposeAsync() + { + if (_server is not null) + { + await _server.DisposeAsync(); + } + } +} + /// /// Runs the official MCP conformance tests against the ConformanceServer. /// Uses a shared so the server is started once /// and reused across all tests, avoiding TCP port conflicts on Windows. /// -public class ServerConformanceTests(ConformanceServerFixture fixture, ITestOutputHelper output) - : IClassFixture +public class ServerConformanceTests( + ConformanceServerFixture fixture, + StatelessMrtrConformanceServerFixture statelessFixture, + ITestOutputHelper output) + : IClassFixture, IClassFixture { [Fact] public async Task RunConformanceTests() @@ -143,8 +176,8 @@ public async Task RunConformanceTest_HttpHeaderValidation() !NodeHelpers.HasSep2243Scenarios(), "SEP-2243 conformance scenarios not available (requires conformance package >= 0.2.0)."); - // SEP-2243 is a draft (2026-07-28) scenario that uses the stateless lifecycle, so it - // requires a stateless server (a stateful server rejects the un-initialized list/call + // SEP-2243 is a 2026-07-28 protocol revision scenario that uses the stateless + // lifecycle, so it requires a stateless server (a stateful server rejects the un-initialized list/call // requests with JSON-RPC -32000). Use a dedicated port range so it never collides with // the stateful class fixture (300x) or the caching stateless server (301x). await using var server = await StatelessConformanceServer.StartAsync( @@ -180,18 +213,20 @@ public async Task RunConformanceTest_HttpCustomHeaderServerValidation() // ConformanceServer.Tools.IncompleteResultTools and ConformanceServer.Prompts.IncompleteResultPrompts // (the class names predate the conformance-suite rename from "incomplete-result-*" to // "input-required-result-*"; the wire-level tool names now match the new convention). - // Each scenario uses the conformance harness's RawMcpSession, which negotiates 2026-07-28 - // so the csharp-sdk emits InputRequiredResult on the wire. These tests skip until the - // installed conformance package ships SEP-2322 scenarios and emits this SDK's - // draft wire string (see ). + // Each scenario uses the conformance harness's RawMcpSession, which negotiates 2026-07-28, + // so the csharp-sdk emits InputRequiredResult on the wire. Because the 2026-07-28 revision is + // served only on a stateless server, the scenarios run against a dedicated stateless server + // (StatelessMrtrConformanceServerFixture); a stateful server refuses these requests. + // These tests skip until the installed conformance package ships SEP-2322 scenarios + // (see ). // // input-required-result-tampered-state and input-required-result-capability-check are // implemented by ConformanceServer.Tools.IncompleteResultTools.ToolWithTamperedState // (HMAC-protected requestState; a tampered requestState surfaces a -32602 JSON-RPC error) // and ToolWithCapabilityCheck (gates inputRequests on the per-request // _meta clientCapabilities envelope). Both behaviors also have in-process wire-level - // regression coverage in MrtrProtocolTests so they stay verified even while the published - // conformance package's draft wire string lags this SDK. + // regression coverage in MrtrProtocolTests so they stay verified independent of the + // published conformance package. [Theory] [InlineData("input-required-result-basic-elicitation")] [InlineData("input-required-result-basic-sampling")] @@ -210,10 +245,10 @@ public async Task RunConformanceTest_HttpCustomHeaderServerValidation() public async Task RunMrtrConformanceTest(string scenario) { Assert.SkipWhen(!NodeHelpers.IsNodeInstalled(), "Node.js is not installed. Skipping conformance tests."); - Assert.SkipWhen(!NodeHelpers.HasMrtrScenarios(), "SEP-2322 MRTR conformance scenarios not yet available in the published @modelcontextprotocol/conformance package (or installed version uses a stale draft wire string)."); + Assert.SkipWhen(!NodeHelpers.HasMrtrScenarios(), "SEP-2322 MRTR conformance scenarios not yet available in the published @modelcontextprotocol/conformance package."); - var result = await RunConformanceTestsAsync( - $"server --url {fixture.ServerUrl} --scenario {scenario}"); + var result = await RunStatelessConformanceTestAsync( + $"server --url {statelessFixture.ServerUrl} --scenario {scenario} --spec-version 2026-07-28"); Assert.True(result.Success, $"MRTR conformance test '{scenario}' failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}"); @@ -227,7 +262,7 @@ public async Task RunMrtrConformanceTest(string scenario) cancellationToken: TestContext.Current.CancellationToken); } - // For draft scenarios that pin --spec-version explicitly, suppress the + // For 2026-07-28 protocol scenarios that pin --spec-version explicitly, suppress the // MCP_CONFORMANCE_PROTOCOL_VERSION override so a duplicate --spec-version is not appended. private async Task<(bool Success, string Output, string Error)> RunStatelessConformanceTestAsync(string arguments) { diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs index 849941d8e..57b12d246 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs @@ -20,7 +20,7 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper private WebApplication? _app; private readonly List _deleteRequestSessionIds = []; - // Don't add the delete endpoint by default to ensure the client still works with basic sessionless servers. + // Don't add the delete endpoint by default to ensure the client still works with basic stateless servers. private async Task StartAsync(bool enableDelete = false) { Builder.Services.Configure(options => @@ -128,7 +128,7 @@ private async Task StartResumeServerAsync(string expectedSessi } [Fact] - public async Task CanCallToolOnSessionlessStreamableHttpServer() + public async Task CanCallToolOnStatelessStreamableHttpServer() { await StartAsync(); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs index d3a3681dd..1ff58f978 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs @@ -916,13 +916,13 @@ public async Task McpServer_UsedOutOfScope_CanSendNotifications() #region SEP-2243 Header Validation Tests [Fact] - public async Task DraftVersion_RejectsMissingMcpMethodHeader() + public async Task July2026ProtocolVersion_RejectsMissingMcpMethodHeader() { - // Draft is sessionless and served only on a stateless server (SEP-2567). + // Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions (SEP-2567) and is served only on a stateless server. await StartAsync(stateless: true); - // Initialize with draft version to enable header validation - await CallInitializeWithDraftVersionAndValidateAsync(); + // Initialize with the 2026-07-28 protocol version to enable header validation + await CallInitializeWithJuly2026ProtocolVersionAndValidateAsync(); // Send a tools/call request without Mcp-Method header — should be rejected using var request = new HttpRequestMessage(HttpMethod.Post, ""); @@ -935,10 +935,10 @@ public async Task DraftVersion_RejectsMissingMcpMethodHeader() } [Fact] - public async Task DraftVersion_RejectsMismatchedMcpMethodHeader() + public async Task July2026ProtocolVersion_RejectsMismatchedMcpMethodHeader() { await StartAsync(stateless: true); - await CallInitializeWithDraftVersionAndValidateAsync(); + await CallInitializeWithJuly2026ProtocolVersionAndValidateAsync(); // Send a tools/call request but set Mcp-Method to wrong value using var request = new HttpRequestMessage(HttpMethod.Post, ""); @@ -951,10 +951,10 @@ public async Task DraftVersion_RejectsMismatchedMcpMethodHeader() } [Fact] - public async Task DraftVersion_AcceptsCorrectMcpMethodHeader() + public async Task July2026ProtocolVersion_AcceptsCorrectMcpMethodHeader() { await StartAsync(stateless: true); - await CallInitializeWithDraftVersionAndValidateAsync(); + await CallInitializeWithJuly2026ProtocolVersionAndValidateAsync(); // Send a tools/call request with correct Mcp-Method and Mcp-Name headers using var request = new HttpRequestMessage(HttpMethod.Post, ""); @@ -968,12 +968,12 @@ public async Task DraftVersion_AcceptsCorrectMcpMethodHeader() } [Fact] - public async Task NonDraftVersion_DoesNotRequireMcpMethodHeader() + public async Task LegacyVersion_DoesNotRequireMcpMethodHeader() { await StartAsync(); await CallInitializeAndValidateAsync(); - // With non-draft version, Mcp-Method header is not required + // With the legacy version, Mcp-Method header is not required using var request = new HttpRequestMessage(HttpMethod.Post, ""); request.Content = JsonContent(CallTool("echo", """{"message":"hello"}""")); request.Headers.Add("MCP-Protocol-Version", "2025-03-26"); @@ -983,12 +983,12 @@ public async Task NonDraftVersion_DoesNotRequireMcpMethodHeader() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } - private async Task CallInitializeWithDraftVersionAndValidateAsync() + private async Task CallInitializeWithJuly2026ProtocolVersionAndValidateAsync() { HttpClient.DefaultRequestHeaders.Remove("mcp-session-id"); using var request = new HttpRequestMessage(HttpMethod.Post, ""); - request.Content = JsonContent(InitializeRequestDraft); + request.Content = JsonContent(InitializeRequestJuly2026Protocol); request.Headers.Add("MCP-Protocol-Version", "2026-07-28"); request.Headers.Add("Mcp-Method", "initialize"); @@ -996,11 +996,11 @@ private async Task CallInitializeWithDraftVersionAndValidateAsync() var rpcResponse = await AssertSingleSseResponseAsync(response); AssertServerInfo(rpcResponse); - // Draft protocol revision (SEP-2567) is sessionless; the server does not return mcp-session-id. - // Subsequent requests carry MCP-Protocol-Version=2026-07-28 to opt back into the draft path. + // Starting with the 2026-07-28 protocol revision (SEP-2567), Streamable HTTP no longer supports sessions; the server does not return mcp-session-id. + // Subsequent requests carry MCP-Protocol-Version=2026-07-28 so each one is handled independently. } - private static string InitializeRequestDraft => """ + private static string InitializeRequestJuly2026Protocol => """ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"IntegrationTestClient","version":"1.0.0"}}} """; diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index 342cf9743..13af5d170 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -36,11 +36,12 @@ }, }; -// The default client now prefers the draft revision (probing with server/discover and falling back -// to a legacy initialize handshake). The "initialize" and "sse-retry" scenarios specifically exercise -// the legacy initialize handshake and SSE resumability (removed in draft) and strictly expect -// initialize as the first message, so pin them to the latest stable version. Other scenarios run on -// the draft default and exercise the server/discover probe plus the transparent legacy fallback. +// The default client now prefers the 2026-07-28 protocol (probing with server/discover and +// falling back to a legacy initialize handshake). The "initialize" and "sse-retry" scenarios +// specifically exercise the legacy initialize handshake and SSE resumability (removed in the +// 2026-07-28 protocol) and strictly expect initialize as the first message, so pin them to the +// latest stable version. Other scenarios run on the 2026-07-28 default and exercise the +// server/discover probe plus the transparent legacy fallback. if (scenario is "initialize" or "sse-retry") { options.ProtocolVersion = "2025-11-25"; diff --git a/tests/ModelContextProtocol.ConformanceServer/Program.cs b/tests/ModelContextProtocol.ConformanceServer/Program.cs index 9416ec8db..b1de1aa98 100644 --- a/tests/ModelContextProtocol.ConformanceServer/Program.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Program.cs @@ -25,8 +25,8 @@ public static async Task MainAsync(string[] args, ILoggerProvider? loggerProvide // because .NET does not have a built-in concurrent HashSet ConcurrentDictionary> subscriptions = new(); - // Allow running the server in the SEP-2575 stateless lifecycle, which the draft - // "caching" (SEP-2549) conformance scenario requires. A "--stateless true|false" + // Allow running the server in the SEP-2575 stateless lifecycle, which the 2026-07-28 + // protocol's "caching" (SEP-2549) conformance scenario requires. A "--stateless true|false" // command-line switch (read via configuration) takes precedence so an in-process test // fixture can opt in or out per-instance deterministically; when it is not supplied, // fall back to the MCP_CONFORMANCE_STATELESS environment variable for standalone runs. diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs index f434c6e01..f93b6ab2b 100644 --- a/tests/ModelContextProtocol.TestSseServer/Program.cs +++ b/tests/ModelContextProtocol.TestSseServer/Program.cs @@ -428,7 +428,7 @@ public static async Task MainAsync(string[] args, ILoggerProvider? loggerProvide .WithHttpTransport(options => { // The test fixture exercises legacy stateful behaviors (SSE + session-id flows). - // Set Stateless = false explicitly now that draft (SEP-2567) defaults to true. + // Set Stateless = false explicitly now that the 2026-07-28 protocol (SEP-2567) defaults to true. options.Stateless = false; options.EnableLegacySse = true; }); diff --git a/tests/ModelContextProtocol.Tests/Client/DraftConnectionTests.cs b/tests/ModelContextProtocol.Tests/Client/July2026ProtocolConnectionTests.cs similarity index 65% rename from tests/ModelContextProtocol.Tests/Client/DraftConnectionTests.cs rename to tests/ModelContextProtocol.Tests/Client/July2026ProtocolConnectionTests.cs index 659bfa4b0..a06548a4f 100644 --- a/tests/ModelContextProtocol.Tests/Client/DraftConnectionTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/July2026ProtocolConnectionTests.cs @@ -7,17 +7,16 @@ namespace ModelContextProtocol.Tests.Client; /// -/// Tests for the draft protocol revision (SEP-2575 + SEP-2567) connection flow on -/// — the client should call server/discover instead of -/// initialize when is set to -/// . +/// Connection-flow tests for the 2026-07-28 protocol revision (SEP-2575 + SEP-2567) +/// on . A client that requests +/// calls server/discover rather than +/// initialize. /// -public class DraftConnectionTests : ClientServerTestBase +public class July2026ProtocolConnectionTests : ClientServerTestBase { - private const string DraftVersion = McpHttpHeaders.DraftProtocolVersion; private const string LatestStableVersion = "2025-11-25"; - public DraftConnectionTests(ITestOutputHelper testOutputHelper) + public July2026ProtocolConnectionTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, startServer: false) { } @@ -26,31 +25,31 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer { services.Configure(options => { - options.ServerInfo = new Implementation { Name = nameof(DraftConnectionTests), Version = "1.0" }; + options.ServerInfo = new Implementation { Name = nameof(July2026ProtocolConnectionTests), Version = "1.0" }; }); } [Fact] - public async Task DraftClient_ConnectingToDraftServer_NegotiatesDraftVersion() + public async Task Client_RequestingJuly2026Protocol_NegotiatesIt() { StartServer(); - var options = new McpClientOptions { ProtocolVersion = DraftVersion }; + var options = new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion }; await using var client = await CreateMcpClientForServer(options); - Assert.Equal(DraftVersion, client.NegotiatedProtocolVersion); + Assert.Equal(McpHttpHeaders.July2026ProtocolVersion, client.NegotiatedProtocolVersion); Assert.NotNull(client.ServerCapabilities); - Assert.Equal(nameof(DraftConnectionTests), client.ServerInfo.Name); + Assert.Equal(nameof(July2026ProtocolConnectionTests), client.ServerInfo.Name); } [Fact] - public async Task LegacyClient_ConnectingToDraftServer_NegotiatesLegacyVersion() + public async Task Client_RequestingLegacyVersion_NegotiatesLegacy() { StartServer(); await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = LatestStableVersion }); - Assert.NotEqual(DraftVersion, client.NegotiatedProtocolVersion); + Assert.NotEqual(McpHttpHeaders.July2026ProtocolVersion, client.NegotiatedProtocolVersion); } [Fact] @@ -70,11 +69,11 @@ public async Task LegacyClient_CanCallServerDiscover() Assert.NotNull(discoverResult); Assert.NotEmpty(discoverResult.SupportedVersions); Assert.Contains(LatestStableVersion, discoverResult.SupportedVersions); - Assert.Equal(nameof(DraftConnectionTests), discoverResult.ServerInfo.Name); + Assert.Equal(nameof(July2026ProtocolConnectionTests), discoverResult.ServerInfo.Name); } [Fact] - public async Task DraftServer_DiscoverIncludesDraftVersion() + public async Task ServerDiscover_IncludesJuly2026ProtocolVersion() { StartServer(); @@ -86,6 +85,6 @@ public async Task DraftServer_DiscoverIncludesDraftVersion() var discoverResult = JsonSerializer.Deserialize(response.Result, McpJsonUtilities.DefaultOptions); Assert.NotNull(discoverResult); - Assert.Contains(DraftVersion, discoverResult.SupportedVersions); + Assert.Contains(McpHttpHeaders.July2026ProtocolVersion, discoverResult.SupportedVersions); } } diff --git a/tests/ModelContextProtocol.Tests/Client/DraftProtocolFallbackTests.cs b/tests/ModelContextProtocol.Tests/Client/July2026ProtocolFallbackTests.cs similarity index 81% rename from tests/ModelContextProtocol.Tests/Client/DraftProtocolFallbackTests.cs rename to tests/ModelContextProtocol.Tests/Client/July2026ProtocolFallbackTests.cs index ee624ede0..f2bc24cb5 100644 --- a/tests/ModelContextProtocol.Tests/Client/DraftProtocolFallbackTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/July2026ProtocolFallbackTests.cs @@ -8,34 +8,29 @@ namespace ModelContextProtocol.Tests.Client; /// -/// Regression tests for the draft-protocol-to-legacy fallback path in -/// . These verify that a client configured with -/// McpClientOptions.ProtocolVersion = McpSession.DraftProtocolVersion -/// correctly probes for a draft-aware server with server/discover, falls -/// back to the legacy initialize handshake when the server is legacy, -/// and accepts whatever supported protocol version the legacy server -/// negotiates - including a version different from the one the client -/// originally requested. +/// Regression tests for the fallback from the 2026-07-28 protocol revision to a legacy protocol in +/// . With default options (ProtocolVersion = null) the client prefers +/// 2026-07-28 but probes with server/discover, falls back to the legacy initialize +/// handshake when the server is legacy, and accepts whatever supported protocol version the legacy +/// server negotiates. Pinning ProtocolVersion to 2026-07-28 instead makes it the +/// minimum too, so the client refuses to fall back. /// /// -/// The originally shipped logic in PerformLegacyInitializeAsync compared -/// the server's response against _options.ProtocolVersion, which under -/// draft is "2026-07-28". When the legacy server downgraded to (say) -/// "2025-06-18", the comparison threw, even though the legacy -/// negotiation succeeded. These tests guard against that regression. +/// The originally shipped logic in PerformLegacyInitializeAsync compared the server's response +/// against the requested version and threw when a legacy server downgraded to (say) "2025-06-18", +/// even though the legacy negotiation succeeded. These tests guard against that regression. /// -public class DraftProtocolFallbackTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) +public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) { [Fact] - public async Task DraftClient_OnMethodNotFound_FallsBackTo_Initialize_AcceptsDowngradedVersion() + public async Task Client_OnMethodNotFound_FallsBackTo_Initialize_AcceptsDowngradedVersion() { var ct = TestContext.Current.CancellationToken; await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-06-18"); - await using var client = await McpClient.CreateAsync(transport, new McpClientOptions - { - ProtocolVersion = McpSession.DraftProtocolVersion, - }, loggerFactory: LoggerFactory, cancellationToken: ct); + // Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback. + await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(), + loggerFactory: LoggerFactory, cancellationToken: ct); Assert.True(transport.ServerDiscoverProbed); Assert.True(transport.LegacyInitializeReceived); @@ -43,24 +38,23 @@ public async Task DraftClient_OnMethodNotFound_FallsBackTo_Initialize_AcceptsDow } [Fact] - public async Task DraftClient_OnInvalidParams_FallsBackTo_Initialize() + public async Task Client_OnInvalidParams_FallsBackTo_Initialize() { var ct = TestContext.Current.CancellationToken; await using var transport = new LegacyServerTestTransport( serverNegotiatedVersion: "2025-11-25", probeErrorCode: (int)McpErrorCode.InvalidParams); - await using var client = await McpClient.CreateAsync(transport, new McpClientOptions - { - ProtocolVersion = McpSession.DraftProtocolVersion, - }, loggerFactory: LoggerFactory, cancellationToken: ct); + // Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback. + await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(), + loggerFactory: LoggerFactory, cancellationToken: ct); Assert.True(transport.LegacyInitializeReceived); Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion); } [Fact] - public async Task DraftClient_WithMinProtocolVersion_RefusesFallback_BelowMinimum() + public async Task Client_WithPinnedJuly2026Version_RefusesFallback_ToLegacyServer() { var ct = TestContext.Current.CancellationToken; await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-06-18"); @@ -69,13 +63,13 @@ public async Task DraftClient_WithMinProtocolVersion_RefusesFallback_BelowMinimu { await using var client = await McpClient.CreateAsync(transport, new McpClientOptions { - ProtocolVersion = McpSession.DraftProtocolVersion, - MinProtocolVersion = McpSession.DraftProtocolVersion, + // Pinning the version makes it the minimum too, so the client refuses to fall back. + ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion, }, loggerFactory: LoggerFactory, cancellationToken: ct); }); Assert.IsType(exception); - Assert.Contains("minimum", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("2026-07-28", exception.Message, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -98,9 +92,9 @@ public async Task LegacyClient_WithExplicitPin_StillRequires_ExactVersionMatch() } [Fact] - public async Task DraftClient_OnHeaderMismatch_Surfaces_NoFallback() + public async Task Client_OnHeaderMismatch_Surfaces_NoFallback() { - // The peer is modern (returns the spec-defined -32001 HeaderMismatch on the probe). + // The peer is modern (returns the spec-defined -32020 HeaderMismatch on the probe). // Falling back to legacy initialize would just produce another malformed envelope. // Verify the connect-time logic surfaces the error to the caller instead of falling back. var ct = TestContext.Current.CancellationToken; @@ -112,7 +106,7 @@ public async Task DraftClient_OnHeaderMismatch_Surfaces_NoFallback() { await using var client = await McpClient.CreateAsync(transport, new McpClientOptions { - ProtocolVersion = McpSession.DraftProtocolVersion, + ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion, }, loggerFactory: LoggerFactory, cancellationToken: ct); }); @@ -122,7 +116,7 @@ public async Task DraftClient_OnHeaderMismatch_Surfaces_NoFallback() } [Fact] - public async Task DraftClient_OnSilentProbe_FallsBackTo_Initialize_AfterConfiguredProbeTimeout() + public async Task Client_OnSilentProbe_FallsBackTo_Initialize_AfterConfiguredProbeTimeout() { // Simulate a legacy server that silently drops the unknown server/discover method (it never // responds to the probe). The client must fall back to legacy initialize once the configured @@ -133,9 +127,9 @@ public async Task DraftClient_OnSilentProbe_FallsBackTo_Initialize_AfterConfigur silentDiscoverProbe: true); var stopwatch = Stopwatch.StartNew(); + // Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback. await using var client = await McpClient.CreateAsync(transport, new McpClientOptions { - ProtocolVersion = McpSession.DraftProtocolVersion, DiscoverProbeTimeout = TimeSpan.FromMilliseconds(250), InitializationTimeout = TestConstants.DefaultTimeout, }, loggerFactory: LoggerFactory, cancellationToken: ct); diff --git a/tests/ModelContextProtocol.Tests/Client/DraftListMetaEmissionTests.cs b/tests/ModelContextProtocol.Tests/Client/July2026ProtocolListMetaEmissionTests.cs similarity index 70% rename from tests/ModelContextProtocol.Tests/Client/DraftListMetaEmissionTests.cs rename to tests/ModelContextProtocol.Tests/Client/July2026ProtocolListMetaEmissionTests.cs index 71215cdad..19ff40287 100644 --- a/tests/ModelContextProtocol.Tests/Client/DraftListMetaEmissionTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/July2026ProtocolListMetaEmissionTests.cs @@ -9,30 +9,29 @@ namespace ModelContextProtocol.Tests.Client; /// /// Verifies that the C# client emits the SEP-2575 _meta envelope on every list-style -/// request (and on server/discover) under the draft protocol revision, even when the -/// caller supplies no RequestOptions / no params. +/// request (and on server/discover) under the 2026-07-28 protocol revision, even when +/// the caller supplies no RequestOptions / no params. /// /// /// Spec PR #2759 promotes params._meta to required on tools/list, /// resources/list, resources/templates/list, prompts/list, and -/// server/discover under draft. This test class drives the C# client through -/// with the draft revision negotiated, attaches a request +/// server/discover. This test class drives the C# client through +/// with the 2026-07-28 protocol negotiated, attaches a request /// filter on each list endpoint that captures the incoming _meta envelope, and asserts /// the three required SEP-2575 keys are present: /// io.modelcontextprotocol/protocolVersion, /// io.modelcontextprotocol/clientInfo, and /// io.modelcontextprotocol/clientCapabilities. /// -public class DraftListMetaEmissionTests : ClientServerTestBase +public class July2026ProtocolListMetaEmissionTests : ClientServerTestBase { - private const string DraftVersion = McpHttpHeaders.DraftProtocolVersion; private const string LatestStableVersion = "2025-11-25"; // Captured _meta envelopes for each request method we exercise. Populated by the per-method // server-side filters and asserted from each test method. private readonly Dictionary _capturedMeta = new(StringComparer.Ordinal); - public DraftListMetaEmissionTests(ITestOutputHelper testOutputHelper) + public July2026ProtocolListMetaEmissionTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, startServer: false) { } @@ -74,62 +73,62 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer } [Fact] - public async Task DraftClient_ListTools_NoOptions_EmitsRequiredMeta() + public async Task Client_ListTools_NoOptions_EmitsRequiredMeta() { StartServer(); - await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = DraftVersion }); + await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion }); await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); - AssertDraftMetaPresent(RequestMethods.ToolsList); + AssertRequiredMetaPresent(RequestMethods.ToolsList); } [Fact] - public async Task DraftClient_ListPrompts_NoOptions_EmitsRequiredMeta() + public async Task Client_ListPrompts_NoOptions_EmitsRequiredMeta() { StartServer(); - await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = DraftVersion }); + await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion }); await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); - AssertDraftMetaPresent(RequestMethods.PromptsList); + AssertRequiredMetaPresent(RequestMethods.PromptsList); } [Fact] - public async Task DraftClient_ListResources_NoOptions_EmitsRequiredMeta() + public async Task Client_ListResources_NoOptions_EmitsRequiredMeta() { StartServer(); - await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = DraftVersion }); + await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion }); await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); - AssertDraftMetaPresent(RequestMethods.ResourcesList); + AssertRequiredMetaPresent(RequestMethods.ResourcesList); } [Fact] - public async Task DraftClient_ListResourceTemplates_NoOptions_EmitsRequiredMeta() + public async Task Client_ListResourceTemplates_NoOptions_EmitsRequiredMeta() { StartServer(); - await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = DraftVersion }); + await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion }); await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken); - AssertDraftMetaPresent(RequestMethods.ResourcesTemplatesList); + AssertRequiredMetaPresent(RequestMethods.ResourcesTemplatesList); } [Fact] - public async Task DraftClient_ServerDiscover_EmitsRequiredMeta() + public async Task Client_ServerDiscover_EmitsRequiredMeta() { // server/discover has no public List-style helper; we drive it via SendRequestAsync directly, - // which still flows through the client's draft-meta injector. + // which still flows through the client's per-request _meta injector. StartServer(); - await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = DraftVersion }); + await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion }); // Hook the server-side handler invocation via a notification handler is awkward here; assert // instead by sending the request and parsing the wire-shape echo from the response context. - // Easier path: rely on the existing JsonRpcRequest capture in the message context — see the - // raw conformance tests for the wire-level proof. For this in-process test, we instead drive - // the request and rely on the response being a valid DiscoverResult; the draft meta injector + // Easier path: rely on the existing JsonRpcRequest capture in the message context (see the + // raw conformance tests for the wire-level proof). For this in-process test, we instead drive + // the request and rely on the response being a valid DiscoverResult; the _meta injector // would otherwise have failed the server's per-request envelope validation. var response = await client.SendRequestAsync( new JsonRpcRequest { Method = RequestMethods.ServerDiscover }, @@ -137,19 +136,19 @@ public async Task DraftClient_ServerDiscover_EmitsRequiredMeta() Assert.NotNull(response.Result); var discover = JsonSerializer.Deserialize(response.Result, McpJsonUtilities.DefaultOptions)!; - Assert.Contains(DraftVersion, discover.SupportedVersions); + Assert.Contains(McpHttpHeaders.July2026ProtocolVersion, discover.SupportedVersions); - // The server enforces draft envelope shape per request; if the client had omitted _meta, the - // request would have failed with -32602 / -32003 rather than returning a DiscoverResult. The + // The server enforces the per-request envelope shape; if the client had omitted _meta, the + // request would have failed with -32602 / -32021 rather than returning a DiscoverResult. The // successful round-trip is the assertion. } [Fact] - public async Task LegacyClient_ListTools_DoesNotEmitDraftMeta() + public async Task LegacyClient_ListTools_DoesNotEmitMeta() { - // Sanity guard: the legacy (non-draft) client must NOT emit the SEP-2575 envelope — the meta - // injector is gated on the negotiated protocol version. If this ever started writing draft keys - // under legacy protocols, every legacy server would reject the request. + // Sanity guard: a client on the session-supporting (legacy) protocol must NOT emit the SEP-2575 + // envelope. The injector is gated on the negotiated protocol version; if it ever started writing + // those keys on a legacy request, every legacy server would reject it. StartServer(); await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = LatestStableVersion }); @@ -164,7 +163,7 @@ public async Task LegacyClient_ListTools_DoesNotEmitDraftMeta() } } - private void AssertDraftMetaPresent(string method) + private void AssertRequiredMetaPresent(string method) { Assert.True(_capturedMeta.TryGetValue(method, out var meta), $"No capture for {method}"); Assert.NotNull(meta); @@ -175,7 +174,7 @@ private void AssertDraftMetaPresent(string method) Assert.True(meta.ContainsKey(MetaKeys.ClientCapabilities), $"Missing clientCapabilities key on {method} _meta envelope"); - // The protocolVersion value must match the negotiated draft version. - Assert.Equal(DraftVersion, meta[MetaKeys.ProtocolVersion]!.GetValue()); + // The protocolVersion value must match the negotiated 2026-07-28 protocol version. + Assert.Equal(McpHttpHeaders.July2026ProtocolVersion, meta[MetaKeys.ProtocolVersion]!.GetValue()); } } diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs index 1a0785630..42af028b2 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs @@ -178,7 +178,7 @@ public virtual Task SendMessageAsync(JsonRpcMessage message, CancellationToken c Result = JsonSerializer.SerializeToNode(new DiscoverResult { Capabilities = new ServerCapabilities(), - SupportedVersions = [McpSession.DraftProtocolVersion], + SupportedVersions = [McpHttpHeaders.July2026ProtocolVersion], ServerInfo = new Implementation { Name = "NopTransport", diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientMetaTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientMetaTests.cs index 18cd5e232..e1e9a08db 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientMetaTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientMetaTests.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Tests.Client; public class McpClientMetaTests : ClientServerTestBase { - // InitializeMeta is carried on the legacy initialize request, which the draft revision removes. + // InitializeMeta is carried on the legacy initialize request, which the 2026-07-28 protocol removes. // The two InitializeMeta_* tests pin to the latest stable version so the handshake actually runs. private const string LatestStableVersion = "2025-11-25"; diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs index e3d90bced..4dda7bc38 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs @@ -584,7 +584,7 @@ public async Task AsClientLoggerProvider_MessagesSentToClient() public async Task ReturnsNegotiatedProtocolVersion(string? protocolVersion) { await using McpClient client = await CreateMcpClientForServer(new() { ProtocolVersion = protocolVersion }); - // A null ProtocolVersion now prefers the draft revision, which the reactive test server advertises. + // A null ProtocolVersion now prefers the 2026-07-28 protocol, which the reactive test server advertises. Assert.Equal(protocolVersion ?? "2026-07-28", client.NegotiatedProtocolVersion); } @@ -795,7 +795,7 @@ await Assert.ThrowsAsync("requestParams", [Fact] public async Task ServerCanPingClient() { - // ping is a legacy-only RPC (removed in the draft revision per SEP-2575), so pin the client + // ping is a legacy-only RPC (removed in the 2026-07-28 protocol per SEP-2575), so pin the client // to a legacy protocol version to exercise the server-initiated ping round-trip. await using McpClient client = await CreateMcpClientForServer(new() { ProtocolVersion = "2025-11-25" }); diff --git a/tests/ModelContextProtocol.Tests/Client/McpRequestHeadersTests.cs b/tests/ModelContextProtocol.Tests/Client/McpRequestHeadersTests.cs index 3dd944ce5..5868c3c63 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpRequestHeadersTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpRequestHeadersTests.cs @@ -18,7 +18,7 @@ public void McpHttpHeaders_HasCorrectValues() [Fact] public void McpErrorCode_HeaderMismatch_HasCorrectValue() { - Assert.Equal(-32001, (int)McpErrorCode.HeaderMismatch); + Assert.Equal(-32020, (int)McpErrorCode.HeaderMismatch); } [Theory] diff --git a/tests/ModelContextProtocol.Tests/Client/MrtrIntegrationTests.cs b/tests/ModelContextProtocol.Tests/Client/MrtrIntegrationTests.cs index b22bcffc3..70a9d1112 100644 --- a/tests/ModelContextProtocol.Tests/Client/MrtrIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/MrtrIntegrationTests.cs @@ -165,7 +165,7 @@ public async Task LegacyRequestOnMrtrSession_LogsWarning() Model = "test-model" }); - // Start the client task — it will send server/discover (draft) and block waiting for response + // Start the client task. It will send server/discover (2026-07-28 protocol) and block waiting for response var clientTask = McpClient.CreateAsync( new StreamClientTransport( clientToServer.Writer.AsStream(), @@ -180,7 +180,7 @@ public async Task LegacyRequestOnMrtrSession_LogsWarning() var serverReader = new StreamReader(clientToServer.Reader.AsStream()); var serverWriter = serverToClient.Writer.AsStream(); - // Read the server/discover request from client (draft revision skips initialize per SEP-2575). + // Read the server/discover request from client (the 2026-07-28 protocol skips initialize per SEP-2575). var discoverLine = await serverReader.ReadLineAsync(TestContext.Current.CancellationToken); Assert.NotNull(discoverLine); var discoverRequest = JsonSerializer.Deserialize(discoverLine, McpJsonUtilities.DefaultOptions); @@ -200,7 +200,7 @@ public async Task LegacyRequestOnMrtrSession_LogsWarning() }; await WriteJsonRpcAsync(serverWriter, discoverResponse); - // Client is now connected with MRTR negotiated (no initialized notification under draft). + // Client is now connected with MRTR negotiated (no initialized notification under the 2026-07-28 protocol). await using var client = await clientTask; Assert.Equal("2026-07-28", client.NegotiatedProtocolVersion); @@ -406,7 +406,7 @@ public async Task IncompleteResultRetry_OmittingRequestState_StripsStaleStateFro var clientToServer = new Pipe(); var serverToClient = new Pipe(); - // Pin to the draft revision so the client performs the server/discover handshake and + // Pin to the 2026-07-28 protocol so the client performs the server/discover handshake and // treats InputRequiredResult as an MRTR round-trip. var clientOptions = new McpClientOptions { ProtocolVersion = "2026-07-28" }; clientOptions.Handlers.ElicitationHandler = (_, _) => diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 3d62f8636..7690a75f9 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -33,8 +33,8 @@ public async Task ConnectAndPing_Stdio(string clientId) // Arrange // Act - // ping was removed in the draft revision (SEP-2575), so pin to the latest stable - // protocol version to keep exercising the legacy ping RPC. Draft liveness relies on + // ping was removed in the 2026-07-28 protocol (SEP-2575), so pin to the latest stable + // protocol version to keep exercising the legacy ping RPC. The 2026-07-28 protocol relies on // the transport/request lifecycle instead of an explicit ping. await using var client = await _fixture.CreateClientAsync(clientId, new McpClientOptions { ProtocolVersion = "2025-11-25" }); await client.PingAsync(cancellationToken: TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs index 3b20f39c2..45cf0379c 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs @@ -74,7 +74,7 @@ public async Task AddIncomingMessageFilter_Intercepts_Request_Messages() { List messageTypes = []; - // Under the draft protocol the client performs a server/discover + tools/list exchange (no + // Under the 2026-07-28 protocol the client performs a server/discover + tools/list exchange (no // fire-and-forget initialized notification), so the tools/list request is a deterministic // synchronization point. Gate recording to it and signal once the filter finishes so a // regression that invokes the filter pipeline more than once per message surfaces as an extra entry. @@ -112,7 +112,7 @@ public async Task AddIncomingMessageFilter_Intercepts_Request_Messages() [Fact] public async Task AddIncomingMessageFilter_Multiple_Filters_Execute_In_Order() { - // Under the draft protocol the client performs a server/discover + tools/list exchange (no + // Under the 2026-07-28 protocol the client performs a server/discover + tools/list exchange (no // fire-and-forget initialized notification), so the tools/list request is a deterministic // synchronization point. Gate the filter logging to it and signal once the outermost filter // finishes so the assertions observe a complete, stable log. @@ -393,7 +393,7 @@ public async Task AddOutgoingMessageFilter_Sees_Responses_Notifications_And_Requ var clientOptions = new McpClientOptions { // This test observes the legacy outgoing flow on the server side: the initialize response and - // the server->client sampling/createMessage request. Under the draft protocol those are replaced + // the server->client sampling/createMessage request. Under the 2026-07-28 protocol those are replaced // by server/discover and implicit MRTR (InputRequiredResult), which is covered by MrtrIntegrationTests. // Pin to the latest stable version to keep exercising the legacy server->client request path here. ProtocolVersion = LatestStableVersion, diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index 795b380a4..4182957cf 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -128,12 +128,12 @@ public async Task Can_List_And_Call_Registered_Prompts() [Fact] public async Task Can_Be_Notified_Of_Prompt_Changes() { - // Under the draft revision, list-changed notifications are delivered only over a + // Under the 2026-07-28 protocol, list-changed notifications are delivered only over a // subscriptions/listen stream (covered by SubscriptionsListenTests). This test pins the // legacy revision to keep coverage of the session-wide broadcast that legacy clients still rely on. await using McpClient client = await CreateMcpClientForServer(new McpClientOptions { - ProtocolVersion = McpSession.LatestProtocolVersion, + ProtocolVersion = McpHttpHeaders.November2025ProtocolVersion, }); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs index 5630d2b77..d8fd0a231 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -162,12 +162,12 @@ public async Task Can_List_And_Call_Registered_ResourceTemplates() [Fact] public async Task Can_Be_Notified_Of_Resource_Changes() { - // Under the draft revision, list-changed notifications are delivered only over a + // Under the 2026-07-28 protocol, list-changed notifications are delivered only over a // subscriptions/listen stream (covered by SubscriptionsListenTests). This test pins the // legacy revision to keep coverage of the session-wide broadcast that legacy clients still rely on. await using McpClient client = await CreateMcpClientForServer(new McpClientOptions { - ProtocolVersion = McpSession.LatestProtocolVersion, + ProtocolVersion = McpHttpHeaders.November2025ProtocolVersion, }); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index 79eace963..5359ec73c 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -188,12 +188,12 @@ public async Task Can_Create_Multiple_Servers_From_Options_And_List_Registered_T [Fact] public async Task Can_Be_Notified_Of_Tool_Changes() { - // Under the draft revision, list-changed notifications are delivered only over a + // Under the 2026-07-28 protocol, list-changed notifications are delivered only over a // subscriptions/listen stream (covered by SubscriptionsListenTests). This test pins the // legacy revision to keep coverage of the session-wide broadcast that legacy clients still rely on. await using McpClient client = await CreateMcpClientForServer(new McpClientOptions { - ProtocolVersion = McpSession.LatestProtocolVersion, + ProtocolVersion = McpHttpHeaders.November2025ProtocolVersion, }); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs index 9a78045b9..3c070130c 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs @@ -59,7 +59,7 @@ private async Task AssertMatchAsync( /// /// Asserts that the given URI does NOT match the template. Uses the default client, which - /// negotiates the draft protocol revision, so the unknown-resource response carries the + /// negotiates the 2026-07-28 protocol revision, so the unknown-resource response carries the /// standard JSON-RPC (-32602). The version-gated /// legacy mapping to (-32002) is covered by /// . @@ -79,7 +79,7 @@ private async Task AssertNoMatchAsync( } // Unknown-resource-URI responses are version-gated: older clients keep the legacy - // -32002 (McpErrorCode.ResourceNotFound), and clients on the draft protocol version that + // -32002 (McpErrorCode.ResourceNotFound), and clients on the 2026-07-28 protocol version that // moves to the standard JSON-RPC code see -32602 (McpErrorCode.InvalidParams). [Theory] [InlineData("2025-11-25", McpErrorCode.ResourceNotFound)] @@ -134,7 +134,7 @@ public async Task MultipleTemplatedResources_MatchesCorrectResource() // Literal template braces in URI should not match (template literal is not a valid URI) var mcpEx = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync("test://params{?a1,a2,a3}", null, TestContext.Current.CancellationToken)); - // Draft maps an unmatched resource URI to InvalidParams (-32602); the legacy -32002 ResourceNotFound + // The 2026-07-28 protocol maps an unmatched resource URI to InvalidParams (-32602); the legacy -32002 ResourceNotFound // mapping is covered by the version-gated ResourceNotFound_ErrorCode_IsVersionGated theory. Assert.Equal(McpErrorCode.InvalidParams, mcpEx.ErrorCode); Assert.Equal("Request failed (remote): Unknown resource URI: 'test://params{?a1,a2,a3}'", mcpEx.Message); diff --git a/tests/ModelContextProtocol.Tests/DiagnosticTests.cs b/tests/ModelContextProtocol.Tests/DiagnosticTests.cs index acaa1b9ae..db6c2c2c4 100644 --- a/tests/ModelContextProtocol.Tests/DiagnosticTests.cs +++ b/tests/ModelContextProtocol.Tests/DiagnosticTests.cs @@ -92,7 +92,7 @@ await WaitForAsync( Assert.Equal(clientListToolsCall.TraceId, serverListToolsCall.TraceId); // Validate that the client trace context encoded to request.params._meta[traceparent]. - // Under the draft revision _meta also carries the per-request envelope (protocolVersion, + // Under the 2026-07-28 protocol _meta also carries the per-request envelope (protocolVersion, // clientInfo, clientCapabilities), so assert on the traceparent property specifically // rather than the entire _meta object. using var listToolsJson = JsonDocument.Parse(clientToServerLog.First(s => s.Contains("\"method\":\"tools/list\""))); diff --git a/tests/ModelContextProtocol.Tests/Protocol/DiscoverProtocolTests.cs b/tests/ModelContextProtocol.Tests/Protocol/DiscoverProtocolTests.cs index 1e32062da..71afd8980 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/DiscoverProtocolTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/DiscoverProtocolTests.cs @@ -5,7 +5,7 @@ namespace ModelContextProtocol.Tests.Protocol; /// -/// Serialization tests for the request/result types introduced by the draft protocol revision (SEP-2575). +/// Serialization tests for the request/result types introduced by the 2026-07-28 protocol revision (SEP-2575). /// public static class DiscoverProtocolTests { diff --git a/tests/ModelContextProtocol.Tests/Protocol/DiscoverResultCacheableTests.cs b/tests/ModelContextProtocol.Tests/Protocol/DiscoverResultCacheableTests.cs index 001c05f95..248bf7768 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/DiscoverResultCacheableTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/DiscoverResultCacheableTests.cs @@ -17,7 +17,7 @@ public static class DiscoverResultCacheableTests { private static DiscoverResult NewDiscoverResult() => new() { - SupportedVersions = ["2025-11-25", McpSession.DraftProtocolVersion], + SupportedVersions = [McpHttpHeaders.November2025ProtocolVersion, McpHttpHeaders.July2026ProtocolVersion], Capabilities = new ServerCapabilities(), ServerInfo = new Implementation { Name = "test-server", Version = "1.0" }, }; diff --git a/tests/ModelContextProtocol.Tests/Protocol/JsonRpcMessageConverterTests.cs b/tests/ModelContextProtocol.Tests/Protocol/JsonRpcMessageConverterTests.cs index ab172aa1e..c23d34bf6 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/JsonRpcMessageConverterTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/JsonRpcMessageConverterTests.cs @@ -766,7 +766,7 @@ public static void Deserialize_ErrorWithNullId_IsValid() { // Per JSON-RPC 2.0 §5.1, when an error occurs before the request id can be determined // (parse error or invalid request), the server MUST respond with id=null. This shape is - // produced by some peers (e.g. Python's simple-streamablehttp-stateless on a draft probe) + // produced by some peers (e.g. Python's simple-streamablehttp-stateless on a 2026-07-28 probe) // and must be accepted so the HTTP-fallback path can recognize the structured signal. string json = """{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Bad Request"}}"""; diff --git a/tests/ModelContextProtocol.Tests/Protocol/DraftErrorDataTests.cs b/tests/ModelContextProtocol.Tests/Protocol/July2026ProtocolErrorDataTests.cs similarity index 96% rename from tests/ModelContextProtocol.Tests/Protocol/DraftErrorDataTests.cs rename to tests/ModelContextProtocol.Tests/Protocol/July2026ProtocolErrorDataTests.cs index fd82f9e0b..88e3e5644 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/DraftErrorDataTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/July2026ProtocolErrorDataTests.cs @@ -4,9 +4,9 @@ namespace ModelContextProtocol.Tests.Protocol; /// -/// Serialization tests for the error data payloads introduced by the draft protocol revision (SEP-2575). +/// Serialization tests for the error data payloads introduced by the 2026-07-28 protocol revision (SEP-2575). /// -public static class DraftErrorDataTests +public static class July2026ProtocolErrorDataTests { [Fact] public static void UnsupportedProtocolVersionErrorData_SerializationRoundTrip_PreservesAllProperties() diff --git a/tests/ModelContextProtocol.Tests/Protocol/SubscriptionsListenProtocolTests.cs b/tests/ModelContextProtocol.Tests/Protocol/SubscriptionsListenProtocolTests.cs index c2d48b888..65c26e035 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/SubscriptionsListenProtocolTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/SubscriptionsListenProtocolTests.cs @@ -5,7 +5,7 @@ namespace ModelContextProtocol.Tests.Protocol; /// -/// Serialization tests for the subscriptions/listen types introduced by the draft protocol revision (SEP-2575). +/// Serialization tests for the subscriptions/listen types introduced by the 2026-07-28 protocol revision (SEP-2575). /// public static class SubscriptionsListenProtocolTests { diff --git a/tests/ModelContextProtocol.Tests/Server/DraftProtocolBackcompatTests.cs b/tests/ModelContextProtocol.Tests/Server/July2026ProtocolBackcompatTests.cs similarity index 88% rename from tests/ModelContextProtocol.Tests/Server/DraftProtocolBackcompatTests.cs rename to tests/ModelContextProtocol.Tests/Server/July2026ProtocolBackcompatTests.cs index ca6134a24..4d78d9435 100644 --- a/tests/ModelContextProtocol.Tests/Server/DraftProtocolBackcompatTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/July2026ProtocolBackcompatTests.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Tests.Server; /// Verifies that the server-to-client request methods (, /// , /// ) keep working when the negotiated protocol revision is -/// 2026-07-28 on a stateful session - for example, stdio. +/// 2026-07-28 on a stateful transport - for example, stdio. /// /// /// Under 2026-07-28 the spec removes the corresponding server-to-client request methods, but @@ -17,12 +17,12 @@ namespace ModelContextProtocol.Tests.Server; /// throw "X is not supported in stateless mode" because is /// ). Stdio is implicitly stateful - one per process - so the /// legacy elicitation/create / sampling/createMessage / roots/list flow still works. -/// A future PR is expected to force 2026-07-28 Streamable HTTP servers to stateless mode, at which -/// point those configurations will start throwing through the existing stateless guard. +/// Starting with 2026-07-28, Streamable HTTP servers are stateless by default, so those configurations +/// throw through the existing stateless guard unless the author explicitly opts back into sessions. /// -public sealed class DraftProtocolBackcompatTests : ClientServerTestBase +public sealed class July2026ProtocolBackcompatTests : ClientServerTestBase { - public DraftProtocolBackcompatTests(ITestOutputHelper testOutputHelper) + public July2026ProtocolBackcompatTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, startServer: false) { } @@ -42,7 +42,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer } [Fact] - public async Task ElicitAsync_OnStatefulDraftSession_ResolvesViaLegacyRequest() + public async Task ElicitAsync_OnStatefulTransport_ResolvesViaLegacyRequest() { StartServer(); await using var client = await CreateMcpClientForServer(new McpClientOptions @@ -64,7 +64,7 @@ public async Task ElicitAsync_OnStatefulDraftSession_ResolvesViaLegacyRequest() } [Fact] - public async Task SampleAsync_OnStatefulDraftSession_ResolvesViaLegacyRequest() + public async Task SampleAsync_OnStatefulTransport_ResolvesViaLegacyRequest() { StartServer(); await using var client = await CreateMcpClientForServer(new McpClientOptions @@ -91,7 +91,7 @@ public async Task SampleAsync_OnStatefulDraftSession_ResolvesViaLegacyRequest() } [Fact] - public async Task RequestRootsAsync_OnStatefulDraftSession_ResolvesViaLegacyRequest() + public async Task RequestRootsAsync_OnStatefulTransport_ResolvesViaLegacyRequest() { StartServer(); await using var client = await CreateMcpClientForServer(new McpClientOptions diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs index e11152301..8fd1d9954 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs @@ -651,19 +651,17 @@ public void OutputSchema_Create_StringReturn_NoEnvelope() Assert.False(tool.ProtocolTool.OutputSchema.Value.TryGetProperty("properties", out _)); } - // SEP-2106 backward-compat: for clients negotiating a pre-2026-06-30 protocol version, + // SEP-2106 backward-compat: for clients negotiating a pre-2026-07-28 protocol version, // non-object structured content is wrapped in the legacy {"result": } envelope. - // Clients on the SEP-2106 protocol ("2026-06-30" and later, including the draft) see the - // natural value shape. In-memory storage stays natural in both modes — only the wire + // Clients on the SEP-2106 protocol ("2026-07-28" and later) see the + // natural value shape. In-memory storage stays natural in both modes; only the wire // emission flips. private const string LegacyProtocolVersion = "2025-11-25"; - private const string DraftSep2106ProtocolVersion = "DRAFT-2026-06-v1"; - private const string Sep2106ProtocolVersion = "2026-06-30"; + private const string Sep2106ProtocolVersion = "2026-07-28"; [Theory] [InlineData(LegacyProtocolVersion, true)] [InlineData(null, true)] - [InlineData(DraftSep2106ProtocolVersion, false)] [InlineData(Sep2106ProtocolVersion, false)] public async Task StructuredContent_StringReturn_WrapsForLegacyClients(string? protocolVersion, bool expectWrapped) { @@ -689,7 +687,6 @@ public async Task StructuredContent_StringReturn_WrapsForLegacyClients(string? p [Theory] [InlineData(LegacyProtocolVersion, true)] [InlineData(null, true)] - [InlineData(DraftSep2106ProtocolVersion, false)] [InlineData(Sep2106ProtocolVersion, false)] public async Task StructuredContent_IntegerReturn_WrapsForLegacyClients(string? protocolVersion, bool expectWrapped) { @@ -715,7 +712,6 @@ public async Task StructuredContent_IntegerReturn_WrapsForLegacyClients(string? [Theory] [InlineData(LegacyProtocolVersion, true)] [InlineData(null, true)] - [InlineData(DraftSep2106ProtocolVersion, false)] [InlineData(Sep2106ProtocolVersion, false)] public async Task StructuredContent_ArrayReturn_WrapsForLegacyClients(string? protocolVersion, bool expectWrapped) { @@ -742,12 +738,11 @@ public async Task StructuredContent_ArrayReturn_WrapsForLegacyClients(string? pr [Theory] [InlineData(LegacyProtocolVersion)] [InlineData(null)] - [InlineData(DraftSep2106ProtocolVersion)] [InlineData(Sep2106ProtocolVersion)] public async Task StructuredContent_ObjectReturn_NeverWrapped(string? protocolVersion) { // Object-typed return: the stored schema is type:"object" — already the form - // expected by clients on protocol versions older than 2026-06-30, so no envelope + // expected by clients on protocol versions older than 2026-07-28, so no envelope // is applied at any protocol version. Wire shape must be identical across versions. McpServerTool tool = McpServerTool.Create(() => new Person("John", 27), new() { @@ -769,11 +764,10 @@ public async Task StructuredContent_ObjectReturn_NeverWrapped(string? protocolVe [Theory] [InlineData(LegacyProtocolVersion)] [InlineData(null)] - [InlineData(DraftSep2106ProtocolVersion)] [InlineData(Sep2106ProtocolVersion)] public async Task StructuredContent_NullableObjectReturn_NeverWrapped(string? protocolVersion) { - // type:["object","null"] — for clients on protocol versions older than 2026-06-30, + // type:["object","null"]: for clients on protocol versions older than 2026-07-28, // the SCHEMA is normalized to plain type:"object" (verified in // Sep2106ListToolsBackCompatTests), but the value side is never envelope-wrapped at // any protocol version. So the emitted structured content stays a plain object diff --git a/tests/ModelContextProtocol.Tests/Server/MrtrHandlerLifecycleTests.cs b/tests/ModelContextProtocol.Tests/Server/MrtrHandlerLifecycleTests.cs index f2cbfef6e..b80effede 100644 --- a/tests/ModelContextProtocol.Tests/Server/MrtrHandlerLifecycleTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/MrtrHandlerLifecycleTests.cs @@ -253,7 +253,8 @@ public async Task ServerDisposal_CancelsHandlerCancellationToken_DuringMrtr() // Disposing the server while a continuation is suspended should log the cancellation of the // pending MRTR continuation once at Debug level (this is the only path that reaches the - // continuation-cancellation log now that HTTP draft is always sessionless). Poll for the + // continuation-cancellation log now that the HTTP transport no longer supports sessions starting with the + // 2026-07-28 protocol). Poll for the // async cancellation to propagate through the handler task. var deadline = DateTime.UtcNow.AddSeconds(30); while (!MockLoggerProvider.LogMessages.Any(m => m.Message.Contains("pending MRTR continuation")) diff --git a/tests/ModelContextProtocol.Tests/Server/MrtrServerBackcompatTests.cs b/tests/ModelContextProtocol.Tests/Server/MrtrServerBackcompatTests.cs index 14b5dd3a9..221a2ffb4 100644 --- a/tests/ModelContextProtocol.Tests/Server/MrtrServerBackcompatTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/MrtrServerBackcompatTests.cs @@ -9,10 +9,10 @@ namespace ModelContextProtocol.Tests.Server; /// /// Tests for the legacy MRTR backcompat resolver in McpServerImpl.InvokeWithInputRequiredResultHandlingAsync. -/// This path runs only when the client did NOT negotiate MRTR (2026-07-28) and the session is stateful - +/// This path runs only when the client did NOT negotiate MRTR (2026-07-28) and the session is stateful, where /// the server dispatches each input request to the client via standard JSON-RPC and re-invokes the handler /// with the merged responses. To exercise it the server must NOT pin a protocol version; the client picks -/// a non-draft version during initialize negotiation. +/// a legacy version during initialize negotiation. /// public class MrtrServerBackcompatTests : ClientServerTestBase { diff --git a/tests/ModelContextProtocol.Tests/Server/NegotiatedProtocolVersionTests.cs b/tests/ModelContextProtocol.Tests/Server/NegotiatedProtocolVersionTests.cs index ff48b71c8..29ae7823f 100644 --- a/tests/ModelContextProtocol.Tests/Server/NegotiatedProtocolVersionTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/NegotiatedProtocolVersionTests.cs @@ -53,25 +53,25 @@ public async Task PerRequestProtocolVersion_IsEstablishedOnce_AndRejectsLaterCha { var ct = TestContext.Current.CancellationToken; - // The first request establishes the draft version for the stateful session (null -> draft). - Assert.IsType(await RoundTripAsync(id: 1, McpSession.DraftProtocolVersion, ct)); + // The first request establishes the 2026-07-28 version for the stateful session (null -> 2026-07-28). + Assert.IsType(await RoundTripAsync(id: 1, McpHttpHeaders.July2026ProtocolVersion, ct)); // Re-sending the same version is an idempotent no-op, not an error. - Assert.IsType(await RoundTripAsync(id: 2, McpSession.DraftProtocolVersion, ct)); + Assert.IsType(await RoundTripAsync(id: 2, McpHttpHeaders.July2026ProtocolVersion, ct)); // Switching to a different (still-supported) version mid-session is rejected. - var error = Assert.IsType(await RoundTripAsync(id: 3, McpSession.LatestProtocolVersion, ct)); + var error = Assert.IsType(await RoundTripAsync(id: 3, McpHttpHeaders.November2025ProtocolVersion, ct)); Assert.Equal((int)McpErrorCode.InvalidRequest, error.Error.Code); Assert.Contains("protocol version cannot change", error.Error.Message, StringComparison.OrdinalIgnoreCase); - // The rejected request must not have mutated the negotiated version: the original draft version still works. - Assert.IsType(await RoundTripAsync(id: 4, McpSession.DraftProtocolVersion, ct)); + // The rejected request must not have mutated the negotiated version: the original 2026-07-28 version still works. + Assert.IsType(await RoundTripAsync(id: 4, McpHttpHeaders.July2026ProtocolVersion, ct)); } private async Task RoundTripAsync(long id, string protocolVersion, CancellationToken cancellationToken) { - // tools/list is available under both the legacy and draft revisions (unlike ping/initialize, - // which the draft revision removed), so it exercises the version guard rather than the + // tools/list is available under both the legacy and 2026-07-28 revisions (unlike ping/initialize, + // which the 2026-07-28 protocol removed), so it exercises the version guard rather than the // per-method availability gate. var request = new JsonRpcRequest { diff --git a/tests/ModelContextProtocol.Tests/Server/PingProtocolGatingTests.cs b/tests/ModelContextProtocol.Tests/Server/PingProtocolGatingTests.cs index 45c978be2..9e5a9e11f 100644 --- a/tests/ModelContextProtocol.Tests/Server/PingProtocolGatingTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/PingProtocolGatingTests.cs @@ -7,7 +7,7 @@ namespace ModelContextProtocol.Tests.Server; /// /// Verifies that the built-in ping handler is gated by protocol version. -/// SEP-2575 (the draft 2026-07-28 revision) removes ping; servers must +/// SEP-2575 (the 2026-07-28 revision) removes ping; servers must /// respond with -32601 MethodNotFound. Legacy protocol versions still /// support ping per the spec. /// @@ -23,12 +23,12 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer } [Fact] - public async Task Ping_OnDraftSession_ReturnsMethodNotFound() + public async Task Ping_OnJuly2026ProtocolSession_ReturnsMethodNotFound() { StartServer(); await using var client = await CreateMcpClientForServer(new McpClientOptions { - ProtocolVersion = McpSession.DraftProtocolVersion, + ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion, }); var ex = await Assert.ThrowsAsync(async () => diff --git a/tests/ModelContextProtocol.Tests/Server/RawStreamConformanceTests.cs b/tests/ModelContextProtocol.Tests/Server/RawStreamConformanceTests.cs index 003077673..818bd2b4b 100644 --- a/tests/ModelContextProtocol.Tests/Server/RawStreamConformanceTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/RawStreamConformanceTests.cs @@ -1,4 +1,4 @@ -#if !NET472 +#if !NET472 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; @@ -14,7 +14,7 @@ namespace ModelContextProtocol.Tests.Server; /// /// Wire-format conformance tests for driven directly against the underlying /// stream — without going through . This exercises the -/// SEP-2575 (sessionless / no-initialize) and SEP-2567 (server/discover) flows by hand-crafting JSON-RPC +/// SEP-2575 (no initialize handshake) and SEP-2567 (server/discover, no session id) flows by hand-crafting JSON-RPC /// messages and asserting on the exact responses the server emits. /// /// @@ -24,7 +24,6 @@ namespace ModelContextProtocol.Tests.Server; /// public sealed class RawStreamConformanceTests : LoggedTest, IAsyncDisposable { - private const string DraftVersion = McpHttpHeaders.DraftProtocolVersion; private readonly Pipe _clientToServer = new(); private readonly Pipe _serverToClient = new(); @@ -77,15 +76,15 @@ private async Task ReadAsync() return JsonNode.Parse(line!)!; } - private static string DraftMetaFragment(string protocolVersion = DraftVersion) => + private static string July2026ProtocolMetaFragment(string protocolVersion = McpHttpHeaders.July2026ProtocolVersion) => @"""_meta"":{""io.modelcontextprotocol/protocolVersion"":""" + protocolVersion + @""",""io.modelcontextprotocol/clientInfo"":{""name"":""raw"",""version"":""1.0""}," + @"""io.modelcontextprotocol/clientCapabilities"":{}}"; [Fact] - public async Task ServerDiscover_ReturnsSupportedVersionsIncludingDraft() + public async Task ServerDiscover_ReturnsSupportedVersionsIncludingJuly2026Protocol() { - await SendAsync(@"{""jsonrpc"":""2.0"",""id"":1,""method"":""server/discover"",""params"":{" + DraftMetaFragment() + "}}"); + await SendAsync(@"{""jsonrpc"":""2.0"",""id"":1,""method"":""server/discover"",""params"":{" + July2026ProtocolMetaFragment() + "}}"); var response = await ReadAsync(); Assert.Equal("2.0", response["jsonrpc"]!.GetValue()); @@ -97,7 +96,7 @@ public async Task ServerDiscover_ReturnsSupportedVersionsIncludingDraft() var supportedVersions = result!["supportedVersions"]!.AsArray() .Select(n => n!.GetValue()) .ToList(); - Assert.Contains(DraftVersion, supportedVersions); + Assert.Contains(McpHttpHeaders.July2026ProtocolVersion, supportedVersions); // Capabilities and serverInfo are mandatory in DiscoverResult per SEP-2575. Assert.NotNull(result["capabilities"]); @@ -112,13 +111,13 @@ public async Task ServerDiscover_ReturnsSupportedVersionsIncludingDraft() } [Fact] - public async Task DraftToolsCall_WithoutInitialize_Succeeds_WhenFullMetaProvided() + public async Task July2026ProtocolToolsCall_WithoutInitialize_Succeeds_WhenFullMetaProvided() { // Spec: under SEP-2575 the client may skip server/discover and go straight to a normal RPC, as long // as every request carries the full _meta envelope with protocolVersion, clientInfo and capabilities. await SendAsync( @"{""jsonrpc"":""2.0"",""id"":42,""method"":""tools/call"",""params"":{""name"":""echo"",""arguments"":{""text"":""hello""}," + - DraftMetaFragment() + "}}"); + July2026ProtocolMetaFragment() + "}}"); var response = await ReadAsync(); Assert.Equal(42, response["id"]!.GetValue()); @@ -130,12 +129,12 @@ await SendAsync( } [Fact] - public async Task DraftRequest_WithUnsupportedProtocolVersion_ReturnsMinus32004WithSupported() + public async Task July2026ProtocolRequest_WithUnsupportedProtocolVersion_ReturnsMinus32022WithSupported() { - // Server should respond with UnsupportedProtocolVersionError (-32004) and a data.supported[] list. + // Server should respond with UnsupportedProtocolVersionError (-32022) and a data.supported[] list. await SendAsync( @"{""jsonrpc"":""2.0"",""id"":7,""method"":""tools/call"",""params"":{""name"":""echo"",""arguments"":{""text"":""x""}," + - DraftMetaFragment("9999-99-99") + "}}"); + July2026ProtocolMetaFragment("9999-99-99") + "}}"); var response = await ReadAsync(); Assert.Equal(7, response["id"]!.GetValue()); @@ -147,13 +146,13 @@ await SendAsync( Assert.NotNull(data); Assert.Equal("9999-99-99", data!["requested"]!.GetValue()); var supported = data["supported"]!.AsArray().Select(n => n!.GetValue()).ToList(); - Assert.Contains(DraftVersion, supported); + Assert.Contains(McpHttpHeaders.July2026ProtocolVersion, supported); } [Fact] - public async Task LegacyInitialize_StillWorks_OnDraftDefaultServer() + public async Task LegacyInitialize_StillWorks_OnJuly2026ProtocolDefaultServer() { - // Dual-era: a draft-default server (ProtocolVersion = DraftVersion in McpServerOptions) must still + // Dual-era: a server defaulting to the 2026-07-28 protocol (ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion in McpServerOptions) must still // accept the legacy initialize handshake from clients that don't speak the new protocol. await SendAsync(@"{""jsonrpc"":""2.0"",""id"":1,""method"":""initialize"",""params"":{""protocolVersion"":""2025-11-25"",""capabilities"":{},""clientInfo"":{""name"":""legacy"",""version"":""1.0""}}}"); @@ -167,9 +166,9 @@ public async Task LegacyInitialize_StillWorks_OnDraftDefaultServer() [Fact] public async Task MixedSequence_Discover_Then_Initialize_Then_ToolsCall_AllSucceed() { - // Dual-era servers must accept draft and legacy traffic on the same connection. The exact mix below + // Dual-era servers must accept 2026-07-28 and legacy traffic on the same connection. The exact mix below // is what a permissive client running against an unknown server would emit while probing. - await SendAsync(@"{""jsonrpc"":""2.0"",""id"":1,""method"":""server/discover"",""params"":{" + DraftMetaFragment() + "}}"); + await SendAsync(@"{""jsonrpc"":""2.0"",""id"":1,""method"":""server/discover"",""params"":{" + July2026ProtocolMetaFragment() + "}}"); var discover = await ReadAsync(); Assert.NotNull(discover["result"]); diff --git a/tests/ModelContextProtocol.Tests/Server/Sep2106ListToolsBackCompatTests.cs b/tests/ModelContextProtocol.Tests/Server/Sep2106ListToolsBackCompatTests.cs index e559a40f5..6ae1ab7a4 100644 --- a/tests/ModelContextProtocol.Tests/Server/Sep2106ListToolsBackCompatTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/Sep2106ListToolsBackCompatTests.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Tests.Server; /// /// SEP-2106 backward-compat at the tools/list emission boundary. Clients negotiating a -/// pre-2026-06-30 protocol version must still receive the legacy +/// pre-2026-07-28 protocol version must still receive the legacy /// {"type":"object","properties":{"result":<schema>},"required":["result"]} /// envelope for non-object output schemas. In-memory storage stays natural; only the /// wire emission flips on the negotiated version. @@ -18,8 +18,7 @@ namespace ModelContextProtocol.Tests.Server; public class Sep2106ListToolsBackCompatTests : ClientServerTestBase { private const string LegacyProtocolVersion = "2025-11-25"; - private const string DraftSep2106ProtocolVersion = "DRAFT-2026-06-v1"; - private const string Sep2106ProtocolVersion = "2026-06-30"; + private const string Sep2106ProtocolVersion = "2026-07-28"; public Sep2106ListToolsBackCompatTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, startServer: false) @@ -28,7 +27,6 @@ public Sep2106ListToolsBackCompatTests(ITestOutputHelper testOutputHelper) [Theory] [InlineData(LegacyProtocolVersion, true)] - [InlineData(DraftSep2106ProtocolVersion, false)] [InlineData(Sep2106ProtocolVersion, false)] public async Task ListTools_StringTool_WrapsOutputSchemaForLegacyClients(string serverProtocolVersion, bool expectWrapped) { @@ -50,7 +48,6 @@ public async Task ListTools_StringTool_WrapsOutputSchemaForLegacyClients(string [Theory] [InlineData(LegacyProtocolVersion, true)] - [InlineData(DraftSep2106ProtocolVersion, false)] [InlineData(Sep2106ProtocolVersion, false)] public async Task ListTools_IntegerTool_WrapsOutputSchemaForLegacyClients(string serverProtocolVersion, bool expectWrapped) { @@ -71,7 +68,6 @@ public async Task ListTools_IntegerTool_WrapsOutputSchemaForLegacyClients(string [Theory] [InlineData(LegacyProtocolVersion, true)] - [InlineData(DraftSep2106ProtocolVersion, false)] [InlineData(Sep2106ProtocolVersion, false)] public async Task ListTools_ArrayTool_WrapsOutputSchemaForLegacyClients(string serverProtocolVersion, bool expectWrapped) { @@ -92,7 +88,6 @@ public async Task ListTools_ArrayTool_WrapsOutputSchemaForLegacyClients(string s [Theory] [InlineData(LegacyProtocolVersion)] - [InlineData(DraftSep2106ProtocolVersion)] [InlineData(Sep2106ProtocolVersion)] public async Task ListTools_ObjectTool_NeverWrapsOutputSchema(string serverProtocolVersion) { @@ -110,14 +105,13 @@ public async Task ListTools_ObjectTool_NeverWrapsOutputSchema(string serverProto [Theory] [InlineData(LegacyProtocolVersion, true)] - [InlineData(DraftSep2106ProtocolVersion, false)] [InlineData(Sep2106ProtocolVersion, false)] public async Task ListTools_NullableObjectTool_NormalizesTypeArrayForLegacyClients(string serverProtocolVersion, bool expectNormalized) { - // For clients on protocol versions older than 2026-06-30, type:["object","null"] + // For clients on protocol versions older than 2026-07-28, type:["object","null"] // must be emitted as plain type:"object" (those versions accept object schemas but - // not type-arrays — and the value side stays a plain object, no envelope). SEP-2106 - // clients (2026-06-30+) see the natural type-array intact per the SEP's + // not type-arrays, and the value side stays a plain object, no envelope). SEP-2106 + // clients (2026-07-28+) see the natural type-array intact per the SEP's // any-JSON-Schema-2020-12 allowance. ConfigureServerWithTools(serverProtocolVersion); await using var client = await CreateMcpClientForServer(new() { ProtocolVersion = serverProtocolVersion }); @@ -152,7 +146,6 @@ public async Task ListTools_NullableObjectTool_NormalizesTypeArrayForLegacyClien [Theory] [InlineData(LegacyProtocolVersion, true)] - [InlineData(DraftSep2106ProtocolVersion, false)] [InlineData(Sep2106ProtocolVersion, false)] public async Task ListTools_DuplicateTypeRefsTool_RewritesRefsWhenWrapped(string serverProtocolVersion, bool expectWrapped) { @@ -169,7 +162,6 @@ public async Task ListTools_DuplicateTypeRefsTool_RewritesRefsWhenWrapped(string [Theory] [InlineData(LegacyProtocolVersion, true)] - [InlineData(DraftSep2106ProtocolVersion, false)] [InlineData(Sep2106ProtocolVersion, false)] public async Task ListTools_RecursiveTypeRefsTool_RewritesRefsWhenWrapped(string serverProtocolVersion, bool expectWrapped) { diff --git a/tests/ModelContextProtocol.Tests/Server/SubscriptionsListenTests.cs b/tests/ModelContextProtocol.Tests/Server/SubscriptionsListenTests.cs index a57c07d55..d0dd2a796 100644 --- a/tests/ModelContextProtocol.Tests/Server/SubscriptionsListenTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/SubscriptionsListenTests.cs @@ -14,7 +14,7 @@ namespace ModelContextProtocol.Tests.Server; /// /// End-to-end tests for the SEP-2575 subscriptions/listen list-changed delivery over an /// in-memory stream transport (the stdio-shaped path exercised by ). -/// Validates that a draft client receives only the change notifications it subscribed to, each tagged +/// Validates that a client on the 2026-07-28 protocol receives only the change notifications it subscribed to, each tagged /// with the subscription id, and that legacy sessions keep receiving the session-wide broadcast. /// public class SubscriptionsListenTests : ClientServerTestBase @@ -31,7 +31,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer } [Fact] - public async Task Draft_ToolsListChangedSubscription_DeliversTaggedNotification_AndWithholdsUnsubscribed() + public async Task July2026Protocol_ToolsListChangedSubscription_DeliversTaggedNotification_AndWithholdsUnsubscribed() { await using McpClient client = await CreateMcpClientForServer(); @@ -80,7 +80,7 @@ public async Task Draft_ToolsListChangedSubscription_DeliversTaggedNotification_ } [Fact] - public async Task Draft_WithoutSubscription_DoesNotBroadcastListChanged() + public async Task July2026Protocol_WithoutSubscription_DoesNotBroadcastListChanged() { await using McpClient client = await CreateMcpClientForServer(); @@ -91,7 +91,7 @@ public async Task Draft_WithoutSubscription_DoesNotBroadcastListChanged() var serverOptions = ServiceProvider.GetRequiredService>().Value; serverOptions.ToolCollection!.Add(McpServerTool.Create([McpServerTool(Name = "AddedTool")] () => "42")); - // The change notification must not be broadcast to a draft client that never opened a + // The change notification must not be broadcast to a client on the 2026-07-28 protocol that never opened a // subscriptions/listen stream. The list-changed handler runs synchronously during Add (before // the ListTools round-trip below completes), so any erroneous broadcast would already be // buffered once the round-trip returns. @@ -108,7 +108,7 @@ public async Task Legacy_ListChanged_IsBroadcast_WithoutSubscription() { await using McpClient client = await CreateMcpClientForServer(new McpClientOptions { - ProtocolVersion = McpSession.LatestProtocolVersion, + ProtocolVersion = McpHttpHeaders.November2025ProtocolVersion, }); var toolsChannel = Channel.CreateUnbounded(); diff --git a/tests/ModelContextProtocol.Tests/Server/TaskDraftGatingTests.cs b/tests/ModelContextProtocol.Tests/Server/TaskProtocolGatingTests.cs similarity index 87% rename from tests/ModelContextProtocol.Tests/Server/TaskDraftGatingTests.cs rename to tests/ModelContextProtocol.Tests/Server/TaskProtocolGatingTests.cs index 97dba0fce..7f9790526 100644 --- a/tests/ModelContextProtocol.Tests/Server/TaskDraftGatingTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/TaskProtocolGatingTests.cs @@ -11,18 +11,18 @@ namespace ModelContextProtocol.Tests.Server; /// -/// Verifies that the SEP-2663 Tasks extension is gated to the draft protocol revision on both the +/// Verifies that the SEP-2663 Tasks extension is gated to the 2026-07-28 protocol revision on both the /// client and the server. Explicit task operations throw on a legacy session; best-effort task /// augmentation silently downgrades to a direct result so that legacy peers never see a task. /// -public class TaskDraftGatingTests : ClientServerTestBase +public class TaskProtocolGatingTests : ClientServerTestBase { private const string LatestStableVersion = "2025-11-25"; private const string ClientCapabilitiesMetaKey = "io.modelcontextprotocol/clientCapabilities"; private const string ExtensionsKey = "extensions"; - public TaskDraftGatingTests(ITestOutputHelper outputHelper) + public TaskProtocolGatingTests(ITestOutputHelper outputHelper) : base(outputHelper) { #if !NET @@ -82,7 +82,7 @@ public async Task LegacyClient_GetTaskAsync_ThrowsInvalidOperationException() var ex = await Assert.ThrowsAsync(async () => await client.GetTaskAsync("some-task-id", ct)); - Assert.Contains("draft protocol revision", ex.Message); + Assert.Contains("newer protocol revision that supports tasks", ex.Message); } [Fact] @@ -94,7 +94,7 @@ public async Task LegacyClient_UpdateTaskAsync_ThrowsInvalidOperationException() var ex = await Assert.ThrowsAsync(async () => await client.UpdateTaskAsync(new UpdateTaskRequestParams { TaskId = "some-task-id" }, ct)); - Assert.Contains("draft protocol revision", ex.Message); + Assert.Contains("newer protocol revision that supports tasks", ex.Message); } [Fact] @@ -106,7 +106,7 @@ public async Task LegacyClient_CancelTaskAsync_ThrowsInvalidOperationException() var ex = await Assert.ThrowsAsync(async () => await client.CancelTaskAsync("some-task-id", ct)); - Assert.Contains("draft protocol revision", ex.Message); + Assert.Contains("newer protocol revision that supports tasks", ex.Message); } [Fact] @@ -134,7 +134,7 @@ public async Task LegacyClient_CallToolRaw_WithForgedTaskOptIn_ServerReturnsDire // Forge a SEP-2575 capabilities envelope carrying the tasks extension opt-in on a legacy // request. The server must still refuse to create a task because the per-request protocol - // version is not the draft revision. + // version is not the 2026-07-28 protocol. var result = await client.CallToolRawAsync( new CallToolRequestParams { @@ -154,7 +154,7 @@ public async Task LegacyClient_RawTasksGetRequest_ReturnsMethodNotFound() var ct = TestContext.Current.CancellationToken; // Bypass the typed GetTaskAsync client guard by sending a raw tasks/get request. The server - // gates tasks/* to the draft revision and must reject this legacy request with MethodNotFound. + // gates tasks/* to the 2026-07-28 protocol and must reject this legacy request with MethodNotFound. var request = new JsonRpcRequest { Method = RequestMethods.TasksGet, @@ -170,9 +170,9 @@ public async Task LegacyClient_RawTasksGetRequest_ReturnsMethodNotFound() } [Fact] - public async Task DraftClient_CallToolRaw_CreatesTask() + public async Task July2026ProtocolClient_CallToolRaw_CreatesTask() { - // Sanity: the default client negotiates the draft revision, so the task flow still works. + // Sanity: the default client negotiates the 2026-07-28 protocol, so the task flow still works. await using var client = await CreateMcpClientForServer(); var ct = TestContext.Current.CancellationToken; @@ -180,7 +180,7 @@ public async Task DraftClient_CallToolRaw_CreatesTask() new CallToolRequestParams { Name = "test-tool", - Arguments = CreateArguments("input", "draft"), + Arguments = CreateArguments("input", "july2026"), }, ct); Assert.True(result.IsTask);