Align the 2026-07-28 wire with spec PR #3002 and SEP-2575 HTTP statuses (#1752)

Co-authored-by: Stephen Halter <halter73@gmail.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: tarekgh <tarek.mahmoud.sayed@gmail.com>
This commit is contained in:
Paul Carleton 2026-07-28 00:08:03 +01:00 • committed by GitHub
commit 26f60ab7f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 894 additions and 101 deletions

View file

@ -324,7 +324,10 @@ public class July2026ProtocolHttpFallbackTests(ITestOutputHelper outputHelper) :
{
SupportedVersions = [McpProtocolVersions.July2026ProtocolVersion],
Capabilities = new ServerCapabilities(),
ServerInfo = new Implementation { Name = "bad-per-request-metadata-server", Version = "1.0" },
Meta = new JsonObject
{
[MetaKeys.ServerInfo] = JsonSerializer.SerializeToNode(new Implementation { Name = "bad-per-request-metadata-server", Version = "1.0" }, McpJsonUtilities.DefaultOptions),
},
TimeToLive = TimeSpan.Zero,
CacheScope = CacheScope.Private,
}, McpJsonUtilities.DefaultOptions),
@ -394,7 +397,10 @@ public class July2026ProtocolHttpFallbackTests(ITestOutputHelper outputHelper) :
{
SupportedVersions = [McpProtocolVersions.July2026ProtocolVersion],
Capabilities = new ServerCapabilities(),
ServerInfo = new Implementation { Name = "per-request-metadata-server", Version = "1.0" },
Meta = new JsonObject
{
[MetaKeys.ServerInfo] = JsonSerializer.SerializeToNode(new Implementation { Name = "per-request-metadata-server", Version = "1.0" }, McpJsonUtilities.DefaultOptions),
},
TimeToLive = TimeSpan.Zero,
CacheScope = CacheScope.Private,
}, McpJsonUtilities.DefaultOptions),

View file

