fix(aspnetcore): make SEP-2575 status mapping deterministic (#1837)

This commit is contained in:
King Star 2026-09-10 03:25:54 +08:00 • committed by GitHub
commit cb3e6e87fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 85 deletions

View file

@ -239,10 +239,9 @@ public abstract partial class MapMcpTests(ITestOutputHelper testOutputHelper) :
[Fact]
public async Task LongRunningToolCall_DoesNotTimeout_WhenNoEventStreamStore()
{
// Regression test for: Tool calls that last over HttpClient timeout without producing
// intermediate notifications will timeout because HttpClient doesn't see the 200 response
// until the first message is written. When primingItem is null (no ISseEventStreamStore),
// we should flush the response stream so HttpClient sees the 200 immediately.
// Legacy Streamable HTTP tool calls that run past the HttpClient timeout without producing
// intermediate notifications need an eager response flush. Per-request-metadata protocol
// revisions instead wait for the first JSON-RPC message so SEP-2575 can map its status.
Builder.Services.AddMcpServer().WithHttpTransport(ConfigureStateless).WithTools<LongRunningTools>();
@ -274,7 +273,11 @@ public abstract partial class MapMcpTests(ITestOutputHelper testOutputHelper) :
TransportMode = transportMode,
}, shortTimeoutClient, LoggerFactory);
await using var mcpClient = await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
await using var mcpClient = await McpClient.CreateAsync(
transport,
new McpClientOptions { ProtocolVersion = "2025-11-25" },
LoggerFactory,
TestContext.Current.CancellationToken);
// Call a tool that takes 2 seconds - this should succeed despite the 1 second HttpClient timeout
// because the response stream is flushed immediately after receiving the request

View file

@ -207,6 +207,27 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
Assert.Equal((int)McpErrorCode.MissingRequiredClientCapability, json["error"]!["code"]!.GetValue<int>());
}
[Fact]
public async Task July2026Post_SlowHandler_MissingRequiredCapability_Returns400()
{
await StartAsync();
var body =
@"{""jsonrpc"":""2.0"",""id"":20,""method"":""tools/call"",""params"":{""name"":""slow_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", "slow_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(20, json["id"]!.GetValue<long>());
Assert.Equal((int)McpErrorCode.MissingRequiredClientCapability, json["error"]!["code"]!.GetValue<int>());
}
[Fact]
public async Task ServerDiscover_WithConfiguredPerRequestMetadataProtocol_ReturnsOnlyConfiguredVersion()
{
@ -500,10 +521,21 @@ public class RawHttpConformanceTests(ITestOutputHelper outputHelper) : KestrelIn
[McpServerToolType]
private sealed class CapabilityTools
{
private static readonly TimeSpan SlowHandlerDelay = TimeSpan.FromSeconds(1);
[McpServerTool(Name = "requires_sampling")]
public static string RequiresSampling() =>
throw new MissingRequiredClientCapabilityException(
new ClientCapabilities { Sampling = new() },
"sampling capability required but not declared by client");
[McpServerTool(Name = "slow_requires_sampling")]
public static async Task<string> SlowRequiresSampling(CancellationToken cancellationToken)
{
await Task.Delay(SlowHandlerDelay, cancellationToken);
throw new MissingRequiredClientCapabilityException(
new ClientCapabilities { Sampling = new() },
"sampling capability required but not declared by client");
}
}
}