using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.AspNetCore.Tests.Utils; using ModelContextProtocol.Client; using ModelContextProtocol.Extensions.Tasks; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using Moq; using System.Security.Claims; namespace ModelContextProtocol.AspNetCore.Tests; public class HttpTaskIntegrationTests(ITestOutputHelper testOutputHelper) : KestrelInMemoryTest(testOutputHelper) { [Fact] public async Task WithTasks_CanCallToolOverHttp() { Builder.Services .AddMcpServer() .WithHttpTransport() .WithTasks(new InMemoryMcpTaskStore { DefaultPollIntervalMs = 10 }) .WithTools(); await using var app = Builder.Build(); app.MapMcp(); await app.StartAsync(TestContext.Current.CancellationToken); await using var transport = new HttpClientTransport( new HttpClientTransportOptions { Endpoint = new("http://localhost:5000") }, HttpClient, LoggerFactory); await using var client = await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); var result = await client.CallToolWithPollingAsync( new CallToolRequestParams { Name = "test" }, cancellationToken: TestContext.Current.CancellationToken); Assert.Equal("Hello World!", Assert.IsType(Assert.Single(result.Content)).Text); } [Fact] public async Task WithTasks_AfterOrdinaryFilter_RunsFilter() { var filterInvocationCount = 0; Builder.Services .AddMcpServer(options => { options.Filters.Request.CallToolFilters.Add(next => async (context, cancellationToken) => { Interlocked.Increment(ref filterInvocationCount); return await next(context, cancellationToken); }); }) .WithHttpTransport() .WithTasks(new InMemoryMcpTaskStore { DefaultPollIntervalMs = 10 }) .WithTools(); await using var app = Builder.Build(); app.MapMcp(); await app.StartAsync(TestContext.Current.CancellationToken); await using var transport = new HttpClientTransport( new HttpClientTransportOptions { Endpoint = new("http://localhost:5000") }, HttpClient, LoggerFactory); await using var client = await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); var result = await client.CallToolWithPollingAsync( new CallToolRequestParams { Name = "test" }, cancellationToken: TestContext.Current.CancellationToken); Assert.Equal("Hello World!", Assert.IsType(Assert.Single(result.Content)).Text); Assert.Equal(1, filterInvocationCount); } [Theory] [InlineData(false)] [InlineData(true)] public async Task WithTasks_AuthorizedTool_Completes(bool registerTasksBeforeAuthorization) { var serverBuilder = Builder.Services .AddMcpServer() .WithHttpTransport(); if (registerTasksBeforeAuthorization) { serverBuilder .WithTasks(new InMemoryMcpTaskStore { DefaultPollIntervalMs = 10 }) .AddAuthorizationFilters(); } else { serverBuilder .AddAuthorizationFilters() .WithTasks(new InMemoryMcpTaskStore { DefaultPollIntervalMs = 10 }); } serverBuilder.WithTools(); Builder.Services.AddAuthorization(); await using var app = Builder.Build(); app.Use(next => async context => { context.User = new ClaimsPrincipal(new ClaimsIdentity( [new Claim(ClaimTypes.NameIdentifier, "test-user")], "TestAuthType")); await next(context); }); app.MapMcp(); await app.StartAsync(TestContext.Current.CancellationToken); await using var transport = new HttpClientTransport( new HttpClientTransportOptions { Endpoint = new("http://localhost:5000") }, HttpClient, LoggerFactory); await using var client = await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); var result = await client.CallToolWithPollingAsync( new CallToolRequestParams { Name = "authorized-test" }, cancellationToken: TestContext.Current.CancellationToken); Assert.Equal("Authorized", Assert.IsType(Assert.Single(result.Content)).Text); } [Theory] [InlineData(false)] [InlineData(true)] public async Task WithTasks_UnauthorizedTool_DoesNotCreateTask(bool registerTasksBeforeAuthorization) { var taskStore = new Mock(MockBehavior.Strict); var serverBuilder = Builder.Services .AddMcpServer() .WithHttpTransport(); if (registerTasksBeforeAuthorization) { serverBuilder .WithTasks(taskStore.Object) .AddAuthorizationFilters(); } else { serverBuilder .AddAuthorizationFilters() .WithTasks(taskStore.Object); } serverBuilder.WithTools(); Builder.Services.AddAuthorization(); await using var app = Builder.Build(); app.MapMcp(); await app.StartAsync(TestContext.Current.CancellationToken); await using var transport = new HttpClientTransport( new HttpClientTransportOptions { Endpoint = new("http://localhost:5000") }, HttpClient, LoggerFactory); await using var client = await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); var exception = await Assert.ThrowsAsync(() => client.CallToolAsTaskAsync( new CallToolRequestParams { Name = "authorized-test" }, TestContext.Current.CancellationToken).AsTask()); Assert.Equal(McpErrorCode.InvalidRequest, exception.ErrorCode); taskStore.Verify( store => store.CreateTaskAsync(It.IsAny()), Times.Never); } [Fact] public async Task WithTasks_ReauthorizesToolChangedByOrdinaryFilter() { var serverBuilder = Builder.Services .AddMcpServer() .WithHttpTransport() .WithTasks(new InMemoryMcpTaskStore { DefaultPollIntervalMs = 10 }) .WithTools() .AddAuthorizationFilters(); serverBuilder.Services.Configure(options => { if (options.ToolCollection is null || !options.ToolCollection.TryGetPrimitive("authorized-test", out var authorizedTool)) { throw new InvalidOperationException("The replacement tool was not registered."); } options.Filters.Request.CallToolFilters.Add(next => async (context, cancellationToken) => { context.MatchedPrimitive = authorizedTool; return await next(context, cancellationToken); }); }); serverBuilder.AddAuthorizationFilters(); Builder.Services.AddAuthorization(); await using var app = Builder.Build(); app.MapMcp(); await app.StartAsync(TestContext.Current.CancellationToken); await using var transport = new HttpClientTransport( new HttpClientTransportOptions { Endpoint = new("http://localhost:5000") }, HttpClient, LoggerFactory); await using var client = await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); var exception = await Assert.ThrowsAsync(() => client.CallToolWithPollingAsync( new CallToolRequestParams { Name = "test" }, cancellationToken: TestContext.Current.CancellationToken).AsTask()); Assert.Contains("Access forbidden: This tool requires authorization.", exception.Message); } [McpServerToolType] private sealed class TestTools { [McpServerTool(Name = "test")] public static string Test() => "Hello World!"; [McpServerTool(Name = "authorized-test")] [Authorize] public static string AuthorizedTest() => "Authorized"; } }