Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions samples/ProtectedMcpServer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
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);

var serverUrl = "http://localhost:7071/";
var inMemoryOAuthServerUrl = "https://localhost:7029";
const int maxConcurrentToolCallsPerUser = 10;

builder.Services.AddAuthentication(options =>
{
Expand Down Expand Up @@ -65,7 +69,36 @@
builder.Services.AddAuthorization();

builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<PartitionedRateLimiter<RequestContext<CallToolRequestParams>>>(_ =>
PartitionedRateLimiter.Create<RequestContext<CallToolRequestParams>, 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<PartitionedRateLimiter<RequestContext<CallToolRequestParams>>>();
using var lease = await toolCallConcurrencyLimiter.AcquireAsync(request, 1, cancellationToken).ConfigureAwait(false);
if (!lease.IsAcquired)
{
return new CallToolResult
{
IsError = true,
Content = [new TextContentBlock { Text = $"Maximum concurrent tool calls ({maxConcurrentToolCallsPerUser}) exceeded. Try again after in-progress calls complete." }]
};
}

return await next(request, cancellationToken).ConfigureAwait(false);
}))
.WithTools<WeatherTools>()
.WithHttpTransport();

Expand Down
3 changes: 2 additions & 1 deletion samples/ProtectedMcpServer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
- `Properties/launchSettings.json`: Development launch configuration