-
Notifications
You must be signed in to change notification settings - Fork 545
Add middleware and authz support for server-side handlers #733
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
58dc73a
Add middleware and authz support for server-side handlers
halter73 76f7297
Fix failing Windows build
halter73 3db0b82
Merge remote-tracking branch 'origin/main' into halter73/middleware
halter73 9b5786d
Use a delegate type for handlers and filters
halter73 d29040f
Add AddAuthorizationFilters
halter73 f9e88ee
Update tool call authorization failures to throw an McpException
halter73 263313e
Remove invalid link from comment
halter73 d3186fd
Address PR feedback
halter73 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,317 @@ | ||
--- | ||
title: Filters | ||
author: halter73 | ||
description: MCP Server Handler Filters | ||
uid: filters | ||
--- | ||
|
||
# MCP Server Handler Filters | ||
|
||
For each handler type in the MCP Server, there are corresponding `AddXXXFilter` methods in `McpServerBuilderExtensions.cs` that allow you to add filters to the handler pipeline. The filters are stored in `McpServerOptions.Filters` and applied during server configuration. | ||
|
||
## Available Filter Methods | ||
|
||
The following filter methods are available: | ||
|
||
- `AddListResourceTemplatesFilter` - Filter for list resource templates handlers | ||
- `AddListToolsFilter` - Filter for list tools handlers | ||
- `AddCallToolFilter` - Filter for call tool handlers | ||
- `AddListPromptsFilter` - Filter for list prompts handlers | ||
- `AddGetPromptFilter` - Filter for get prompt handlers | ||
- `AddListResourcesFilter` - Filter for list resources handlers | ||
- `AddReadResourceFilter` - Filter for read resource handlers | ||
- `AddCompleteFilter` - Filter for completion handlers | ||
- `AddSubscribeToResourcesFilter` - Filter for resource subscription handlers | ||
- `AddUnsubscribeFromResourcesFilter` - Filter for resource unsubscription handlers | ||
- `AddSetLoggingLevelFilter` - Filter for logging level handlers | ||
|
||
## Usage | ||
|
||
Filters are functions that take a handler and return a new handler, allowing you to wrap the original handler with additional functionality: | ||
|
||
```csharp | ||
services.AddMcpServer() | ||
.WithListToolsHandler(async (context, cancellationToken) => | ||
{ | ||
// Your base handler logic | ||
return new ListToolsResult { Tools = GetTools() }; | ||
}) | ||
.AddListToolsFilter(next => async (context, cancellationToken) => | ||
stephentoub marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var logger = context.Services?.GetService<ILogger<Program>>(); | ||
|
||
// Pre-processing logic | ||
logger?.LogInformation("Before handler execution"); | ||
|
||
var result = await next(context, cancellationToken); | ||
|
||
// Post-processing logic | ||
logger?.LogInformation("After handler execution"); | ||
return result; | ||
}); | ||
``` | ||
|
||
## Filter Execution Order | ||
|
||
```csharp | ||
services.AddMcpServer() | ||
.WithListToolsHandler(baseHandler) | ||
.AddListToolsFilter(filter1) // Executes first (outermost) | ||
.AddListToolsFilter(filter2) // Executes second | ||
.AddListToolsFilter(filter3); // Executes third (closest to handler) | ||
``` | ||
|
||
Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filter2 -> filter1` | ||
|
||
## Common Use Cases | ||
|
||
### Logging | ||
```csharp | ||
.AddListToolsFilter(next => async (context, cancellationToken) => | ||
{ | ||
var logger = context.Services?.GetService<ILogger<Program>>(); | ||
|
||
logger?.LogInformation($"Processing request from {context.Meta.ProgressToken}"); | ||
var result = await next(context, cancellationToken); | ||
logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools"); | ||
return result; | ||
}); | ||
``` | ||
|
||
### Error Handling | ||
```csharp | ||
.AddCallToolFilter(next => async (context, cancellationToken) => | ||
{ | ||
try | ||
{ | ||
return await next(context, cancellationToken); | ||
} | ||
catch (Exception ex) | ||
{ | ||
return new CallToolResult | ||
{ | ||
Content = new[] { new TextContent { Type = "text", Text = $"Error: {ex.Message}" } }, | ||
IsError = true | ||
}; | ||
} | ||
}); | ||
``` | ||
|
||
### Performance Monitoring | ||
```csharp | ||
.AddListToolsFilter(next => async (context, cancellationToken) => | ||
{ | ||
var logger = context.Services?.GetService<ILogger<Program>>(); | ||
|
||
var stopwatch = Stopwatch.StartNew(); | ||
var result = await next(context, cancellationToken); | ||
stopwatch.Stop(); | ||
logger?.LogInformation($"Handler took {stopwatch.ElapsedMilliseconds}ms"); | ||
return result; | ||
}); | ||
``` | ||
|
||
### Caching | ||
```csharp | ||
.AddListResourcesFilter(next => async (context, cancellationToken) => | ||
{ | ||
var cache = context.Services!.GetRequiredService<IMemoryCache>(); | ||
halter73 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var cacheKey = $"resources:{context.Params.Cursor}"; | ||
if (cache.TryGetValue(cacheKey, out var cached)) | ||
halter73 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
return (ListResourcesResult)cached; | ||
} | ||
|
||
var result = await next(context, cancellationToken); | ||
cache.Set(cacheKey, result, TimeSpan.FromMinutes(5)); | ||
return result; | ||
}); | ||
``` | ||
|
||
## Built-in Authorization Filters | ||
|
||
When using the ASP.NET Core integration (`ModelContextProtocol.AspNetCore`), you can add authorization filters to support `[Authorize]` and `[AllowAnonymous]` attributes on MCP server tools, prompts, and resources by calling `AddAuthorizationFilters()` on your MCP server builder. | ||
|
||
### Enabling Authorization Filters | ||
|
||
To enable authorization support, call `AddAuthorizationFilters()` when configuring your MCP server: | ||
|
||
```csharp | ||
services.AddMcpServer() | ||
.WithHttpTransport() | ||
.AddAuthorizationFilters() // Enable authorization filter support | ||
.WithTools<WeatherTools>(); | ||
``` | ||
|
||
**Important**: You should always call `AddAuthorizationFilters()` when using ASP.NET Core integration if you want to use authorization attributes like `[Authorize]` on your MCP server tools, prompts, or resources. | ||
|
||
### Authorization Attributes Support | ||
|
||
The MCP server automatically respects the following authorization attributes: | ||
|
||
- **`[Authorize]`** - Requires authentication for access | ||
- **`[Authorize(Roles = "RoleName")]`** - Requires specific roles | ||
- **`[Authorize(Policy = "PolicyName")]`** - Requires specific authorization policies | ||
- **`[AllowAnonymous]`** - Explicitly allows anonymous access (overrides `[Authorize]`) | ||
|
||
### Tool Authorization | ||
|
||
Tools can be decorated with authorization attributes to control access: | ||
|
||
```csharp | ||
[McpServerToolType] | ||
public class WeatherTools | ||
{ | ||
[McpServerTool, Description("Gets public weather data")] | ||
public static string GetWeather(string location) | ||
{ | ||
return $"Weather for {location}: Sunny, 25°C"; | ||
} | ||
|
||
[McpServerTool, Description("Gets detailed weather forecast")] | ||
[Authorize] // Requires authentication | ||
public static string GetDetailedForecast(string location) | ||
{ | ||
return $"Detailed forecast for {location}: ..."; | ||
} | ||
|
||
[McpServerTool, Description("Manages weather alerts")] | ||
[Authorize(Roles = "Admin")] // Requires Admin role | ||
public static string ManageWeatherAlerts(string alertType) | ||
{ | ||
return $"Managing alert: {alertType}"; | ||
} | ||
} | ||
``` | ||
|
||
### Class-Level Authorization | ||
|
||
You can apply authorization at the class level, which affects all tools in the class: | ||
|
||
```csharp | ||
[McpServerToolType] | ||
[Authorize] // All tools require authentication | ||
public class RestrictedTools | ||
{ | ||
[McpServerTool, Description("Restricted tool accessible to authenticated users")] | ||
public static string RestrictedOperation() | ||
{ | ||
return "Restricted operation completed"; | ||
} | ||
|
||
[McpServerTool, Description("Public tool accessible to anonymous users")] | ||
[AllowAnonymous] // Overrides class-level [Authorize] | ||
public static string PublicOperation() | ||
{ | ||
return "Public operation completed"; | ||
} | ||
} | ||
``` | ||
|
||
### How Authorization Filters Work | ||
|
||
The authorization filters work differently for list operations versus individual operations: | ||
|
||
#### List Operations (ListTools, ListPrompts, ListResources) | ||
For list operations, the filters automatically remove unauthorized items from the results. Users only see tools, prompts, or resources they have permission to access. | ||
|
||
#### Individual Operations (CallTool, GetPrompt, ReadResource) | ||
For individual operations, the filters throw an `McpException` with "Access forbidden" message. These get turned into JSON-RPC errors if uncaught by middleware. | ||
|
||
### Filter Execution Order and Authorization | ||
|
||
Authorization filters are applied automatically when you call `AddAuthorizationFilters()`. These filters run at a specific point in the filter pipeline, which means: | ||
|
||
**Filters added before authorization filters** can see: | ||
- Unauthorized requests for operations before they are rejected by the authorization filters | ||
- Complete listings for unauthorized primitives before they are filtered out by the authorization filters | ||
|
||
**Filters added after authorization filters** will only see: | ||
- Authorized requests that passed authorization checks | ||
- Filtered listings containing only authorized primitives | ||
|
||
This allows you to implement logging, metrics, or other cross-cutting concerns that need to see all requests, while still maintaining proper authorization: | ||
|
||
```csharp | ||
services.AddMcpServer() | ||
.WithHttpTransport() | ||
.AddListToolsFilter(next => async (context, cancellationToken) => | ||
{ | ||
var logger = context.Services?.GetService<ILogger<Program>>(); | ||
|
||
// This filter runs BEFORE authorization - sees all tools | ||
logger?.LogInformation("Request for tools list - will see all tools"); | ||
var result = await next(context, cancellationToken); | ||
logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools after authorization"); | ||
return result; | ||
}) | ||
.AddAuthorizationFilters() // Authorization filtering happens here | ||
halter73 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.AddListToolsFilter(next => async (context, cancellationToken) => | ||
{ | ||
var logger = context.Services?.GetService<ILogger<Program>>(); | ||
|
||
// This filter runs AFTER authorization - only sees authorized tools | ||
var result = await next(context, cancellationToken); | ||
logger?.LogInformation($"Post-auth filter sees {result.Tools?.Count ?? 0} authorized tools"); | ||
return result; | ||
}) | ||
.WithTools<WeatherTools>(); | ||
``` | ||
|
||
### Setup Requirements | ||
|
||
To use authorization features, you must configure authentication and authorization in your ASP.NET Core application and call `AddAuthorizationFilters()`: | ||
|
||
```csharp | ||
var builder = WebApplication.CreateBuilder(args); | ||
|
||
builder.Services.AddAuthentication("Bearer") | ||
.AddJwtBearer(options => { /* JWT configuration */ }) | ||
.AddMcp(options => { /* Resource metadata configuration */ }); | ||
builder.Services.AddAuthorization(); | ||
|
||
builder.Services.AddMcpServer() | ||
.WithHttpTransport() | ||
.AddAuthorizationFilters() // Required for authorization support | ||
.WithTools<WeatherTools>() | ||
.AddCallToolFilter(next => async (context, cancellationToken) => | ||
{ | ||
// Custom call tool logic | ||
return await next(context, cancellationToken); | ||
}); | ||
halter73 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var app = builder.Build(); | ||
|
||
app.MapMcp(); | ||
app.Run(); | ||
``` | ||
|
||
### Custom Authorization Filters | ||
|
||
You can also create custom authorization filters using the filter methods: | ||
|
||
```csharp | ||
.AddCallToolFilter(next => async (context, cancellationToken) => | ||
{ | ||
// Custom authorization logic | ||
if (context.User?.Identity?.IsAuthenticated != true) | ||
{ | ||
return new CallToolResult | ||
{ | ||
Content = [new TextContent { Text = "Custom: Authentication required" }], | ||
IsError = true | ||
}; | ||
} | ||
|
||
return await next(context, cancellationToken); | ||
}); | ||
``` | ||
|
||
### RequestContext | ||
|
||
Within filters, you have access to: | ||
|
||
- `context.User` - The current user's `ClaimsPrincipal` | ||
- `context.Services` - The request's service provider for resolving authorization services | ||
- `context.MatchedPrimitive` - The matched tool/prompt/resource with its metadata including authorization attributes via `context.MatchedPrimitive.Metadata` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,7 @@ items: | |
items: | ||
- name: Logging | ||
uid: logging | ||
- name: Server Features | ||
items: | ||
- name: Filters | ||
uid: filters |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.