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:
parent
6787c0cc19
commit
26f60ab7f4
20 changed files with 894 additions and 101 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue