Add initial task extension conformance coverage (#1734)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7c99928c-a943-49f4-9a63-8b04a0e4d18b
This commit is contained in:
parent
f7c61fafdb
commit
83523b0a42
3 changed files with 463 additions and 43 deletions
|
|
@ -8,6 +8,7 @@ using System.Net;
|
|||
using System.Net.ServerSentEvents;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace ModelContextProtocol.AspNetCore.Tests;
|
||||
|
||||
|
|
@ -32,6 +33,18 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
|
|||
Name = nameof(HttpHeaderConformanceTests),
|
||||
Version = "1.0",
|
||||
};
|
||||
#pragma warning disable MCPEXP002
|
||||
options.RequestHandlers =
|
||||
[
|
||||
new McpServerRequestHandler
|
||||
{
|
||||
Method = "extension/get",
|
||||
RoutingNameParameter = "itemId",
|
||||
Handler = static (_, _) => new ValueTask<JsonNode?>(
|
||||
new JsonObject { ["resultType"] = "complete" }),
|
||||
},
|
||||
];
|
||||
#pragma warning restore MCPEXP002
|
||||
}).WithTools(Tools).WithHttpTransport();
|
||||
|
||||
_app = Builder.Build();
|
||||
|
|
@ -110,6 +123,23 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
|
|||
|
||||
#region Server-side validation tests
|
||||
|
||||
[Fact]
|
||||
public async Task Server_UsesCustomHandlerRoutingNameMetadata()
|
||||
{
|
||||
await StartAsync();
|
||||
await ProbeWithJuly2026ProtocolVersionAsync();
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
request.Content = JsonContent(
|
||||
"""{"jsonrpc":"2.0","id":2,"method":"extension/get","params":{"itemId":"item-42","_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"TestClient","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}""");
|
||||
request.Headers.Add(McpHttpHeaders.ProtocolVersion, "2026-07-28");
|
||||
request.Headers.Add(McpHttpHeaders.Method, "extension/get");
|
||||
request.Headers.Add(McpHttpHeaders.Name, "item-42");
|
||||
|
||||
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Server_AcceptsUnionIntegerCanonicalForm()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -127,6 +127,34 @@ public class ServerConformanceTests(
|
|||
$"MRTR conformance test '{scenario}' failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("tasks-wire-fields")]
|
||||
[InlineData("tasks-request-state-removal")]
|
||||
[InlineData("tasks-mrtr-input")]
|
||||
// Most remaining scenarios require per-tool task execution configuration that the SDK
|
||||
// does not currently expose; status notifications await an upstream harness rewrite.
|
||||
// Keep them listed here for incremental enablement.
|
||||
// [InlineData("tasks-lifecycle")]
|
||||
// [InlineData("tasks-capability-negotiation")]
|
||||
// [InlineData("tasks-request-headers")]
|
||||
// [InlineData("tasks-dispatch-and-envelope")]
|
||||
// [InlineData("tasks-status-notifications")]
|
||||
// [InlineData("tasks-required-task-error")]
|
||||
// [InlineData("tasks-mrtr-composition")]
|
||||
public async Task RunTasksExtensionConformanceTest(string scenario)
|
||||
{
|
||||
Assert.SkipWhen(!NodeHelpers.IsNodeInstalled(), "Node.js is not installed. Skipping conformance tests.");
|
||||
Assert.SkipWhen(
|
||||
!NodeHelpers.HasTasksExtensionScenarios(),
|
||||
"SEP-2663 Tasks extension scenarios are not available in the installed conformance package.");
|
||||
|
||||
var result = await RunStatelessConformanceTestAsync(
|
||||
$"server --url {fixture.StatelessServerUrl} --scenario {scenario}");
|
||||
|
||||
Assert.True(result.Success,
|
||||
$"Tasks extension conformance test '{scenario}' failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}");
|
||||
}
|
||||
|
||||
private async Task<(bool Success, string Output, string Error)> RunConformanceTestsAsync(string arguments)
|
||||
{
|
||||
return await NodeHelpers.RunServerConformanceAsync(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http.Json;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.AspNetCore.Tests.Utils;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Extensions.Tasks;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using ModelContextProtocol.Tests.Utils;
|
||||
|
|
@ -11,6 +12,7 @@ using System.Net;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace ModelContextProtocol.AspNetCore.Tests;
|
||||
|
|
@ -606,6 +608,31 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
|
|||
Assert.Equal("us-west-2", capturedHeaders["Mcp-Param-Region"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TasksClient_SendsRoutingNameHeader_EndToEnd()
|
||||
{
|
||||
var capturedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
await StartHeaderCapturingServer(capturedHeaders, supportsTasks: true);
|
||||
|
||||
await using var transport = new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new("http://localhost:5000/mcp"),
|
||||
TransportMode = HttpTransportMode.StreamableHttp,
|
||||
}, HttpClient, LoggerFactory);
|
||||
|
||||
await using var client = await McpClient.CreateAsync(
|
||||
transport,
|
||||
new McpClientOptions { ProtocolVersion = "2026-07-28" },
|
||||
loggerFactory: LoggerFactory,
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
capturedHeaders.Clear();
|
||||
await client.GetTaskAsync("task-42", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal("tasks/get", capturedHeaders[McpHttpHeaders.Method]);
|
||||
Assert.Equal("task-42", capturedHeaders[McpHttpHeaders.Name]);
|
||||
}
|
||||
|
||||
private async Task StartHeaderToolServer()
|
||||
{
|
||||
Builder.Services.Configure<JsonOptions>(options =>
|
||||
|
|
@ -683,7 +710,9 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
|
|||
await _app.StartAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
private async Task StartHeaderCapturingServer(Dictionary<string, string> capturedHeaders)
|
||||
private async Task StartHeaderCapturingServer(
|
||||
Dictionary<string, string> capturedHeaders,
|
||||
bool supportsTasks = false)
|
||||
{
|
||||
Builder.Services.Configure<JsonOptions>(options =>
|
||||
{
|
||||
|
|
@ -706,12 +735,45 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
|
|||
Result = JsonSerializer.SerializeToNode(new InitializeResult
|
||||
{
|
||||
ProtocolVersion = "2025-11-25",
|
||||
Capabilities = new() { Tools = new() },
|
||||
Capabilities = new()
|
||||
{
|
||||
Tools = new(),
|
||||
Extensions = supportsTasks
|
||||
? new Dictionary<string, object>
|
||||
{
|
||||
["io.modelcontextprotocol/tasks"] = new JsonObject(),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
ServerInfo = new Implementation { Name = "header-capture", Version = "1.0" },
|
||||
}, McpJsonUtilities.DefaultOptions)
|
||||
});
|
||||
}
|
||||
|
||||
if (request.Method == "server/discover" && supportsTasks)
|
||||
{
|
||||
return Results.Json(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
Result = JsonSerializer.SerializeToNode(new DiscoverResult
|
||||
{
|
||||
SupportedVersions = ["2026-07-28"],
|
||||
Capabilities = new()
|
||||
{
|
||||
Tools = new(),
|
||||
Extensions = new Dictionary<string, object>
|
||||
{
|
||||
["io.modelcontextprotocol/tasks"] = new JsonObject(),
|
||||
},
|
||||
},
|
||||
ServerInfo = new Implementation { Name = "header-capture", Version = "1.0" },
|
||||
TimeToLive = TimeSpan.Zero,
|
||||
CacheScope = CacheScope.Private,
|
||||
ResultType = "complete",
|
||||
}, McpJsonUtilities.DefaultOptions),
|
||||
});
|
||||
}
|
||||
|
||||
if (request.Method == "tools/list")
|
||||
{
|
||||
return Results.Json(new JsonRpcResponse
|
||||
|
|
@ -732,7 +794,7 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
|
|||
});
|
||||
}
|
||||
|
||||
if (request.Method == "tools/call")
|
||||
if (request.Method is "tools/call" or "tasks/get")
|
||||
{
|
||||
// Capture all MCP headers for verification
|
||||
foreach (var header in context.Request.Headers)
|
||||
|
|
@ -743,6 +805,16 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
|
|||
}
|
||||
}
|
||||
|
||||
if (request.Method == "tasks/get")
|
||||
{
|
||||
return Results.Json(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
Result = JsonNode.Parse(
|
||||
"""{"taskId":"task-42","status":"working","createdAt":"2026-01-01T00:00:00Z","lastUpdatedAt":"2026-01-01T00:00:00Z"}"""),
|
||||
});
|
||||
}
|
||||
|
||||
var parameters = JsonSerializer.Deserialize(request.Params, GetJsonTypeInfo<CallToolRequestParams>());
|
||||
return Results.Json(new JsonRpcResponse
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue