Split configuration of request and message filters (#1308)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Stephen Halter 2026-02-19 17:42:35 -08:00 • committed by GitHub
commit d21aa9e5a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,22 +1,22 @@
---
title: Filters
author: halter73
description: MCP Server Handler Filters
description: MCP Server Filters
uid: filters
---
# MCP Server Handler Filters
# MCP Server Filters
The MCP Server provides two levels of filters for intercepting and modifying request processing:
1. **Message Filters** - Low-level filters (`AddIncomingMessageFilter`, `AddOutgoingMessageFilter`) that intercept all JSON-RPC messages before routing
2. **Request-Specific Filters** - Handler-level filters (e.g., `AddListToolsFilter`, `AddCallToolFilter`) that target specific MCP operations
1. **Message Filters** - Low-level filters (`AddIncomingFilter`, `AddOutgoingFilter`) configured via `WithMessageFilters(...)` that intercept all JSON-RPC messages before routing.
2. **Request-Specific Filters** - Handler-level filters (e.g., `AddListToolsFilter`, `AddCallToolFilter`) configured via `WithRequestFilters(...)` that target specific MCP operations.
The filters are stored in `McpServerOptions.Filters` and applied during server configuration.
The filters are stored in `McpServerOptions.Filters`.
## Available Request-Specific Filter Methods
The following filter methods are available:
The following request filter methods are available on `IMcpRequestFilterBuilder` inside `WithRequestFilters(...)`:
- `AddListResourceTemplatesFilter` - Filter for list resource templates handlers
- `AddListToolsFilter` - Filter for list tools handlers
@ -32,10 +32,11 @@ The following filter methods are available:
## Message Filters
In addition to the request-specific filters above, there are low-level message filters that intercept all JSON-RPC messages before they are routed to specific handlers:
In addition to the request-specific filters above, there are low-level message filters that intercept all JSON-RPC messages before they are routed to specific handlers.
Configure these on `IMcpMessageFilterBuilder` inside `WithMessageFilters(...)`:
- `AddIncomingMessageFilter` - Filter for all incoming JSON-RPC messages (requests and notifications)
- `AddOutgoingMessageFilter` - Filter for all outgoing JSON-RPC messages (responses and notifications)
- `AddIncomingFilter` - Filter for all incoming JSON-RPC messages (requests and notifications)
- `AddOutgoingFilter` - Filter for all outgoing JSON-RPC messages (responses and notifications)
### When to Use Message Filters
@ -49,22 +50,25 @@ Message filters operate at a lower level than request-specific filters and are u
### Incoming Message Filter
`AddIncomingMessageFilter` intercepts all incoming JSON-RPC messages before they are dispatched to request-specific handlers:
`AddIncomingFilter` intercepts all incoming JSON-RPC messages before they are dispatched to request-specific handlers:
```csharp
services.AddMcpServer()
.AddIncomingMessageFilter(next => async (context, cancellationToken) =>
.WithMessageFilters(messageFilters =>
{
var logger = context.Services?.GetService<ILogger<Program>>();
// Access the raw JSON-RPC message
if (context.JsonRpcMessage is JsonRpcRequest request)
messageFilters.AddIncomingFilter(next => async (context, cancellationToken) =>
{
logger?.LogInformation($"Incoming request: {request.Method}");
}
var logger = context.Services?.GetService<ILogger<Program>>();
// Call next to continue processing
await next(context, cancellationToken);
// Access the raw JSON-RPC message
if (context.JsonRpcMessage is JsonRpcRequest request)
{
logger?.LogInformation($"Incoming request: {request.Method}");
}
// Call next to continue processing
await next(context, cancellationToken);
});
})
.WithTools<MyTools>();
```
@ -83,46 +87,52 @@ Inside an incoming message filter, you have access to:
You can skip the default handler by not calling `next`. This is useful for implementing custom protocol methods:
```csharp
.AddIncomingMessageFilter(next => async (context, cancellationToken) =>
.WithMessageFilters(messageFilters =>
{
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == "custom/myMethod")
messageFilters.AddIncomingFilter(next => async (context, cancellationToken) =>
{
// Handle the custom method directly
var response = new JsonRpcResponse
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == "custom/myMethod")
{
Id = request.Id,
Result = JsonSerializer.SerializeToNode(new { message = "Custom response" })
};
await context.Server.SendMessageAsync(response, cancellationToken);
return; // Don't call next - we handled it
}
// Handle the custom method directly
var response = new JsonRpcResponse
{
Id = request.Id,
Result = JsonSerializer.SerializeToNode(new { message = "Custom response" })
};
await context.Server.SendMessageAsync(response, cancellationToken);
return; // Don't call next - we handled it
}
await next(context, cancellationToken);
await next(context, cancellationToken);
});
})
```
### Outgoing Message Filter
`AddOutgoingMessageFilter` intercepts all outgoing JSON-RPC messages before they are sent to the client:
`AddOutgoingFilter` intercepts all outgoing JSON-RPC messages before they are sent to the client:
```csharp
services.AddMcpServer()
.AddOutgoingMessageFilter(next => async (context, cancellationToken) =>
.WithMessageFilters(messageFilters =>
{
var logger = context.Services?.GetService<ILogger<Program>>();
// Inspect outgoing messages
switch (context.JsonRpcMessage)
messageFilters.AddOutgoingFilter(next => async (context, cancellationToken) =>
{
case JsonRpcResponse response:
logger?.LogInformation($"Sending response for request {response.Id}");
break;
case JsonRpcNotification notification:
logger?.LogInformation($"Sending notification: {notification.Method}");
break;
}
var logger = context.Services?.GetService<ILogger<Program>>();
await next(context, cancellationToken);
// Inspect outgoing messages
switch (context.JsonRpcMessage)
{
case JsonRpcResponse response:
logger?.LogInformation($"Sending response for request {response.Id}");
break;
case JsonRpcNotification notification:
logger?.LogInformation($"Sending notification: {notification.Method}");
break;
}
await next(context, cancellationToken);
});
})
.WithTools<MyTools>();
```
@ -132,16 +142,19 @@ services.AddMcpServer()
You can suppress outgoing messages by not calling `next`:
```csharp
.AddOutgoingMessageFilter(next => async (context, cancellationToken) =>
.WithMessageFilters(messageFilters =>
{
// Suppress specific notifications
if (context.JsonRpcMessage is JsonRpcNotification notification &&
notification.Method == "notifications/progress")
messageFilters.AddOutgoingFilter(next => async (context, cancellationToken) =>
{
return; // Don't send this notification
}
// Suppress specific notifications
if (context.JsonRpcMessage is JsonRpcNotification notification &&
notification.Method == "notifications/progress")
{
return; // Don't send this notification
}
await next(context, cancellationToken);
await next(context, cancellationToken);
});
})
```
@ -150,26 +163,29 @@ You can suppress outgoing messages by not calling `next`:
Outgoing message filters can send additional messages by calling `next` with a new `MessageContext`:
```csharp
.AddOutgoingMessageFilter(next => async (context, cancellationToken) =>
.WithMessageFilters(messageFilters =>
{
// Send an extra notification before certain responses
if (context.JsonRpcMessage is JsonRpcResponse response &&
response.Result is JsonObject result &&
result.ContainsKey("tools"))
messageFilters.AddOutgoingFilter(next => async (context, cancellationToken) =>
{
var notification = new JsonRpcNotification
// Send an extra notification before certain responses
if (context.JsonRpcMessage is JsonRpcResponse response &&
response.Result is JsonObject result &&
result.ContainsKey("tools"))
{
Method = "custom/toolsListed",
Params = new JsonObject { ["timestamp"] = DateTime.UtcNow.ToString("O") },
Context = new JsonRpcMessageContext
var notification = new JsonRpcNotification
{
RelatedTransport = context.JsonRpcMessage.Context?.RelatedTransport
}
};
await next(new MessageContext(context.Server, notification), cancellationToken);
}
Method = "custom/toolsListed",
Params = new JsonObject { ["timestamp"] = DateTime.UtcNow.ToString("O") },
Context = new JsonRpcMessageContext
{
RelatedTransport = context.JsonRpcMessage.Context?.RelatedTransport
}
};
await next(new MessageContext(context.Server, notification), cancellationToken);
}
await next(context, cancellationToken);
await next(context, cancellationToken);
});
})
```
@ -179,11 +195,17 @@ Message filters execute in registration order, with the first registered filter
```csharp
services.AddMcpServer()
.AddIncomingMessageFilter(incomingFilter1) // Incoming: executes first (outermost)
.AddIncomingMessageFilter(incomingFilter2) // Incoming: executes second
.AddOutgoingMessageFilter(outgoingFilter1) // Outgoing: executes first (outermost)
.AddOutgoingMessageFilter(outgoingFilter2) // Outgoing: executes second
.AddListToolsFilter(toolsFilter) // Request-specific filter
.WithMessageFilters(messageFilters =>
{
messageFilters.AddIncomingFilter(incomingFilter1); // Incoming: executes first (outermost)
messageFilters.AddIncomingFilter(incomingFilter2); // Incoming: executes second
messageFilters.AddOutgoingFilter(outgoingFilter1); // Outgoing: executes first (outermost)
messageFilters.AddOutgoingFilter(outgoingFilter2); // Outgoing: executes second
})
.WithRequestFilters(requestFilters =>
{
requestFilters.AddListToolsFilter(toolsFilter); // Request-specific filter
})
.WithTools<MyTools>();
```
@ -218,21 +240,25 @@ OutgoingFilter1 (after next)
The `Items` dictionary allows you to pass data between filters processing the same message:
```csharp
.AddIncomingMessageFilter(next => async (context, cancellationToken) =>
.WithMessageFilters(messageFilters =>
{
context.Items["requestStartTime"] = DateTime.UtcNow;
await next(context, cancellationToken);
})
.AddIncomingMessageFilter(next => async (context, cancellationToken) =>
{
await next(context, cancellationToken);
if (context.Items.TryGetValue("requestStartTime", out var startTime))
messageFilters.AddIncomingFilter(next => async (context, cancellationToken) =>
{
var elapsed = DateTime.UtcNow - (DateTime)startTime;
var logger = context.Services?.GetService<ILogger<Program>>();
logger?.LogInformation($"Request processed in {elapsed.TotalMilliseconds}ms");
}
context.Items["requestStartTime"] = DateTime.UtcNow;
await next(context, cancellationToken);
});
messageFilters.AddIncomingFilter(next => async (context, cancellationToken) =>
{
await next(context, cancellationToken);
if (context.Items.TryGetValue("requestStartTime", out var startTime))
{
var elapsed = DateTime.UtcNow - (DateTime)startTime;
var logger = context.Services?.GetService<ILogger<Program>>();
logger?.LogInformation($"Request processed in {elapsed.TotalMilliseconds}ms");
}
});
})
```
@ -247,18 +273,21 @@ services.AddMcpServer()
// Your base handler logic
return new ListToolsResult { Tools = GetTools() };
})
.AddListToolsFilter(next => async (context, cancellationToken) =>
.WithRequestFilters(requestFilters =>
{
var logger = context.Services?.GetService<ILogger<Program>>();
requestFilters.AddListToolsFilter(next => async (context, cancellationToken) =>
{
var logger = context.Services?.GetService<ILogger<Program>>();
// Pre-processing logic
logger?.LogInformation("Before handler execution");
// Pre-processing logic
logger?.LogInformation("Before handler execution");
var result = await next(context, cancellationToken);
var result = await next(context, cancellationToken);
// Post-processing logic
logger?.LogInformation("After handler execution");
return result;
// Post-processing logic
logger?.LogInformation("After handler execution");
return result;
});
});
```
@ -267,9 +296,12 @@ services.AddMcpServer()
```csharp
services.AddMcpServer()
.WithListToolsHandler(baseHandler)
.AddListToolsFilter(filter1) // Executes first (outermost)
.AddListToolsFilter(filter2) // Executes second
.AddListToolsFilter(filter3); // Executes third (closest to handler)
.WithRequestFilters(requestFilters =>
{
requestFilters.AddListToolsFilter(filter1); // Executes first (outermost)
requestFilters.AddListToolsFilter(filter2); // Executes second
requestFilters.AddListToolsFilter(filter3); // Executes third (closest to handler)
});
```
Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filter2 -> filter1`
@ -279,76 +311,91 @@ Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filt
### Logging
```csharp
.AddListToolsFilter(next => async (context, cancellationToken) =>
.WithRequestFilters(requestFilters =>
{
var logger = context.Services?.GetService<ILogger<Program>>();
requestFilters.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;
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) =>
.WithRequestFilters(requestFilters =>
{
try
requestFilters.AddCallToolFilter(next => async (context, cancellationToken) =>
{
return await next(context, cancellationToken);
}
catch (Exception ex)
{
return new CallToolResult
try
{
Content = new[] { new TextContent { Type = "text", Text = $"Error: {ex.Message}" } },
IsError = true
};
}
return await next(context, cancellationToken);
}
catch (Exception ex)
{
var logger = context.Services?.GetService<ILogger<Program>>();
logger?.LogError(ex, "Error while processing CallTool request for {ProgressToken}", context.Meta.ProgressToken);
return new CallToolResult
{
Content = new[] { new TextContent { Type = "text", Text = "An unexpected error occurred while processing the tool call." } },
IsError = true
};
}
});
});
```
### Performance Monitoring
```csharp
.AddListToolsFilter(next => async (context, cancellationToken) =>
.WithRequestFilters(requestFilters =>
{
var logger = context.Services?.GetService<ILogger<Program>>();
requestFilters.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;
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) =>
.WithRequestFilters(requestFilters =>
{
var cache = context.Services!.GetRequiredService<IMemoryCache>();
var cacheKey = $"resources:{context.Params.Cursor}";
if (cache.TryGetValue(cacheKey, out var cached))
requestFilters.AddListResourcesFilter(next => async (context, cancellationToken) =>
{
return (ListResourcesResult)cached;
}
var cache = context.Services!.GetRequiredService<IMemoryCache>();
var result = await next(context, cancellationToken);
cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
return result;
var cacheKey = $"resources:{context.Params.Cursor}";
if (cache.TryGetValue(cacheKey, out var cached))
{
return (ListResourcesResult)cached;
}
var result = await next(context, cancellationToken);
cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
return result;
});
});
```
## Built-in Authorization Filters
## Built-in Authorization Request 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
### Enabling Authorization Request Filters
To enable authorization support, call `AddAuthorizationFilters()` when configuring your MCP server:
@ -455,25 +502,31 @@ This allows you to implement logging, metrics, or other cross-cutting concerns t
```csharp
services.AddMcpServer()
.WithHttpTransport()
.AddListToolsFilter(next => async (context, cancellationToken) =>
.WithRequestFilters(requestFilters =>
{
var logger = context.Services?.GetService<ILogger<Program>>();
requestFilters.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;
// 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
.AddListToolsFilter(next => async (context, cancellationToken) =>
.WithRequestFilters(requestFilters =>
{
var logger = context.Services?.GetService<ILogger<Program>>();
requestFilters.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;
// 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>();
```
@ -494,10 +547,13 @@ builder.Services.AddMcpServer()
.WithHttpTransport()
.AddAuthorizationFilters() // Required for authorization support
.WithTools<WeatherTools>()
.AddCallToolFilter(next => async (context, cancellationToken) =>
.WithRequestFilters(requestFilters =>
{
// Custom call tool logic
return await next(context, cancellationToken);
requestFilters.AddCallToolFilter(next => async (context, cancellationToken) =>
{
// Custom call tool logic
return await next(context, cancellationToken);
});
});
var app = builder.Build();
@ -511,19 +567,22 @@ app.Run();
You can also create custom authorization filters using the filter methods:
```csharp
.AddCallToolFilter(next => async (context, cancellationToken) =>
.WithRequestFilters(requestFilters =>
{
// Custom authorization logic
if (context.User?.Identity?.IsAuthenticated != true)
requestFilters.AddCallToolFilter(next => async (context, cancellationToken) =>
{
return new CallToolResult
// Custom authorization logic
if (context.User?.Identity?.IsAuthenticated != true)
{
Content = [new TextContent { Text = "Custom: Authentication required" }],
IsError = true
};
}
return new CallToolResult
{
Content = [new TextContent { Text = "Custom: Authentication required" }],
IsError = true
};
}
return await next(context, cancellationToken);
return await next(context, cancellationToken);
});
});
```