Follow-ups to #1610: de-draft 2026-07-28 terminology, renumber error codes, ProtocolVersion floor (#1671)

This commit is contained in:
Stephen Halter 2026-06-25 14:07:52 -07:00 • committed by GitHub
commit d6b1615988
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 785 additions and 898 deletions

View file

@ -0,0 +1,90 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Text.Json;
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
/// <c>initialize</c>.
/// </summary>
public class July2026ProtocolConnectionTests : ClientServerTestBase
{
private const string LatestStableVersion = "2025-11-25";
public July2026ProtocolConnectionTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper, startServer: false)
{
}
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
services.Configure<McpServerOptions>(options =>
{
options.ServerInfo = new Implementation { Name = nameof(July2026ProtocolConnectionTests), Version = "1.0" };
});
}
[Fact]
public async Task Client_RequestingJuly2026Protocol_NegotiatesIt()
{
StartServer();
var options = new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion };
await using var client = await CreateMcpClientForServer(options);
Assert.Equal(McpHttpHeaders.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
Assert.NotNull(client.ServerCapabilities);
Assert.Equal(nameof(July2026ProtocolConnectionTests), client.ServerInfo.Name);
}
[Fact]
public async Task Client_RequestingLegacyVersion_NegotiatesLegacy()
{
StartServer();
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = LatestStableVersion });
Assert.NotEqual(McpHttpHeaders.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
}
[Fact]
public async Task LegacyClient_CanCallServerDiscover()
{
// server/discover is registered unconditionally, so a legacy client can probe it
// (e.g., to learn capabilities without doing a second initialize).
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 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);
}
[Fact]
public async Task ServerDiscover_IncludesJuly2026ProtocolVersion()
{
StartServer();
await using var client = await CreateMcpClientForServer();
var response = await client.SendRequestAsync(
new JsonRpcRequest { Method = RequestMethods.ServerDiscover },
TestContext.Current.CancellationToken);
var discoverResult = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(discoverResult);
Assert.Contains(McpHttpHeaders.July2026ProtocolVersion, discoverResult.SupportedVersions);
}
}

View file

@ -0,0 +1,266 @@
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Tests.Utils;
using System.Diagnostics;
using System.Text.Json;
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
/// <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
/// 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.
/// </remarks>
public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper)
{
[Fact]
public async Task Client_OnMethodNotFound_FallsBackTo_Initialize_AcceptsDowngradedVersion()
{
var ct = TestContext.Current.CancellationToken;
await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-06-18");
// 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);
}
[Fact]
public async Task Client_OnInvalidParams_FallsBackTo_Initialize()
{
var ct = TestContext.Current.CancellationToken;
await using var transport = new LegacyServerTestTransport(
serverNegotiatedVersion: "2025-11-25",
probeErrorCode: (int)McpErrorCode.InvalidParams);
// Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback.
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(),
loggerFactory: LoggerFactory, cancellationToken: ct);
Assert.True(transport.LegacyInitializeReceived);
Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion);
}
[Fact]
public async Task Client_WithPinnedJuly2026Version_RefusesFallback_ToLegacyServer()
{
var ct = TestContext.Current.CancellationToken;
await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-06-18");
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,
}, loggerFactory: LoggerFactory, cancellationToken: ct);
});
Assert.IsType<McpException>(exception);
Assert.Contains("2026-07-28", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task LegacyClient_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");
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
{
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
{
ProtocolVersion = "2025-11-25",
}, loggerFactory: LoggerFactory, cancellationToken: ct);
});
Assert.IsType<McpException>(exception);
Assert.Contains("mismatch", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[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.
// 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",
probeErrorCode: (int)McpErrorCode.HeaderMismatch);
var exception = await Assert.ThrowsAnyAsync<McpException>(async () =>
{
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
{
ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion,
}, loggerFactory: LoggerFactory, cancellationToken: ct);
});
Assert.True(transport.ServerDiscoverProbed);
Assert.False(transport.LegacyInitializeReceived);
Assert.Equal(McpErrorCode.HeaderMismatch, ((McpProtocolException)exception).ErrorCode);
}
[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
// DiscoverProbeTimeout elapses, well before the much larger InitializationTimeout.
var ct = TestContext.Current.CancellationToken;
await using var transport = new LegacyServerTestTransport(
serverNegotiatedVersion: "2025-11-25",
silentDiscoverProbe: true);
var stopwatch = Stopwatch.StartNew();
// Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback.
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
{
DiscoverProbeTimeout = TimeSpan.FromMilliseconds(250),
InitializationTimeout = TestConstants.DefaultTimeout,
}, loggerFactory: LoggerFactory, cancellationToken: ct);
stopwatch.Stop();
Assert.True(transport.ServerDiscoverProbed);
Assert.True(transport.LegacyInitializeReceived);
Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion);
// The fallback was driven by the short probe timeout, not the 60s InitializationTimeout.
Assert.True(
stopwatch.Elapsed < TimeSpan.FromSeconds(30),
$"Fallback should have happened shortly after the {nameof(McpClientOptions.DiscoverProbeTimeout)}, but took {stopwatch.Elapsed}.");
}
[Theory]
[InlineData(0)]
[InlineData(-1000)]
public void DiscoverProbeTimeout_Setter_Rejects_NonPositiveValues(int milliseconds)
{
var options = new McpClientOptions();
Assert.Throws<ArgumentOutOfRangeException>(() => options.DiscoverProbeTimeout = TimeSpan.FromMilliseconds(milliseconds));
}
[Fact]
public void DiscoverProbeTimeout_Setter_Accepts_PositiveAndInfiniteValues()
{
var options = new McpClientOptions();
// Default is the documented 5 seconds.
Assert.Equal(TimeSpan.FromSeconds(5), options.DiscoverProbeTimeout);
options.DiscoverProbeTimeout = TimeSpan.FromSeconds(30);
Assert.Equal(TimeSpan.FromSeconds(30), options.DiscoverProbeTimeout);
// Timeout.InfiniteTimeSpan disables the separate probe timeout (bounded by InitializationTimeout only).
options.DiscoverProbeTimeout = Timeout.InfiniteTimeSpan;
Assert.Equal(Timeout.InfiniteTimeSpan, options.DiscoverProbeTimeout);
}
/// <summary>
/// Minimal in-memory transport that simulates a legacy 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(
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 bool ServerDiscoverProbed { get; private set; }
public bool LegacyInitializeReceived { 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:
ServerDiscoverProbed = true;
if (silentDiscoverProbe)
{
// Model a legacy server that drops the unknown method without replying.
break;
}
_ = WriteAsync(new JsonRpcError
{
Id = discoverReq.Id,
Error = new JsonRpcErrorDetail
{
Code = probeErrorCode,
Message = probeErrorCode == (int)McpErrorCode.MethodNotFound
? "Method not found"
: "Invalid params",
},
});
break;
case JsonRpcRequest { Method: RequestMethods.Initialize } initReq:
LegacyInitializeReceived = true;
_ = WriteAsync(new JsonRpcResponse
{
Id = initReq.Id,
Result = JsonSerializer.SerializeToNode(new InitializeResult
{
ProtocolVersion = serverNegotiatedVersion,
Capabilities = new ServerCapabilities(),
ServerInfo = new Implementation { Name = "legacy-test-server", Version = "1.0.0" },
}, McpJsonUtilities.DefaultOptions),
});
break;
}
}
private Task WriteAsync(JsonRpcMessage message)
=> _incomingToClient.Writer.WriteAsync(message, CancellationToken.None).AsTask();
private sealed class TransportChannel(
Channel<JsonRpcMessage> incoming,
LegacyServerTestTransport 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;
}
}
}
}

View file

