Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com> Copilot-Session: 80653343-5dcb-43cb-89b0-8ac8e572c4f7
This commit is contained in:
parent
7838900e8b
commit
6787c0cc19
3 changed files with 219 additions and 27 deletions
|
|
@ -322,6 +322,40 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
|
|||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
|
||||
Assert.Equal("2025-11-25", json["result"]!["protocolVersion"]!.GetValue<string>());
|
||||
|
||||
// resultType is a 2026-07-28 result field, and the initialize handshake only ever negotiates
|
||||
// 2025-11-25 or earlier. It must not appear on the InitializeResult, or strict 2025-11-25 clients
|
||||
// reject the handshake (issue #1721).
|
||||
Assert.False(json["result"]!.AsObject().ContainsKey("resultType"),
|
||||
"InitializeResult must not carry resultType on a 2025-11-25 handshake.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DownlevelToolsList_On2025_11_25_OmitsResultTypeAndCacheHints()
|
||||
{
|
||||
await StartAsync();
|
||||
|
||||
// A 2025-11-25 client completes the initialize handshake and then sends subsequent requests with
|
||||
// the MCP-Protocol-Version header pinned to the negotiated revision. The server must not decorate
|
||||
// the result with the 2026-07-28-exclusive resultType/ttlMs/cacheScope fields (issue #1721).
|
||||
var initBody = @"{""jsonrpc"":""2.0"",""id"":1,""method"":""initialize"",""params"":{""protocolVersion"":""2025-11-25"",""capabilities"":{},""clientInfo"":{""name"":""initialize-handshake"",""version"":""1.0""}}}";
|
||||
using (var initRequest = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(initBody) })
|
||||
using (var initResponse = await HttpClient.SendAsync(initRequest, TestContext.Current.CancellationToken))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, initResponse.StatusCode);
|
||||
}
|
||||
|
||||
var body = @"{""jsonrpc"":""2.0"",""id"":2,""method"":""tools/list"",""params"":{}}";
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) };
|
||||
request.Headers.Add(ProtocolVersionHeader, McpProtocolVersions.November2025ProtocolVersion);
|
||||
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
|
||||
var result = json["result"]!.AsObject();
|
||||
Assert.False(result.ContainsKey("resultType"), "resultType must be absent on a 2025-11-25 tools/list result.");
|
||||
Assert.False(result.ContainsKey("ttlMs"), "ttlMs must be absent on a 2025-11-25 tools/list result.");
|
||||
Assert.False(result.ContainsKey("cacheScope"), "cacheScope must be absent on a 2025-11-25 tools/list result.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.Equal(expectedAssemblyName.Name, result.ServerInfo.Name);
|
||||
Assert.Equal(expectedAssemblyName.Version?.ToString() ?? "1.0.0", result.ServerInfo.Version);
|
||||
Assert.Equal("2024-11-05", result.ProtocolVersion);
|
||||
|
|
@ -386,7 +386,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.NotNull(result.Capabilities.Extensions);
|
||||
Assert.True(result.Capabilities.Extensions.ContainsKey("io.myext"));
|
||||
});
|
||||
|
|
@ -406,7 +406,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.NotNull(result.Capabilities.Experimental);
|
||||
Assert.True(result.Capabilities.Experimental.ContainsKey("customFeature"));
|
||||
});
|
||||
|
|
@ -438,7 +438,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
|
||||
// Use reflection to verify every public property on ServerCapabilities is non-null.
|
||||
// This catches cases where new capability properties are added but not copied
|
||||
|
|
@ -485,7 +485,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result?.Completion);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.Equal(["test"], result.Completion.Values);
|
||||
Assert.Equal(2, result.Completion.Total);
|
||||
Assert.True(result.Completion.HasMore);
|
||||
|
|
@ -529,7 +529,7 @@ public class McpServerTests : LoggedTest
|
|||
Assert.NotNull(response);
|
||||
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result?.Completion);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.Equal(["cat"], result.Completion.Values);
|
||||
Assert.Equal(1, result.Completion.Total);
|
||||
|
||||
|
|
@ -573,7 +573,7 @@ public class McpServerTests : LoggedTest
|
|||
Assert.NotNull(response);
|
||||
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result?.Completion);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.Empty(result.Completion.Values);
|
||||
|
||||
await transport.DisposeAsync();
|
||||
|
|
@ -623,7 +623,7 @@ public class McpServerTests : LoggedTest
|
|||
Assert.NotNull(response);
|
||||
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result?.Completion);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.Equal(["us-east-1", "us-west-2"], result.Completion.Values);
|
||||
Assert.Equal(2, result.Completion.Total);
|
||||
|
||||
|
|
@ -679,7 +679,7 @@ public class McpServerTests : LoggedTest
|
|||
Assert.NotNull(response);
|
||||
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result?.Completion);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
// Custom handler values + auto-populated values should be combined
|
||||
Assert.Equal(["custom-value", "dog", "cat"], result.Completion.Values);
|
||||
Assert.Equal(3, result.Completion.Total);
|
||||
|
|
@ -727,7 +727,7 @@ public class McpServerTests : LoggedTest
|
|||
Assert.NotNull(response);
|
||||
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result?.Completion);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.Equal(["a", "b"], result.Completion.Values);
|
||||
|
||||
await transport.DisposeAsync();
|
||||
|
|
@ -766,7 +766,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result?.ResourceTemplates);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.NotEmpty(result.ResourceTemplates);
|
||||
Assert.Equal("test", result.ResourceTemplates[0].UriTemplate);
|
||||
});
|
||||
|
|
@ -796,7 +796,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result?.Resources);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.NotEmpty(result.Resources);
|
||||
Assert.Equal("test", result.Resources[0].Uri);
|
||||
});
|
||||
|
|
@ -832,7 +832,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result?.Contents);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.NotEmpty(result.Contents);
|
||||
|
||||
TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]);
|
||||
|
|
@ -870,7 +870,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result?.Prompts);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.NotEmpty(result.Prompts);
|
||||
Assert.Equal("test", result.Prompts[0].Name);
|
||||
});
|
||||
|
|
@ -900,7 +900,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.Equal("test", result.Description);
|
||||
});
|
||||
}
|
||||
|
|
@ -935,7 +935,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.NotEmpty(result.Tools);
|
||||
Assert.Equal("test", result.Tools[0].Name);
|
||||
});
|
||||
|
|
@ -971,7 +971,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.NotEmpty(result.Content);
|
||||
Assert.Equal("test", Assert.IsType<TextContentBlock>(result.Content[0]).Text);
|
||||
});
|
||||
|
|
@ -1011,6 +1011,31 @@ public class McpServerTests : LoggedTest
|
|||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Can_Handle_SetLoggingLevel_Requests_WithoutHandler_OmitsResultType()
|
||||
{
|
||||
// With no custom SetLoggingLevelHandler configured, the server uses its default logging/setLevel
|
||||
// handler. logging/setLevel is a legacy (<= 2025-11-25) method, so the default handler must also
|
||||
// serialize its result as an empty object {} without the 2026-07-28 resultType field (issue #1721).
|
||||
await Can_Handle_Requests(
|
||||
new ServerCapabilities
|
||||
{
|
||||
Logging = new()
|
||||
},
|
||||
method: RequestMethods.LoggingSetLevel,
|
||||
configureOptions: null,
|
||||
assertResult: (_, response) =>
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<EmptyResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.ResultType);
|
||||
|
||||
// The wire response must be exactly {} with no additional properties.
|
||||
var obj = Assert.IsType<JsonObject>(response);
|
||||
Assert.Empty(obj);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Can_Handle_Call_Tool_Requests_With_McpException()
|
||||
{
|
||||
|
|
@ -1033,7 +1058,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.True(result.IsError);
|
||||
Assert.NotEmpty(result.Content);
|
||||
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
|
||||
|
|
@ -1062,7 +1087,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.True(result.IsError);
|
||||
Assert.NotEmpty(result.Content);
|
||||
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
|
||||
|
|
@ -1098,7 +1123,7 @@ public class McpServerTests : LoggedTest
|
|||
{
|
||||
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("complete", result.ResultType);
|
||||
Assert.Null(result.ResultType);
|
||||
Assert.True(result.IsError, "Input validation errors should be returned as tool execution errors (IsError=true), not protocol errors");
|
||||
Assert.NotEmpty(result.Content);
|
||||
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
|
||||
|
|
@ -1359,7 +1384,7 @@ public class McpServerTests : LoggedTest
|
|||
Assert.NotNull(response.Result);
|
||||
var initResult = JsonSerializer.Deserialize<InitializeResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(initResult);
|
||||
Assert.Equal("complete", initResult.ResultType);
|
||||
Assert.Null(initResult.ResultType);
|
||||
Assert.NotNull(initResult.ServerInfo);
|
||||
|
||||
await transport.DisposeAsync();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.Tests.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for issue #1721: the SDK-provided <c>resultType</c>, <c>ttlMs</c>, and
|
||||
/// <c>cacheScope</c> result decorations are exclusive to the 2026-07-28 protocol revision and
|
||||
/// must be absent from result objects on a session negotiated to an earlier revision
|
||||
/// (e.g. <c>2025-11-25</c>). Emitting them breaks clients that strictly validate the 2025-11-25
|
||||
/// schema.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These drive the in-process <see cref="ClientServerTestBase"/> client/server pair and inspect the
|
||||
/// raw JSON-RPC result wire shape (via <see cref="McpSession.SendRequestAsync(JsonRpcRequest, System.Threading.CancellationToken)"/>)
|
||||
/// so the assertions are about the actual serialized fields rather than deserialized objects.
|
||||
/// <c>tools/list</c> exercises the <c>SetHandler</c> decoration path (its result is both an
|
||||
/// <see cref="ICacheableResult"/> and a <see cref="Result"/>), while <c>tools/call</c> exercises the
|
||||
/// <c>SetWithAlternateHandler</c> path.
|
||||
/// </remarks>
|
||||
public class ProtocolVersionResultDecorationTests : ClientServerTestBase
|
||||
{
|
||||
public ProtocolVersionResultDecorationTests(ITestOutputHelper testOutputHelper)
|
||||
: base(testOutputHelper)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
|
||||
{
|
||||
mcpServerBuilder.WithTools([McpServerTool.Create(() => "ok", new() { Name = "echo" })]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToolsList_On2025_11_25Session_OmitsResultTypeAndCacheHints()
|
||||
{
|
||||
await using var client = await CreateMcpClientForServer(
|
||||
new McpClientOptions { ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion });
|
||||
|
||||
Assert.Equal(McpProtocolVersions.November2025ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
|
||||
var response = await client.SendRequestAsync(
|
||||
new JsonRpcRequest { Method = RequestMethods.ToolsList },
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
var result = response.Result!.AsObject();
|
||||
Assert.False(result.ContainsKey("resultType"), "resultType must be absent on a 2025-11-25 tools/list result.");
|
||||
Assert.False(result.ContainsKey("ttlMs"), "ttlMs must be absent on a 2025-11-25 tools/list result.");
|
||||
Assert.False(result.ContainsKey("cacheScope"), "cacheScope must be absent on a 2025-11-25 tools/list result.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToolsCall_On2025_11_25Session_OmitsResultType()
|
||||
{
|
||||
await using var client = await CreateMcpClientForServer(
|
||||
new McpClientOptions { ProtocolVersion = McpProtocolVersions.November2025ProtocolVersion });
|
||||
|
||||
Assert.Equal(McpProtocolVersions.November2025ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
|
||||
var response = await client.SendRequestAsync(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
Method = RequestMethods.ToolsCall,
|
||||
Params = System.Text.Json.JsonSerializer.SerializeToNode(
|
||||
new CallToolRequestParams { Name = "echo" }, McpJsonUtilities.DefaultOptions),
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
var result = response.Result!.AsObject();
|
||||
Assert.False(result.ContainsKey("resultType"), "resultType must be absent on a 2025-11-25 tools/call result.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToolsList_On2026_07_28Session_IncludesResultTypeAndCacheHints()
|
||||
{
|
||||
await using var client = await CreateMcpClientForServer(
|
||||
new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion });
|
||||
|
||||
Assert.Equal(McpProtocolVersions.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
|
||||
var response = await client.SendRequestAsync(
|
||||
new JsonRpcRequest { Method = RequestMethods.ToolsList },
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
var result = response.Result!.AsObject();
|
||||
Assert.Equal("complete", result["resultType"]!.GetValue<string>());
|
||||
Assert.True(result.ContainsKey("ttlMs"), "ttlMs must be present on a 2026-07-28 tools/list result.");
|
||||
Assert.True(result.ContainsKey("cacheScope"), "cacheScope must be present on a 2026-07-28 tools/list result.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToolsCall_On2026_07_28Session_IncludesResultType()
|
||||
{
|
||||
await using var client = await CreateMcpClientForServer(
|
||||
new McpClientOptions { ProtocolVersion = McpProtocolVersions.July2026ProtocolVersion });
|
||||
|
||||
Assert.Equal(McpProtocolVersions.July2026ProtocolVersion, client.NegotiatedProtocolVersion);
|
||||
|
||||
var response = await client.SendRequestAsync(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
Method = RequestMethods.ToolsCall,
|
||||
Params = System.Text.Json.JsonSerializer.SerializeToNode(
|
||||
new CallToolRequestParams { Name = "echo" }, McpJsonUtilities.DefaultOptions),
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
var result = response.Result!.AsObject();
|
||||
Assert.Equal("complete", result["resultType"]!.GetValue<string>());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue