Skip to content

Commit d344c65

Browse files
authored
Add middleware and authz support for server-side handlers (#733)
1 parent 0f0aad0 commit d344c65

File tree

59 files changed

+2871
-435
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2871
-435
lines changed

docs/concepts/filters.md

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
---
2+
title: Filters
3+
author: halter73
4+
description: MCP Server Handler Filters
5+
uid: filters
6+
---
7+
8+
# MCP Server Handler Filters
9+
10+
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.
11+
12+
## Available Filter Methods
13+
14+
The following filter methods are available:
15+
16+
- `AddListResourceTemplatesFilter` - Filter for list resource templates handlers
17+
- `AddListToolsFilter` - Filter for list tools handlers
18+
- `AddCallToolFilter` - Filter for call tool handlers
19+
- `AddListPromptsFilter` - Filter for list prompts handlers
20+
- `AddGetPromptFilter` - Filter for get prompt handlers
21+
- `AddListResourcesFilter` - Filter for list resources handlers
22+
- `AddReadResourceFilter` - Filter for read resource handlers
23+
- `AddCompleteFilter` - Filter for completion handlers
24+
- `AddSubscribeToResourcesFilter` - Filter for resource subscription handlers
25+
- `AddUnsubscribeFromResourcesFilter` - Filter for resource unsubscription handlers
26+
- `AddSetLoggingLevelFilter` - Filter for logging level handlers
27+
28+
## Usage
29+
30+
Filters are functions that take a handler and return a new handler, allowing you to wrap the original handler with additional functionality:
31+
32+
```csharp
33+
services.AddMcpServer()
34+
.WithListToolsHandler(async (context, cancellationToken) =>
35+
{
36+
// Your base handler logic
37+
return new ListToolsResult { Tools = GetTools() };
38+
})
39+
.AddListToolsFilter(next => async (context, cancellationToken) =>
40+
{
41+
var logger = context.Services?.GetService<ILogger<Program>>();
42+
43+
// Pre-processing logic
44+
logger?.LogInformation("Before handler execution");
45+
46+
var result = await next(context, cancellationToken);
47+
48+
// Post-processing logic
49+
logger?.LogInformation("After handler execution");
50+
return result;
51+
});
52+
```
53+
54+
## Filter Execution Order
55+
56+
```csharp
57+
services.AddMcpServer()
58+
.WithListToolsHandler(baseHandler)
59+
.AddListToolsFilter(filter1) // Executes first (outermost)
60+
.AddListToolsFilter(filter2) // Executes second
61+
.AddListToolsFilter(filter3); // Executes third (closest to handler)
62+
```
63+
64+
Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filter2 -> filter1`
65+
66+
## Common Use Cases
67+
68+
### Logging
69+
```csharp
70+
.AddListToolsFilter(next => async (context, cancellationToken) =>
71+
{
72+
var logger = context.Services?.GetService<ILogger<Program>>();
73+
74+
logger?.LogInformation($"Processing request from {context.Meta.ProgressToken}");
75+
var result = await next(context, cancellationToken);
76+
logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools");
77+
return result;
78+
});
79+
```
80+
81+
### Error Handling
82+
```csharp
83+
.AddCallToolFilter(next => async (context, cancellationToken) =>
84+
{
85+
try
86+
{
87+
return await next(context, cancellationToken);
88+
}
89+
catch (Exception ex)
90+
{
91+
return new CallToolResult
92+
{
93+
Content = new[] { new TextContent { Type = "text", Text = $"Error: {ex.Message}" } },
94+
IsError = true
95+
};
96+
}
97+
});
98+
```
99+
100+
### Performance Monitoring
101+
```csharp
102+
.AddListToolsFilter(next => async (context, cancellationToken) =>
103+
{
104+
var logger = context.Services?.GetService<ILogger<Program>>();
105+
106+
var stopwatch = Stopwatch.StartNew();
107+
var result = await next(context, cancellationToken);
108+
stopwatch.Stop();
109+
logger?.LogInformation($"Handler took {stopwatch.ElapsedMilliseconds}ms");
110+
return result;
111+
});
112+
```
113+
114+
### Caching
115+
```csharp
116+
.AddListResourcesFilter(next => async (context, cancellationToken) =>
117+
{
118+
var cache = context.Services!.GetRequiredService<IMemoryCache>();
119+
120+
var cacheKey = $"resources:{context.Params.Cursor}";
121+
if (cache.TryGetValue(cacheKey, out var cached))
122+
{
123+
return (ListResourcesResult)cached;
124+
}
125+
126+
var result = await next(context, cancellationToken);
127+
cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
128+
return result;
129+
});
130+
```
131+
132+
## Built-in Authorization Filters
133+
134+
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.
135+
136+
### Enabling Authorization Filters
137+
138+
To enable authorization support, call `AddAuthorizationFilters()` when configuring your MCP server:
139+
140+
```csharp
141+
services.AddMcpServer()
142+
.WithHttpTransport()
143+
.AddAuthorizationFilters() // Enable authorization filter support
144+
.WithTools<WeatherTools>();
145+
```
146+
147+
**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.
148+
149+
### Authorization Attributes Support
150+
151+
The MCP server automatically respects the following authorization attributes:
152+
153+
- **`[Authorize]`** - Requires authentication for access
154+
- **`[Authorize(Roles = "RoleName")]`** - Requires specific roles
155+
- **`[Authorize(Policy = "PolicyName")]`** - Requires specific authorization policies
156+
- **`[AllowAnonymous]`** - Explicitly allows anonymous access (overrides `[Authorize]`)
157+
158+
### Tool Authorization
159+
160+
Tools can be decorated with authorization attributes to control access:
161+
162+
```csharp
163+
[McpServerToolType]
164+
public class WeatherTools
165+
{
166+
[McpServerTool, Description("Gets public weather data")]
167+
public static string GetWeather(string location)
168+
{
169+
return $"Weather for {location}: Sunny, 25°C";
170+
}
171+
172+
[McpServerTool, Description("Gets detailed weather forecast")]
173+
[Authorize] // Requires authentication
174+
public static string GetDetailedForecast(string location)
175+
{
176+
return $"Detailed forecast for {location}: ...";
177+
}
178+
179+
[McpServerTool, Description("Manages weather alerts")]
180+
[Authorize(Roles = "Admin")] // Requires Admin role
181+
public static string ManageWeatherAlerts(string alertType)
182+
{
183+
return $"Managing alert: {alertType}";
184+
}
185+
}
186+
```
187+
188+
### Class-Level Authorization
189+
190+
You can apply authorization at the class level, which affects all tools in the class:
191+
192+
```csharp
193+
[McpServerToolType]
194+
[Authorize] // All tools require authentication
195+
public class RestrictedTools
196+
{
197+
[McpServerTool, Description("Restricted tool accessible to authenticated users")]
198+
public static string RestrictedOperation()
199+
{
200+
return "Restricted operation completed";
201+
}
202+
203+
[McpServerTool, Description("Public tool accessible to anonymous users")]
204+
[AllowAnonymous] // Overrides class-level [Authorize]
205+
public static string PublicOperation()
206+
{
207+
return "Public operation completed";
208+
}
209+
}
210+
```
211+
212+
### How Authorization Filters Work
213+
214+
The authorization filters work differently for list operations versus individual operations:
215+
216+
#### List Operations (ListTools, ListPrompts, ListResources)
217+
For list operations, the filters automatically remove unauthorized items from the results. Users only see tools, prompts, or resources they have permission to access.
218+
219+
#### Individual Operations (CallTool, GetPrompt, ReadResource)
220+
For individual operations, the filters throw an `McpException` with "Access forbidden" message. These get turned into JSON-RPC errors if uncaught by middleware.
221+
222+
### Filter Execution Order and Authorization
223+
224+
Authorization filters are applied automatically when you call `AddAuthorizationFilters()`. These filters run at a specific point in the filter pipeline, which means:
225+
226+
**Filters added before authorization filters** can see:
227+
- Unauthorized requests for operations before they are rejected by the authorization filters
228+
- Complete listings for unauthorized primitives before they are filtered out by the authorization filters
229+
230+
**Filters added after authorization filters** will only see:
231+
- Authorized requests that passed authorization checks
232+
- Filtered listings containing only authorized primitives
233+
234+
This allows you to implement logging, metrics, or other cross-cutting concerns that need to see all requests, while still maintaining proper authorization:
235+
236+
```csharp
237+
services.AddMcpServer()
238+
.WithHttpTransport()
239+
.AddListToolsFilter(next => async (context, cancellationToken) =>
240+
{
241+
var logger = context.Services?.GetService<ILogger<Program>>();
242+
243+
// This filter runs BEFORE authorization - sees all tools
244+
logger?.LogInformation("Request for tools list - will see all tools");
245+
var result = await next(context, cancellationToken);
246+
logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools after authorization");
247+
return result;
248+
})
249+
.AddAuthorizationFilters() // Authorization filtering happens here
250+
.AddListToolsFilter(next => async (context, cancellationToken) =>
251+
{
252+
var logger = context.Services?.GetService<ILogger<Program>>();
253+
254+
// This filter runs AFTER authorization - only sees authorized tools
255+
var result = await next(context, cancellationToken);
256+
logger?.LogInformation($"Post-auth filter sees {result.Tools?.Count ?? 0} authorized tools");
257+
return result;
258+
})
259+
.WithTools<WeatherTools>();
260+
```
261+
262+
### Setup Requirements
263+
264+
To use authorization features, you must configure authentication and authorization in your ASP.NET Core application and call `AddAuthorizationFilters()`:
265+
266+
```csharp
267+
var builder = WebApplication.CreateBuilder(args);
268+
269+
builder.Services.AddAuthentication("Bearer")
270+
.AddJwtBearer(options => { /* JWT configuration */ })
271+
.AddMcp(options => { /* Resource metadata configuration */ });
272+
builder.Services.AddAuthorization();
273+
274+
builder.Services.AddMcpServer()
275+
.WithHttpTransport()
276+
.AddAuthorizationFilters() // Required for authorization support
277+
.WithTools<WeatherTools>()
278+
.AddCallToolFilter(next => async (context, cancellationToken) =>
279+
{
280+
// Custom call tool logic
281+
return await next(context, cancellationToken);
282+
});
283+
284+
var app = builder.Build();
285+
286+
app.MapMcp();
287+
app.Run();
288+
```
289+
290+
### Custom Authorization Filters
291+
292+
You can also create custom authorization filters using the filter methods:
293+
294+
```csharp
295+
.AddCallToolFilter(next => async (context, cancellationToken) =>
296+
{
297+
// Custom authorization logic
298+
if (context.User?.Identity?.IsAuthenticated != true)
299+
{
300+
return new CallToolResult
301+
{
302+
Content = [new TextContent { Text = "Custom: Authentication required" }],
303+
IsError = true
304+
};
305+
}
306+
307+
return await next(context, cancellationToken);
308+
});
309+
```
310+
311+
### RequestContext
312+
313+
Within filters, you have access to:
314+
315+
- `context.User` - The current user's `ClaimsPrincipal`
316+
- `context.Services` - The request's service provider for resolving authorization services
317+
- `context.MatchedPrimitive` - The matched tool/prompt/resource with its metadata including authorization attributes via `context.MatchedPrimitive.Metadata`

docs/concepts/toc.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ items:
1313
items:
1414
- name: Logging
1515
uid: logging
16+
- name: Server Features
17+
items:
18+
- name: Filters
19+
uid: filters

samples/AspNetCoreMcpServer/Properties/launchSettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"applicationUrl": "http://localhost:3001",
88
"environmentVariables": {
99
"ASPNETCORE_ENVIRONMENT": "Development",
10-
"OTEL_SERVICE_NAME": "aspnetcore-mcp-server",
10+
"OTEL_SERVICE_NAME": "aspnetcore-mcp-server"
1111
}
1212
},
1313
"https": {
@@ -16,7 +16,7 @@
1616
"applicationUrl": "https://localhost:7133;http://localhost:3001",
1717
"environmentVariables": {
1818
"ASPNETCORE_ENVIRONMENT": "Development",
19-
"OTEL_SERVICE_NAME": "aspnetcore-mcp-server",
19+
"OTEL_SERVICE_NAME": "aspnetcore-mcp-server"
2020
}
2121
}
2222
}

0 commit comments

Comments
 (0)