@ -0,0 +1,180 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Client;
/// <summary>
/// Verifies that the C# client emits the SEP-2575 <c>_meta</c> envelope on every list-style
/// request (and on <c>server/discover</c>) under the 2026-07-28 protocol revision, even when
/// the caller supplies no <c>RequestOptions</c> / no params.
/// </summary>
/// <remarks>
/// Spec PR #2759 promotes <c>params._meta</c> to required on <c>tools/list</c>,
/// <c>resources/list</c>, <c>resources/templates/list</c>, <c>prompts/list</c>, and
/// <c>server/discover</c>. This test class drives the C# client through
/// <see cref="ClientServerTestBase"/> with the 2026-07-28 protocol negotiated, attaches a request
/// filter on each list endpoint that captures the incoming <c>_meta</c> envelope, and asserts
/// the three required SEP-2575 keys are present:
/// <c>io.modelcontextprotocol/protocolVersion</c>,
/// <c>io.modelcontextprotocol/clientInfo</c>, and
/// <c>io.modelcontextprotocol/clientCapabilities</c>.
/// </remarks>
public class July2026ProtocolListMetaEmissionTests : ClientServerTestBase
{
private const string LatestStableVersion = "2025-11-25";
// Captured _meta envelopes for each request method we exercise. Populated by the per-method
// server-side filters and asserted from each test method.
private readonly Dictionary<string, JsonObject?> _capturedMeta = new(StringComparer.Ordinal);
public July2026ProtocolListMetaEmissionTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper, startServer: false)
{
}
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
mcpServerBuilder.WithRequestFilters(filters =>
{
filters.AddListToolsFilter(next => async (request, cancellationToken) =>
{
_capturedMeta[RequestMethods.ToolsList] = request.Params?.Meta;
return await next(request, cancellationToken);
});
filters.AddListPromptsFilter(next => async (request, cancellationToken) =>
{
_capturedMeta[RequestMethods.PromptsList] = request.Params?.Meta;
return await next(request, cancellationToken);
});
filters.AddListResourcesFilter(next => async (request, cancellationToken) =>
{
_capturedMeta[RequestMethods.ResourcesList] = request.Params?.Meta;
return await next(request, cancellationToken);
});
filters.AddListResourceTemplatesFilter(next => async (request, cancellationToken) =>
{
_capturedMeta[RequestMethods.ResourcesTemplatesList] = request.Params?.Meta;
return await next(request, cancellationToken);
});
});
// No-op list handlers (so the requests complete) — content is irrelevant; we only assert the
// incoming envelope.
mcpServerBuilder
.WithListToolsHandler((_, _) => new ValueTask<ListToolsResult>(new ListToolsResult { Tools = [] }))
.WithListPromptsHandler((_, _) => new ValueTask<ListPromptsResult>(new ListPromptsResult { Prompts = [] }))
.WithListResourcesHandler((_, _) => new ValueTask<ListResourcesResult>(new ListResourcesResult { Resources = [] }))
.WithListResourceTemplatesHandler((_, _) => new ValueTask<ListResourceTemplatesResult>(
new ListResourceTemplatesResult { ResourceTemplates = [] }));
}
[Fact]
public async Task Client_ListTools_NoOptions_EmitsRequiredMeta()
{
StartServer();
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion });
await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
AssertRequiredMetaPresent(RequestMethods.ToolsList);
}
[Fact]
public async Task Client_ListPrompts_NoOptions_EmitsRequiredMeta()
{
StartServer();
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion });
await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
AssertRequiredMetaPresent(RequestMethods.PromptsList);
}
[Fact]
public async Task Client_ListResources_NoOptions_EmitsRequiredMeta()
{
StartServer();
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion });
await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
AssertRequiredMetaPresent(RequestMethods.ResourcesList);
}
[Fact]
public async Task Client_ListResourceTemplates_NoOptions_EmitsRequiredMeta()
{
StartServer();
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion });
await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
AssertRequiredMetaPresent(RequestMethods.ResourcesTemplatesList);
}
[Fact]
public async Task Client_ServerDiscover_EmitsRequiredMeta()
{
// server/discover has no public List-style helper; we drive it via SendRequestAsync directly,
// which still flows through the client's per-request _meta injector.
StartServer();
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion });
// Hook the server-side handler invocation via a notification handler is awkward here; assert
// instead by sending the request and parsing the wire-shape echo from the response context.
// Easier path: rely on the existing JsonRpcRequest capture in the message context (see the
// raw conformance tests for the wire-level proof). For this in-process test, we instead drive
// the request and rely on the response being a valid DiscoverResult; the _meta injector
// would otherwise have failed the server's per-request envelope validation.
var response = await client.SendRequestAsync(
new JsonRpcRequest { Method = RequestMethods.ServerDiscover },
TestContext.Current.CancellationToken);
Assert.NotNull(response.Result);
var discover = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions)!;
Assert.Contains(McpHttpHeaders.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
// successful round-trip is the assertion.
}
[Fact]
public async Task LegacyClient_ListTools_DoesNotEmitMeta()
{
// Sanity guard: a client on the session-supporting (legacy) protocol must NOT emit the SEP-2575
// envelope. The injector is gated on the negotiated protocol version; if it ever started writing
// those keys on a legacy request, every legacy server would reject it.
StartServer();
await using var client = await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = LatestStableVersion });
await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
var meta = _capturedMeta[RequestMethods.ToolsList];
if (meta is not null)
{
Assert.False(meta.ContainsKey(MetaKeys.ProtocolVersion));
Assert.False(meta.ContainsKey(MetaKeys.ClientInfo));
Assert.False(meta.ContainsKey(MetaKeys.ClientCapabilities));
}
}
private void AssertRequiredMetaPresent(string method)
{
Assert.True(_capturedMeta.TryGetValue(method, out var meta), $"No capture for {method}");
Assert.NotNull(meta);
Assert.True(meta!.ContainsKey(MetaKeys.ProtocolVersion),
$"Missing protocolVersion key on {method} _meta envelope");
Assert.True(meta.ContainsKey(MetaKeys.ClientInfo),
$"Missing clientInfo key on {method} _meta envelope");
Assert.True(meta.ContainsKey(MetaKeys.ClientCapabilities),
$"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>());
}
}

View file

@ -178,7 +178,7 @@ public class McpClientCreationTests(ITestOutputHelper testOutputHelper) : Logged
Result = JsonSerializer.SerializeToNode(new DiscoverResult
{
Capabilities = new ServerCapabilities(),
SupportedVersions = [McpSession.DraftProtocolVersion],
SupportedVersions = [McpHttpHeaders.July2026ProtocolVersion],
ServerInfo = new Implementation
{
Name = "NopTransport",

View file

@ -9,7 +9,7 @@ namespace ModelContextProtocol.Tests.Client;
public class McpClientMetaTests : ClientServerTestBase
{
// InitializeMeta is carried on the legacy initialize request, which the draft revision removes.
// InitializeMeta is carried on the legacy initialize request, which the 2026-07-28 protocol removes.
// The two InitializeMeta_* tests pin to the latest stable version so the handshake actually runs.
private const string LatestStableVersion = "2025-11-25";

View file

@ -584,7 +584,7 @@ public class McpClientTests : ClientServerTestBase
public async Task ReturnsNegotiatedProtocolVersion(string? protocolVersion)
{
await using McpClient client = await CreateMcpClientForServer(new() { ProtocolVersion = protocolVersion });
// A null ProtocolVersion now prefers the draft revision, which the reactive test server advertises.
// A null ProtocolVersion now prefers the 2026-07-28 protocol, which the reactive test server advertises.
Assert.Equal(protocolVersion ?? "2026-07-28", client.NegotiatedProtocolVersion);
}
@ -795,7 +795,7 @@ public class McpClientTests : ClientServerTestBase
[Fact]
public async Task ServerCanPingClient()
{
// ping is a legacy-only RPC (removed in the draft revision per SEP-2575), so pin the client
// ping is a legacy-only RPC (removed in the 2026-07-28 protocol per SEP-2575), so pin the client
// to a legacy protocol version to exercise the server-initiated ping round-trip.
await using McpClient client = await CreateMcpClientForServer(new() { ProtocolVersion = "2025-11-25" });

View file

@ -18,7 +18,7 @@ public class McpRequestHeadersTests
[Fact]
public void McpErrorCode_HeaderMismatch_HasCorrectValue()
{
Assert.Equal(-32001, (int)McpErrorCode.HeaderMismatch);
Assert.Equal(-32020, (int)McpErrorCode.HeaderMismatch);
}
[Theory]

View file

@ -165,7 +165,7 @@ public class MrtrIntegrationTests : ClientServerTestBase
Model = "test-model"
});
// Start the client task — it will send server/discover (draft) and block waiting for response
// Start the client task. It will send server/discover (2026-07-28 protocol) and block waiting for response
var clientTask = McpClient.CreateAsync(
new StreamClientTransport(
clientToServer.Writer.AsStream(),
@ -180,7 +180,7 @@ public class MrtrIntegrationTests : ClientServerTestBase
var serverReader = new StreamReader(clientToServer.Reader.AsStream());
var serverWriter = serverToClient.Writer.AsStream();
// Read the server/discover request from client (draft revision skips initialize per SEP-2575).
// Read the server/discover request from client (the 2026-07-28 protocol skips initialize per SEP-2575).
var discoverLine = await serverReader.ReadLineAsync(TestContext.Current.CancellationToken);
Assert.NotNull(discoverLine);
var discoverRequest = JsonSerializer.Deserialize<JsonRpcRequest>(discoverLine, McpJsonUtilities.DefaultOptions);
@ -200,7 +200,7 @@ public class MrtrIntegrationTests : ClientServerTestBase
};
await WriteJsonRpcAsync(serverWriter, discoverResponse);
// Client is now connected with MRTR negotiated (no initialized notification under draft).
// Client is now connected with MRTR negotiated (no initialized notification under the 2026-07-28 protocol).
await using var client = await clientTask;
Assert.Equal("2026-07-28", client.NegotiatedProtocolVersion);
@ -406,7 +406,7 @@ public class MrtrIntegrationTests : ClientServerTestBase
var clientToServer = new Pipe();
var serverToClient = new Pipe();
// Pin to the draft revision so the client performs the server/discover handshake and
// Pin to the 2026-07-28 protocol so the client performs the server/discover handshake and
// treats InputRequiredResult as an MRTR round-trip.
var clientOptions = new McpClientOptions { ProtocolVersion = "2026-07-28" };
clientOptions.Handlers.ElicitationHandler = (_, _) =>

View file

@ -33,8 +33,8 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// Arrange
// Act
// ping was removed in the draft revision (SEP-2575), so pin to the latest stable
// protocol version to keep exercising the legacy ping RPC. Draft liveness relies on
// ping was removed in the 2026-07-28 protocol (SEP-2575), so pin to the latest stable
// protocol version to keep exercising the legacy ping RPC. The 2026-07-28 protocol relies on
// the transport/request lifecycle instead of an explicit ping.
await using var client = await _fixture.CreateClientAsync(clientId, new McpClientOptions { ProtocolVersion = "2025-11-25" });
await client.PingAsync(cancellationToken: TestContext.Current.CancellationToken);

View file

@ -74,7 +74,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
{
List<string> messageTypes = [];
// Under the draft protocol the client performs a server/discover + tools/list exchange (no
// Under the 2026-07-28 protocol the client performs a server/discover + tools/list exchange (no
// fire-and-forget initialized notification), so the tools/list request is a deterministic
// synchronization point. Gate recording to it and signal once the filter finishes so a
// regression that invokes the filter pipeline more than once per message surfaces as an extra entry.
@ -112,7 +112,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
[Fact]
public async Task AddIncomingMessageFilter_Multiple_Filters_Execute_In_Order()
{
// Under the draft protocol the client performs a server/discover + tools/list exchange (no
// Under the 2026-07-28 protocol the client performs a server/discover + tools/list exchange (no
// fire-and-forget initialized notification), so the tools/list request is a deterministic
// synchronization point. Gate the filter logging to it and signal once the outermost filter
// finishes so the assertions observe a complete, stable log.
@ -393,7 +393,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
var clientOptions = new McpClientOptions
{
// This test observes the legacy outgoing flow on the server side: the initialize response and
// the server->client sampling/createMessage request. Under the draft protocol those are replaced
// the server->client sampling/createMessage request. Under the 2026-07-28 protocol those are replaced
// by server/discover and implicit MRTR (InputRequiredResult), which is covered by MrtrIntegrationTests.
// Pin to the latest stable version to keep exercising the legacy server->client request path here.
ProtocolVersion = LatestStableVersion,

View file

@ -1,4 +1,4 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -128,12 +128,12 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
[Fact]
public async Task Can_Be_Notified_Of_Prompt_Changes()
{
// Under the draft revision, list-changed notifications are delivered only over a
// Under the 2026-07-28 protocol, list-changed notifications are delivered only over a
// subscriptions/listen stream (covered by SubscriptionsListenTests). This test pins the
// legacy revision to keep coverage of the session-wide broadcast that legacy clients still rely on.
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
{
ProtocolVersion = McpSession.LatestProtocolVersion,
ProtocolVersion = McpHttpHeaders.November2025ProtocolVersion,
});

View file

@ -1,4 +1,4 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -162,12 +162,12 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
[Fact]
public async Task Can_Be_Notified_Of_Resource_Changes()
{
// Under the draft revision, list-changed notifications are delivered only over a
// Under the 2026-07-28 protocol, list-changed notifications are delivered only over a
// subscriptions/listen stream (covered by SubscriptionsListenTests). This test pins the
// legacy revision to keep coverage of the session-wide broadcast that legacy clients still rely on.
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
{
ProtocolVersion = McpSession.LatestProtocolVersion,
ProtocolVersion = McpHttpHeaders.November2025ProtocolVersion,
});

View file

@ -1,4 +1,4 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -188,12 +188,12 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
[Fact]
public async Task Can_Be_Notified_Of_Tool_Changes()
{
// Under the draft revision, list-changed notifications are delivered only over a
// Under the 2026-07-28 protocol, list-changed notifications are delivered only over a
// subscriptions/listen stream (covered by SubscriptionsListenTests). This test pins the
// legacy revision to keep coverage of the session-wide broadcast that legacy clients still rely on.
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
{
ProtocolVersion = McpSession.LatestProtocolVersion,
ProtocolVersion = McpHttpHeaders.November2025ProtocolVersion,
});
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);

View file

@ -59,7 +59,7 @@ public sealed class McpServerResourceRoutingTests(ITestOutputHelper testOutputHe
/// <summary>
/// Asserts that the given URI does NOT match the template. Uses the default client, which
/// negotiates the draft protocol revision, so the unknown-resource response carries the
/// negotiates the 2026-07-28 protocol revision, so the unknown-resource response carries the
/// standard JSON-RPC <see cref="McpErrorCode.InvalidParams"/> (-32602). The version-gated
/// legacy mapping to <see cref="McpErrorCode.ResourceNotFound"/> (-32002) is covered by
/// <see cref="ResourceNotFound_ErrorCode_IsVersionGated"/>.
@ -79,7 +79,7 @@ public sealed class McpServerResourceRoutingTests(ITestOutputHelper testOutputHe
}
// Unknown-resource-URI responses are version-gated: older clients keep the legacy
// -32002 (McpErrorCode.ResourceNotFound), and clients on the draft protocol version that
// -32002 (McpErrorCode.ResourceNotFound), and clients on the 2026-07-28 protocol version that
// moves to the standard JSON-RPC code see -32602 (McpErrorCode.InvalidParams).
[Theory]
[InlineData("2025-11-25", McpErrorCode.ResourceNotFound)]
@ -134,7 +134,7 @@ public sealed class McpServerResourceRoutingTests(ITestOutputHelper testOutputHe
// Literal template braces in URI should not match (template literal is not a valid URI)
var mcpEx = await Assert.ThrowsAsync<McpProtocolException>(async () => await client.ReadResourceAsync("test://params{?a1,a2,a3}", null, TestContext.Current.CancellationToken));
// Draft maps an unmatched resource URI to InvalidParams (-32602); the legacy -32002 ResourceNotFound
// The 2026-07-28 protocol maps an unmatched resource URI to InvalidParams (-32602); the legacy -32002 ResourceNotFound
// mapping is covered by the version-gated ResourceNotFound_ErrorCode_IsVersionGated theory.
Assert.Equal(McpErrorCode.InvalidParams, mcpEx.ErrorCode);
Assert.Equal("Request failed (remote): Unknown resource URI: 'test://params{?a1,a2,a3}'", mcpEx.Message);

View file

@ -92,7 +92,7 @@ public class DiagnosticTests
Assert.Equal(clientListToolsCall.TraceId, serverListToolsCall.TraceId);
// Validate that the client trace context encoded to request.params._meta[traceparent].
// Under the draft revision _meta also carries the per-request envelope (protocolVersion,
// Under the 2026-07-28 protocol _meta also carries the per-request envelope (protocolVersion,
// clientInfo, clientCapabilities), so assert on the traceparent property specifically
// rather than the entire _meta object.
using var listToolsJson = JsonDocument.Parse(clientToServerLog.First(s => s.Contains("\"method\":\"tools/list\"")));

View file

@ -5,7 +5,7 @@ using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Protocol;
/// <summary>
/// Serialization tests for the request/result types introduced by the draft protocol revision (SEP-2575).
/// Serialization tests for the request/result types introduced by the 2026-07-28 protocol revision (SEP-2575).
/// </summary>
public static class DiscoverProtocolTests
{

View file

@ -17,7 +17,7 @@ public static class DiscoverResultCacheableTests
{
private static DiscoverResult NewDiscoverResult() => new()
{
SupportedVersions = ["2025-11-25", McpSession.DraftProtocolVersion],
SupportedVersions = [McpHttpHeaders.November2025ProtocolVersion, McpHttpHeaders.July2026ProtocolVersion],
Capabilities = new ServerCapabilities(),
ServerInfo = new Implementation { Name = "test-server", Version = "1.0" },
};

View file

@ -766,7 +766,7 @@ public static class JsonRpcMessageConverterTests
{
// Per JSON-RPC 2.0 §5.1, when an error occurs before the request id can be determined
// (parse error or invalid request), the server MUST respond with id=null. This shape is
// produced by some peers (e.g. Python's simple-streamablehttp-stateless on a draft probe)
// produced by some peers (e.g. Python's simple-streamablehttp-stateless on a 2026-07-28 probe)
// and must be accepted so the HTTP-fallback path can recognize the structured signal.
string json = """{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Bad Request"}}""";

View file

@ -0,0 +1,67 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
/// <summary>
/// Serialization tests for the error data payloads introduced by the 2026-07-28 protocol revision (SEP-2575).
/// </summary>
public static class July2026ProtocolErrorDataTests
{
[Fact]
public static void UnsupportedProtocolVersionErrorData_SerializationRoundTrip_PreservesAllProperties()
{
var original = new UnsupportedProtocolVersionErrorData
{
Supported = new List<string> { "2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25" },
Requested = "2026-07-28",
};
var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<UnsupportedProtocolVersionErrorData>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal(4, deserialized.Supported.Count);
Assert.Contains("2025-11-25", deserialized.Supported);
Assert.Equal("2026-07-28", deserialized.Requested);
}
[Fact]
public static void MissingRequiredClientCapabilityErrorData_SerializationRoundTrip_PreservesAllProperties()
{
var original = new MissingRequiredClientCapabilityErrorData
{
RequiredCapabilities = new ClientCapabilities
{
Sampling = new SamplingCapability(),
},
};
var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<MissingRequiredClientCapabilityErrorData>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.NotNull(deserialized.RequiredCapabilities.Sampling);
}
[Fact]
public static void UnsupportedProtocolVersionException_ExposesRequestedAndSupported()
{
var ex = new UnsupportedProtocolVersionException("2099-12-31", ["2025-11-25", "2025-06-18"]);
Assert.Equal(McpErrorCode.UnsupportedProtocolVersion, ex.ErrorCode);
Assert.Equal("2099-12-31", ex.Requested);
Assert.Equal(2, ex.Supported.Count);
Assert.Contains("2025-11-25", ex.Supported);
}
[Fact]
public static void MissingRequiredClientCapabilityException_ExposesRequiredCapabilities()
{
var caps = new ClientCapabilities { Roots = new RootsCapability() };
var ex = new MissingRequiredClientCapabilityException(caps);
Assert.Equal(McpErrorCode.MissingRequiredClientCapability, ex.ErrorCode);
Assert.Same(caps, ex.RequiredCapabilities);
}
}

View file

@ -5,7 +5,7 @@ using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Protocol;
/// <summary>
/// Serialization tests for the subscriptions/listen types introduced by the draft protocol revision (SEP-2575).
/// Serialization tests for the subscriptions/listen types introduced by the 2026-07-28 protocol revision (SEP-2575).
/// </summary>
public static class SubscriptionsListenProtocolTests
{

Some files were not shown because too many files have changed in this diff Show more