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
6 changed files with 1755 additions and 589 deletions
|
|
@ -9,12 +9,12 @@ namespace ModelContextProtocol.Tests.Client;
|
|||
/// <summary>
|
||||
/// Connection-flow tests for the 2026-07-28 protocol revision (SEP-2575 + SEP-2567)
|
||||
/// on <see cref="McpClient"/>. A client that requests
|
||||
/// <see cref="McpHttpHeaders.July2026ProtocolVersion"/> calls <c>server/discover</c> rather than
|
||||
/// <see cref="McpProtocolVersions.July2026ProtocolVersion"/> calls <c>server/discover</c> rather than
|
||||
/// <c>initialize</c>.
|
||||
/// </summary>
|
||||
public class July2026ProtocolConnectionTests : ClientServerTestBase
|
||||
{
|
||||
private const string LatestStableVersion = "2025-11-25";
|
||||
private const string LatestStableVersion = McpProtocolVersions.November2025ProtocolVersion;
|
||||
|
||||
public July2026ProtocolConnectionTests(ITestOutputHelper testOutputHelper)
|
||||
: base(testOutputHelper, startServer: false)
|
||||
|
|
@ -34,42 +34,40 @@ public class July2026ProtocolConnectionTests : ClientServerTestBase
|
|||
{
|
||||
StartServer();
|
||||
|
||||
var options = new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion };
|
||||
var options = new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion };
|
||||
await using var client = await CreateMcpClientForServer(options);
|
||||
|
||||
Assert.Equal(McpHttpHeaders.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
Assert.Equal(McpProtocolVersions.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
Assert.NotNull(client.ServerCapabilities);
|
||||
Assert.Equal(nameof(July2026ProtocolConnectionTests), client.ServerInfo.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Client_RequestingLegacyVersion_NegotiatesLegacy()
|
||||
public async Task Client_RequestingInitializeHandshakeVersion_NegotiatesIt()
|
||||
{
|
||||
StartServer();
|
||||
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = LatestStableVersion });
|
||||
|
||||
Assert.NotEqual(McpHttpHeaders.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
Assert.NotEqual(McpProtocolVersions.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyClient_CanCallServerDiscover()
|
||||
public async Task InitializeHandshakeClient_CannotCallServerDiscover()
|
||||
{
|
||||
// server/discover is registered unconditionally, so a legacy client can probe it
|
||||
// (e.g., to learn capabilities without doing a second initialize).
|
||||
// server/discover is registered unconditionally so the protocol boundary filter can return a structured
|
||||
// error, but initialize-handshake clients cannot use it after negotiating an older protocol version.
|
||||
StartServer();
|
||||
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = LatestStableVersion });
|
||||
|
||||
var response = await client.SendRequestAsync(
|
||||
new JsonRpcRequest { Method = RequestMethods.ServerDiscover },
|
||||
TestContext.Current.CancellationToken);
|
||||
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
|
||||
await client.SendRequestAsync(
|
||||
new JsonRpcRequest { Method = RequestMethods.ServerDiscover },
|
||||
TestContext.Current.CancellationToken));
|
||||
|
||||
var discoverResult = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(discoverResult);
|
||||
Assert.NotEmpty(discoverResult.SupportedVersions);
|
||||
Assert.Contains(LatestStableVersion, discoverResult.SupportedVersions);
|
||||
Assert.Equal(nameof(July2026ProtocolConnectionTests), discoverResult.ServerInfo.Name);
|
||||
Assert.Equal(McpErrorCode.MethodNotFound, exception.ErrorCode);
|
||||
Assert.Contains(RequestMethods.ServerDiscover, exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -85,6 +83,6 @@ public class July2026ProtocolConnectionTests : ClientServerTestBase
|
|||
|
||||
var discoverResult = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(discoverResult);
|
||||
Assert.Contains(McpHttpHeaders.July2026ProtocolVersion, discoverResult.SupportedVersions);
|
||||
Assert.Equal([McpProtocolVersions.July2026ProtocolVersion], discoverResult.SupportedVersions);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,17 @@ using System.Threading.Channels;
|
|||
namespace ModelContextProtocol.Tests.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for the fallback from the 2026-07-28 protocol revision to a legacy protocol in
|
||||
/// Regression tests for the fallback from the 2026-07-28 protocol revision to an initialize-handshake protocol in
|
||||
/// <see cref="McpClient"/>. With default options (<c>ProtocolVersion = null</c>) the client prefers
|
||||
/// 2026-07-28 but probes with <c>server/discover</c>, falls back to the legacy <c>initialize</c>
|
||||
/// handshake when the server is legacy, and accepts whatever supported protocol version the legacy
|
||||
/// 2026-07-28 but probes with <c>server/discover</c>, falls back to the <c>initialize</c>
|
||||
/// handshake when the server only supports that path, and accepts whatever supported protocol version the
|
||||
/// server negotiates. Pinning <c>ProtocolVersion</c> to <c>2026-07-28</c> instead makes it the
|
||||
/// minimum too, so the client refuses to fall back.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The originally shipped logic in <c>PerformLegacyInitializeAsync</c> compared the server's response
|
||||
/// against the requested version and threw when a legacy server downgraded to (say) <c>"2025-06-18"</c>,
|
||||
/// even though the legacy negotiation succeeded. These tests guard against that regression.
|
||||
/// The originally shipped initialize-handshake fallback logic compared the server's response
|
||||
/// against the requested version and threw when an initialize-handshake server downgraded to (say)
|
||||
/// <c>"2025-06-18"</c>, even though negotiation succeeded. These tests guard against that regression.
|
||||
/// </remarks>
|
||||
public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper)
|
||||
{
|
||||
|
|
@ -26,45 +26,66 @@ public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) :
|
|||
public async Task Client_OnMethodNotFound_FallsBackTo_Initialize_AcceptsDowngradedVersion()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-06-18");
|
||||
await using var transport = new InitializeHandshakeServerTestTransport(serverNegotiatedVersion: McpProtocolVersions.June2025ProtocolVersion);
|
||||
|
||||
// Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback.
|
||||
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(),
|
||||
loggerFactory: LoggerFactory, cancellationToken: ct);
|
||||
|
||||
Assert.True(transport.ServerDiscoverProbed);
|
||||
Assert.True(transport.LegacyInitializeReceived);
|
||||
Assert.Equal("2025-06-18", client.NegotiatedProtocolVersion);
|
||||
Assert.True(transport.InitializeReceived);
|
||||
Assert.Equal(McpProtocolVersions.November2025ProtocolVersion, transport.InitializeProtocolVersion);
|
||||
Assert.Equal(McpProtocolVersions.June2025ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Client_OnInvalidParams_FallsBackTo_Initialize()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await using var transport = new LegacyServerTestTransport(
|
||||
serverNegotiatedVersion: "2025-11-25",
|
||||
await using var transport = new InitializeHandshakeServerTestTransport(
|
||||
serverNegotiatedVersion: McpProtocolVersions.November2025ProtocolVersion,
|
||||
probeErrorCode: (int)McpErrorCode.InvalidParams);
|
||||
|
||||
// Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback.
|
||||
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(),
|
||||
loggerFactory: LoggerFactory, cancellationToken: ct);
|
||||
|
||||
Assert.True(transport.LegacyInitializeReceived);
|
||||
Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion);
|
||||
Assert.True(transport.InitializeReceived);
|
||||
Assert.Equal(McpProtocolVersions.November2025ProtocolVersion, transport.InitializeProtocolVersion);
|
||||
Assert.Equal(McpProtocolVersions.November2025ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Client_WithPinnedJuly2026Version_RefusesFallback_ToLegacyServer()
|
||||
public async Task Client_OnInitializeFallback_RejectsPerRequestMetadataInitializeResponse()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-06-18");
|
||||
await using var transport = new InitializeHandshakeServerTestTransport(
|
||||
serverNegotiatedVersion: McpProtocolVersions.July2026ProtocolVersion);
|
||||
|
||||
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
|
||||
{
|
||||
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(),
|
||||
loggerFactory: LoggerFactory, cancellationToken: ct);
|
||||
});
|
||||
|
||||
Assert.IsType<McpException>(exception);
|
||||
Assert.True(transport.InitializeReceived);
|
||||
Assert.Equal(McpProtocolVersions.November2025ProtocolVersion, transport.InitializeProtocolVersion);
|
||||
Assert.Contains("mismatch", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Client_WithPinnedJuly2026Version_RefusesFallback_ToInitializeHandshakeServer()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await using var transport = new InitializeHandshakeServerTestTransport(serverNegotiatedVersion: McpProtocolVersions.June2025ProtocolVersion);
|
||||
|
||||
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
|
||||
{
|
||||
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
|
||||
{
|
||||
// Pinning the version makes it the minimum too, so the client refuses to fall back.
|
||||
ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion,
|
||||
ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion,
|
||||
}, loggerFactory: LoggerFactory, cancellationToken: ct);
|
||||
});
|
||||
|
||||
|
|
@ -73,17 +94,17 @@ public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) :
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyClient_WithExplicitPin_StillRequires_ExactVersionMatch()
|
||||
public async Task InitializeHandshakeClient_WithExplicitPin_StillRequires_ExactVersionMatch()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
// Server responds with a DIFFERENT version than the one the user pinned.
|
||||
await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-03-26");
|
||||
await using var transport = new InitializeHandshakeServerTestTransport(serverNegotiatedVersion: McpProtocolVersions.March2025ProtocolVersion);
|
||||
|
||||
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
|
||||
{
|
||||
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
|
||||
{
|
||||
ProtocolVersion = "2025-11-25",
|
||||
ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion,
|
||||
}, loggerFactory: LoggerFactory, cancellationToken: ct);
|
||||
});
|
||||
|
||||
|
|
@ -94,36 +115,50 @@ public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) :
|
|||
[Fact]
|
||||
public async Task Client_OnHeaderMismatch_Surfaces_NoFallback()
|
||||
{
|
||||
// The peer is modern (returns the spec-defined -32020 HeaderMismatch on the probe).
|
||||
// Falling back to legacy initialize would just produce another malformed envelope.
|
||||
// The peer uses per-request metadata (returns the spec-defined -32020 HeaderMismatch on the probe).
|
||||
// Falling back to initialize would just produce another malformed envelope.
|
||||
// Verify the connect-time logic surfaces the error to the caller instead of falling back.
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await using var transport = new LegacyServerTestTransport(
|
||||
serverNegotiatedVersion: "2025-11-25",
|
||||
await using var transport = new InitializeHandshakeServerTestTransport(
|
||||
serverNegotiatedVersion: McpProtocolVersions.November2025ProtocolVersion,
|
||||
probeErrorCode: (int)McpErrorCode.HeaderMismatch);
|
||||
|
||||
var exception = await Assert.ThrowsAnyAsync<McpException>(async () =>
|
||||
{
|
||||
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
|
||||
{
|
||||
ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion,
|
||||
ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion,
|
||||
}, loggerFactory: LoggerFactory, cancellationToken: ct);
|
||||
});
|
||||
|
||||
Assert.True(transport.ServerDiscoverProbed);
|
||||
Assert.False(transport.LegacyInitializeReceived);
|
||||
Assert.False(transport.InitializeReceived);
|
||||
Assert.Equal(McpErrorCode.HeaderMismatch, ((McpProtocolException)exception).ErrorCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Client_OnUnsupportedProtocolVersion_WithPerRequestMetadataVersion_RetriesDiscover()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await using var transport = new PerRequestMetadataRetryTestTransport();
|
||||
|
||||
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(),
|
||||
loggerFactory: LoggerFactory, cancellationToken: ct);
|
||||
|
||||
Assert.Equal(2, transport.ServerDiscoverRequests);
|
||||
Assert.False(transport.InitializeReceived);
|
||||
Assert.Equal(McpProtocolVersions.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Client_OnSilentProbe_FallsBackTo_Initialize_AfterConfiguredProbeTimeout()
|
||||
{
|
||||
// Simulate a legacy server that silently drops the unknown server/discover method (it never
|
||||
// responds to the probe). The client must fall back to legacy initialize once the configured
|
||||
// Simulate an initialize-handshake server that silently drops the unknown server/discover method (it never
|
||||
// responds to the probe). The client must fall back to initialize once the configured
|
||||
// DiscoverProbeTimeout elapses, well before the much larger InitializationTimeout.
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await using var transport = new LegacyServerTestTransport(
|
||||
serverNegotiatedVersion: "2025-11-25",
|
||||
await using var transport = new InitializeHandshakeServerTestTransport(
|
||||
serverNegotiatedVersion: McpProtocolVersions.November2025ProtocolVersion,
|
||||
silentDiscoverProbe: true);
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
|
@ -136,8 +171,9 @@ public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) :
|
|||
stopwatch.Stop();
|
||||
|
||||
Assert.True(transport.ServerDiscoverProbed);
|
||||
Assert.True(transport.LegacyInitializeReceived);
|
||||
Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion);
|
||||
Assert.True(transport.InitializeReceived);
|
||||
Assert.Equal(McpProtocolVersions.November2025ProtocolVersion, transport.InitializeProtocolVersion);
|
||||
Assert.Equal(McpProtocolVersions.November2025ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
|
||||
// The fallback was driven by the short probe timeout, not the 60s InitializationTimeout.
|
||||
Assert.True(
|
||||
|
|
@ -171,23 +207,25 @@ public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) :
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal in-memory transport that simulates a legacy server: rejects
|
||||
/// Minimal in-memory transport that simulates an initialize-handshake server: rejects
|
||||
/// <c>server/discover</c> (with a configurable JSON-RPC error code, or by
|
||||
/// silently dropping the request) and responds to <c>initialize</c> with a
|
||||
/// configurable protocol version.
|
||||
/// </summary>
|
||||
private sealed class LegacyServerTestTransport(
|
||||
private sealed class InitializeHandshakeServerTestTransport(
|
||||
string serverNegotiatedVersion,
|
||||
int probeErrorCode = (int)McpErrorCode.MethodNotFound,
|
||||
bool silentDiscoverProbe = false) : IClientTransport
|
||||
{
|
||||
private readonly Channel<JsonRpcMessage> _incomingToClient = Channel.CreateUnbounded<JsonRpcMessage>();
|
||||
|
||||
public string Name => "legacy-server-test-transport";
|
||||
public string Name => "initialize-handshake-server-test-transport";
|
||||
|
||||
public bool ServerDiscoverProbed { get; private set; }
|
||||
|
||||
public bool LegacyInitializeReceived { get; private set; }
|
||||
public bool InitializeReceived { get; private set; }
|
||||
|
||||
public string? InitializeProtocolVersion { get; private set; }
|
||||
|
||||
public Task<ITransport> ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
@ -205,7 +243,7 @@ public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) :
|
|||
ServerDiscoverProbed = true;
|
||||
if (silentDiscoverProbe)
|
||||
{
|
||||
// Model a legacy server that drops the unknown method without replying.
|
||||
// Model an initialize-handshake server that drops the unknown method without replying.
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +261,9 @@ public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) :
|
|||
break;
|
||||
|
||||
case JsonRpcRequest { Method: RequestMethods.Initialize } initReq:
|
||||
LegacyInitializeReceived = true;
|
||||
InitializeReceived = true;
|
||||
var initializeRequest = JsonSerializer.Deserialize<InitializeRequestParams>(initReq.Params, McpJsonUtilities.DefaultOptions);
|
||||
InitializeProtocolVersion = initializeRequest?.ProtocolVersion;
|
||||
_ = WriteAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = initReq.Id,
|
||||
|
|
@ -231,7 +271,7 @@ public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) :
|
|||
{
|
||||
ProtocolVersion = serverNegotiatedVersion,
|
||||
Capabilities = new ServerCapabilities(),
|
||||
ServerInfo = new Implementation { Name = "legacy-test-server", Version = "1.0.0" },
|
||||
ServerInfo = new Implementation { Name = "initialize-handshake-test-server", Version = "1.0.0" },
|
||||
}, McpJsonUtilities.DefaultOptions),
|
||||
});
|
||||
break;
|
||||
|
|
@ -243,7 +283,105 @@ public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) :
|
|||
|
||||
private sealed class TransportChannel(
|
||||
Channel<JsonRpcMessage> incoming,
|
||||
LegacyServerTestTransport parent) : ITransport
|
||||
InitializeHandshakeServerTestTransport parent) : ITransport
|
||||
{
|
||||
public ChannelReader<JsonRpcMessage> MessageReader => incoming.Reader;
|
||||
public bool IsConnected { get; private set; } = true;
|
||||
public string? SessionId => null;
|
||||
|
||||
public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
parent.HandleOutgoingMessage(message);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
incoming.Writer.TryComplete();
|
||||
IsConnected = false;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PerRequestMetadataRetryTestTransport : IClientTransport
|
||||
{
|
||||
private readonly Channel<JsonRpcMessage> _incomingToClient = Channel.CreateUnbounded<JsonRpcMessage>();
|
||||
|
||||
public string Name => "per-request-metadata-retry-test-transport";
|
||||
|
||||
public int ServerDiscoverRequests { get; private set; }
|
||||
|
||||
public bool InitializeReceived { get; private set; }
|
||||
|
||||
public Task<ITransport> ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ITransport transport = new TransportChannel(_incomingToClient, this);
|
||||
return Task.FromResult(transport);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => default;
|
||||
|
||||
private void HandleOutgoingMessage(JsonRpcMessage message)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case JsonRpcRequest { Method: RequestMethods.ServerDiscover } discoverReq:
|
||||
ServerDiscoverRequests++;
|
||||
|
||||
if (ServerDiscoverRequests == 1)
|
||||
{
|
||||
_ = WriteAsync(new JsonRpcError
|
||||
{
|
||||
Id = discoverReq.Id,
|
||||
Error = new JsonRpcErrorDetail
|
||||
{
|
||||
Code = (int)McpErrorCode.UnsupportedProtocolVersion,
|
||||
Message = "Unsupported protocol version",
|
||||
Data = CreateUnsupportedProtocolVersionData(),
|
||||
},
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = WriteAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = discoverReq.Id,
|
||||
Result = JsonSerializer.SerializeToNode(new DiscoverResult
|
||||
{
|
||||
SupportedVersions = [McpProtocolVersions.July2026ProtocolVersion],
|
||||
Capabilities = new ServerCapabilities(),
|
||||
ServerInfo = new Implementation { Name = "per-request-metadata-test-server", Version = "1.0.0" },
|
||||
}, McpJsonUtilities.DefaultOptions),
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case JsonRpcRequest { Method: RequestMethods.Initialize }:
|
||||
InitializeReceived = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Task WriteAsync(JsonRpcMessage message)
|
||||
=> _incomingToClient.Writer.WriteAsync(message, CancellationToken.None).AsTask();
|
||||
|
||||
private static JsonElement CreateUnsupportedProtocolVersionData()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(new UnsupportedProtocolVersionErrorData
|
||||
{
|
||||
Requested = McpProtocolVersions.July2026ProtocolVersion,
|
||||
Supported = [McpProtocolVersions.July2026ProtocolVersion],
|
||||
}, McpJsonUtilities.DefaultOptions);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
|
||||
private sealed class TransportChannel(
|
||||
Channel<JsonRpcMessage> incoming,
|
||||
PerRequestMetadataRetryTestTransport parent) : ITransport
|
||||
{
|
||||
public ChannelReader<JsonRpcMessage> MessageReader => incoming.Reader;
|
||||
public bool IsConnected { get; private set; } = true;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ public class July2026ProtocolListMetaEmissionTests : ClientServerTestBase
|
|||
public async Task Client_ListTools_NoOptions_EmitsRequiredMeta()
|
||||
{
|
||||
StartServer();
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion });
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion });
|
||||
|
||||
await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ public class July2026ProtocolListMetaEmissionTests : ClientServerTestBase
|
|||
public async Task Client_ListPrompts_NoOptions_EmitsRequiredMeta()
|
||||
{
|
||||
StartServer();
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion });
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion });
|
||||
|
||||
await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ public class July2026ProtocolListMetaEmissionTests : ClientServerTestBase
|
|||
public async Task Client_ListResources_NoOptions_EmitsRequiredMeta()
|
||||
{
|
||||
StartServer();
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion });
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion });
|
||||
|
||||
await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ public class July2026ProtocolListMetaEmissionTests : ClientServerTestBase
|
|||
public async Task Client_ListResourceTemplates_NoOptions_EmitsRequiredMeta()
|
||||
{
|
||||
StartServer();
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion });
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion });
|
||||
|
||||
await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ public class July2026ProtocolListMetaEmissionTests : ClientServerTestBase
|
|||
// server/discover has no public List-style helper; we drive it via SendRequestAsync directly,
|
||||
// which still flows through the client's per-request _meta injector.
|
||||
StartServer();
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion });
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion });
|
||||
|
||||
// Hook the server-side handler invocation via a notification handler is awkward here; assert
|
||||
// instead by sending the request and parsing the wire-shape echo from the response context.
|
||||
|
|
@ -136,7 +136,7 @@ public class July2026ProtocolListMetaEmissionTests : ClientServerTestBase
|
|||
|
||||
Assert.NotNull(response.Result);
|
||||
var discover = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions)!;
|
||||
Assert.Contains(McpHttpHeaders.July2026ProtocolVersion, discover.SupportedVersions);
|
||||
Assert.Contains(McpProtocolVersions.July2026ProtocolVersion, discover.SupportedVersions);
|
||||
|
||||
// The server enforces the per-request envelope shape; if the client had omitted _meta, the
|
||||
// request would have failed with -32602 / -32021 rather than returning a DiscoverResult. The
|
||||
|
|
@ -144,11 +144,11 @@ public class July2026ProtocolListMetaEmissionTests : ClientServerTestBase
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyClient_ListTools_DoesNotEmitMeta()
|
||||
public async Task InitializeHandshakeClient_ListTools_DoesNotEmitMeta()
|
||||
{
|
||||
// Sanity guard: a client on the session-supporting (legacy) protocol must NOT emit the SEP-2575
|
||||
// Sanity guard: a client on the session-supporting initialize-handshake protocol must NOT emit the SEP-2575
|
||||
// envelope. The injector is gated on the negotiated protocol version; if it ever started writing
|
||||
// those keys on a legacy request, every legacy server would reject it.
|
||||
// those keys on an initialize-handshake request, every initialize-handshake server would reject it.
|
||||
StartServer();
|
||||
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = LatestStableVersion });
|
||||
|
||||
|
|
@ -175,6 +175,6 @@ public class July2026ProtocolListMetaEmissionTests : ClientServerTestBase
|
|||
$"Missing clientCapabilities key on {method} _meta envelope");
|
||||
|
||||
// The protocolVersion value must match the negotiated 2026-07-28 protocol version.
|
||||
Assert.Equal(McpHttpHeaders.July2026ProtocolVersion, meta[MetaKeys.ProtocolVersion]!.GetValue<string>());
|
||||
Assert.Equal(McpProtocolVersions.July2026ProtocolVersion, meta[MetaKeys.ProtocolVersion]!.GetValue<string>());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ public class McpClientCreationTests(ITestOutputHelper testOutputHelper) : Logged
|
|||
Result = JsonSerializer.SerializeToNode(new DiscoverResult
|
||||
{
|
||||
Capabilities = new ServerCapabilities(),
|
||||
SupportedVersions = [McpHttpHeaders.July2026ProtocolVersion],
|
||||
SupportedVersions = [McpProtocolVersions.July2026ProtocolVersion],
|
||||
ServerInfo = new Implementation
|
||||
{
|
||||
Name = "NopTransport",
|
||||
|
|
|
|||
|
|
@ -483,7 +483,7 @@ public class McpClientTests : ClientServerTestBase
|
|||
[Fact]
|
||||
public async Task AsClientLoggerProvider_MessagesSentToClient()
|
||||
{
|
||||
await using McpClient client = await CreateMcpClientForServer();
|
||||
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion });
|
||||
|
||||
ILoggerProvider loggerProvider = Server.AsClientLoggerProvider();
|
||||
Assert.Throws<ArgumentNullException>("categoryName", () => loggerProvider.CreateLogger(null!));
|
||||
|
|
@ -765,7 +765,7 @@ public class McpClientTests : ClientServerTestBase
|
|||
[Fact]
|
||||
public async Task SetLoggingLevelAsync_WithRequestParams_SetsLevel()
|
||||
{
|
||||
await using McpClient client = await CreateMcpClientForServer();
|
||||
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion });
|
||||
|
||||
// Should not throw
|
||||
await client.SetLoggingLevelAsync(
|
||||
|
|
@ -795,9 +795,9 @@ public class McpClientTests : ClientServerTestBase
|
|||
[Fact]
|
||||
public async Task ServerCanPingClient()
|
||||
{
|
||||
// ping is a legacy-only RPC (removed in the 2026-07-28 protocol per SEP-2575), so pin the client
|
||||
// to a legacy protocol version to exercise the server-initiated ping round-trip.
|
||||
await using McpClient client = await CreateMcpClientForServer(new() { ProtocolVersion = "2025-11-25" });
|
||||
// ping is available only on initialize-handshake revisions (removed in the 2026-07-28 protocol
|
||||
// per SEP-2575), so pin the client to exercise the server-initiated ping round-trip.
|
||||
await using McpClient client = await CreateMcpClientForServer(new() { ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion });
|
||||
|
||||
var pingRequest = new JsonRpcRequest { Method = RequestMethods.Ping };
|
||||
var response = await Server.SendRequestAsync(pingRequest, TestContext.Current.CancellationToken);
|
||||
|
|
|
|||
|
|
@ -28,8 +28,9 @@ public class McpRequestHeadersTests
|
|||
[InlineData("2024-11-05", false)]
|
||||
[InlineData(null, false)]
|
||||
[InlineData("", false)]
|
||||
public void SupportsStandardHeaders_ReturnsExpected(string? version, bool expected)
|
||||
public void RequiresStandardHeaders_ReturnsExpected(string? version, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, McpHttpHeaders.SupportsStandardHeaders(version));
|
||||
Assert.Equal(expected, McpProtocolVersions.RequiresStandardHeaders(version));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue