Skip to content

Commit 276bde4

Browse files
halter73Copilot
andcommitted
Surface HeaderMismatch errors instead of falling back to legacy
McpClientImpl.ConnectAsync's draft probe path catches McpProtocolException to fall back to a legacy `initialize` exchange. The handler had passthrough cases for the two modern draft error codes whose semantics are not ""I'm a legacy server"" (UnsupportedProtocolVersion -32004 and MissingRequiredClientCapability -32003) but was missing the third: HeaderMismatch -32001 from SEP-2243. That code means the body's `_meta.io.modelcontextprotocol/protocolVersion` does not match the `MCP-Protocol-Version` HTTP header — which is a client-side bug that should surface to the caller, not a signal to retry with `initialize`. Add the third passthrough so all three modern draft codes propagate, with an in-memory transport regression test that exercises the path against a server which only returns `-32001`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 00d57f7 commit 276bde4

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

src/ModelContextProtocol.Core/Client/McpClientImpl.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,16 +340,25 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
340340
// our capability set. Surface as-is (no fallback): the user must add capabilities.
341341
throw;
342342
}
343+
catch (McpProtocolException ex) when (ex.ErrorCode == McpErrorCode.HeaderMismatch)
344+
{
345+
// Spec-recognized modern-server signal: -32001. The server is modern but rejected
346+
// our request envelope (e.g., the MCP-Protocol-Version HTTP header didn't match
347+
// the body _meta.io.modelcontextprotocol/protocolVersion). Surface as-is (no
348+
// fallback): falling back to legacy initialize wouldn't fix a malformed envelope.
349+
throw;
350+
}
343351
catch (McpProtocolException)
344352
{
345353
// Per spec PR #2844, the fallback MUST NOT be keyed to a single error code —
346354
// any non-modern JSON-RPC error from the probe indicates a legacy server.
347355
// Common causes include MethodNotFound from a server that has no
348356
// server/discover handler, InvalidParams from a server confused by the
349357
// SEP-2575 _meta envelope, ParseError from a server that can't handle our
350-
// payload shape, or any other transport-defined error. The two modern-server
358+
// payload shape, or any other transport-defined error. The three modern-server
351359
// signals (-32004 UnsupportedProtocolVersion, -32003
352-
// MissingRequiredClientCapability) are caught above and never reach here.
360+
// MissingRequiredClientCapability, -32001 HeaderMismatch) are caught above and
361+
// never reach here.
353362
fallbackToLegacy = true;
354363
}
355364
catch (OperationCanceledException) when (probeCts.IsCancellationRequested && !initializationCts.IsCancellationRequested)

tests/ModelContextProtocol.Tests/Client/DraftProtocolFallbackTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,30 @@ public async Task LegacyClient_WithExplicitPin_StillRequires_ExactVersionMatch()
9696
Assert.Contains("mismatch", exception.Message, StringComparison.OrdinalIgnoreCase);
9797
}
9898

99+
[Fact]
100+
public async Task DraftClient_OnHeaderMismatch_Surfaces_NoFallback()
101+
{
102+
// The peer is modern (returns the spec-defined -32001 HeaderMismatch on the probe).
103+
// Falling back to legacy initialize would just produce another malformed envelope.
104+
// Verify the connect-time logic surfaces the error to the caller instead of falling back.
105+
var ct = TestContext.Current.CancellationToken;
106+
await using var transport = new LegacyServerTestTransport(
107+
serverNegotiatedVersion: "2025-11-25",
108+
probeErrorCode: (int)McpErrorCode.HeaderMismatch);
109+
110+
var exception = await Assert.ThrowsAnyAsync<McpException>(async () =>
111+
{
112+
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
113+
{
114+
ProtocolVersion = McpSession.DraftProtocolVersion,
115+
}, loggerFactory: LoggerFactory, cancellationToken: ct);
116+
});
117+
118+
Assert.True(transport.ServerDiscoverProbed);
119+
Assert.False(transport.LegacyInitializeReceived);
120+
Assert.Equal(McpErrorCode.HeaderMismatch, ((McpProtocolException)exception).ErrorCode);
121+
}
122+
99123
/// <summary>
100124
/// Minimal in-memory transport that simulates a legacy server: rejects
101125
/// <c>server/discover</c> (with a configurable JSON-RPC error code) and

0 commit comments

Comments
 (0)