Follow-ups to #1610: de-draft 2026-07-28 terminology, renumber error codes, ProtocolVersion floor (#1671)
This commit is contained in:
parent
90c853103d
commit
d6b1615988
5 changed files with 785 additions and 898 deletions
|
|
@ -76,9 +76,9 @@ internal sealed partial class AutoDetectingClientSessionTransport : ITransport
|
|||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1174,10 +1174,10 @@ public abstract partial class McpClient : McpSession
|
|||
{
|
||||
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 @@ public abstract partial class McpClient : McpSession
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>tasks/get</c>, <c>tasks/update</c>, or <c>tasks/cancel</c> 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 <c>tasks/get</c>, <c>tasks/update</c>, or <c>tasks/cancel</c>
|
||||
/// on an older session is a programming error rather than a recoverable protocol condition.
|
||||
/// </summary>
|
||||
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)"}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,21 +289,21 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
|
||||
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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
}
|
||||
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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
}
|
||||
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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
}
|
||||
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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
.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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
_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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
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 sealed partial class McpClientImpl : McpClient
|
|||
_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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
|
||||
const int maxRetries = 10;
|
||||
|
||||
InjectDraftMetaIfNeeded(request);
|
||||
InjectRequestMetaIfNeeded(request);
|
||||
|
||||
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
|
|
@ -665,7 +658,7 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
}
|
||||
|
||||
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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
paramsObj.Remove("inputResponses");
|
||||
|
||||
request = new JsonRpcRequest { Method = request.Method, Params = paramsObj, Context = request.Context };
|
||||
InjectDraftMetaIfNeeded(request);
|
||||
InjectRequestMetaIfNeeded(request);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -695,25 +688,25 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Injects the draft-protocol per-request <c>_meta</c> 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 <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.
|
||||
/// </summary>
|
||||
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 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
/// <summary>Logs a warning if the session negotiated MRTR but the server sent a legacy JSON-RPC request.</summary>
|
||||
private void WarnIfLegacyRequestOnMrtrSession(string method)
|
||||
{
|
||||
if (IsDraftProtocol())
|
||||
if (IsJuly2026OrLaterProtocol())
|
||||
{
|
||||
LogLegacyRequestOnMrtrSession(_endpointName, method);
|
||||
}
|
||||
|
|
@ -764,7 +757,7 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
/// <summary>Logs a warning if the session did not negotiate MRTR but the server sent an InputRequiredResult.</summary>
|
||||
private void WarnIfInputRequiredResultOnNonMrtrSession(string method)
|
||||
{
|
||||
if (!IsDraftProtocol())
|
||||
if (!IsJuly2026OrLaterProtocol())
|
||||
{
|
||||
LogInputRequiredResultOnNonMrtrSession(_endpointName, method, _negotiatedProtocolVersion);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,52 +52,23 @@ public sealed class McpClientOptions
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When non-<see langword="null"/>, this version is requested from the server. Setting it to a
|
||||
/// legacy (non-draft) version such as <c>2025-11-25</c> opts out of the draft revision and forces
|
||||
/// the <c>initialize</c> handshake; the handshake then fails if the server's negotiated version
|
||||
/// does not match.
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When <see langword="null"/> (the default), the client prefers the draft revision
|
||||
/// (<see cref="McpSession.DraftProtocolVersion"/>): it probes with <c>server/discover</c> and
|
||||
/// automatically falls back to a legacy <c>initialize</c> handshake, downgrading to any version
|
||||
/// the server advertises, when the server does not support the draft revision.
|
||||
/// 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
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public string? ProtocolVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum protocol version the client will accept during version negotiation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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
|
||||
/// <see cref="McpException"/> instead.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is useful when the client requires features (such as the draft revision's removal of the
|
||||
/// <c>initialize</c> handshake or <c>Mcp-Session-Id</c>) that are not available in older protocol
|
||||
/// revisions. Because the client already prefers the draft revision by default, setting this to
|
||||
/// <see cref="McpSession.DraftProtocolVersion"/> disables the automatic legacy-server fallback
|
||||
/// that otherwise switches to the <c>initialize</c> handshake.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If <see langword="null"/> (the default), the client falls back to any version the server
|
||||
/// advertises, including legacy versions such as 2025-11-25.
|
||||
/// </para>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // The draft revision is already the default; pin the minimum to refuse the legacy fallback.
|
||||
/// var clientOptions = new McpClientOptions
|
||||
/// {
|
||||
/// MinProtocolVersion = McpSession.DraftProtocolVersion,
|
||||
/// };
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </remarks>
|
||||
public string? MinProtocolVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a timeout for the client-server initialization handshake sequence.
|
||||
/// </summary>
|
||||
|
|
@ -128,10 +99,10 @@ public sealed class McpClientOptions
|
|||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This timeout only has an effect when the client prefers the draft protocol revision — that is,
|
||||
/// when <see cref="ProtocolVersion"/> is <see langword="null"/> (the default) or
|
||||
/// <see cref="McpSession.DraftProtocolVersion"/>. In that mode the client first probes the server
|
||||
/// with a <c>server/discover</c> request. A legacy server that predates the draft revision may
|
||||
/// 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
|
||||
/// 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
|
||||
|
|
@ -140,7 +111,7 @@ public sealed class McpClientOptions
|
|||
/// <para>
|
||||
/// 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
|
||||
/// <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
|
||||
|
|
|
|||
|
|
@ -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 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 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
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 sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
|
||||
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 sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
}
|
||||
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 sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
|
||||
/// <summary>
|
||||
/// Reads the protocol version from a request's <c>_meta/io.modelcontextprotocol/protocolVersion</c> field,
|
||||
/// introduced by the draft protocol revision (SEP-2575). Returns <see langword="null"/> for messages that
|
||||
/// Introduced by the 2026-07-28 protocol revision (SEP-2575). Returns <see langword="null"/> for messages that
|
||||
/// don't have that field.
|
||||
/// </summary>
|
||||
private static string? ExtractProtocolVersionFromMeta(JsonRpcMessage message)
|
||||
|
|
|
|||
Loading…
Reference in a new issue