Add stateless Streamable HTTP support (#392)

- It allows a single MCP session spanning multiple requests
  to be handled by different servers without sharing state
- It does require the servers share data protection keys, but this
  is standard for ASP.NET Core cookies and antiforgery as well
- It immediately throws for unsupported operations like sampling
This commit is contained in:
Stephen Halter 2025-05-14 04:51:03 -07:00 • committed by GitHub
commit ce9f73f43f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -71,7 +71,6 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
Assert.Contains("IdleTimeout", ex.Message);
}
[Fact]
public async Task NegativeMaxIdleSessionCount_Throws_ArgumentOutOfRangeException()
{
@ -360,6 +359,47 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
Assert.Equal(11, currentSseItem);
}
[Fact]
public async Task AsyncLocalSetInRunSessionHandlerCallback_Flows_ToAllToolCalls()
{
var asyncLocal = new AsyncLocal<string>();
var totalSessionCount = 0;
Builder.Services.AddMcpServer()
.WithHttpTransport(options =>
{
options.RunSessionHandler = async (httpContext, mcpServer, cancellationToken) =>
{
asyncLocal.Value = $"RunSessionHandler ({totalSessionCount++})";
await mcpServer.RunAsync(cancellationToken);
};
});
Builder.Services.AddSingleton(McpServerTool.Create([McpServerTool(Name = "async-local-session")] () => asyncLocal.Value));
await StartAsync();
var firstSessionId = await CallInitializeAndValidateAsync();
async Task CallAsyncLocalToolAndValidateAsync(int expectedSessionIndex)
{
var response = await HttpClient.PostAsync("", JsonContent(CallTool("async-local-session")), TestContext.Current.CancellationToken);
var rpcResponse = await AssertSingleSseResponseAsync(response);
var callToolResponse = AssertType<CallToolResponse>(rpcResponse.Result);
var callToolContent = Assert.Single(callToolResponse.Content);
Assert.Equal("text", callToolContent.Type);
Assert.Equal($"RunSessionHandler ({expectedSessionIndex})", callToolContent.Text);
}
await CallAsyncLocalToolAndValidateAsync(expectedSessionIndex: 0);
await CallInitializeAndValidateAsync();
await CallAsyncLocalToolAndValidateAsync(expectedSessionIndex: 1);
SetSessionId(firstSessionId);
await CallAsyncLocalToolAndValidateAsync(expectedSessionIndex: 0);
}
[Fact]
public async Task IdleSessions_ArePruned_AfterIdleTimeout()
{