@ -32,7 +32,8 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
options.ProtocolVersion = protocolVersion;
})
.WithHttpTransport()
.WithTools([McpServerTool.Create((string text) => $"echo:{text}", new() { Name = "echo" })]);
.WithTools([McpServerTool.Create((string text) => $"echo:{text}", new() { Name = "echo" })])
.WithTools<CapabilityTools>();
_app = Builder.Build();
_app.MapMcp();
@ -106,6 +107,9 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
Assert.Equal("echo:hi", json["result"]!["content"]![0]!["text"]!.GetValue<string>());
Assert.Equal(
nameof(RawHttpConformanceTests),
json["result"]!["_meta"]![MetaKeys.ServerInfo]!["name"]!.GetValue<string>());
// Per SEP-2567, starting with the 2026-07-28 protocol revision Streamable HTTP no longer
// supports sessions: the server MUST NOT issue a Mcp-Session-Id.
@ -134,6 +138,73 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
Assert.Equal(JsonValueKind.Number, json["result"]!["ttlMs"]!.GetValueKind());
Assert.Equal(0, json["result"]!["ttlMs"]!.GetValue<long>());
Assert.Equal("private", json["result"]!["cacheScope"]!.GetValue<string>());
Assert.Null(json["result"]!["serverInfo"]);
Assert.Equal(
nameof(RawHttpConformanceTests),
json["result"]!["_meta"]![MetaKeys.ServerInfo]!["name"]!.GetValue<string>());
}
[Fact]
public async Task July2026Post_UnknownMethod_Returns404_WithMethodNotFound()
{
await StartAsync();
var body = @"{""jsonrpc"":""2.0"",""id"":17,""method"":""unknown/method"",""params"":{" + July2026ProtocolMetaFragment() + "}}";
using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) };
request.Headers.Add(ProtocolVersionHeader, McpProtocolVersions.July2026ProtocolVersion);
request.Headers.Add("Mcp-Method", "unknown/method");
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
Assert.Equal(17, json["id"]!.GetValue<long>());
Assert.Equal((int)McpErrorCode.MethodNotFound, json["error"]!["code"]!.GetValue<int>());
}
[Theory]
[InlineData("initialize")]
[InlineData("ping")]
[InlineData("logging/setLevel")]
[InlineData("resources/subscribe")]
[InlineData("resources/unsubscribe")]
public async Task July2026Post_RemovedMethod_Returns404_WithMethodNotFound(string method)
{
await StartAsync();
var body = @"{""jsonrpc"":""2.0"",""id"":18,""method"":""" + method +
@""",""params"":{" + July2026ProtocolMetaFragment() + "}}";
using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) };
request.Headers.Add(ProtocolVersionHeader, McpProtocolVersions.July2026ProtocolVersion);
request.Headers.Add("Mcp-Method", method);
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
Assert.Equal(18, json["id"]!.GetValue<long>());
Assert.Equal((int)McpErrorCode.MethodNotFound, json["error"]!["code"]!.GetValue<int>());
}
[Fact]
public async Task July2026Post_MissingRequiredCapability_Returns400()
{
await StartAsync();
var body =
@"{""jsonrpc"":""2.0"",""id"":19,""method"":""tools/call"",""params"":{""name"":""requires_sampling"",""arguments"":{}," +
July2026ProtocolMetaFragment() + "}}";
using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) };
request.Headers.Add(ProtocolVersionHeader, McpProtocolVersions.July2026ProtocolVersion);
request.Headers.Add("Mcp-Method", "tools/call");
request.Headers.Add("Mcp-Name", "requires_sampling");
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
Assert.Equal(19, json["id"]!.GetValue<long>());
Assert.Equal((int)McpErrorCode.MissingRequiredClientCapability, json["error"]!["code"]!.GetValue<int>());
}
[Fact]
@ -179,7 +250,7 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
Assert.NotNull(data);
Assert.Equal("9999-99-99", data!["requested"]!.GetValue<string>());
var supported = data["supported"]!.AsArray().Select(n => n!.GetValue<string>()).ToList();
Assert.Contains(McpProtocolVersions.July2026ProtocolVersion, supported);
Assert.Equal([McpProtocolVersions.July2026ProtocolVersion], supported);
}
[Fact]
@ -258,7 +329,7 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
}
[Fact]
public async Task July2026Post_MissingBodyProtocolVersion_ReturnsHeaderMismatch_Minus32020()
public async Task July2026Post_MissingBodyProtocolVersion_ReturnsInvalidParams_Minus32602()
{
await StartAsync();
@ -269,9 +340,12 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
request.Headers.Add("Mcp-Method", "server/discover");
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
// A missing (rather than mismatched) _meta protocol version is an Invalid params rejection
// per SEP-2575; -32020 HeaderMismatch is reserved for values present on both sides that
// disagree.
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
Assert.Equal((int)McpErrorCode.HeaderMismatch, json["error"]!["code"]!.GetValue<int>());
Assert.Equal((int)McpErrorCode.InvalidParams, json["error"]!["code"]!.GetValue<int>());
Assert.Contains(MetaKeys.ProtocolVersion, json["error"]!["message"]!.GetValue<string>(), StringComparison.Ordinal);
}
@ -372,4 +446,64 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
// McpEndpointRouteBuilderExtensions only maps GET when Stateless == false.
Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode);
}
[Fact]
public async Task July2026Post_MissingClientCapabilities_Returns400_WithInvalidParams()
{
await StartAsync();
var body =
@"{""jsonrpc"":""2.0"",""id"":20,""method"":""server/discover"",""params"":{""_meta"":{" +
@"""io.modelcontextprotocol/protocolVersion"":""" + McpProtocolVersions.July2026ProtocolVersion + @"""}}}";
using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) };
request.Headers.Add(ProtocolVersionHeader, McpProtocolVersions.July2026ProtocolVersion);
request.Headers.Add("Mcp-Method", "server/discover");
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
Assert.Equal(20, json["id"]!.GetValue<long>());
Assert.Equal((int)McpErrorCode.InvalidParams, json["error"]!["code"]!.GetValue<int>());
Assert.Contains(MetaKeys.ClientCapabilities, json["error"]!["message"]!.GetValue<string>(), StringComparison.Ordinal);
}
[Theory]
[InlineData("5")]
[InlineData("\"caps\"")]
[InlineData("[]")]
[InlineData("true")]
public async Task July2026Post_MalformedClientCapabilities_Returns400_WithInvalidParams(string clientCapabilitiesJson)
{
await StartAsync();
var body =
@"{""jsonrpc"":""2.0"",""id"":21,""method"":""server/discover"",""params"":{""_meta"":{" +
@"""io.modelcontextprotocol/protocolVersion"":""" + McpProtocolVersions.July2026ProtocolVersion + @"""," +
@"""io.modelcontextprotocol/clientInfo"":{""name"":""raw"",""version"":""1.0""}," +
@"""io.modelcontextprotocol/clientCapabilities"":" + clientCapabilitiesJson + "}}}";
using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) };
request.Headers.Add(ProtocolVersionHeader, McpProtocolVersions.July2026ProtocolVersion);
request.Headers.Add("Mcp-Method", "server/discover");
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
// A present-but-wrong-shape clientCapabilities must be rejected up front with -32602 / 400,
// not surface later as a generic internal error on a 200 response.
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
Assert.Equal(21, json["id"]!.GetValue<long>());
Assert.Equal((int)McpErrorCode.InvalidParams, json["error"]!["code"]!.GetValue<int>());
Assert.Contains(MetaKeys.ClientCapabilities, json["error"]!["message"]!.GetValue<string>(), StringComparison.Ordinal);
}
[McpServerToolType]
private sealed class CapabilityTools
{
[McpServerTool(Name = "requires_sampling")]
public static string RequiresSampling() =>
throw new MissingRequiredClientCapabilityException(
new ClientCapabilities { Sampling = new() },
"sampling capability required but not declared by client");
}
}

View file

@ -766,7 +766,10 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
["io.modelcontextprotocol/tasks"] = new JsonObject(),
},
},
ServerInfo = new Implementation { Name = "header-capture", Version = "1.0" },
Meta = new JsonObject
{
[MetaKeys.ServerInfo] = JsonSerializer.SerializeToNode(new Implementation { Name = "header-capture", Version = "1.0" }, McpJsonUtilities.DefaultOptions),
},
TimeToLive = TimeSpan.Zero,
CacheScope = CacheScope.Private,
ResultType = "complete",

View file

@ -1158,8 +1158,14 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
private static DiscoverResult AssertDiscoverServerInfo(JsonRpcResponse rpcResponse)
{
var discoverResult = AssertType<DiscoverResult>(rpcResponse.Result);
Assert.Equal(nameof(StreamableHttpServerConformanceTests), discoverResult.ServerInfo.Name);
Assert.Equal("73", discoverResult.ServerInfo.Version);
// Server identity is carried in the result _meta, not the discover body.
var serverInfoNode = discoverResult.Meta?[MetaKeys.ServerInfo];
Assert.NotNull(serverInfoNode);
var serverInfo = JsonSerializer.Deserialize<Implementation>(serverInfoNode, McpJsonUtilities.DefaultOptions);
Assert.NotNull(serverInfo);
Assert.Equal(nameof(StreamableHttpServerConformanceTests), serverInfo.Name);
Assert.Equal("73", serverInfo.Version);
return discoverResult;
}

View file

@ -321,6 +321,27 @@ try
}
break;
}
case "json-schema-ref-no-deref":
{
// SEP-2106: listing tools must not dereference network $refs in a tool's
// inputSchema — the scenario's canary endpoint observes any such fetch.
await mcpClient.ListToolsAsync();
break;
}
case "sep-2322-client-request-state":
{
// SEP-2322 (MRTR): drive the client's input-required auto-loop. The mock
// inspects the raw tools/call params: requestState echoed byte-exact (and
// omitted when the server sent none), a fresh JSON-RPC id per retry, no
// MRTR params bleeding into the unrelated call, and a missing resultType
// parsing as a terminal (complete) result.
await mcpClient.ListToolsAsync();
await mcpClient.CallToolAsync(toolName: "test_mrtr_echo_state", arguments: new Dictionary<string, object?>());
await mcpClient.CallToolAsync(toolName: "test_mrtr_unrelated", arguments: new Dictionary<string, object?>());
await mcpClient.CallToolAsync(toolName: "test_mrtr_no_state", arguments: new Dictionary<string, object?>());
await mcpClient.CallToolAsync(toolName: "test_mrtr_no_result_type", arguments: new Dictionary<string, object?>());
break;
}
default:
// No extra processing for other scenarios
break;

View file

@ -73,6 +73,7 @@ public class Program
.WithTools<ConformanceTools>()
.WithTools<ConformanceTaskTools>()
.WithTools<IncompleteResultTools>()
.WithTools<Sep2575DiagnosticTools>()
.WithTools([ConformanceTools.CreateJsonSchema202012Tool()])
.WithRequestFilters(filters => filters.AddCallToolFilter(next => async (request, cancellationToken) =>
{

View file

@ -0,0 +1,62 @@
using ModelContextProtocol;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace ConformanceServer.Tools;
/// <summary>
/// Diagnostic tools exercised by the SEP-2575 <c>server-stateless</c> conformance scenario. Each
/// tool exists so the harness can observe a framework behavior (capability enforcement, response
/// stream discipline, per-request log gating) that plain application tools never trigger.
/// </summary>
[McpServerToolType]
public class Sep2575DiagnosticTools
{
/// <summary>
/// Requires the client to have declared the <c>sampling</c> capability in the per-request
/// <c>_meta/io.modelcontextprotocol/clientCapabilities</c>. Used to verify the server rejects
/// undeclared-capability calls with MissingRequiredClientCapabilityError (-32021).
/// </summary>
[McpServerTool(Name = "test_missing_capability")]
[Description("Requires the sampling client capability; used to verify MissingRequiredClientCapabilityError (-32021) (SEP-2575)")]
public static string MissingCapability(RequestContext<CallToolRequestParams> context)
{
if (context.Server.ClientCapabilities?.Sampling is not null)
{
return "Client declared the sampling capability; tool executed.";
}
throw new MissingRequiredClientCapabilityException(
new ClientCapabilities { Sampling = new() },
"sampling capability required but not declared by client");
}
/// <summary>
/// Returns a plain result. Per SEP-2575 the response stream must carry no independent top-level
/// JSON-RPC requests; a plain response trivially satisfies this. The scenario declares no
/// elicitation capability, so this tool must not initiate an elicitation.
/// </summary>
[McpServerTool(Name = "test_streaming_elicitation")]
[Description("Streams only result frames; used to verify response streams carry no independent JSON-RPC requests (SEP-2575)")]
public static string StreamingElicitation()
{
return "stream observed: result frames only, no top-level requests";
}
/// <summary>
/// Attempts to emit a log message through the client-logger pipeline, which gates on the
/// per-request <c>_meta/io.modelcontextprotocol/logLevel</c>. When the client did not opt in,
/// no notifications/message may be sent for the request.
/// </summary>
[McpServerTool(Name = "test_logging_tool")]
[Description("Attempts to emit a log message; the framework must drop it when the client did not set _meta.../logLevel (SEP-2575)")]
public static string LoggingTool(RequestContext<CallToolRequestParams> context)
{
#pragma warning disable MCP9004 // AsClientLoggerProvider is deprecated with the legacy logging/setLevel flow but remains the gated client-log pipeline.
ILogger logger = context.Server.AsClientLoggerProvider().CreateLogger(nameof(Sep2575DiagnosticTools));
#pragma warning restore MCP9004
logger.LogInformation("test_logging_tool executed");
return "Log attempted; framework gates on _meta.../logLevel.";
}
}

View file

@ -3,6 +3,7 @@ using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Client;
@ -52,6 +53,39 @@ public class July2026ProtocolConnectionTests : ClientServerTestBase
Assert.NotEqual(McpProtocolVersions.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
}
[Fact]
public async Task Client_AllowsDiscoverResultWithoutServerInfo()
{
ConfigureDiscoverServerInfo(meta => meta.Remove(MetaKeys.ServerInfo));
StartServer();
var options = new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion };
await using var client = await CreateMcpClientForServer(options);
Assert.Equal(McpProtocolVersions.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
Assert.Throws<InvalidOperationException>(() => _ = client.ServerInfo);
}
[Fact]
public async Task Client_RejectsMalformedDiscoverServerInfo()
{
ConfigureDiscoverServerInfo(meta => meta[MetaKeys.ServerInfo] = "invalid");
StartServer();
var options = new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion };
await Assert.ThrowsAsync<JsonException>(() => CreateMcpClientForServer(options));
}
[Fact]
public async Task Client_RejectsNullDiscoverServerInfo()
{
ConfigureDiscoverServerInfo(meta => meta[MetaKeys.ServerInfo] = null);
StartServer();
var options = new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion };
await Assert.ThrowsAsync<JsonException>(() => CreateMcpClientForServer(options));
}
[Fact]
public async Task InitializeHandshakeClient_CannotCallServerDiscover()
{
@ -86,4 +120,19 @@ public class July2026ProtocolConnectionTests : ClientServerTestBase
Assert.Equal("complete", discoverResult.ResultType);
Assert.Equal([McpProtocolVersions.July2026ProtocolVersion], discoverResult.SupportedVersions);
}
private void ConfigureDiscoverServerInfo(Action<JsonObject> configure)
{
McpServerBuilder.WithMessageFilters(filters => filters.AddOutgoingFilter(next => async (context, cancellationToken) =>
{
if (context.JsonRpcMessage is JsonRpcResponse { Result: JsonObject result } &&
result["supportedVersions"] is not null &&
result["_meta"] is JsonObject meta)
{
configure(meta);
}
await next(context, cancellationToken);
}));
}
}

View file

@ -3,6 +3,7 @@ using ModelContextProtocol.Protocol;
using ModelContextProtocol.Tests.Utils;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Channels;
namespace ModelContextProtocol.Tests.Client;
@ -351,7 +352,10 @@ public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) :
{
SupportedVersions = [McpProtocolVersions.July2026ProtocolVersion],
Capabilities = new ServerCapabilities(),
ServerInfo = new Implementation { Name = "per-request-metadata-test-server", Version = "1.0.0" },
Meta = new JsonObject
{
[MetaKeys.ServerInfo] = JsonSerializer.SerializeToNode(new Implementation { Name = "per-request-metadata-test-server", Version = "1.0.0" }, McpJsonUtilities.DefaultOptions),
},
}, McpJsonUtilities.DefaultOptions),
});
}

View file

@ -4,6 +4,7 @@ using ModelContextProtocol.Protocol;
using ModelContextProtocol.Tests.Utils;
using System.IO.Pipelines;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Channels;
namespace ModelContextProtocol.Tests.Client;
@ -179,10 +180,13 @@ public class McpClientCreationTests(ITestOutputHelper testOutputHelper) : Logged
{
Capabilities = new ServerCapabilities(),
SupportedVersions = [McpProtocolVersions.July2026ProtocolVersion],
ServerInfo = new Implementation
Meta = new JsonObject
{
Name = "NopTransport",
Version = "1.0.0"
[MetaKeys.ServerInfo] = JsonSerializer.SerializeToNode(new Implementation
{
Name = "NopTransport",
Version = "1.0.0"
}, McpJsonUtilities.DefaultOptions),
},
}, McpJsonUtilities.DefaultOptions),
});

View file

@ -19,6 +19,12 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
mcpServerBuilder.WithResources<SubscribableResources>();
}
private Task<McpClient> CreateLegacyMcpClientForServer() =>
CreateMcpClientForServer(new()
{
ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion,
});
[McpServerResourceType]
private sealed class SubscribableResources
{
@ -30,7 +36,7 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
public async Task SubscribeToResourceAsync_WithHandler_ReceivesNotifications()
{
// Arrange
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateLegacyMcpClientForServer();
const string resourceUri = "test://resource/1";
var notificationReceived = new TaskCompletionSource<ResourceUpdatedNotificationParams>();
@ -61,7 +67,7 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
public async Task SubscribeToResourceAsync_WithHandler_FiltersNotificationsByUri()
{
// Arrange
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateLegacyMcpClientForServer();
const string subscribedUri = "test://resource/1";
const string otherUri = "test://resource/2";
var notificationCount = 0;
@ -107,7 +113,7 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
public async Task SubscribeToResourceAsync_WithHandler_DisposalUnsubscribes()
{
// Arrange
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateLegacyMcpClientForServer();
const string resourceUri = "test://resource/1";
var notificationCount = 0;
@ -148,7 +154,7 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
public async Task SubscribeToResourceAsync_WithHandler_UriOverload_ReceivesNotifications()
{
// Arrange
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateLegacyMcpClientForServer();
var resourceUri = new Uri("test://resource/1");
var notificationReceived = new TaskCompletionSource<ResourceUpdatedNotificationParams>();
@ -179,7 +185,7 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
public async Task SubscribeToResourceAsync_WithNullHandler_ThrowsArgumentNullException()
{
// Arrange
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateLegacyMcpClientForServer();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
@ -193,7 +199,7 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
public async Task SubscribeToResourceAsync_WithNullUri_ThrowsArgumentNullException()
{
// Arrange
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateLegacyMcpClientForServer();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
@ -207,7 +213,7 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
public async Task SubscribeToResourceAsync_WithEmptyUri_ThrowsArgumentException()
{
// Arrange
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateLegacyMcpClientForServer();
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(async () =>
@ -221,7 +227,7 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
public async Task SubscribeToResourceAsync_MultipleSubscriptions_BothReceiveNotifications()
{
// Arrange
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateLegacyMcpClientForServer();
const string uri1 = "test://resource/1";
const string uri2 = "test://resource/2";
var notification1Received = new TaskCompletionSource<bool>();
@ -278,7 +284,7 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
public async Task SubscribeToResourceAsync_DisposalIsIdempotent()
{
// Arrange
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateLegacyMcpClientForServer();
const string resourceUri = "test://resource/1";
var subscription = await client.SubscribeToResourceAsync(
@ -299,7 +305,7 @@ public class McpClientResourceSubscriptionTests : ClientServerTestBase
public async Task SubscribeToResourceAsync_MultipleHandlersSameUri_BothReceiveNotifications()
{
// Arrange
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateLegacyMcpClientForServer();
const string resourceUri = "test://resource/1";
var handler1Called = new TaskCompletionSource<bool>();
var handler2Called = new TaskCompletionSource<bool>();

View file

@ -195,7 +195,10 @@ public class MrtrIntegrationTests : ClientServerTestBase
{
SupportedVersions = new List<string> { "2026-07-28" },
Capabilities = new ServerCapabilities(),
ServerInfo = new Implementation { Name = "MockMrtrServer", Version = "1.0" },
Meta = new JsonObject
{
[MetaKeys.ServerInfo] = JsonSerializer.SerializeToNode(new Implementation { Name = "MockMrtrServer", Version = "1.0" }, McpJsonUtilities.DefaultOptions),
},
}, McpJsonUtilities.DefaultOptions),
};
await WriteJsonRpcAsync(serverWriter, discoverResponse);
@ -445,7 +448,10 @@ public class MrtrIntegrationTests : ClientServerTestBase
{
SupportedVersions = ["2026-07-28"],
Capabilities = new ServerCapabilities { Tools = new() },
ServerInfo = new Implementation { Name = "MrtrServer", Version = "1.0" }
Meta = new JsonObject
{
[MetaKeys.ServerInfo] = JsonSerializer.SerializeToNode(new Implementation { Name = "MrtrServer", Version = "1.0" }, McpJsonUtilities.DefaultOptions),
},
}, McpJsonUtilities.DefaultOptions),
};
await WriteJsonRpcAsync(serverWriter, discoverResponse);

View file

@ -277,6 +277,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
TaskCompletionSource<bool> tcs = new();
await using var client = await _fixture.CreateClientAsync(clientId, new()
{
ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion,
Handlers = new()
{
NotificationHandlers =
@ -307,6 +308,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
TaskCompletionSource<bool> receivedNotification = new();
await using var client = await _fixture.CreateClientAsync(clientId, new()
{
ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion,
Handlers = new()
{
NotificationHandlers =
@ -727,6 +729,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
TaskCompletionSource<bool> tcs = new();
await using var client = await _fixture.CreateClientAsync(clientId, new()
{
ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion,
Handlers = new()
{
NotificationHandlers =
@ -756,6 +759,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
TaskCompletionSource<bool> receivedNotification = new();
await using var client = await _fixture.CreateClientAsync(clientId, new()
{
ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion,
Handlers = new()
{
NotificationHandlers =

View file

@ -256,7 +256,10 @@ public class McpServerBuilderExtensionsRequestFilterTests(ITestOutputHelper test
[Fact]
public async Task AddSubscribeToResourcesFilter_Logs_When_SubscribeToResources_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateMcpClientForServer(new()
{
ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion,
});
await client.SubscribeToResourceAsync("test://resource/123", cancellationToken: TestContext.Current.CancellationToken);
@ -268,7 +271,10 @@ public class McpServerBuilderExtensionsRequestFilterTests(ITestOutputHelper test
[Fact]
public async Task AddUnsubscribeFromResourcesFilter_Logs_When_UnsubscribeFromResources_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await using McpClient client = await CreateMcpClientForServer(new()
{
ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion,
});
await client.UnsubscribeFromResourceAsync("test://resource/123", cancellationToken: TestContext.Current.CancellationToken);

View file

@ -292,7 +292,10 @@ public class CacheableResultWarningTests : LoggedTest
{
SupportedVersions = [serverProtocolVersion],
Capabilities = new ServerCapabilities(),
ServerInfo = new Implementation { Name = "MockServer", Version = "1.0" },
Meta = new JsonObject
{
[MetaKeys.ServerInfo] = JsonSerializer.SerializeToNode(new Implementation { Name = "MockServer", Version = "1.0" }, McpJsonUtilities.DefaultOptions),
},
}, McpJsonUtilities.DefaultOptions),
}, cancellationToken);
}

View file

@ -44,7 +44,6 @@ public static class DiscoverProtocolTests
{
Tools = new ToolsCapability { ListChanged = true },
},
ServerInfo = new Implementation { Name = "test-server", Version = "2.0" },
Instructions = "Use this server for testing.",
};
@ -55,7 +54,6 @@ public static class DiscoverProtocolTests
Assert.Equal(["2025-11-25", "2026-07-28"], deserialized.SupportedVersions);
Assert.NotNull(deserialized.Capabilities.Tools);
Assert.True(deserialized.Capabilities.Tools.ListChanged);
Assert.Equal("test-server", deserialized.ServerInfo.Name);
Assert.Equal("Use this server for testing.", deserialized.Instructions);
}
@ -66,7 +64,6 @@ public static class DiscoverProtocolTests
{
SupportedVersions = new List<string> { "2026-07-28" },
Capabilities = new ServerCapabilities(),
ServerInfo = new Implementation { Name = "minimal-server", Version = "1.0" },
};
var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);

View file

@ -8,8 +8,8 @@ namespace ModelContextProtocol.Tests.Protocol;
/// Targeted tests for the SEP-2549 caching hints (<c>ttlMs</c> and <c>cacheScope</c>) on
/// <see cref="DiscoverResult"/>. Spec PR #2855 promotes both fields to required on the discover
/// response. <see cref="DiscoverResult"/> has <c>required</c> CLR properties for
/// <see cref="DiscoverResult.SupportedVersions"/>, <see cref="DiscoverResult.Capabilities"/>, and
/// <see cref="DiscoverResult.ServerInfo"/>, which prevents reuse of the parameterized
/// <see cref="DiscoverResult.SupportedVersions"/> and <see cref="DiscoverResult.Capabilities"/>,
/// which prevents reuse of the parameterized
/// <see cref="CacheableResultTests"/> helper (it instantiates via reflection). This file covers the
/// same property-shape assertions for <see cref="DiscoverResult"/>.
/// </summary>
@ -19,7 +19,6 @@ public static class DiscoverResultCacheableTests
{
SupportedVersions = [McpProtocolVersions.November2025ProtocolVersion, McpProtocolVersions.July2026ProtocolVersion],
Capabilities = new ServerCapabilities(),
ServerInfo = new Implementation { Name = "test-server", Version = "1.0" },
};
[Fact]

View file

@ -82,7 +82,22 @@ public sealed class NegotiatedProtocolVersionTests : LoggedTest, IAsyncDisposabl
}
[Fact]
public async Task PerRequestMetadata_RejectsRequestMissingRequiredMetadata()
public async Task PerRequestMetadata_ServesRequestMissingClientInfo()
{
var ct = TestContext.Current.CancellationToken;
// clientInfo is optional: a request whose _meta omits it is served.
var response = await RoundTripAsync(
id: 1,
McpProtocolVersions.July2026ProtocolVersion,
ct,
includeClientInfo: false);
Assert.IsType<JsonRpcResponse>(response);
}
[Fact]
public async Task PerRequestMetadata_RejectsRequestMissingClientCapabilities()
{
var ct = TestContext.Current.CancellationToken;
@ -91,10 +106,10 @@ public sealed class NegotiatedProtocolVersionTests : LoggedTest, IAsyncDisposabl
id: 1,
McpProtocolVersions.July2026ProtocolVersion,
ct,
includeClientInfo: false));
includeClientCapabilities: false));
Assert.Equal((int)McpErrorCode.InvalidParams, error.Error.Code);
Assert.Contains(MetaKeys.ClientInfo, error.Error.Message, StringComparison.Ordinal);
Assert.Contains(MetaKeys.ClientCapabilities, error.Error.Message, StringComparison.Ordinal);
}
[Fact]
@ -176,25 +191,29 @@ public sealed class NegotiatedProtocolVersionTests : LoggedTest, IAsyncDisposabl
Assert.Contains(RequestMethods.SubscriptionsListen, error.Error.Message, StringComparison.Ordinal);
}
[Fact]
public async Task LoggingSetLevel_WithPerRequestMetadataProtocolVersion_IsRejected()
[Theory]
[InlineData("initialize")]
[InlineData("ping")]
[InlineData("logging/setLevel")]
[InlineData("resources/subscribe")]
[InlineData("resources/unsubscribe")]
public async Task RemovedMethod_WithPerRequestMetadataProtocolVersion_IsRejected(string method)
{
var ct = TestContext.Current.CancellationToken;
var request = new JsonRpcRequest
{
Id = new RequestId(1),
Method = RequestMethods.LoggingSetLevel,
Method = method,
Params = new JsonObject
{
["level"] = "info",
["_meta"] = PerRequestMetadata(),
},
};
var error = Assert.IsType<JsonRpcError>(await SendAndReceiveAsync(request, ct));
Assert.Equal((int)McpErrorCode.MethodNotFound, error.Error.Code);
Assert.Contains(RequestMethods.LoggingSetLevel, error.Error.Message, StringComparison.Ordinal);
Assert.Contains(method, error.Error.Message, StringComparison.Ordinal);
}
private async Task<JsonRpcMessage> RoundTripAsync(

View file

@ -98,10 +98,10 @@ public sealed class RawStreamConformanceTests : LoggedTest, IAsyncDisposable
.ToList();
Assert.Contains(McpProtocolVersions.July2026ProtocolVersion, supportedVersions);
// Capabilities and serverInfo are mandatory in DiscoverResult per SEP-2575.
// Capabilities are mandatory in DiscoverResult; server identity is result metadata.
Assert.NotNull(result["capabilities"]);
Assert.NotNull(result["serverInfo"]);
Assert.Equal("raw-conformance-server", result["serverInfo"]!["name"]!.GetValue<string>());
Assert.Null(result["serverInfo"]);
Assert.Equal("raw-conformance-server", result["_meta"]![MetaKeys.ServerInfo]!["name"]!.GetValue<string>());
// Spec PR #2855 makes ttlMs and cacheScope required on DiscoverResult; the server emits the
// safest defaults (immediately stale, not shareable) when the application hasn't customized.
@ -126,6 +126,7 @@ public sealed class RawStreamConformanceTests : LoggedTest, IAsyncDisposable
var content = result!["content"]!.AsArray();
Assert.Single(content);
Assert.Equal("echo:hello", content[0]!["text"]!.GetValue<string>());
Assert.Equal("raw-conformance-server", result["_meta"]![MetaKeys.ServerInfo]!["name"]!.GetValue<string>());
}
[Fact]
@ -146,7 +147,7 @@ public sealed class RawStreamConformanceTests : LoggedTest, IAsyncDisposable
Assert.NotNull(data);
Assert.Equal("9999-99-99", data!["requested"]!.GetValue<string>());
var supported = data["supported"]!.AsArray().Select(n => n!.GetValue<string>()).ToList();
Assert.Contains(McpProtocolVersions.July2026ProtocolVersion, supported);
Assert.Equal([McpProtocolVersions.July2026ProtocolVersion], supported);
}
[Fact]
@ -161,6 +162,7 @@ public sealed class RawStreamConformanceTests : LoggedTest, IAsyncDisposable
var result = response["result"];
Assert.NotNull(result);
Assert.Equal("2025-11-25", result!["protocolVersion"]!.GetValue<string>());
Assert.Null(result["_meta"]?[MetaKeys.ServerInfo]);
}
[Fact]

View file

@ -0,0 +1,91 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace ModelContextProtocol.Tests.Server;
/// <summary>
/// Verifies that the legacy <c>resources/subscribe</c> and <c>resources/unsubscribe</c> RPCs are
/// gated by protocol version. SEP-2575 (the 2026-07-28 revision) removes them in favor of
/// <c>subscriptions/listen</c> with <c>resourceSubscriptions</c>; servers must respond with
/// <c>-32601 MethodNotFound</c>. Initialize-handshake protocol versions still support the legacy
/// RPCs per the spec.
/// </summary>
public sealed class ResourceSubscriptionProtocolGatingTests : ClientServerTestBase
{
public ResourceSubscriptionProtocolGatingTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper, startServer: false)
{
}
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
mcpServerBuilder.WithResources<SubscribableResources>();
}
[McpServerResourceType]
private sealed class SubscribableResources
{
[McpServerResource(UriTemplate = "test://resource/{id}"), Description("A subscribable test resource")]
public static string GetResource(string id) => $"Resource content: {id}";
}
[Fact]
public async Task Subscribe_OnJuly2026ProtocolSession_ReturnsMethodNotFound()
{
StartServer();
await using var client = await CreateMcpClientForServer(new McpClientOptions
{
ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion,
});
var ex = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.SubscribeToResourceAsync(
new SubscribeRequestParams { Uri = "test://resource/1" },
TestContext.Current.CancellationToken));
Assert.Equal(McpErrorCode.MethodNotFound, ex.ErrorCode);
// The rejection must point callers at the SEP-2575 replacement.
Assert.Contains(RequestMethods.SubscriptionsListen, ex.Message, StringComparison.Ordinal);
}
[Fact]
public async Task Unsubscribe_OnJuly2026ProtocolSession_ReturnsMethodNotFound()
{
StartServer();
await using var client = await CreateMcpClientForServer(new McpClientOptions
{
ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion,
});
var ex = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.UnsubscribeFromResourceAsync(
new UnsubscribeRequestParams { Uri = "test://resource/1" },
TestContext.Current.CancellationToken));
Assert.Equal(McpErrorCode.MethodNotFound, ex.ErrorCode);
Assert.Contains(RequestMethods.SubscriptionsListen, ex.Message, StringComparison.Ordinal);
}
[Fact]
public async Task Subscribe_OnInitializeHandshakeSession_StillSucceeds()
{
// Default server config; client pinned to 2025-11-25.
StartServer();
await using var client = await CreateMcpClientForServer(new McpClientOptions
{
ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion,
});
// Should complete without throwing on the initialize-handshake revision.
await client.SubscribeToResourceAsync(
new SubscribeRequestParams { Uri = "test://resource/1" },
TestContext.Current.CancellationToken);
await client.UnsubscribeFromResourceAsync(
new UnsubscribeRequestParams { Uri = "test://resource/1" },
TestContext.Current.CancellationToken);
}
}