From 153aca7af840c442ea7850d74871b4735ae25278 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:13:58 +0000 Subject: [PATCH 1/3] Initial plan From 919382181b1095beda4458059178f54ed580440d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:21:36 +0000 Subject: [PATCH 2/3] Add per-user concurrent tool-call limiter filter to ProtectedMcpServer Co-authored-by: halter73 <54385+halter73@users.noreply.github.com> --- samples/ProtectedMcpServer/Program.cs | 26 ++++++++++++++++++++++++++ samples/ProtectedMcpServer/README.md | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/samples/ProtectedMcpServer/Program.cs b/samples/ProtectedMcpServer/Program.cs index 97f8456a2..217c6fafe 100644 --- a/samples/ProtectedMcpServer/Program.cs +++ b/samples/ProtectedMcpServer/Program.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; using ModelContextProtocol.AspNetCore.Authentication; using ProtectedMcpServer.Tools; using System.Net.Http.Headers; using System.Security.Claims; +using System.Threading.RateLimiting; var builder = WebApplication.CreateBuilder(args); @@ -65,7 +68,30 @@ builder.Services.AddAuthorization(); builder.Services.AddHttpContextAccessor(); +var toolCallConcurrencyLimiter = PartitionedRateLimiter.Create, string>(context => + RateLimitPartition.GetConcurrencyLimiter( + context.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? context.User?.Identity?.Name ?? "anonymous", + _ => new ConcurrencyLimiterOptions + { + PermitLimit = 10, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0 + })); builder.Services.AddMcpServer() + .WithRequestFilters(filters => filters.AddCallToolFilter(next => async (request, cancellationToken) => + { + using var lease = await toolCallConcurrencyLimiter.AcquireAsync(request, 1, cancellationToken).ConfigureAwait(false); + if (!lease.IsAcquired) + { + return new CallToolResult + { + IsError = true, + Content = [new TextContentBlock { Text = "Too many concurrent tool calls for this user. Try again later." }] + }; + } + + return await next(request, cancellationToken).ConfigureAwait(false); + })) .WithTools() .WithHttpTransport(); diff --git a/samples/ProtectedMcpServer/README.md b/samples/ProtectedMcpServer/README.md index ecbfee633..0db0a7437 100644 --- a/samples/ProtectedMcpServer/README.md +++ b/samples/ProtectedMcpServer/README.md @@ -7,6 +7,7 @@ This sample demonstrates how to create an MCP server that requires OAuth 2.0 aut The Protected MCP Server sample shows how to: - Create an MCP server with OAuth 2.0 protection - Configure JWT bearer token authentication +- Apply a per-user concurrent tool-call limit (10 in-flight calls) with a call-tool request filter - Implement protected MCP tools and resources - Integrate with ASP.NET Core authentication and authorization - Provide OAuth resource metadata for client discovery @@ -122,4 +123,4 @@ The weather tools use the National Weather Service API at `api.weather.gov` to f - `Program.cs`: Server setup with authentication and MCP configuration - `Tools/WeatherTools.cs`: Weather tool implementations - `Tools/HttpClientExt.cs`: HTTP client extensions -- `Properties/launchSettings.json`: Development launch configuration \ No newline at end of file +- `Properties/launchSettings.json`: Development launch configuration From 6f211008ba26c4292f3c69d11edf88f0e970814c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:31:54 +0000 Subject: [PATCH 3/3] Polish ProtectedMcpServer tool-call limiter configuration Co-authored-by: halter73 <54385+halter73@users.noreply.github.com> --- samples/ProtectedMcpServer/Program.cs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/samples/ProtectedMcpServer/Program.cs b/samples/ProtectedMcpServer/Program.cs index 217c6fafe..df81bb254 100644 --- a/samples/ProtectedMcpServer/Program.cs +++ b/samples/ProtectedMcpServer/Program.cs @@ -12,6 +12,7 @@ var serverUrl = "http://localhost:7071/"; var inMemoryOAuthServerUrl = "https://localhost:7029"; +const int maxConcurrentToolCallsPerUser = 10; builder.Services.AddAuthentication(options => { @@ -68,25 +69,31 @@ builder.Services.AddAuthorization(); builder.Services.AddHttpContextAccessor(); -var toolCallConcurrencyLimiter = PartitionedRateLimiter.Create, string>(context => - RateLimitPartition.GetConcurrencyLimiter( - context.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? context.User?.Identity?.Name ?? "anonymous", - _ => new ConcurrencyLimiterOptions - { - PermitLimit = 10, - QueueProcessingOrder = QueueProcessingOrder.OldestFirst, - QueueLimit = 0 - })); +builder.Services.AddSingleton>>(_ => + PartitionedRateLimiter.Create, string>(context => + RateLimitPartition.GetConcurrencyLimiter( + context.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? + context.User?.Identity?.Name ?? + context.JsonRpcMessage.Context?.RelatedTransport?.SessionId ?? + "anonymous", + _ => new ConcurrencyLimiterOptions + { + PermitLimit = maxConcurrentToolCallsPerUser, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0 + }))); builder.Services.AddMcpServer() .WithRequestFilters(filters => filters.AddCallToolFilter(next => async (request, cancellationToken) => { + var services = request.Services ?? throw new InvalidOperationException("Request context does not have an associated service provider."); + var toolCallConcurrencyLimiter = services.GetRequiredService>>(); using var lease = await toolCallConcurrencyLimiter.AcquireAsync(request, 1, cancellationToken).ConfigureAwait(false); if (!lease.IsAcquired) { return new CallToolResult { IsError = true, - Content = [new TextContentBlock { Text = "Too many concurrent tool calls for this user. Try again later." }] + Content = [new TextContentBlock { Text = $"Maximum concurrent tool calls ({maxConcurrentToolCallsPerUser}) exceeded. Try again after in-progress calls complete." }] }; }