Tighten draft protocol negotiation boundaries (#1692)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
This commit is contained in:
parent
a8d374b280
commit
fd38f51e27
18 changed files with 1755 additions and 589 deletions
|
|
@ -9,25 +9,6 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// </remarks>
|
||||
internal static class McpHttpHeaders
|
||||
{
|
||||
/// <summary>
|
||||
/// The 2026-07-28 MCP protocol revision (SEP-2575 + SEP-2567). It removed the <c>initialize</c>
|
||||
/// handshake and <c>Mcp-Session-Id</c>, so Streamable HTTP no longer has sessions; it also enabled
|
||||
/// MRTR (SEP-2322) and made the standard MCP request headers (<c>Mcp-Method</c>, <c>Mcp-Name</c>)
|
||||
/// required. Behaviors that began at this revision are gated by ordinal-comparing the per-request
|
||||
/// version against it (see <see cref="IsJuly2026OrLaterProtocolVersion"/>), so it underpins the more
|
||||
/// semantically named helpers. It is also the latest revision this SDK supports, so clients prefer it
|
||||
/// by default.
|
||||
/// </summary>
|
||||
public const string July2026ProtocolVersion = "2026-07-28";
|
||||
|
||||
/// <summary>
|
||||
/// The 2025-11-25 MCP protocol revision: the latest revision that still supports Streamable HTTP
|
||||
/// sessions (the <c>initialize</c> handshake and <c>Mcp-Session-Id</c>); newer revisions remove them.
|
||||
/// It is the default version for the legacy <c>initialize</c> and session-resume code paths, and the
|
||||
/// version the server advertises when a peer requests an unsupported version on the legacy handshake.
|
||||
/// </summary>
|
||||
public const string November2025ProtocolVersion = "2025-11-25";
|
||||
|
||||
/// <summary>The session identifier header.</summary>
|
||||
public const string SessionId = "Mcp-Session-Id";
|
||||
|
||||
|
|
@ -71,27 +52,4 @@ internal static class McpHttpHeaders
|
|||
/// </summary>
|
||||
internal const string ToolContextKey = "Mcp.Tool";
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the given protocol version is <see cref="July2026ProtocolVersion"/>
|
||||
/// or later, the revision that removed the <c>initialize</c> handshake and Streamable HTTP sessions.
|
||||
/// Protocol versions are ISO-8601 dates, so an ordinal comparison orders them chronologically.
|
||||
/// </summary>
|
||||
internal static bool IsJuly2026OrLaterProtocolVersion(string? protocolVersion)
|
||||
=> !string.IsNullOrEmpty(protocolVersion)
|
||||
&& StringComparer.Ordinal.Compare(protocolVersion, July2026ProtocolVersion) >= 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the given protocol version requires standard MCP request headers
|
||||
/// (<c>Mcp-Method</c>, <c>Mcp-Name</c>).
|
||||
/// </summary>
|
||||
public static bool SupportsStandardHeaders(string? protocolVersion)
|
||||
=> IsJuly2026OrLaterProtocolVersion(protocolVersion);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the negotiated protocol version reports unresolvable
|
||||
/// resource URIs with the standard JSON-RPC <see cref="McpErrorCode.InvalidParams"/> (-32602)
|
||||
/// rather than the legacy <see cref="McpErrorCode.ResourceNotFound"/> (-32002).
|
||||
/// </summary>
|
||||
internal static bool UseInvalidParamsForMissingResource(string? protocolVersion)
|
||||
=> IsJuly2026OrLaterProtocolVersion(protocolVersion);
|
||||
}
|
||||
|
|
|
|||
113
src/Common/McpProtocolVersions.cs
Normal file
113
src/Common/McpProtocolVersions.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Internal helpers for MCP protocol revision strings and protocol-era behavior gates.
|
||||
/// </summary>
|
||||
internal static class McpProtocolVersions
|
||||
{
|
||||
/// <summary>
|
||||
/// The 2026-07-28 MCP protocol revision (SEP-2575 + SEP-2567). It removed the <c>initialize</c>
|
||||
/// handshake and <c>Mcp-Session-Id</c>, so Streamable HTTP no longer has sessions; it also enabled
|
||||
/// MRTR (SEP-2322) and made the standard MCP request headers (<c>Mcp-Method</c>, <c>Mcp-Name</c>)
|
||||
/// required. Behaviors that began at this revision are gated by ordinal-comparing the per-request
|
||||
/// version against it (see <see cref="IsJuly2026OrLaterProtocolVersion"/>), so it underpins the more
|
||||
/// semantically named helpers. It is also the latest revision this SDK supports, so clients prefer it
|
||||
/// by default.
|
||||
/// </summary>
|
||||
public const string July2026ProtocolVersion = "2026-07-28";
|
||||
|
||||
/// <summary>
|
||||
/// The 2025-11-25 MCP protocol revision: the latest revision that still supports Streamable HTTP
|
||||
/// sessions (the <c>initialize</c> handshake and <c>Mcp-Session-Id</c>); newer revisions remove them.
|
||||
/// It is the default version for the <c>initialize</c> and session-resume code paths, and the version
|
||||
/// the server advertises when a peer requests an unsupported version on the initialize handshake.
|
||||
/// </summary>
|
||||
public const string November2025ProtocolVersion = "2025-11-25";
|
||||
|
||||
/// <summary>The 2025-06-18 MCP protocol revision.</summary>
|
||||
public const string June2025ProtocolVersion = "2025-06-18";
|
||||
|
||||
/// <summary>The 2025-03-26 MCP protocol revision.</summary>
|
||||
public const string March2025ProtocolVersion = "2025-03-26";
|
||||
|
||||
/// <summary>The 2024-11-05 MCP protocol revision.</summary>
|
||||
public const string November2024ProtocolVersion = "2024-11-05";
|
||||
|
||||
/// <summary>
|
||||
/// Protocol versions that still use the <c>initialize</c> handshake.
|
||||
/// </summary>
|
||||
internal static readonly string[] InitializeHandshakeProtocolVersions =
|
||||
[
|
||||
November2024ProtocolVersion,
|
||||
March2025ProtocolVersion,
|
||||
June2025ProtocolVersion,
|
||||
November2025ProtocolVersion,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Protocol versions that use per-request metadata instead of the <c>initialize</c> handshake.
|
||||
/// </summary>
|
||||
internal static readonly string[] PerRequestMetadataProtocolVersions =
|
||||
[
|
||||
July2026ProtocolVersion,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// All protocol versions supported by this implementation.
|
||||
/// </summary>
|
||||
internal static readonly string[] SupportedProtocolVersions =
|
||||
[
|
||||
.. InitializeHandshakeProtocolVersions,
|
||||
.. PerRequestMetadataProtocolVersions,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the given protocol version is <see cref="July2026ProtocolVersion"/>
|
||||
/// or later, the revision that removed the <c>initialize</c> handshake and Streamable HTTP sessions.
|
||||
/// Protocol versions are ISO-8601 dates, so an ordinal comparison orders them chronologically.
|
||||
/// </summary>
|
||||
internal static bool IsJuly2026OrLaterProtocolVersion(string? protocolVersion)
|
||||
=> !string.IsNullOrEmpty(protocolVersion)
|
||||
&& StringComparer.Ordinal.Compare(protocolVersion, July2026ProtocolVersion) >= 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the given protocol version is supported by this implementation.
|
||||
/// </summary>
|
||||
internal static bool IsSupportedProtocolVersion(string? protocolVersion)
|
||||
=> protocolVersion is not null && SupportedProtocolVersions.Contains(protocolVersion);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the given protocol version is available through the
|
||||
/// <c>initialize</c> handshake.
|
||||
/// </summary>
|
||||
internal static bool SupportsInitializeHandshake(string? protocolVersion)
|
||||
=> protocolVersion is not null && InitializeHandshakeProtocolVersions.Contains(protocolVersion);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the given protocol version requires the handshake-free
|
||||
/// per-request metadata path.
|
||||
/// </summary>
|
||||
internal static bool RequiresPerRequestMetadata(string? protocolVersion)
|
||||
=> IsJuly2026OrLaterProtocolVersion(protocolVersion);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the given protocol version requires standard MCP request headers
|
||||
/// (<c>Mcp-Method</c>, <c>Mcp-Name</c>).
|
||||
/// </summary>
|
||||
internal static bool RequiresStandardHeaders(string? protocolVersion)
|
||||
=> RequiresPerRequestMetadata(protocolVersion);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the given protocol version supports Streamable HTTP sessions.
|
||||
/// </summary>
|
||||
internal static bool SupportsHttpSessions(string? protocolVersion)
|
||||
=> !RequiresPerRequestMetadata(protocolVersion);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the negotiated protocol version reports unresolvable
|
||||
/// resource URIs with the standard JSON-RPC <see cref="McpErrorCode.InvalidParams"/> (-32602)
|
||||
/// rather than the legacy <see cref="McpErrorCode.ResourceNotFound"/> (-32002).
|
||||
/// </summary>
|
||||
internal static bool UseInvalidParamsForMissingResource(string? protocolVersion)
|
||||
=> IsJuly2026OrLaterProtocolVersion(protocolVersion);
|
||||
}
|
||||
|
|
@ -64,10 +64,10 @@ public class HttpServerTransportOptions
|
|||
/// Starting with the <c>2026-07-28</c> protocol revision, Streamable HTTP no longer supports sessions:
|
||||
/// the revision removed <c>Mcp-Session-Id</c> (SEP-2567), so over HTTP its requests are only ever served
|
||||
/// when this property is <see langword="true"/>. When it is <see langword="false"/>, such a request is
|
||||
/// refused with a <c>-32022 UnsupportedProtocolVersion</c> error so that a dual-era client downgrades to
|
||||
/// the legacy <c>initialize</c> handshake and obtains the session the server was configured to provide.
|
||||
/// A request that carries an <c>Mcp-Session-Id</c> is always rejected by the <c>2026-07-28</c> and later
|
||||
/// revisions, regardless of this property's value.
|
||||
/// refused with a <c>-32022 UnsupportedProtocolVersion</c> error so that a dual-path client downgrades to
|
||||
/// the <c>initialize</c> handshake and obtains the session the server was configured to provide.
|
||||
/// A request that carries an <c>Mcp-Session-Id</c> on the <c>2026-07-28</c> and later revisions is ignored;
|
||||
/// the server must not mint or echo session IDs for those revisions.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool Stateless { get; set; } = true;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
<Compile Include="..\Common\Experimentals.cs" Link="Experimentals.cs" />
|
||||
<Compile Include="..\Common\Obsoletions.cs" Link="Obsoletions.cs" />
|
||||
<Compile Include="..\Common\McpHttpHeaders.cs" Link="McpHttpHeaders.cs" />
|
||||
<Compile Include="..\Common\McpProtocolVersions.cs" Link="McpProtocolVersions.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace ModelContextProtocol.AspNetCore;
|
||||
|
|
@ -32,24 +33,15 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
/// <summary>
|
||||
/// All protocol versions supported by this implementation.
|
||||
/// Keep in sync with McpSessionHandler.SupportedProtocolVersions in ModelContextProtocol.Core.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> s_supportedProtocolVersions =
|
||||
[
|
||||
"2024-11-05",
|
||||
"2025-03-26",
|
||||
"2025-06-18",
|
||||
McpHttpHeaders.November2025ProtocolVersion,
|
||||
McpHttpHeaders.July2026ProtocolVersion,
|
||||
];
|
||||
private static readonly string[] s_supportedProtocolVersions = McpProtocolVersions.SupportedProtocolVersions;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// later). Used when refusing a 2026-07-28 request on a stateful (Stateless = false) server so a dual-path
|
||||
/// client falls back to the initialize handshake instead of retrying the 2026-07-28 version.
|
||||
/// </summary>
|
||||
private static readonly string[] s_sessionSupportingProtocolVersions =
|
||||
[.. s_supportedProtocolVersions.Where(static v => !McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(v))];
|
||||
private static readonly string[] s_sessionSupportingProtocolVersions = McpProtocolVersions.InitializeHandshakeProtocolVersions;
|
||||
|
||||
private static readonly JsonTypeInfo<JsonRpcMessage> s_messageTypeInfo = GetRequiredJsonTypeInfo<JsonRpcMessage>();
|
||||
private static readonly JsonTypeInfo<JsonRpcError> s_errorTypeInfo = GetRequiredJsonTypeInfo<JsonRpcError>();
|
||||
|
|
@ -63,7 +55,8 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
public async Task HandlePostRequestAsync(HttpContext context)
|
||||
{
|
||||
if (!ValidateProtocolVersionHeader(context, out var protocolVersionError))
|
||||
var configuredSupportedProtocolVersions = GetConfiguredSupportedProtocolVersions(mcpServerOptionsSnapshot.Value.ProtocolVersion);
|
||||
if (!ValidateProtocolVersionHeader(context, configuredSupportedProtocolVersions, out var protocolVersionError))
|
||||
{
|
||||
await WriteJsonRpcErrorDetailAsync(context, protocolVersionError, StatusCodes.Status400BadRequest);
|
||||
return;
|
||||
|
|
@ -111,6 +104,12 @@ internal sealed class StreamableHttpHandler(
|
|||
// Notifications carry no id, so this stays default (null), which is correct.
|
||||
var requestId = message is JsonRpcRequest jsonRpcRequest ? jsonRpcRequest.Id : default;
|
||||
|
||||
if (!ValidateProtocolVersionEnvelope(context, message, out var protocolVersionEnvelopeError))
|
||||
{
|
||||
await WriteJsonRpcErrorDetailAsync(context, protocolVersionEnvelopeError, StatusCodes.Status400BadRequest, requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateMcpHeaders(context, message, mcpServerOptionsSnapshot.Value.ToolCollection, out var errorMessage))
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context, errorMessage, StatusCodes.Status400BadRequest, (int)McpErrorCode.HeaderMismatch, requestId);
|
||||
|
|
@ -137,7 +136,8 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
public async Task HandleGetRequestAsync(HttpContext context)
|
||||
{
|
||||
if (!ValidateProtocolVersionHeader(context, out var protocolVersionError))
|
||||
var configuredSupportedProtocolVersions = GetConfiguredSupportedProtocolVersions(mcpServerOptionsSnapshot.Value.ProtocolVersion);
|
||||
if (!ValidateProtocolVersionHeader(context, configuredSupportedProtocolVersions, out var protocolVersionError))
|
||||
{
|
||||
await WriteJsonRpcErrorDetailAsync(context, protocolVersionError, StatusCodes.Status400BadRequest);
|
||||
return;
|
||||
|
|
@ -148,11 +148,12 @@ internal sealed class StreamableHttpHandler(
|
|||
// 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))
|
||||
if (RequiresPerRequestMetadataProtocol(context))
|
||||
{
|
||||
context.Response.Headers.Allow = HttpMethods.Post;
|
||||
await WriteJsonRpcErrorAsync(context,
|
||||
"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);
|
||||
"Method Not Allowed: The GET endpoint is not supported by the 2026-07-28 and later protocol revisions. Use subscriptions/listen via POST instead.",
|
||||
StatusCodes.Status405MethodNotAllowed);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +255,8 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
public async Task HandleDeleteRequestAsync(HttpContext context)
|
||||
{
|
||||
if (!ValidateProtocolVersionHeader(context, out var protocolVersionError))
|
||||
var configuredSupportedProtocolVersions = GetConfiguredSupportedProtocolVersions(mcpServerOptionsSnapshot.Value.ProtocolVersion);
|
||||
if (!ValidateProtocolVersionHeader(context, configuredSupportedProtocolVersions, out var protocolVersionError))
|
||||
{
|
||||
await WriteJsonRpcErrorDetailAsync(context, protocolVersionError, StatusCodes.Status400BadRequest);
|
||||
return;
|
||||
|
|
@ -264,11 +266,12 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
// 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))
|
||||
if (RequiresPerRequestMetadataProtocol(context))
|
||||
{
|
||||
context.Response.Headers.Allow = HttpMethods.Post;
|
||||
await WriteJsonRpcErrorAsync(context,
|
||||
"Bad Request: The DELETE endpoint is not supported by the 2026-07-28 and later protocol revisions.",
|
||||
StatusCodes.Status400BadRequest);
|
||||
"Method Not Allowed: The DELETE endpoint is not supported by the 2026-07-28 and later protocol revisions.",
|
||||
StatusCodes.Status405MethodNotAllowed);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -374,25 +377,14 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
private async ValueTask<StreamableHttpSession?> GetOrCreateSessionAsync(HttpContext context, JsonRpcMessage message, RequestId requestId = default)
|
||||
{
|
||||
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
|
||||
|
||||
// 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 (RequiresPerRequestMetadataProtocol(context))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
// 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 by the 2026-07-28 and later protocol revisions (SEP-2567).",
|
||||
StatusCodes.Status400BadRequest, requestId: requestId);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!HttpServerTransportOptions.Stateless)
|
||||
{
|
||||
// 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
|
||||
// revision cannot provide. Refuse it so a dual-path client falls back to the
|
||||
// initialize handshake and gets the session it asked for (SEP-2575 fallback semantics).
|
||||
await WriteUnsupportedProtocolVersionErrorAsync(context, requestId);
|
||||
return null;
|
||||
|
|
@ -402,6 +394,7 @@ internal sealed class StreamableHttpHandler(
|
|||
return await StartNewSessionAsync(context);
|
||||
}
|
||||
|
||||
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
// In stateful mode, only allow creating new sessions for initialize requests.
|
||||
|
|
@ -435,13 +428,13 @@ internal sealed class StreamableHttpHandler(
|
|||
/// <summary>
|
||||
/// Returns <see langword="true"/> when the request's <c>MCP-Protocol-Version</c> 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 <c>initialize</c> handshake
|
||||
/// do not use an Mcp-Session-Id and never perform the <c>initialize</c> handshake
|
||||
/// (SEP-2575 + SEP-2567).
|
||||
/// </summary>
|
||||
private static bool IsJuly2026OrLaterProtocol(HttpContext context)
|
||||
private static bool RequiresPerRequestMetadataProtocol(HttpContext context)
|
||||
{
|
||||
var protocolVersionHeader = context.Request.Headers[McpProtocolVersionHeaderName].ToString();
|
||||
return McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(protocolVersionHeader);
|
||||
return McpProtocolVersions.RequiresPerRequestMetadata(protocolVersionHeader);
|
||||
}
|
||||
|
||||
private async ValueTask<StreamableHttpSession> StartNewSessionAsync(HttpContext context)
|
||||
|
|
@ -665,11 +658,14 @@ internal sealed class StreamableHttpHandler(
|
|||
/// rejection uses the <see cref="McpErrorCode.UnsupportedProtocolVersion"/> error code with a data payload
|
||||
/// listing the server's supported versions so the client can select a fallback.
|
||||
/// </summary>
|
||||
private static bool ValidateProtocolVersionHeader(HttpContext context, [NotNullWhen(false)] out JsonRpcErrorDetail? errorDetail)
|
||||
private static bool ValidateProtocolVersionHeader(
|
||||
HttpContext context,
|
||||
IReadOnlyList<string> supportedProtocolVersions,
|
||||
[NotNullWhen(false)] out JsonRpcErrorDetail? errorDetail)
|
||||
{
|
||||
var protocolVersionHeader = context.Request.Headers[McpProtocolVersionHeaderName].ToString();
|
||||
if (!string.IsNullOrEmpty(protocolVersionHeader) &&
|
||||
!s_supportedProtocolVersions.Contains(protocolVersionHeader))
|
||||
!supportedProtocolVersions.Contains(protocolVersionHeader))
|
||||
{
|
||||
errorDetail = new JsonRpcErrorDetail
|
||||
{
|
||||
|
|
@ -678,7 +674,7 @@ internal sealed class StreamableHttpHandler(
|
|||
Data = JsonSerializer.SerializeToNode(
|
||||
new UnsupportedProtocolVersionErrorData
|
||||
{
|
||||
Supported = [.. s_supportedProtocolVersions],
|
||||
Supported = [.. supportedProtocolVersions],
|
||||
Requested = protocolVersionHeader,
|
||||
},
|
||||
GetRequiredJsonTypeInfo<UnsupportedProtocolVersionErrorData>()),
|
||||
|
|
@ -690,11 +686,108 @@ internal sealed class StreamableHttpHandler(
|
|||
return true;
|
||||
}
|
||||
|
||||
private static string[] GetConfiguredSupportedProtocolVersions(string? protocolVersion)
|
||||
{
|
||||
if (protocolVersion is null)
|
||||
{
|
||||
return s_supportedProtocolVersions;
|
||||
}
|
||||
|
||||
if (!McpProtocolVersions.IsSupportedProtocolVersion(protocolVersion))
|
||||
{
|
||||
throw new McpException(
|
||||
$"Unsupported server protocol version '{protocolVersion}'. Supported protocol versions: " +
|
||||
string.Join(", ", McpProtocolVersions.SupportedProtocolVersions) + ".");
|
||||
}
|
||||
|
||||
return [protocolVersion];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that HTTP requests using per-request metadata declare the same protocol version in both
|
||||
/// the <c>MCP-Protocol-Version</c> header and body <c>_meta</c> envelope.
|
||||
/// </summary>
|
||||
private static bool ValidateProtocolVersionEnvelope(
|
||||
HttpContext context,
|
||||
JsonRpcMessage message,
|
||||
[NotNullWhen(false)] out JsonRpcErrorDetail? errorDetail)
|
||||
{
|
||||
if (message is not (JsonRpcRequest or JsonRpcNotification))
|
||||
{
|
||||
errorDetail = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
var protocolVersionHeader = context.Request.Headers[McpProtocolVersionHeaderName].ToString();
|
||||
bool hasProtocolVersionMeta = TryGetProtocolVersionMeta(message, out var protocolVersionMeta);
|
||||
|
||||
if (!McpProtocolVersions.RequiresPerRequestMetadata(protocolVersionHeader) &&
|
||||
!McpProtocolVersions.RequiresPerRequestMetadata(protocolVersionMeta))
|
||||
{
|
||||
errorDetail = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(protocolVersionHeader))
|
||||
{
|
||||
errorDetail = CreateHeaderMismatchError(
|
||||
$"Bad Request: The {McpProtocolVersionHeaderName} header is required when the request body declares a per-request metadata protocol version.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasProtocolVersionMeta)
|
||||
{
|
||||
errorDetail = CreateHeaderMismatchError(
|
||||
$"Bad Request: The body _meta/{MetaKeys.ProtocolVersion} field is required when the {McpProtocolVersionHeaderName} header declares a per-request metadata protocol version.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(protocolVersionHeader, protocolVersionMeta, StringComparison.Ordinal))
|
||||
{
|
||||
errorDetail = CreateHeaderMismatchError(
|
||||
$"Bad Request: The {McpProtocolVersionHeaderName} header value '{protocolVersionHeader}' does not match body _meta/{MetaKeys.ProtocolVersion} value '{protocolVersionMeta}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
errorDetail = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static JsonRpcErrorDetail CreateHeaderMismatchError(string message) => new()
|
||||
{
|
||||
Code = (int)McpErrorCode.HeaderMismatch,
|
||||
Message = message,
|
||||
};
|
||||
|
||||
private static bool TryGetProtocolVersionMeta(JsonRpcMessage message, [NotNullWhen(true)] out string? protocolVersion)
|
||||
{
|
||||
var parameters = message switch
|
||||
{
|
||||
JsonRpcRequest request => request.Params,
|
||||
JsonRpcNotification notification => notification.Params,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
string? value = null;
|
||||
if (parameters is JsonObject paramsObj &&
|
||||
paramsObj["_meta"] is JsonObject metaObj &&
|
||||
metaObj[MetaKeys.ProtocolVersion] is JsonValue protocolVersionValue &&
|
||||
protocolVersionValue.TryGetValue(out value) &&
|
||||
!string.IsNullOrEmpty(value))
|
||||
{
|
||||
protocolVersion = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
protocolVersion = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="McpErrorCode.UnsupportedProtocolVersion"/> with a supported-versions list
|
||||
/// that excludes 2026-07-28 and later. A dual-era client then falls back to the legacy initialize handshake
|
||||
/// that excludes 2026-07-28 and later. A dual-path client then falls back to the initialize handshake
|
||||
/// (SEP-2575).
|
||||
/// </summary>
|
||||
private static Task WriteUnsupportedProtocolVersionErrorAsync(HttpContext context, RequestId requestId = default)
|
||||
|
|
@ -703,7 +796,7 @@ internal sealed class StreamableHttpHandler(
|
|||
var errorDetail = new JsonRpcErrorDetail
|
||||
{
|
||||
Code = (int)McpErrorCode.UnsupportedProtocolVersion,
|
||||
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). " +
|
||||
Message = $"Bad Request: Starting with protocol version '{McpProtocolVersions.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
|
||||
|
|
@ -731,7 +824,7 @@ internal sealed class StreamableHttpHandler(
|
|||
{
|
||||
// Only validate for protocol versions that support standard headers.
|
||||
var protocolVersion = context.Request.Headers[McpProtocolVersionHeaderName].ToString();
|
||||
if (!McpHttpHeaders.SupportsStandardHeaders(protocolVersion))
|
||||
if (!McpProtocolVersions.RequiresStandardHeaders(protocolVersion))
|
||||
{
|
||||
errorMessage = null;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -294,24 +294,20 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
// 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 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
|
||||
// prefers the 2026-07-28 revision and automatically falls back to the initialize
|
||||
// handshake when the server doesn't support it. The initialize 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))
|
||||
if (_options.ProtocolVersion is null || McpProtocolVersions.RequiresPerRequestMetadata(_options.ProtocolVersion))
|
||||
{
|
||||
string preferredVersion = _options.ProtocolVersion ?? McpHttpHeaders.July2026ProtocolVersion;
|
||||
|
||||
// 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;
|
||||
string preferredVersion = _options.ProtocolVersion ?? McpProtocolVersions.July2026ProtocolVersion;
|
||||
|
||||
DiscoverResult? discoverResult = null;
|
||||
bool fallbackToLegacy = false;
|
||||
bool fallbackToInitialize = false;
|
||||
IList<string>? serverSupportedVersions = null;
|
||||
string discoverVersion = preferredVersion;
|
||||
|
||||
// Apply a probe timeout so dual-era clients don't block forever waiting for a
|
||||
// legacy server that silently drops unknown methods (per stdio.mdx fallback rules).
|
||||
// Apply a probe timeout so dual-path clients don't block forever waiting for an
|
||||
// initialize-handshake server that silently drops unknown methods (per stdio.mdx fallback rules).
|
||||
// The probe timeout is configurable via McpClientOptions.DiscoverProbeTimeout and is
|
||||
// always bounded by InitializationTimeout (only applied when it is the tighter bound).
|
||||
var probeTimeout = _options.DiscoverProbeTimeout;
|
||||
|
|
@ -323,75 +319,101 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
|
||||
try
|
||||
{
|
||||
discoverResult = await SendRequestAsync(
|
||||
RequestMethods.ServerDiscover,
|
||||
new DiscoverRequestParams(),
|
||||
McpJsonUtilities.JsonContext.Default.DiscoverRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.DiscoverResult,
|
||||
cancellationToken: probeCts.Token).ConfigureAwait(false);
|
||||
discoverResult = await SendDiscoverAsync(discoverVersion, probeCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (UnsupportedProtocolVersionException ex)
|
||||
{
|
||||
// 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;
|
||||
// Spec-recognized SEP-2575 signal: -32022 with data.supported[]. The server is
|
||||
// refusing our preferred version. Retry with a supported per-request metadata
|
||||
// version if one exists; otherwise fall back to initialize with the highest
|
||||
// mutually supported initialize-capable version.
|
||||
serverSupportedVersions = (IList<string>)ex.Supported;
|
||||
var retryVersion = serverSupportedVersions
|
||||
.Where(McpProtocolVersions.PerRequestMetadataProtocolVersions.Contains)
|
||||
.OrderByDescending(v => v, StringComparer.Ordinal)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (retryVersion is not null)
|
||||
{
|
||||
if (_options.ProtocolVersion is { } pinnedVersion &&
|
||||
StringComparer.Ordinal.Compare(retryVersion, pinnedVersion) < 0)
|
||||
{
|
||||
throw new McpException(
|
||||
$"The server does not support the requested protocol version '{pinnedVersion}'. " +
|
||||
"Leave McpClientOptions.ProtocolVersion unset to allow automatic fallback to an older version. " +
|
||||
$"Server-supported versions: {string.Join(", ", serverSupportedVersions)}.");
|
||||
}
|
||||
|
||||
discoverVersion = retryVersion;
|
||||
discoverResult = await SendDiscoverAsync(discoverVersion, probeCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
fallbackToInitialize = true;
|
||||
}
|
||||
}
|
||||
catch (MissingRequiredClientCapabilityException)
|
||||
{
|
||||
// Spec-recognized modern-server signal: -32021. The server is modern but rejected
|
||||
// Spec-recognized SEP-2575 signal: -32021. The server 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: -32020. The server is modern but rejected
|
||||
// Spec-recognized SEP-2575 signal: -32020. The server 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.
|
||||
// fallback): falling back to initialize wouldn't fix a malformed envelope.
|
||||
throw;
|
||||
}
|
||||
catch (McpProtocolException ex) when (
|
||||
ex.ErrorCode == McpErrorCode.InvalidRequest &&
|
||||
ex.Message.Contains(McpHttpHeaders.SessionId, StringComparison.Ordinal))
|
||||
{
|
||||
// Local transport validation: a 2026-07-28+ response must not carry HTTP session state.
|
||||
// This is not evidence of an initialize-handshake server, so do not fall back.
|
||||
throw;
|
||||
}
|
||||
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.
|
||||
// Any non-SEP-2575 JSON-RPC error from the probe indicates an initialize-handshake 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
|
||||
// payload shape, or any other transport-defined error. The three SEP-2575
|
||||
// signals (-32022 UnsupportedProtocolVersion, -32021
|
||||
// MissingRequiredClientCapability, -32020 HeaderMismatch) are caught above and
|
||||
// never reach here.
|
||||
fallbackToLegacy = true;
|
||||
fallbackToInitialize = true;
|
||||
}
|
||||
catch (OperationCanceledException) when (probeCts.IsCancellationRequested && !initializationCts.IsCancellationRequested)
|
||||
{
|
||||
// Probe timeout elapsed without a response. Per stdio.mdx fallback rules, no
|
||||
// response within a reasonable timeout means the server is legacy. Fall back.
|
||||
fallbackToLegacy = true;
|
||||
// response within a reasonable timeout means the server requires initialize. Fall back.
|
||||
fallbackToInitialize = true;
|
||||
}
|
||||
|
||||
if (discoverResult is not null && !discoverResult.SupportedVersions.Contains(preferredVersion))
|
||||
if (discoverResult is not null && !discoverResult.SupportedVersions.Contains(discoverVersion))
|
||||
{
|
||||
// Server is reachable and supports server/discover, but doesn't support the
|
||||
// 2026-07-28 version. Fall back to legacy initialize with the highest
|
||||
// mutually-supported version from supportedVersions[].
|
||||
fallbackToLegacy = true;
|
||||
// version we are using. Fall back to initialize with the highest
|
||||
// mutually-supported initialize-capable version from supportedVersions[].
|
||||
fallbackToInitialize = true;
|
||||
serverSupportedVersions = discoverResult.SupportedVersions;
|
||||
}
|
||||
|
||||
if (fallbackToLegacy)
|
||||
if (fallbackToInitialize)
|
||||
{
|
||||
// Reset negotiated state and try legacy initialize.
|
||||
// Reset negotiated state and try initialize.
|
||||
_negotiatedProtocolVersion = null;
|
||||
_sessionHandler.NegotiatedProtocolVersion = null;
|
||||
|
||||
string fallbackVersion = serverSupportedVersions?
|
||||
.Where(McpSessionHandler.SupportedProtocolVersions.Contains)
|
||||
.Where(McpProtocolVersions.InitializeHandshakeProtocolVersions.Contains)
|
||||
.OrderByDescending(v => v, StringComparer.Ordinal)
|
||||
.FirstOrDefault()
|
||||
?? McpHttpHeaders.November2025ProtocolVersion;
|
||||
?? McpProtocolVersions.November2025ProtocolVersion;
|
||||
|
||||
// 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
|
||||
|
|
@ -403,11 +425,11 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
$"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."
|
||||
? "The server appears to require the initialize handshake."
|
||||
: $"Server-supported versions: {string.Join(", ", serverSupportedVersions)}."));
|
||||
}
|
||||
|
||||
await PerformLegacyInitializeAsync(fallbackVersion, initializationCts.Token).ConfigureAwait(false);
|
||||
await PerformInitializeHandshakeAsync(fallbackVersion, initializationCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -422,14 +444,29 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
_serverInfo = discoverResult.ServerInfo;
|
||||
_serverInstructions = discoverResult.Instructions;
|
||||
}
|
||||
|
||||
async Task<DiscoverResult> SendDiscoverAsync(string protocolVersion, CancellationToken cancellationToken)
|
||||
{
|
||||
// Eagerly set the negotiated version so InjectRequestMetaIfNeeded recognizes us as being
|
||||
// on a per-request metadata revision when SendRequestAsync is invoked for server/discover.
|
||||
_negotiatedProtocolVersion = protocolVersion;
|
||||
_sessionHandler.NegotiatedProtocolVersion = protocolVersion;
|
||||
|
||||
return await SendRequestAsync(
|
||||
RequestMethods.ServerDiscover,
|
||||
new DiscoverRequestParams(),
|
||||
McpJsonUtilities.JsonContext.Default.DiscoverRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.DiscoverResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy initialize handshake. Reached only when the caller explicitly pinned a
|
||||
// initialize handshake. Reached only when the caller explicitly pinned a
|
||||
// ProtocolVersion that still supports Streamable HTTP sessions (opting out of the default), so
|
||||
// _options.ProtocolVersion is non-null here.
|
||||
string requestProtocol = _options.ProtocolVersion ?? McpHttpHeaders.November2025ProtocolVersion;
|
||||
await PerformLegacyInitializeAsync(requestProtocol, initializationCts.Token).ConfigureAwait(false);
|
||||
string requestProtocol = _options.ProtocolVersion ?? McpProtocolVersions.November2025ProtocolVersion;
|
||||
await PerformInitializeHandshakeAsync(requestProtocol, initializationCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||
|
|
@ -449,10 +486,10 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the legacy initialize handshake (initialize request + initialized notification),
|
||||
/// Performs the initialize handshake (initialize request + initialized notification),
|
||||
/// records the negotiated protocol version, and stores the server capabilities/info/instructions.
|
||||
/// </summary>
|
||||
private async Task PerformLegacyInitializeAsync(string requestProtocol, CancellationToken cancellationToken)
|
||||
private async Task PerformInitializeHandshakeAsync(string requestProtocol, CancellationToken cancellationToken)
|
||||
{
|
||||
var initializeResponse = await SendRequestAsync(
|
||||
RequestMethods.Initialize,
|
||||
|
|
@ -479,18 +516,16 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
_serverInstructions = initializeResponse.Instructions;
|
||||
|
||||
// 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.
|
||||
// When no version was pinned, accept any supported initialize-handshake response. initialize cannot negotiate
|
||||
// the 2026-07-28 and later protocol revisions.
|
||||
bool isResponseProtocolValid;
|
||||
if (_options.ProtocolVersion is { } optionsProtocol && !McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(optionsProtocol))
|
||||
if (_options.ProtocolVersion is { } optionsProtocol && !McpProtocolVersions.IsJuly2026OrLaterProtocolVersion(optionsProtocol))
|
||||
{
|
||||
isResponseProtocolValid = optionsProtocol == initializeResponse.ProtocolVersion;
|
||||
}
|
||||
else
|
||||
{
|
||||
isResponseProtocolValid = McpSessionHandler.SupportedProtocolVersions.Contains(initializeResponse.ProtocolVersion);
|
||||
isResponseProtocolValid = McpProtocolVersions.InitializeHandshakeProtocolVersions.Contains(initializeResponse.ProtocolVersion);
|
||||
}
|
||||
if (!isResponseProtocolValid)
|
||||
{
|
||||
|
|
@ -525,7 +560,7 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
_serverInstructions = resumeOptions.ServerInstructions;
|
||||
_negotiatedProtocolVersion = resumeOptions.NegotiatedProtocolVersion
|
||||
?? _options.ProtocolVersion
|
||||
?? McpHttpHeaders.November2025ProtocolVersion;
|
||||
?? McpProtocolVersions.November2025ProtocolVersion;
|
||||
|
||||
// Update session handler with the negotiated protocol version for telemetry
|
||||
_sessionHandler.NegotiatedProtocolVersion = _negotiatedProtocolVersion;
|
||||
|
|
@ -691,7 +726,7 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
/// <summary>
|
||||
/// Injects the 2026-07-28 protocol's per-request <c>_meta</c> 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.
|
||||
/// (SEP-2575). No-op on an initialize-handshake session.
|
||||
/// </summary>
|
||||
private void InjectRequestMetaIfNeeded(JsonRpcRequest request)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -52,16 +52,20 @@ public sealed class McpClientOptions
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Supported values are <c>2024-11-05</c>, <c>2025-03-26</c>, <c>2025-06-18</c>, <c>2025-11-25</c>,
|
||||
/// and <c>2026-07-28</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When <see langword="null"/> (the default), the client prefers the latest revision (<c>2026-07-28</c>),
|
||||
/// which removed the <c>initialize</c> handshake and Streamable HTTP sessions. It probes with
|
||||
/// <c>server/discover</c> and automatically falls back to the legacy <c>initialize</c> handshake,
|
||||
/// downgrading to any version the server advertises, when the server does not support that revision.
|
||||
/// <c>server/discover</c> and automatically falls back to the <c>initialize</c> handshake,
|
||||
/// downgrading to an initialize-capable version the server advertises, when the server does not support that revision.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When non-<see langword="null"/>, 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
|
||||
/// <see cref="McpException"/> instead of falling back. Setting it to <c>2026-07-28</c> therefore disables
|
||||
/// the automatic legacy-server fallback, and setting it to a version that still supports Streamable HTTP
|
||||
/// the automatic initialize-handshake server fallback, and setting it to a version that still supports Streamable HTTP
|
||||
/// sessions, such as <c>2025-11-25</c>, forces the <c>initialize</c> 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.
|
||||
|
|
@ -90,7 +94,7 @@ public sealed class McpClientOptions
|
|||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout applied to the <c>server/discover</c> probe that the client issues
|
||||
/// before falling back to the legacy <c>initialize</c> handshake.
|
||||
/// before falling back to the <c>initialize</c> handshake.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The probe timeout. The default value is 5 seconds. Use
|
||||
|
|
@ -102,17 +106,17 @@ public sealed class McpClientOptions
|
|||
/// This timeout only has an effect when the client prefers the <c>2026-07-28</c> protocol revision, that is,
|
||||
/// when <see cref="ProtocolVersion"/> is <see langword="null"/> (the default) or <c>2026-07-28</c>.
|
||||
/// In that mode the client first probes the server with a
|
||||
/// <c>server/discover</c> request. A legacy server that predates the <c>2026-07-28</c> revision may
|
||||
/// <c>server/discover</c> request. A server that predates the <c>2026-07-28</c> 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 <c>initialize</c> handshake on the
|
||||
/// same connection. When the caller pins a legacy <see cref="ProtocolVersion"/>, no probe is issued
|
||||
/// client concludes the server requires <c>initialize</c> and falls back to that handshake on the
|
||||
/// same connection. When the caller pins an initialize-capable <see cref="ProtocolVersion"/>, no probe is issued
|
||||
/// and this value has no effect.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The default is intentionally short so that dual-era clients fall back quickly against legacy
|
||||
/// The default is intentionally short so that dual-path clients fall back quickly against initialize-handshake
|
||||
/// 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 server on the new revision
|
||||
/// server has had a chance to respond. The probe is always also bounded by
|
||||
/// satellite links) where a short probe could trigger the initialize fallback before a server on the
|
||||
/// per-request metadata revision has had a chance to respond. The probe is always also bounded by
|
||||
/// <see cref="InitializationTimeout"/>, which governs the overall connect budget: if this value is
|
||||
/// greater than or equal to <see cref="InitializationTimeout"/>, the probe is effectively bounded by
|
||||
/// <see cref="InitializationTimeout"/> alone.
|
||||
|
|
|
|||
|
|
@ -67,19 +67,19 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
// 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 protocol error codes
|
||||
// connect-time logic can react. For example, the three per-request metadata 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.
|
||||
// initialize-handshake servers that don't understand the SEP-2575 _meta envelope) become generic
|
||||
// McpProtocolException instances and trigger the initialize-handshake fallback 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 protocol error codes are also surfaced for non-400 status codes
|
||||
// The three per-request metadata 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 ||
|
||||
IsModernProtocolErrorCode((McpErrorCode)parsedError.Error.Code)))
|
||||
IsPerRequestMetadataProtocolErrorCode((McpErrorCode)parsedError.Error.Code)))
|
||||
{
|
||||
throw McpSessionHandler.CreateRemoteProtocolExceptionFromError(parsedError);
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
await response.EnsureSuccessStatusCodeWithResponseBodyAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static bool IsModernProtocolErrorCode(McpErrorCode code) =>
|
||||
private static bool IsPerRequestMetadataProtocolErrorCode(McpErrorCode code) =>
|
||||
code is McpErrorCode.UnsupportedProtocolVersion
|
||||
or McpErrorCode.MissingRequiredClientCapability
|
||||
or McpErrorCode.HeaderMismatch;
|
||||
|
|
@ -217,7 +217,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
if (rpcRequest.Method == RequestMethods.Initialize && rpcResponseOrError is JsonRpcResponse initResponse)
|
||||
{
|
||||
// We've successfully initialized! Copy session-id and protocol version, then start GET request if any.
|
||||
if (response.Headers.TryGetValues("Mcp-Session-Id", out var sessionIdValues))
|
||||
if (response.Headers.TryGetValues(McpHttpHeaders.SessionId, out var sessionIdValues))
|
||||
{
|
||||
SessionId = sessionIdValues.FirstOrDefault();
|
||||
}
|
||||
|
|
@ -528,7 +528,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
string? protocolVersion,
|
||||
string? lastEventId = null)
|
||||
{
|
||||
if (sessionId is not null)
|
||||
if (sessionId is not null && McpProtocolVersions.SupportsHttpSessions(protocolVersion))
|
||||
{
|
||||
headers.Add(McpHttpHeaders.SessionId, sessionId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public abstract partial class McpSession : IAsyncDisposable
|
|||
/// definition of "is this peer speaking the 2026-07-28 or later revision" used by both the client and server.
|
||||
/// </remarks>
|
||||
internal bool IsJuly2026OrLaterProtocol() =>
|
||||
McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(NegotiatedProtocolVersion);
|
||||
McpProtocolVersions.IsJuly2026OrLaterProtocolVersion(NegotiatedProtocolVersion);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a JSON-RPC request to the connected session and waits for a response.
|
||||
|
|
|
|||
|
|
@ -29,18 +29,10 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
"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.");
|
||||
|
||||
/// <summary>
|
||||
/// All protocol versions supported by this implementation. The version constants live on
|
||||
/// All protocol versions supported by this implementation. The era-specific lists live on
|
||||
/// <see cref="McpHttpHeaders"/> so the shared source file is the single source of truth.
|
||||
/// Keep in sync with s_supportedProtocolVersions in StreamableHttpHandler.
|
||||
/// </summary>
|
||||
internal static readonly string[] SupportedProtocolVersions =
|
||||
[
|
||||
"2024-11-05",
|
||||
"2025-03-26",
|
||||
"2025-06-18",
|
||||
McpHttpHeaders.November2025ProtocolVersion,
|
||||
McpHttpHeaders.July2026ProtocolVersion,
|
||||
];
|
||||
internal static readonly string[] SupportedProtocolVersions = McpProtocolVersions.SupportedProtocolVersions;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given protocol version supports priming events.
|
||||
|
|
@ -53,7 +45,7 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
/// </remarks>
|
||||
internal static bool SupportsPrimingEvent(string? protocolVersion)
|
||||
{
|
||||
const string MinResumabilityProtocolVersion = McpHttpHeaders.November2025ProtocolVersion;
|
||||
const string MinResumabilityProtocolVersion = McpProtocolVersions.November2025ProtocolVersion;
|
||||
|
||||
if (protocolVersion is null)
|
||||
{
|
||||
|
|
@ -81,7 +73,7 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
return string.Compare(protocolVersion, McpHttpHeaders.July2026ProtocolVersion, StringComparison.Ordinal) >= 0;
|
||||
return string.Compare(protocolVersion, McpProtocolVersions.July2026ProtocolVersion, StringComparison.Ordinal) >= 0;
|
||||
}
|
||||
|
||||
private readonly bool _isServer;
|
||||
|
|
@ -162,7 +154,7 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
(request, jsonRpcRequest, cancellationToken) =>
|
||||
{
|
||||
string? perRequestVersion = jsonRpcRequest?.Context?.ProtocolVersion ?? NegotiatedProtocolVersion;
|
||||
if (McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(perRequestVersion))
|
||||
if (McpProtocolVersions.RequiresPerRequestMetadata(perRequestVersion))
|
||||
{
|
||||
throw new McpProtocolException(
|
||||
$"Method '{RequestMethods.Ping}' is not available on protocol version '{perRequestVersion}'.",
|
||||
|
|
@ -583,8 +575,8 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
// 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
|
||||
// -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.
|
||||
// which conformant 2026-07-28 clients recognize as a SEP-2575 signal and surface as-is rather
|
||||
// than mistaking it for an initialize-handshake server and falling back to initialize.
|
||||
if (context.ProtocolVersion is { } existing && !string.Equals(existing, protocolVersionValue, StringComparison.Ordinal))
|
||||
{
|
||||
throw new McpProtocolException(
|
||||
|
|
@ -618,7 +610,7 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used by <see cref="Client.McpClient"/> 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
|
||||
/// info, and client capabilities on every outgoing request (replacing what the
|
||||
/// <c>initialize</c> handshake previously negotiated once).
|
||||
/// </remarks>
|
||||
internal static void InjectRequestMeta(
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
<Compile Include="..\Common\Experimentals.cs" Link="Experimentals.cs" />
|
||||
<Compile Include="..\Common\EncodingUtilities.cs" Link="EncodingUtilities.cs" />
|
||||
<Compile Include="..\Common\McpHttpHeaders.cs" Link="McpHttpHeaders.cs" />
|
||||
<Compile Include="..\Common\McpProtocolVersions.cs" Link="McpProtocolVersions.cs" />
|
||||
<Compile Include="..\Common\HttpResponseMessageExtensions.cs" Link="HttpResponseMessageExtensions.cs" />
|
||||
<Compile Include="..\Common\ServerSentEvents\**\*.cs" Link="ServerSentEvents\%(RecursiveDir)%(FileName)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -8,16 +8,19 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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 <c>initialize</c> handshake.
|
||||
/// to learn what a server supports without performing the <c>initialize</c> handshake.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class DiscoverResult : Result, ICacheableResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of MCP protocol version strings that the server supports.
|
||||
/// Gets or sets the list of MCP protocol version strings the server supports for subsequent
|
||||
/// per-request metadata requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The client should choose a version from this list for use in subsequent requests.
|
||||
/// The client should choose a version from this list for subsequent requests that carry the
|
||||
/// 2026-07-28-style per-request <c>_meta</c> envelope. Versions that require the
|
||||
/// <c>initialize</c> handshake are negotiated through <c>initialize</c> instead.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("supportedVersions")]
|
||||
public required IList<string> SupportedVersions { get; set; }
|
||||
|
|
|
|||
|
|
@ -22,16 +22,17 @@ namespace ModelContextProtocol.Protocol;
|
|||
public sealed class InitializeRequestParams : RequestParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the version of the Model Context Protocol that the client wants to use.
|
||||
/// Gets or sets the initialize-handshake Model Context Protocol version that the client wants to use.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Protocol version is specified using a date-based versioning scheme in the format "YYYY-MM-DD".
|
||||
/// The client and server must agree on a protocol version to communicate successfully.
|
||||
/// The client and server must agree on an initialize-capable protocol version to communicate successfully.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// During initialization, the server will check if it supports this requested version. If there's a
|
||||
/// mismatch, the server will reject the connection with a version mismatch error.
|
||||
/// During initialization, the server will check if it supports this requested version. Protocol
|
||||
/// revisions starting with <c>2026-07-28</c> do not use <c>initialize</c>; clients select them with
|
||||
/// <c>server/discover</c> and per-request metadata instead.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// See the <see href="https://spec.modelcontextprotocol.io/specification/">protocol specification</see> for version details.
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ namespace ModelContextProtocol.Protocol;
|
|||
public sealed class InitializeResult : Result
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the version of the Model Context Protocol that the server will use for this session.
|
||||
/// Gets or sets the initialize-handshake Model Context Protocol version that the server will use for this session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is the protocol version the server has agreed to use, which should match the client's
|
||||
/// This is the initialize-capable protocol version the server has agreed to use, which should match the client's
|
||||
/// requested version. If there's a mismatch, the client should throw an exception to prevent
|
||||
/// communication issues due to incompatible protocol versions.
|
||||
/// </para>
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ public abstract class JsonRpcMessage
|
|||
// Per JSON-RPC 2.0, when an error occurs before the request id can be determined
|
||||
// (e.g. parse error or invalid request), the server MUST respond with id=null.
|
||||
// Accept null-id error responses so callers can recognize the structured signal
|
||||
// (e.g. an HTTP 400 body whose JSON-RPC envelope carries a non-modern error code).
|
||||
// (e.g. an HTTP 400 body whose JSON-RPC envelope carries a non-SEP-2575 error code).
|
||||
return new JsonRpcError
|
||||
{
|
||||
Id = id,
|
||||
|
|
|
|||
|
|
@ -159,15 +159,15 @@ public static class RequestMethods
|
|||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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 <c>initialize</c> handshake.
|
||||
/// to learn what a server supports without performing the <c>initialize</c> handshake.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The server's response includes its supported protocol versions, capabilities, implementation
|
||||
/// information, and optional usage instructions.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Servers SHOULD implement this method. Legacy clients MAY ignore it. Clients on the 2026-07-28 revision
|
||||
/// typically call this once during connection establishment.
|
||||
/// Servers SHOULD implement this method. Initialize-handshake clients MAY ignore it. Clients on the
|
||||
/// 2026-07-28 revision typically call this once during connection establishment.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public const string ServerDiscover = "server/discover";
|
||||
|
|
@ -179,7 +179,7 @@ public static class RequestMethods
|
|||
/// <remarks>
|
||||
/// <para>
|
||||
/// This RPC is introduced in the 2026-07-28 protocol revision (SEP-2575) and replaces the unsolicited
|
||||
/// HTTP GET endpoint and the legacy <see cref="ResourcesSubscribe"/> / <see cref="ResourcesUnsubscribe"/>
|
||||
/// HTTP GET endpoint and the initialize-handshake <see cref="ResourcesSubscribe"/> / <see cref="ResourcesUnsubscribe"/>
|
||||
/// request methods.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
|
|
|||
|
|
@ -27,11 +27,22 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
private readonly NotificationHandlers _notificationHandlers;
|
||||
private readonly RequestHandlers _requestHandlers;
|
||||
private readonly McpSessionHandler _sessionHandler;
|
||||
private readonly string[] _supportedProtocolVersions;
|
||||
private readonly string[] _initializeHandshakeProtocolVersions;
|
||||
private readonly string[] _perRequestMetadataProtocolVersions;
|
||||
private readonly SemaphoreSlim _disposeLock = new(1, 1);
|
||||
private readonly ConcurrentDictionary<string, CancellationTokenSource> _taskCancellationSources = new();
|
||||
private readonly ConcurrentDictionary<string, MrtrContinuation> _mrtrContinuations = new();
|
||||
private readonly ConcurrentDictionary<RequestId, MrtrContext> _mrtrContextsByRequestId = new();
|
||||
|
||||
private static readonly string[] s_perRequestMetadataKeys =
|
||||
[
|
||||
MetaKeys.ProtocolVersion,
|
||||
MetaKeys.ClientInfo,
|
||||
MetaKeys.ClientCapabilities,
|
||||
MetaKeys.LogLevel,
|
||||
];
|
||||
|
||||
// Track MRTR handler tasks using the same inFlightCount + TCS pattern as
|
||||
// McpSessionHandler.ProcessMessagesCoreAsync. Starts at 1 for DisposeAsync itself.
|
||||
private int _mrtrInFlightCount = 1;
|
||||
|
|
@ -72,6 +83,9 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
_sessionTransport = transport;
|
||||
ServerOptions = options;
|
||||
Services = serviceProvider;
|
||||
_supportedProtocolVersions = GetConfiguredSupportedProtocolVersions(options.ProtocolVersion);
|
||||
_initializeHandshakeProtocolVersions = [.. _supportedProtocolVersions.Where(McpProtocolVersions.SupportsInitializeHandshake)];
|
||||
_perRequestMetadataProtocolVersions = [.. _supportedProtocolVersions.Where(McpProtocolVersions.RequiresPerRequestMetadata)];
|
||||
_serverOnlyEndpointName = $"Server ({options.ServerInfo?.Name ?? DefaultImplementation.Name} {options.ServerInfo?.Version ?? DefaultImplementation.Version})";
|
||||
_endpointName = _serverOnlyEndpointName;
|
||||
_servicesScopePerRequest = options.ScopeRequests;
|
||||
|
|
@ -161,33 +175,93 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
/// MUST be populated per-request. Per-request client capabilities and client info are consumed request-scoped
|
||||
/// by <see cref="DestinationBoundMcpServer"/> and are not read from server-wide state by request handlers. The
|
||||
/// shared <see cref="_clientInfo"/> write below is best-effort and used only to derive the session endpoint
|
||||
/// name for logging/telemetry. 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).
|
||||
/// name for logging/telemetry. For initialize-handshake clients the per-request values are absent and the built-in
|
||||
/// filter is a no-op (the values were captured during the initialize handler).
|
||||
/// </remarks>
|
||||
private JsonRpcMessageFilter PrependMetaReadingFilter(JsonRpcMessageFilter inner)
|
||||
{
|
||||
JsonRpcMessageFilter metaReadingFilter = next => async (message, cancellationToken) =>
|
||||
{
|
||||
if (message is JsonRpcRequest { Method: not RequestMethods.Initialize } request && request.Context is { } context)
|
||||
if (message is JsonRpcRequest { Method: RequestMethods.Initialize } initializeRequest)
|
||||
{
|
||||
ValidateInitializeRequestBoundary(initializeRequest);
|
||||
}
|
||||
else if (message is JsonRpcRequest request)
|
||||
{
|
||||
var context = request.Context;
|
||||
bool endpointNameNeedsRefresh = false;
|
||||
bool hasProtocolVersionMeta = HasMetaKey(request, MetaKeys.ProtocolVersion);
|
||||
bool hasReservedPerRequestMeta = TryGetPerRequestMetadataKey(request, out var reservedPerRequestMetaKey);
|
||||
|
||||
if (context.ProtocolVersion is { } protocolVersion)
|
||||
if (context?.ProtocolVersion is { } protocolVersion)
|
||||
{
|
||||
bool protocolVersionAlreadyEstablished = _negotiatedProtocolVersion is not null;
|
||||
if (protocolVersionAlreadyEstablished)
|
||||
{
|
||||
SetNegotiatedProtocolVersion(protocolVersion);
|
||||
}
|
||||
|
||||
// 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 (-32022) carrying the supported list.
|
||||
if (!McpSessionHandler.SupportedProtocolVersions.Contains(protocolVersion))
|
||||
if (!_supportedProtocolVersions.Contains(protocolVersion))
|
||||
{
|
||||
throw new UnsupportedProtocolVersionException(
|
||||
requested: protocolVersion,
|
||||
supported: McpSessionHandler.SupportedProtocolVersions);
|
||||
supported: _supportedProtocolVersions);
|
||||
}
|
||||
|
||||
SetNegotiatedProtocolVersion(protocolVersion);
|
||||
if (McpProtocolVersions.RequiresPerRequestMetadata(protocolVersion))
|
||||
{
|
||||
ValidateRequiredPerRequestMetadata(
|
||||
protocolVersion,
|
||||
hasProtocolVersionMeta,
|
||||
context.ClientInfo is not null,
|
||||
context.ClientCapabilities is not null);
|
||||
}
|
||||
else if (McpProtocolVersions.SupportsInitializeHandshake(protocolVersion))
|
||||
{
|
||||
if (_negotiatedProtocolVersion is null && hasProtocolVersionMeta)
|
||||
{
|
||||
throw new UnsupportedProtocolVersionException(
|
||||
requested: protocolVersion,
|
||||
supported: _perRequestMetadataProtocolVersions,
|
||||
message: $"Protocol version '{protocolVersion}' requires the initialize handshake and cannot be selected through per-request metadata.");
|
||||
}
|
||||
|
||||
if (hasReservedPerRequestMeta)
|
||||
{
|
||||
ThrowReservedPerRequestMetadata(requestedProtocolVersion: protocolVersion, reservedPerRequestMetaKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (!protocolVersionAlreadyEstablished)
|
||||
{
|
||||
SetNegotiatedProtocolVersion(protocolVersion);
|
||||
}
|
||||
}
|
||||
else if (_negotiatedProtocolVersion is null)
|
||||
{
|
||||
if (request.Method == RequestMethods.ServerDiscover)
|
||||
{
|
||||
throw new McpProtocolException(
|
||||
$"The '{RequestMethods.ServerDiscover}' request requires per-request metadata declaring a supported protocol version.",
|
||||
McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
if (hasReservedPerRequestMeta)
|
||||
{
|
||||
ThrowReservedPerRequestMetadata(requestedProtocolVersion: null, reservedPerRequestMetaKey);
|
||||
}
|
||||
}
|
||||
else if (McpProtocolVersions.SupportsInitializeHandshake(_negotiatedProtocolVersion) && hasReservedPerRequestMeta)
|
||||
{
|
||||
ThrowReservedPerRequestMetadata(_negotiatedProtocolVersion, reservedPerRequestMetaKey);
|
||||
}
|
||||
|
||||
if (context.ClientInfo is { } clientInfo &&
|
||||
ValidateRequestMethodBoundary(request);
|
||||
|
||||
if (context?.ClientInfo is { } clientInfo &&
|
||||
(_clientInfo is null || !string.Equals(_clientInfo.Name, clientInfo.Name, StringComparison.Ordinal) ||
|
||||
!string.Equals(_clientInfo.Version, clientInfo.Version, StringComparison.Ordinal)))
|
||||
{
|
||||
|
|
@ -207,6 +281,10 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
_sessionHandler.EndpointName = _endpointName;
|
||||
}
|
||||
}
|
||||
else if (message is JsonRpcNotification notification)
|
||||
{
|
||||
ValidateNotificationBoundary(notification);
|
||||
}
|
||||
|
||||
await next(message, cancellationToken).ConfigureAwait(false);
|
||||
};
|
||||
|
|
@ -214,6 +292,142 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
return next => metaReadingFilter(inner(next));
|
||||
}
|
||||
|
||||
private static void ValidateRequiredPerRequestMetadata(
|
||||
string protocolVersion,
|
||||
bool hasProtocolVersionMeta,
|
||||
bool hasClientInfoMeta,
|
||||
bool hasClientCapabilitiesMeta)
|
||||
{
|
||||
if (!hasProtocolVersionMeta)
|
||||
{
|
||||
ThrowMissingPerRequestMetadata(protocolVersion, MetaKeys.ProtocolVersion);
|
||||
}
|
||||
|
||||
if (!hasClientInfoMeta)
|
||||
{
|
||||
ThrowMissingPerRequestMetadata(protocolVersion, MetaKeys.ClientInfo);
|
||||
}
|
||||
|
||||
if (!hasClientCapabilitiesMeta)
|
||||
{
|
||||
ThrowMissingPerRequestMetadata(protocolVersion, MetaKeys.ClientCapabilities);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowMissingPerRequestMetadata(string protocolVersion, string key) =>
|
||||
throw new McpProtocolException(
|
||||
$"Requests using protocol version '{protocolVersion}' must include '_meta/{key}'.",
|
||||
McpErrorCode.InvalidParams);
|
||||
|
||||
private static void ThrowReservedPerRequestMetadata(string? requestedProtocolVersion, string key) =>
|
||||
throw new McpProtocolException(
|
||||
requestedProtocolVersion is null
|
||||
? $"The reserved per-request metadata key '_meta/{key}' requires a protocol version that uses per-request metadata."
|
||||
: $"The reserved per-request metadata key '_meta/{key}' is not valid with protocol version '{requestedProtocolVersion}'.",
|
||||
McpErrorCode.InvalidRequest);
|
||||
|
||||
private static bool TryGetPerRequestMetadataKey(JsonRpcRequest request, out string key)
|
||||
{
|
||||
foreach (var candidate in s_perRequestMetadataKeys)
|
||||
{
|
||||
if (HasMetaKey(request, candidate))
|
||||
{
|
||||
key = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
key = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasMetaKey(JsonRpcRequest request, string key) =>
|
||||
request.Params is JsonObject paramsObj &&
|
||||
paramsObj["_meta"] is JsonObject metaObj &&
|
||||
metaObj.ContainsKey(key);
|
||||
|
||||
private void ValidateInitializeRequestBoundary(JsonRpcRequest request)
|
||||
{
|
||||
if (request.Context?.ProtocolVersion is { } protocolVersion &&
|
||||
!McpProtocolVersions.SupportsInitializeHandshake(protocolVersion))
|
||||
{
|
||||
throw new UnsupportedProtocolVersionException(
|
||||
requested: protocolVersion,
|
||||
supported: _initializeHandshakeProtocolVersions,
|
||||
message: $"Protocol version '{protocolVersion}' is not available through the initialize handshake.");
|
||||
}
|
||||
|
||||
if (TryGetPerRequestMetadataKey(request, out var key))
|
||||
{
|
||||
ThrowReservedPerRequestMetadata(TryGetStringParam(request, "protocolVersion"), key);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? TryGetStringParam(JsonRpcRequest request, string propertyName)
|
||||
{
|
||||
if (request.Params is JsonObject paramsObj &&
|
||||
paramsObj[propertyName] is JsonValue value &&
|
||||
value.TryGetValue(out string? result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string[] GetConfiguredSupportedProtocolVersions(string? protocolVersion)
|
||||
{
|
||||
if (protocolVersion is null)
|
||||
{
|
||||
return McpProtocolVersions.SupportedProtocolVersions;
|
||||
}
|
||||
|
||||
if (!McpProtocolVersions.IsSupportedProtocolVersion(protocolVersion))
|
||||
{
|
||||
throw new McpException(
|
||||
$"Unsupported server protocol version '{protocolVersion}'. Supported protocol versions: " +
|
||||
string.Join(", ", McpProtocolVersions.SupportedProtocolVersions) + ".");
|
||||
}
|
||||
|
||||
return [protocolVersion];
|
||||
}
|
||||
|
||||
private void ValidateNotificationBoundary(JsonRpcNotification notification)
|
||||
{
|
||||
if (notification.Method == NotificationMethods.InitializedNotification &&
|
||||
McpProtocolVersions.RequiresPerRequestMetadata(notification.Context?.ProtocolVersion ?? _negotiatedProtocolVersion))
|
||||
{
|
||||
throw new McpProtocolException(
|
||||
$"The notification '{NotificationMethods.InitializedNotification}' is only valid after the initialize handshake.",
|
||||
McpErrorCode.InvalidRequest);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateRequestMethodBoundary(JsonRpcRequest request)
|
||||
{
|
||||
bool usesPerRequestMetadata = IsJuly2026OrLaterProtocolRequest(request);
|
||||
|
||||
if (!usesPerRequestMetadata &&
|
||||
request.Method is RequestMethods.SubscriptionsListen
|
||||
or RequestMethods.TasksGet
|
||||
or RequestMethods.TasksUpdate
|
||||
or RequestMethods.TasksCancel
|
||||
or RequestMethods.ServerDiscover)
|
||||
{
|
||||
throw new McpProtocolException(
|
||||
$"The method '{request.Method}' requires a newer protocol revision that supports per-request metadata; " +
|
||||
$"the negotiated protocol version is '{NegotiatedProtocolVersion ?? "(none)"}'.",
|
||||
McpErrorCode.MethodNotFound);
|
||||
}
|
||||
|
||||
if (usesPerRequestMetadata && request.Method == RequestMethods.LoggingSetLevel)
|
||||
{
|
||||
throw new McpProtocolException(
|
||||
$"The method '{RequestMethods.LoggingSetLevel}' is not available on protocol version '{request.Context?.ProtocolVersion ?? NegotiatedProtocolVersion}'. Use per-request _meta/{MetaKeys.LogLevel} instead.",
|
||||
McpErrorCode.MethodNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? SessionId => _sessionTransport.SessionId;
|
||||
|
||||
|
|
@ -356,26 +570,54 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
UpdateEndpointNameWithClientInfo();
|
||||
_sessionHandler.EndpointName = _endpointName;
|
||||
|
||||
// Negotiate a protocol version. If the server options provide one, use that.
|
||||
// Otherwise, try to use whatever the client requested as long as it's supported.
|
||||
// If it's not supported, fall back to the latest supported version.
|
||||
// Negotiate an initialize-handshake protocol version. initialize is not available in the 2026-07-28
|
||||
// and later protocol revisions, so those versions must use server/discover with
|
||||
// per-request _meta instead.
|
||||
string? protocolVersion = options.ProtocolVersion;
|
||||
protocolVersion ??= request?.ProtocolVersion is string clientProtocolVersion &&
|
||||
McpSessionHandler.SupportedProtocolVersions.Contains(clientProtocolVersion) ?
|
||||
clientProtocolVersion :
|
||||
McpHttpHeaders.November2025ProtocolVersion;
|
||||
if (protocolVersion is { } configuredProtocolVersion &&
|
||||
McpProtocolVersions.IsJuly2026OrLaterProtocolVersion(configuredProtocolVersion))
|
||||
{
|
||||
throw new UnsupportedProtocolVersionException(
|
||||
configuredProtocolVersion,
|
||||
_initializeHandshakeProtocolVersions,
|
||||
$"Protocol version '{configuredProtocolVersion}' is not available through the initialize handshake.");
|
||||
}
|
||||
|
||||
// The legacy initialize handshake is authoritative: it may supersede a protocol version
|
||||
// a prior server/discover probe established on the same connection (the dual-era
|
||||
if (protocolVersion is null)
|
||||
{
|
||||
if (request?.ProtocolVersion is string clientProtocolVersion)
|
||||
{
|
||||
if (McpProtocolVersions.IsJuly2026OrLaterProtocolVersion(clientProtocolVersion))
|
||||
{
|
||||
throw new UnsupportedProtocolVersionException(
|
||||
clientProtocolVersion,
|
||||
_initializeHandshakeProtocolVersions,
|
||||
$"Protocol version '{clientProtocolVersion}' is not available through the initialize handshake.");
|
||||
}
|
||||
|
||||
protocolVersion = McpProtocolVersions.SupportsInitializeHandshake(clientProtocolVersion) ?
|
||||
clientProtocolVersion :
|
||||
McpProtocolVersions.November2025ProtocolVersion;
|
||||
}
|
||||
else
|
||||
{
|
||||
protocolVersion = McpProtocolVersions.November2025ProtocolVersion;
|
||||
}
|
||||
}
|
||||
|
||||
string negotiatedProtocolVersion = protocolVersion ?? McpProtocolVersions.November2025ProtocolVersion;
|
||||
|
||||
// The initialize handshake is authoritative: it may supersede a protocol version
|
||||
// a prior server/discover probe established on the same connection (the dual-path
|
||||
// fallback path a permissive client takes against an unknown server). Unlike the
|
||||
// per-request 2026-07-28 version - which SetNegotiatedProtocolVersion locks once negotiated -
|
||||
// initialize force-sets the version.
|
||||
_negotiatedProtocolVersion = protocolVersion;
|
||||
_sessionHandler.NegotiatedProtocolVersion = protocolVersion;
|
||||
_negotiatedProtocolVersion = negotiatedProtocolVersion;
|
||||
_sessionHandler.NegotiatedProtocolVersion = negotiatedProtocolVersion;
|
||||
|
||||
return new InitializeResult
|
||||
{
|
||||
ProtocolVersion = protocolVersion,
|
||||
ProtocolVersion = negotiatedProtocolVersion,
|
||||
Instructions = options.ServerInstructions,
|
||||
ServerInfo = options.ServerInfo ?? DefaultImplementation,
|
||||
Capabilities = ServerCapabilities ?? new(),
|
||||
|
|
@ -389,9 +631,9 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
/// Registers the <c>server/discover</c> request handler introduced by the 2026-07-28 protocol revision (SEP-2575).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The handler is registered unconditionally so legacy clients can probe it too. It returns the server's
|
||||
/// supported protocol versions (<see cref="McpSessionHandler.SupportedProtocolVersions"/>), server
|
||||
/// capabilities, server info, and optional instructions.
|
||||
/// The handler is registered unconditionally so requests can be routed to the protocol boundary filters. Successful
|
||||
/// <c>server/discover</c> responses advertise only protocol versions available through per-request metadata; versions
|
||||
/// that require the <c>initialize</c> handshake are negotiated through <c>initialize</c> instead.
|
||||
/// </remarks>
|
||||
private void ConfigureDiscover(McpServerOptions options)
|
||||
{
|
||||
|
|
@ -400,7 +642,7 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
{
|
||||
return new ValueTask<DiscoverResult>(new DiscoverResult
|
||||
{
|
||||
SupportedVersions = [.. McpSessionHandler.SupportedProtocolVersions],
|
||||
SupportedVersions = [.. _perRequestMetadataProtocolVersions],
|
||||
Capabilities = ServerCapabilities ?? new(),
|
||||
ServerInfo = options.ServerInfo ?? DefaultImplementation,
|
||||
Instructions = options.ServerInstructions,
|
||||
|
|
@ -435,6 +677,14 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
_requestHandlers.Set(RequestMethods.SubscriptionsListen,
|
||||
async (request, jsonRpcRequest, cancellationToken) =>
|
||||
{
|
||||
if (!IsJuly2026OrLaterProtocolRequest(jsonRpcRequest))
|
||||
{
|
||||
throw new McpProtocolException(
|
||||
$"The method '{RequestMethods.SubscriptionsListen}' requires a newer protocol revision that supports per-request subscriptions; " +
|
||||
$"the negotiated protocol version is '{NegotiatedProtocolVersion ?? "(none)"}'.",
|
||||
McpErrorCode.MethodNotFound);
|
||||
}
|
||||
|
||||
var requested = request?.Notifications ?? new SubscriptionsListenNotifications();
|
||||
|
||||
// A stateless session (Streamable HTTP with no session) cannot deliver out-of-band
|
||||
|
|
@ -442,7 +692,7 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
// changes back to the client (tracked by #1662). Rather than hold the POST open forever only
|
||||
// to deliver nothing - pinning the connection and its request scope - acknowledge the listen
|
||||
// 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
|
||||
// negotiation, so it is not an initialize-handshake-server signal and never triggers a client fallback to the
|
||||
// initialize handshake.
|
||||
if (!HasStatefulTransport())
|
||||
{
|
||||
|
|
@ -522,7 +772,7 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
/// </remarks>
|
||||
private async Task SendListChangedNotificationAsync(string notificationMethod)
|
||||
{
|
||||
// Legacy clients never open a subscriptions/listen stream, so they keep the session-wide broadcast.
|
||||
// Initialize-handshake clients never open a subscriptions/listen stream, so they keep the session-wide broadcast.
|
||||
// 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())
|
||||
|
|
@ -805,8 +1055,8 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
cancelTaskHandler ??= (static async (request, _) => throw new McpProtocolException($"Unknown task: '{request.Params?.TaskId}'", McpErrorCode.InvalidParams));
|
||||
|
||||
// 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 2026-07-28 requests.
|
||||
// MethodNotFound when the request was negotiated under an initialize-handshake protocol version. The handlers
|
||||
// stay registered so a dual-path server still serves them for 2026-07-28 requests.
|
||||
getTaskHandler = GateTaskMethodToJuly2026OrLaterProtocol(getTaskHandler, RequestMethods.TasksGet);
|
||||
updateTaskHandler = GateTaskMethodToJuly2026OrLaterProtocol(updateTaskHandler, RequestMethods.TasksUpdate);
|
||||
cancelTaskHandler = GateTaskMethodToJuly2026OrLaterProtocol(cancelTaskHandler, RequestMethods.TasksCancel);
|
||||
|
|
@ -883,7 +1133,7 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
listResourceTemplatesHandler ??= (static async (_, __) => new ListResourceTemplatesResult());
|
||||
readResourceHandler ??= (static async (request, _) =>
|
||||
{
|
||||
var errorCode = McpHttpHeaders.UseInvalidParamsForMissingResource(request.Server.NegotiatedProtocolVersion)
|
||||
var errorCode = McpProtocolVersions.UseInvalidParamsForMissingResource(request.Server.NegotiatedProtocolVersion)
|
||||
? McpErrorCode.InvalidParams
|
||||
: McpErrorCode.ResourceNotFound;
|
||||
throw new McpProtocolException($"Unknown resource URI: '{request.Params?.Uri}'", errorCode);
|
||||
|
|
@ -1560,6 +1810,13 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
RequestMethods.LoggingSetLevel,
|
||||
(request, jsonRpcRequest, cancellationToken) =>
|
||||
{
|
||||
if (IsJuly2026OrLaterProtocolRequest(jsonRpcRequest))
|
||||
{
|
||||
throw new McpProtocolException(
|
||||
$"The method '{RequestMethods.LoggingSetLevel}' is not available on protocol version '{jsonRpcRequest.Context?.ProtocolVersion ?? NegotiatedProtocolVersion}'. Use per-request _meta/{MetaKeys.LogLevel} instead.",
|
||||
McpErrorCode.MethodNotFound);
|
||||
}
|
||||
|
||||
// Store the provided level.
|
||||
if (request is not null)
|
||||
{
|
||||
|
|
@ -1794,7 +2051,7 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
|
||||
/// <inheritdoc cref="IsJuly2026OrLaterProtocolRequest(JsonRpcRequest?)"/>
|
||||
internal bool IsJuly2026OrLaterProtocolRequest(JsonRpcMessageContext? requestContext) =>
|
||||
McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(
|
||||
McpProtocolVersions.IsJuly2026OrLaterProtocolVersion(
|
||||
requestContext?.ProtocolVersion ?? NegotiatedProtocolVersion);
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public sealed class McpServerOptions
|
|||
/// Gets or sets information about this server implementation, including its name and version.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This information is sent to the client during initialization to identify the server.
|
||||
/// This information is sent to the client during initialization or discovery to identify the server.
|
||||
/// It's displayed in client logs and can be used for debugging and compatibility checks.
|
||||
/// </remarks>
|
||||
public Implementation? ServerInfo { get; set; }
|
||||
|
|
@ -33,11 +33,23 @@ public sealed class McpServerOptions
|
|||
/// Gets or sets the protocol version supported by this server, using a date-based versioning scheme.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The protocol version defines which features and message formats this server supports.
|
||||
/// This uses a date-based versioning scheme in the format "YYYY-MM-DD".
|
||||
/// If <see langword="null"/>, the server will advertise to the client the version requested
|
||||
/// by the client if that version is known to be supported, and otherwise will advertise the latest
|
||||
/// version supported by the server.
|
||||
/// <para>
|
||||
/// The protocol version defines which features and message formats this server supports. Supported
|
||||
/// values are <c>2024-11-05</c>, <c>2025-03-26</c>, <c>2025-06-18</c>, <c>2025-11-25</c>, and
|
||||
/// <c>2026-07-28</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If <see langword="null"/>, the server supports all of the versions listed above. For clients using
|
||||
/// the <c>initialize</c> handshake, the server returns the requested initialize-capable version when it
|
||||
/// is supported and otherwise returns <c>2025-11-25</c>. For clients using <c>server/discover</c> and
|
||||
/// per-request metadata, the server advertises the supported per-request metadata versions; currently
|
||||
/// this is <c>2026-07-28</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Set this property to a specific supported value to pin the server to that version. Setting it to
|
||||
/// <c>2026-07-28</c> makes the server reject <c>initialize</c> handshakes; setting it to an earlier
|
||||
/// value makes the server reject <c>2026-07-28</c> per-request metadata.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public string? ProtocolVersion { get; set; }
|
||||
|
||||
|
|
@ -74,12 +86,13 @@ public sealed class McpServerOptions
|
|||
public bool ScopeRequests { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets preexisting knowledge about the client including its name and version to help support
|
||||
/// stateless Streamable HTTP servers that encode this knowledge in the mcp-session-id header.
|
||||
/// Gets or sets preexisting knowledge about the client including its name and version.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When not specified, this information is sourced from the client's initialize request.
|
||||
/// When not specified, this information is sourced from the client's <c>initialize</c> request or,
|
||||
/// for protocol versions that use per-request metadata, from the current request's <c>_meta</c> field.
|
||||
/// This is typically set during session migration in conjunction with <see cref="KnownClientCapabilities"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Implementation? KnownClientInfo { get; set; }
|
||||
|
|
@ -90,7 +103,8 @@ public sealed class McpServerOptions
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When not specified, this information is sourced from the client's initialize request.
|
||||
/// When not specified, this information is sourced from the client's <c>initialize</c> request or,
|
||||
/// for protocol versions that use per-request metadata, from the current request's <c>_meta</c> field.
|
||||
/// This is typically set during session migration in conjunction with <see cref="KnownClientInfo"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
|
|
|||
Loading…
Reference in a new issue