Implement SEP-2663 Tasks Extension (#1579)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Pranav Senthilnathan 2026-06-11 13:10:45 -07:00 • committed by GitHub
commit dbb7a20176
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 8190 additions and 11405 deletions

View file

@ -72,12 +72,23 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
{
List<string> messageTypes = [];
// The client sends notifications/initialized fire-and-forget, so unlike the initialize and
// tools/list request/response exchanges it has no synchronization point the test can await.
// Signal once the filter finishes processing it so the strict counts below observe a
// complete, stable log instead of racing the still-in-flight notification.
var initializedNotificationProcessed = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
McpServerBuilder
.WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{
var messageTypeName = context.JsonRpcMessage.GetType().Name;
messageTypes.Add(messageTypeName);
await next(context, cancellationToken);
if (context.JsonRpcMessage is JsonRpcNotification { Method: NotificationMethods.InitializedNotification })
{
initializedNotificationProcessed.TrySetResult(true);
}
}))
.WithTools<TestTool>();
@ -87,6 +98,10 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
// Wait for the fire-and-forget initialized notification to flow through the filter pipeline
// before snapshotting the counts; otherwise the strict counts below can race the notification.
await initializedNotificationProcessed.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
// The message filter should intercept JsonRpcRequest messages.
// Use strict counts so a regression that invokes the filter pipeline more than once per
// incoming message (analogous to the SendRequestAsync double-wrap regression on the outgoing

View file

@ -284,57 +284,4 @@ public class McpServerOptionsSetupTests
Assert.NotNull(options.Capabilities?.Completions);
}
#endregion
#region TaskStore Tests
[Fact]
public void TaskStore_IsPopulatedFromDI_WhenNotExplicitlySet()
{
var services = new ServiceCollection();
services.AddMcpServer();
services.AddSingleton<IMcpTaskStore, InMemoryMcpTaskStore>();
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.IsType<InMemoryMcpTaskStore>(options.TaskStore);
}
[Fact]
public void TaskStore_ExplicitOption_TakesPrecedenceOverDI()
{
var explicitStore = new InMemoryMcpTaskStore();
var services = new ServiceCollection();
services.AddMcpServer(options => options.TaskStore = explicitStore);
services.AddSingleton<IMcpTaskStore, InMemoryMcpTaskStore>();
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.Same(explicitStore, options.TaskStore);
}
[Fact]
public void TaskStore_RemainsNull_WhenNothingIsRegistered()
{
var services = new ServiceCollection();
services.AddMcpServer();
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.Null(options.TaskStore);
}
[Fact]
public void TaskStore_CanBeOverriddenToNull_AfterDIRegistration()
{
var services = new ServiceCollection();
services.AddMcpServer();
services.AddSingleton<IMcpTaskStore, InMemoryMcpTaskStore>();
services.Configure<McpServerOptions>(options => options.TaskStore = null);
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.Null(options.TaskStore);
}
#endregion
}