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
21 changed files with 1288 additions and 1078 deletions

View file

@ -1,22 +1,22 @@
--- ---
title: Filters title: Filters
author: halter73 author: halter73
description: MCP Server Handler Filters description: MCP Server Filters
uid: filters uid: filters
--- ---
# MCP Server Handler Filters # MCP Server Filters
The MCP Server provides two levels of filters for intercepting and modifying request processing: 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 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`) that target specific MCP operations 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 ## 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 - `AddListResourceTemplatesFilter` - Filter for list resource templates handlers
- `AddListToolsFilter` - Filter for list tools handlers - `AddListToolsFilter` - Filter for list tools handlers
@ -32,10 +32,11 @@ The following filter methods are available:
## Message Filters ## 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) - `AddIncomingFilter` - Filter for all incoming JSON-RPC messages (requests and notifications)
- `AddOutgoingMessageFilter` - Filter for all outgoing JSON-RPC messages (responses and notifications) - `AddOutgoingFilter` - Filter for all outgoing JSON-RPC messages (responses and notifications)
### When to Use Message Filters ### When to Use Message Filters
@ -49,11 +50,13 @@ Message filters operate at a lower level than request-specific filters and are u
### Incoming Message Filter ### 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 ```csharp
services.AddMcpServer() services.AddMcpServer()
.AddIncomingMessageFilter(next => async (context, cancellationToken) => .WithMessageFilters(messageFilters =>
{
messageFilters.AddIncomingFilter(next => async (context, cancellationToken) =>
{ {
var logger = context.Services?.GetService<ILogger<Program>>(); var logger = context.Services?.GetService<ILogger<Program>>();
@ -65,6 +68,7 @@ services.AddMcpServer()
// Call next to continue processing // Call next to continue processing
await next(context, cancellationToken); await next(context, cancellationToken);
});
}) })
.WithTools<MyTools>(); .WithTools<MyTools>();
``` ```
@ -83,8 +87,10 @@ 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: You can skip the default handler by not calling `next`. This is useful for implementing custom protocol methods:
```csharp ```csharp
.AddIncomingMessageFilter(next => async (context, cancellationToken) => .WithMessageFilters(messageFilters =>
{ {
messageFilters.AddIncomingFilter(next => async (context, cancellationToken) =>
{
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == "custom/myMethod") if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == "custom/myMethod")
{ {
// Handle the custom method directly // Handle the custom method directly
@ -98,16 +104,19 @@ You can skip the default handler by not calling `next`. This is useful for imple
} }
await next(context, cancellationToken); await next(context, cancellationToken);
});
}) })
``` ```
### Outgoing Message Filter ### 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 ```csharp
services.AddMcpServer() services.AddMcpServer()
.AddOutgoingMessageFilter(next => async (context, cancellationToken) => .WithMessageFilters(messageFilters =>
{
messageFilters.AddOutgoingFilter(next => async (context, cancellationToken) =>
{ {
var logger = context.Services?.GetService<ILogger<Program>>(); var logger = context.Services?.GetService<ILogger<Program>>();
@ -123,6 +132,7 @@ services.AddMcpServer()
} }
await next(context, cancellationToken); await next(context, cancellationToken);
});
}) })
.WithTools<MyTools>(); .WithTools<MyTools>();
``` ```
@ -132,8 +142,10 @@ services.AddMcpServer()
You can suppress outgoing messages by not calling `next`: You can suppress outgoing messages by not calling `next`:
```csharp ```csharp
.AddOutgoingMessageFilter(next => async (context, cancellationToken) => .WithMessageFilters(messageFilters =>
{ {
messageFilters.AddOutgoingFilter(next => async (context, cancellationToken) =>
{
// Suppress specific notifications // Suppress specific notifications
if (context.JsonRpcMessage is JsonRpcNotification notification && if (context.JsonRpcMessage is JsonRpcNotification notification &&
notification.Method == "notifications/progress") notification.Method == "notifications/progress")
@ -142,6 +154,7 @@ You can suppress outgoing messages by not calling `next`:
} }
await next(context, cancellationToken); await next(context, cancellationToken);
});
}) })
``` ```
@ -150,8 +163,10 @@ You can suppress outgoing messages by not calling `next`:
Outgoing message filters can send additional messages by calling `next` with a new `MessageContext`: Outgoing message filters can send additional messages by calling `next` with a new `MessageContext`:
```csharp ```csharp
.AddOutgoingMessageFilter(next => async (context, cancellationToken) => .WithMessageFilters(messageFilters =>
{ {
messageFilters.AddOutgoingFilter(next => async (context, cancellationToken) =>
{
// Send an extra notification before certain responses // Send an extra notification before certain responses
if (context.JsonRpcMessage is JsonRpcResponse response && if (context.JsonRpcMessage is JsonRpcResponse response &&
response.Result is JsonObject result && response.Result is JsonObject result &&
@ -170,6 +185,7 @@ Outgoing message filters can send additional messages by calling `next` with a n
} }
await next(context, cancellationToken); await next(context, cancellationToken);
});
}) })
``` ```
@ -179,11 +195,17 @@ Message filters execute in registration order, with the first registered filter
```csharp ```csharp
services.AddMcpServer() services.AddMcpServer()
.AddIncomingMessageFilter(incomingFilter1) // Incoming: executes first (outermost) .WithMessageFilters(messageFilters =>
.AddIncomingMessageFilter(incomingFilter2) // Incoming: executes second {
.AddOutgoingMessageFilter(outgoingFilter1) // Outgoing: executes first (outermost) messageFilters.AddIncomingFilter(incomingFilter1); // Incoming: executes first (outermost)
.AddOutgoingMessageFilter(outgoingFilter2) // Outgoing: executes second messageFilters.AddIncomingFilter(incomingFilter2); // Incoming: executes second
.AddListToolsFilter(toolsFilter) // Request-specific filter messageFilters.AddOutgoingFilter(outgoingFilter1); // Outgoing: executes first (outermost)
messageFilters.AddOutgoingFilter(outgoingFilter2); // Outgoing: executes second
})
.WithRequestFilters(requestFilters =>
{
requestFilters.AddListToolsFilter(toolsFilter); // Request-specific filter
})
.WithTools<MyTools>(); .WithTools<MyTools>();
``` ```
@ -218,13 +240,16 @@ OutgoingFilter1 (after next)
The `Items` dictionary allows you to pass data between filters processing the same message: The `Items` dictionary allows you to pass data between filters processing the same message:
```csharp ```csharp
.AddIncomingMessageFilter(next => async (context, cancellationToken) => .WithMessageFilters(messageFilters =>
{ {
messageFilters.AddIncomingFilter(next => async (context, cancellationToken) =>
{
context.Items["requestStartTime"] = DateTime.UtcNow; context.Items["requestStartTime"] = DateTime.UtcNow;
await next(context, cancellationToken); await next(context, cancellationToken);
}) });
.AddIncomingMessageFilter(next => async (context, cancellationToken) =>
{ messageFilters.AddIncomingFilter(next => async (context, cancellationToken) =>
{
await next(context, cancellationToken); await next(context, cancellationToken);
if (context.Items.TryGetValue("requestStartTime", out var startTime)) if (context.Items.TryGetValue("requestStartTime", out var startTime))
@ -233,6 +258,7 @@ The `Items` dictionary allows you to pass data between filters processing the sa
var logger = context.Services?.GetService<ILogger<Program>>(); var logger = context.Services?.GetService<ILogger<Program>>();
logger?.LogInformation($"Request processed in {elapsed.TotalMilliseconds}ms"); logger?.LogInformation($"Request processed in {elapsed.TotalMilliseconds}ms");
} }
});
}) })
``` ```
@ -247,7 +273,9 @@ services.AddMcpServer()
// Your base handler logic // Your base handler logic
return new ListToolsResult { Tools = GetTools() }; return new ListToolsResult { Tools = GetTools() };
}) })
.AddListToolsFilter(next => async (context, cancellationToken) => .WithRequestFilters(requestFilters =>
{
requestFilters.AddListToolsFilter(next => async (context, cancellationToken) =>
{ {
var logger = context.Services?.GetService<ILogger<Program>>(); var logger = context.Services?.GetService<ILogger<Program>>();
@ -260,6 +288,7 @@ services.AddMcpServer()
logger?.LogInformation("After handler execution"); logger?.LogInformation("After handler execution");
return result; return result;
}); });
});
``` ```
## Filter Execution Order ## Filter Execution Order
@ -267,9 +296,12 @@ services.AddMcpServer()
```csharp ```csharp
services.AddMcpServer() services.AddMcpServer()
.WithListToolsHandler(baseHandler) .WithListToolsHandler(baseHandler)
.AddListToolsFilter(filter1) // Executes first (outermost) .WithRequestFilters(requestFilters =>
.AddListToolsFilter(filter2) // Executes second {
.AddListToolsFilter(filter3); // Executes third (closest to handler) 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` Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filter2 -> filter1`
@ -279,42 +311,53 @@ Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filt
### Logging ### Logging
```csharp ```csharp
.AddListToolsFilter(next => async (context, cancellationToken) => .WithRequestFilters(requestFilters =>
{ {
requestFilters.AddListToolsFilter(next => async (context, cancellationToken) =>
{
var logger = context.Services?.GetService<ILogger<Program>>(); var logger = context.Services?.GetService<ILogger<Program>>();
logger?.LogInformation($"Processing request from {context.Meta.ProgressToken}"); logger?.LogInformation($"Processing request from {context.Meta.ProgressToken}");
var result = await next(context, cancellationToken); var result = await next(context, cancellationToken);
logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools"); logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools");
return result; return result;
});
}); });
``` ```
### Error Handling ### Error Handling
```csharp ```csharp
.AddCallToolFilter(next => async (context, cancellationToken) => .WithRequestFilters(requestFilters =>
{ {
requestFilters.AddCallToolFilter(next => async (context, cancellationToken) =>
{
try try
{ {
return await next(context, cancellationToken); return await next(context, cancellationToken);
} }
catch (Exception ex) 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 return new CallToolResult
{ {
Content = new[] { new TextContent { Type = "text", Text = $"Error: {ex.Message}" } }, Content = new[] { new TextContent { Type = "text", Text = "An unexpected error occurred while processing the tool call." } },
IsError = true IsError = true
}; };
} }
});
}); });
``` ```
### Performance Monitoring ### Performance Monitoring
```csharp ```csharp
.AddListToolsFilter(next => async (context, cancellationToken) => .WithRequestFilters(requestFilters =>
{ {
requestFilters.AddListToolsFilter(next => async (context, cancellationToken) =>
{
var logger = context.Services?.GetService<ILogger<Program>>(); var logger = context.Services?.GetService<ILogger<Program>>();
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
@ -322,14 +365,17 @@ Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filt
stopwatch.Stop(); stopwatch.Stop();
logger?.LogInformation($"Handler took {stopwatch.ElapsedMilliseconds}ms"); logger?.LogInformation($"Handler took {stopwatch.ElapsedMilliseconds}ms");
return result; return result;
});
}); });
``` ```
### Caching ### Caching
```csharp ```csharp
.AddListResourcesFilter(next => async (context, cancellationToken) => .WithRequestFilters(requestFilters =>
{ {
requestFilters.AddListResourcesFilter(next => async (context, cancellationToken) =>
{
var cache = context.Services!.GetRequiredService<IMemoryCache>(); var cache = context.Services!.GetRequiredService<IMemoryCache>();
var cacheKey = $"resources:{context.Params.Cursor}"; var cacheKey = $"resources:{context.Params.Cursor}";
@ -341,14 +387,15 @@ Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filt
var result = await next(context, cancellationToken); var result = await next(context, cancellationToken);
cache.Set(cacheKey, result, TimeSpan.FromMinutes(5)); cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
return result; 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. 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: To enable authorization support, call `AddAuthorizationFilters()` when configuring your MCP server:
@ -455,7 +502,9 @@ This allows you to implement logging, metrics, or other cross-cutting concerns t
```csharp ```csharp
services.AddMcpServer() services.AddMcpServer()
.WithHttpTransport() .WithHttpTransport()
.AddListToolsFilter(next => async (context, cancellationToken) => .WithRequestFilters(requestFilters =>
{
requestFilters.AddListToolsFilter(next => async (context, cancellationToken) =>
{ {
var logger = context.Services?.GetService<ILogger<Program>>(); var logger = context.Services?.GetService<ILogger<Program>>();
@ -464,9 +513,12 @@ services.AddMcpServer()
var result = await next(context, cancellationToken); var result = await next(context, cancellationToken);
logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools after authorization"); logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools after authorization");
return result; return result;
});
}) })
.AddAuthorizationFilters() // Authorization filtering happens here .AddAuthorizationFilters() // Authorization filtering happens here
.AddListToolsFilter(next => async (context, cancellationToken) => .WithRequestFilters(requestFilters =>
{
requestFilters.AddListToolsFilter(next => async (context, cancellationToken) =>
{ {
var logger = context.Services?.GetService<ILogger<Program>>(); var logger = context.Services?.GetService<ILogger<Program>>();
@ -474,6 +526,7 @@ services.AddMcpServer()
var result = await next(context, cancellationToken); var result = await next(context, cancellationToken);
logger?.LogInformation($"Post-auth filter sees {result.Tools?.Count ?? 0} authorized tools"); logger?.LogInformation($"Post-auth filter sees {result.Tools?.Count ?? 0} authorized tools");
return result; return result;
});
}) })
.WithTools<WeatherTools>(); .WithTools<WeatherTools>();
``` ```
@ -494,11 +547,14 @@ builder.Services.AddMcpServer()
.WithHttpTransport() .WithHttpTransport()
.AddAuthorizationFilters() // Required for authorization support .AddAuthorizationFilters() // Required for authorization support
.WithTools<WeatherTools>() .WithTools<WeatherTools>()
.AddCallToolFilter(next => async (context, cancellationToken) => .WithRequestFilters(requestFilters =>
{
requestFilters.AddCallToolFilter(next => async (context, cancellationToken) =>
{ {
// Custom call tool logic // Custom call tool logic
return await next(context, cancellationToken); return await next(context, cancellationToken);
}); });
});
var app = builder.Build(); var app = builder.Build();
@ -511,8 +567,10 @@ app.Run();
You can also create custom authorization filters using the filter methods: You can also create custom authorization filters using the filter methods:
```csharp ```csharp
.AddCallToolFilter(next => async (context, cancellationToken) => .WithRequestFilters(requestFilters =>
{ {
requestFilters.AddCallToolFilter(next => async (context, cancellationToken) =>
{
// Custom authorization logic // Custom authorization logic
if (context.User?.Identity?.IsAuthenticated != true) if (context.User?.Identity?.IsAuthenticated != true)
{ {
@ -524,6 +582,7 @@ You can also create custom authorization filters using the filter methods:
} }
return await next(context, cancellationToken); return await next(context, cancellationToken);
});
}); });
``` ```

View file

@ -22,4 +22,9 @@ internal static class Obsoletions
public const string LegacyTitledEnumSchema_DiagnosticId = "MCP9001"; public const string LegacyTitledEnumSchema_DiagnosticId = "MCP9001";
public const string LegacyTitledEnumSchema_Message = "The EnumSchema and LegacyTitledEnumSchema APIs are deprecated as of specification version 2025-11-25 and will be removed in a future major version. See SEP-1330 for more information."; public const string LegacyTitledEnumSchema_Message = "The EnumSchema and LegacyTitledEnumSchema APIs are deprecated as of specification version 2025-11-25 and will be removed in a future major version. See SEP-1330 for more information.";
public const string LegacyTitledEnumSchema_Url = "https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330"; public const string LegacyTitledEnumSchema_Url = "https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330";
public const string MessageAndRequestFilter_DiagnosticId = "MCP9002";
public const string MessageAndRequestFilter_Url = "https://github.com/modelcontextprotocol/csharp-sdk/pull/1308";
public const string MessageFilter_Message = "Use WithMessageFilters() instead.";
public const string RequestFilter_Message = "Use WithRequestFilters() instead.";
} }

View file

@ -43,7 +43,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private void ConfigureListToolsFilter(McpServerOptions options) private void ConfigureListToolsFilter(McpServerOptions options)
{ {
options.Filters.ListToolsFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.ListToolsFilters.Add(next => async (context, cancellationToken) =>
{ {
context.Items[AuthorizationFilterInvokedKey] = true; context.Items[AuthorizationFilterInvokedKey] = true;
@ -57,7 +57,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private static void CheckListToolsFilter(McpServerOptions options) private static void CheckListToolsFilter(McpServerOptions options)
{ {
options.Filters.ListToolsFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.ListToolsFilters.Add(next => async (context, cancellationToken) =>
{ {
var result = await next(context, cancellationToken); var result = await next(context, cancellationToken);
@ -73,7 +73,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private void ConfigureCallToolFilter(McpServerOptions options) private void ConfigureCallToolFilter(McpServerOptions options)
{ {
options.Filters.CallToolFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.CallToolFilters.Add(next => async (context, cancellationToken) =>
{ {
var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context); var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context);
if (!authResult.Succeeded) if (!authResult.Succeeded)
@ -89,7 +89,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private static void CheckCallToolFilter(McpServerOptions options) private static void CheckCallToolFilter(McpServerOptions options)
{ {
options.Filters.CallToolFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.CallToolFilters.Add(next => async (context, cancellationToken) =>
{ {
if (HasAuthorizationMetadata(context.MatchedPrimitive) if (HasAuthorizationMetadata(context.MatchedPrimitive)
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) && !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
@ -103,7 +103,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private void ConfigureListResourcesFilter(McpServerOptions options) private void ConfigureListResourcesFilter(McpServerOptions options)
{ {
options.Filters.ListResourcesFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.ListResourcesFilters.Add(next => async (context, cancellationToken) =>
{ {
context.Items[AuthorizationFilterInvokedKey] = true; context.Items[AuthorizationFilterInvokedKey] = true;
@ -117,7 +117,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private static void CheckListResourcesFilter(McpServerOptions options) private static void CheckListResourcesFilter(McpServerOptions options)
{ {
options.Filters.ListResourcesFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.ListResourcesFilters.Add(next => async (context, cancellationToken) =>
{ {
var result = await next(context, cancellationToken); var result = await next(context, cancellationToken);
@ -133,7 +133,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private void ConfigureListResourceTemplatesFilter(McpServerOptions options) private void ConfigureListResourceTemplatesFilter(McpServerOptions options)
{ {
options.Filters.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) =>
{ {
context.Items[AuthorizationFilterInvokedKey] = true; context.Items[AuthorizationFilterInvokedKey] = true;
@ -147,7 +147,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private static void CheckListResourceTemplatesFilter(McpServerOptions options) private static void CheckListResourceTemplatesFilter(McpServerOptions options)
{ {
options.Filters.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) =>
{ {
var result = await next(context, cancellationToken); var result = await next(context, cancellationToken);
@ -163,7 +163,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private void ConfigureReadResourceFilter(McpServerOptions options) private void ConfigureReadResourceFilter(McpServerOptions options)
{ {
options.Filters.ReadResourceFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.ReadResourceFilters.Add(next => async (context, cancellationToken) =>
{ {
context.Items[AuthorizationFilterInvokedKey] = true; context.Items[AuthorizationFilterInvokedKey] = true;
@ -179,7 +179,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private static void CheckReadResourceFilter(McpServerOptions options) private static void CheckReadResourceFilter(McpServerOptions options)
{ {
options.Filters.ReadResourceFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.ReadResourceFilters.Add(next => async (context, cancellationToken) =>
{ {
if (HasAuthorizationMetadata(context.MatchedPrimitive) if (HasAuthorizationMetadata(context.MatchedPrimitive)
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) && !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
@ -193,7 +193,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private void ConfigureListPromptsFilter(McpServerOptions options) private void ConfigureListPromptsFilter(McpServerOptions options)
{ {
options.Filters.ListPromptsFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.ListPromptsFilters.Add(next => async (context, cancellationToken) =>
{ {
context.Items[AuthorizationFilterInvokedKey] = true; context.Items[AuthorizationFilterInvokedKey] = true;
@ -207,7 +207,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private static void CheckListPromptsFilter(McpServerOptions options) private static void CheckListPromptsFilter(McpServerOptions options)
{ {
options.Filters.ListPromptsFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.ListPromptsFilters.Add(next => async (context, cancellationToken) =>
{ {
var result = await next(context, cancellationToken); var result = await next(context, cancellationToken);
@ -223,7 +223,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private void ConfigureGetPromptFilter(McpServerOptions options) private void ConfigureGetPromptFilter(McpServerOptions options)
{ {
options.Filters.GetPromptFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.GetPromptFilters.Add(next => async (context, cancellationToken) =>
{ {
context.Items[AuthorizationFilterInvokedKey] = true; context.Items[AuthorizationFilterInvokedKey] = true;
@ -239,7 +239,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
private static void CheckGetPromptFilter(McpServerOptions options) private static void CheckGetPromptFilter(McpServerOptions options)
{ {
options.Filters.GetPromptFilters.Add(next => async (context, cancellationToken) => options.Filters.Request.GetPromptFilters.Add(next => async (context, cancellationToken) =>
{ {
if (HasAuthorizationMetadata(context.MatchedPrimitive) if (HasAuthorizationMetadata(context.MatchedPrimitive)
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) && !context.Items.ContainsKey(AuthorizationFilterInvokedKey))

View file

@ -0,0 +1,42 @@
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Server;
/// <summary>
/// Provides grouped message filter collections.
/// </summary>
public sealed class McpMessageFilters
{
/// <summary>
/// Gets the filters for all incoming JSON-RPC messages.
/// </summary>
/// <remarks>
/// <para>
/// These filters intercept all incoming JSON-RPC messages before they are processed by the server,
/// including requests, notifications, responses, and errors. The filters can perform logging,
/// authentication, rate limiting, or other cross-cutting concerns that apply to all message types.
/// </para>
/// <para>
/// Message filters are applied before request-specific filters. If a message filter does not call
/// the next handler in the pipeline, the default handlers will not be executed.
/// </para>
/// </remarks>
public IList<McpMessageFilter> IncomingFilters { get; } = [];
/// <summary>
/// Gets the filters for all outgoing JSON-RPC messages.
/// </summary>
/// <remarks>
/// <para>
/// These filters intercept all outgoing JSON-RPC messages before they are sent to the client,
/// including responses, notifications, and errors. The filters can perform logging,
/// redaction, auditing, or other cross-cutting concerns that apply to all message types.
/// </para>
/// <para>
/// If a message filter does not call the next handler in the pipeline, the message will not be sent.
/// Filters may also call the next handler multiple times with different messages to emit additional
/// server-to-client messages.
/// </para>
/// </remarks>
public IList<McpMessageFilter> OutgoingFilters { get; } = [];
}

View file

@ -0,0 +1,157 @@
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Server;
/// <summary>
/// Provides grouped request-specific filter collections.
/// </summary>
public sealed class McpRequestFilters
{
/// <summary>
/// Gets the filters for the list-tools handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that return a list of available tools when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ToolsList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more tools.
/// </para>
/// <para>
/// These filters work alongside any tools defined in the <see cref="McpServerTool"/> collection.
/// Tools from both sources will be combined when returning results to clients.
/// </para>
/// </remarks>
public IList<McpRequestFilter<ListToolsRequestParams, ListToolsResult>> ListToolsFilters { get; } = [];
/// <summary>
/// Gets the filters for the call-tool handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that are invoked when a client makes a call to a tool that isn't found in the <see cref="McpServerTool"/> collection.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ToolsCall"/> requests. The handler should implement logic to execute the requested tool and return appropriate results.
/// </remarks>
public IList<McpRequestFilter<CallToolRequestParams, CallToolResult>> CallToolFilters { get; } = [];
/// <summary>
/// Gets the filters for the list-prompts handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that return a list of available prompts when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.PromptsList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more prompts.
/// </para>
/// <para>
/// These filters work alongside any prompts defined in the <see cref="McpServerPrompt"/> collection.
/// Prompts from both sources will be combined when returning results to clients.
/// </para>
/// </remarks>
public IList<McpRequestFilter<ListPromptsRequestParams, ListPromptsResult>> ListPromptsFilters { get; } = [];
/// <summary>
/// Gets the filters for the get-prompt handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that are invoked when a client requests details for a specific prompt that isn't found in the <see cref="McpServerPrompt"/> collection.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.PromptsGet"/> requests. The handler should implement logic to fetch or generate the requested prompt and return appropriate results.
/// </remarks>
public IList<McpRequestFilter<GetPromptRequestParams, GetPromptResult>> GetPromptFilters { get; } = [];
/// <summary>
/// Gets the filters for the list-resource-templates handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that return a list of available resource templates when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesTemplatesList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resource templates.
/// </remarks>
public IList<McpRequestFilter<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>> ListResourceTemplatesFilters { get; } = [];
/// <summary>
/// Gets the filters for the list-resources handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that return a list of available resources when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resources.
/// </remarks>
public IList<McpRequestFilter<ListResourcesRequestParams, ListResourcesResult>> ListResourcesFilters { get; } = [];
/// <summary>
/// Gets the filters for the read-resource handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that are invoked when a client requests the content of a specific resource identified by its URI.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesRead"/> requests. The handler should implement logic to locate and retrieve the requested resource.
/// </remarks>
public IList<McpRequestFilter<ReadResourceRequestParams, ReadResourceResult>> ReadResourceFilters { get; } = [];
/// <summary>
/// Gets the filters for the complete-handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that provide auto-completion suggestions for prompt arguments or resource references in the Model Context Protocol.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.CompletionComplete"/> requests. The handler processes auto-completion requests, returning a list of suggestions based on the
/// reference type and current argument value.
/// </remarks>
public IList<McpRequestFilter<CompleteRequestParams, CompleteResult>> CompleteFilters { get; } = [];
/// <summary>
/// Gets the filters for the subscribe-to-resources handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that are invoked when a client wants to receive notifications about changes to specific resources or resource patterns.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesSubscribe"/> requests. The handler should implement logic to register the client's interest in the specified resources
/// and set up the necessary infrastructure to send notifications when those resources change.
/// </para>
/// <para>
/// After a successful subscription, the server should send resource change notifications to the client
/// whenever a relevant resource is created, updated, or deleted.
/// </para>
/// </remarks>
public IList<McpRequestFilter<SubscribeRequestParams, EmptyResult>> SubscribeToResourcesFilters { get; } = [];
/// <summary>
/// Gets the filters for the unsubscribe-from-resources handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that are invoked when a client wants to stop receiving notifications about previously subscribed resources.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesUnsubscribe"/> requests. The handler should implement logic to remove the client's subscriptions to the specified resources
/// and clean up any associated resources.
/// </para>
/// <para>
/// After a successful unsubscription, the server should no longer send resource change notifications
/// to the client for the specified resources.
/// </para>
/// </remarks>
public IList<McpRequestFilter<UnsubscribeRequestParams, EmptyResult>> UnsubscribeFromResourcesFilters { get; } = [];
/// <summary>
/// Gets the filters for the set-logging-level handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that process <see cref="RequestMethods.LoggingSetLevel"/> requests from clients. When set, it enables
/// clients to control which log messages they receive by specifying a minimum severity threshold.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.LoggingSetLevel"/> requests.
/// </para>
/// <para>
/// After handling a level change request, the server typically begins sending log messages
/// at or above the specified level to the client as notifications/message notifications.
/// </para>
/// </remarks>
public IList<McpRequestFilter<SetLevelRequestParams, EmptyResult>> SetLoggingLevelFilters { get; } = [];
}

View file

@ -12,183 +12,12 @@ namespace ModelContextProtocol.Server;
public sealed class McpServerFilters public sealed class McpServerFilters
{ {
/// <summary> /// <summary>
/// Gets the filters for all incoming JSON-RPC messages. /// Gets the filters for incoming and outgoing JSON-RPC messages.
/// </summary> /// </summary>
/// <remarks> public McpMessageFilters Message { get; } = new();
/// <para>
/// These filters intercept all incoming JSON-RPC messages before they are processed by the server,
/// including requests, notifications, responses, and errors. The filters can perform logging,
/// authentication, rate limiting, or other cross-cutting concerns that apply to all message types.
/// </para>
/// <para>
/// Message filters are applied before request-specific filters. If a message filter does not call
/// the next handler in the pipeline, the default handlers will not be executed.
/// </para>
/// </remarks>
public List<McpMessageFilter> IncomingMessageFilters { get; } = [];
/// <summary> /// <summary>
/// Gets the filters for all outgoing JSON-RPC messages. /// Gets the filters for request-specific MCP handler pipelines.
/// </summary> /// </summary>
/// <remarks> public McpRequestFilters Request { get; } = new();
/// <para>
/// These filters intercept all outgoing JSON-RPC messages before they are sent to the client,
/// including responses, notifications, and errors. The filters can perform logging,
/// redaction, auditing, or other cross-cutting concerns that apply to all message types.
/// </para>
/// <para>
/// If a message filter does not call the next handler in the pipeline, the message will not be sent.
/// Filters may also call the next handler multiple times with different messages to emit additional
/// server-to-client messages.
/// </para>
/// </remarks>
public List<McpMessageFilter> OutgoingMessageFilters { get; } = [];
/// <summary>
/// Gets the filters for the list-tools handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that return a list of available tools when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ToolsList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more tools.
/// </para>
/// <para>
/// These filters work alongside any tools defined in the <see cref="McpServerTool"/> collection.
/// Tools from both sources will be combined when returning results to clients.
/// </para>
/// </remarks>
public List<McpRequestFilter<ListToolsRequestParams, ListToolsResult>> ListToolsFilters { get; } = [];
/// <summary>
/// Gets the filters for the call-tool handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that are invoked when a client makes a call to a tool that isn't found in the <see cref="McpServerTool"/> collection.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ToolsCall"/> requests. The handler should implement logic to execute the requested tool and return appropriate results.
/// </remarks>
public List<McpRequestFilter<CallToolRequestParams, CallToolResult>> CallToolFilters { get; } = [];
/// <summary>
/// Gets the filters for the list-prompts handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that return a list of available prompts when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.PromptsList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more prompts.
/// </para>
/// <para>
/// These filters work alongside any prompts defined in the <see cref="McpServerPrompt"/> collection.
/// Prompts from both sources will be combined when returning results to clients.
/// </para>
/// </remarks>
public List<McpRequestFilter<ListPromptsRequestParams, ListPromptsResult>> ListPromptsFilters { get; } = [];
/// <summary>
/// Gets the filters for the get-prompt handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that are invoked when a client requests details for a specific prompt that isn't found in the <see cref="McpServerPrompt"/> collection.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.PromptsGet"/> requests. The handler should implement logic to fetch or generate the requested prompt and return appropriate results.
/// </remarks>
public List<McpRequestFilter<GetPromptRequestParams, GetPromptResult>> GetPromptFilters { get; } = [];
/// <summary>
/// Gets the filters for the list-resource-templates handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that return a list of available resource templates when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesTemplatesList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resource templates.
/// </remarks>
public List<McpRequestFilter<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>> ListResourceTemplatesFilters { get; } = [];
/// <summary>
/// Gets the filters for the list-resources handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that return a list of available resources when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resources.
/// </remarks>
public List<McpRequestFilter<ListResourcesRequestParams, ListResourcesResult>> ListResourcesFilters { get; } = [];
/// <summary>
/// Gets the filters for the read-resource handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that are invoked when a client requests the content of a specific resource identified by its URI.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesRead"/> requests. The handler should implement logic to locate and retrieve the requested resource.
/// </remarks>
public List<McpRequestFilter<ReadResourceRequestParams, ReadResourceResult>> ReadResourceFilters { get; } = [];
/// <summary>
/// Gets the filters for the complete-handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that provide auto-completion suggestions for prompt arguments or resource references in the Model Context Protocol.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.CompletionComplete"/> requests. The handler processes auto-completion requests, returning a list of suggestions based on the
/// reference type and current argument value.
/// </remarks>
public List<McpRequestFilter<CompleteRequestParams, CompleteResult>> CompleteFilters { get; } = [];
/// <summary>
/// Gets the filters for the subscribe-to-resources handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that are invoked when a client wants to receive notifications about changes to specific resources or resource patterns.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesSubscribe"/> requests. The handler should implement logic to register the client's interest in the specified resources
/// and set up the necessary infrastructure to send notifications when those resources change.
/// </para>
/// <para>
/// After a successful subscription, the server should send resource change notifications to the client
/// whenever a relevant resource is created, updated, or deleted.
/// </para>
/// </remarks>
public List<McpRequestFilter<SubscribeRequestParams, EmptyResult>> SubscribeToResourcesFilters { get; } = [];
/// <summary>
/// Gets the filters for the unsubscribe-from-resources handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that are invoked when a client wants to stop receiving notifications about previously subscribed resources.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesUnsubscribe"/> requests. The handler should implement logic to remove the client's subscriptions to the specified resources
/// and clean up any associated resources.
/// </para>
/// <para>
/// After a successful unsubscription, the server should no longer send resource change notifications
/// to the client for the specified resources.
/// </para>
/// </remarks>
public List<McpRequestFilter<UnsubscribeRequestParams, EmptyResult>> UnsubscribeFromResourcesFilters { get; } = [];
/// <summary>
/// Gets the filters for the set-logging-level handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that process <see cref="RequestMethods.LoggingSetLevel"/> requests from clients. When set, it enables
/// clients to control which log messages they receive by specifying a minimum severity threshold.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.LoggingSetLevel"/> requests.
/// </para>
/// <para>
/// After handling a level change request, the server typically begins sending log messages
/// at or above the specified level to the client as notifications/message notifications.
/// </para>
/// </remarks>
public List<McpRequestFilter<SetLevelRequestParams, EmptyResult>> SetLoggingLevelFilters { get; } = [];
} }

View file

@ -119,8 +119,8 @@ internal sealed partial class McpServerImpl : McpServer
} }
// And initialize the session. // And initialize the session.
var incomingMessageFilter = BuildMessageFilterPipeline(options.Filters.IncomingMessageFilters); var incomingMessageFilter = BuildMessageFilterPipeline(options.Filters.Message.IncomingFilters);
var outgoingMessageFilter = BuildMessageFilterPipeline(options.Filters.OutgoingMessageFilters); var outgoingMessageFilter = BuildMessageFilterPipeline(options.Filters.Message.OutgoingFilters);
_sessionHandler = new McpSessionHandler( _sessionHandler = new McpSessionHandler(
isServer: true, isServer: true,
_sessionTransport, _sessionTransport,
@ -260,7 +260,7 @@ internal sealed partial class McpServerImpl : McpServer
} }
completeHandler ??= (static async (_, __) => new CompleteResult()); completeHandler ??= (static async (_, __) => new CompleteResult());
completeHandler = BuildFilterPipeline(completeHandler, options.Filters.CompleteFilters); completeHandler = BuildFilterPipeline(completeHandler, options.Filters.Request.CompleteFilters);
ServerCapabilities.Completions = new(); ServerCapabilities.Completions = new();
@ -366,9 +366,9 @@ internal sealed partial class McpServerImpl : McpServer
// subscribe = true; // subscribe = true;
} }
listResourcesHandler = BuildFilterPipeline(listResourcesHandler, options.Filters.ListResourcesFilters); listResourcesHandler = BuildFilterPipeline(listResourcesHandler, options.Filters.Request.ListResourcesFilters);
listResourceTemplatesHandler = BuildFilterPipeline(listResourceTemplatesHandler, options.Filters.ListResourceTemplatesFilters); listResourceTemplatesHandler = BuildFilterPipeline(listResourceTemplatesHandler, options.Filters.Request.ListResourceTemplatesFilters);
readResourceHandler = BuildFilterPipeline(readResourceHandler, options.Filters.ReadResourceFilters, handler => readResourceHandler = BuildFilterPipeline(readResourceHandler, options.Filters.Request.ReadResourceFilters, handler =>
async (request, cancellationToken) => async (request, cancellationToken) =>
{ {
// Initial handler that sets MatchedPrimitive // Initial handler that sets MatchedPrimitive
@ -405,8 +405,8 @@ internal sealed partial class McpServerImpl : McpServer
throw; throw;
} }
}); });
subscribeHandler = BuildFilterPipeline(subscribeHandler, options.Filters.SubscribeToResourcesFilters); subscribeHandler = BuildFilterPipeline(subscribeHandler, options.Filters.Request.SubscribeToResourcesFilters);
unsubscribeHandler = BuildFilterPipeline(unsubscribeHandler, options.Filters.UnsubscribeFromResourcesFilters); unsubscribeHandler = BuildFilterPipeline(unsubscribeHandler, options.Filters.Request.UnsubscribeFromResourcesFilters);
ServerCapabilities.Resources.ListChanged = listChanged; ServerCapabilities.Resources.ListChanged = listChanged;
ServerCapabilities.Resources.Subscribe = subscribe; ServerCapabilities.Resources.Subscribe = subscribe;
@ -496,8 +496,8 @@ internal sealed partial class McpServerImpl : McpServer
listChanged = true; listChanged = true;
} }
listPromptsHandler = BuildFilterPipeline(listPromptsHandler, options.Filters.ListPromptsFilters); listPromptsHandler = BuildFilterPipeline(listPromptsHandler, options.Filters.Request.ListPromptsFilters);
getPromptHandler = BuildFilterPipeline(getPromptHandler, options.Filters.GetPromptFilters, handler => getPromptHandler = BuildFilterPipeline(getPromptHandler, options.Filters.Request.GetPromptFilters, handler =>
async (request, cancellationToken) => async (request, cancellationToken) =>
{ {
// Initial handler that sets MatchedPrimitive // Initial handler that sets MatchedPrimitive
@ -618,8 +618,8 @@ internal sealed partial class McpServerImpl : McpServer
listChanged = true; listChanged = true;
} }
listToolsHandler = BuildFilterPipeline(listToolsHandler, options.Filters.ListToolsFilters); listToolsHandler = BuildFilterPipeline(listToolsHandler, options.Filters.Request.ListToolsFilters);
callToolHandler = BuildFilterPipeline(callToolHandler, options.Filters.CallToolFilters, handler => callToolHandler = BuildFilterPipeline(callToolHandler, options.Filters.Request.CallToolFilters, handler =>
async (request, cancellationToken) => async (request, cancellationToken) =>
{ {
// Initial handler that sets MatchedPrimitive // Initial handler that sets MatchedPrimitive
@ -819,7 +819,7 @@ internal sealed partial class McpServerImpl : McpServer
// Apply filters to the handler // Apply filters to the handler
if (setLoggingLevelHandler is not null) if (setLoggingLevelHandler is not null)
{ {
setLoggingLevelHandler = BuildFilterPipeline(setLoggingLevelHandler, options.Filters.SetLoggingLevelFilters); setLoggingLevelHandler = BuildFilterPipeline(setLoggingLevelHandler, options.Filters.Request.SetLoggingLevelFilters);
} }
ServerCapabilities.Logging = new(); ServerCapabilities.Logging = new();
@ -903,7 +903,7 @@ internal sealed partial class McpServerImpl : McpServer
private static McpRequestHandler<TParams, TResult> BuildFilterPipeline<TParams, TResult>( private static McpRequestHandler<TParams, TResult> BuildFilterPipeline<TParams, TResult>(
McpRequestHandler<TParams, TResult> baseHandler, McpRequestHandler<TParams, TResult> baseHandler,
List<McpRequestFilter<TParams, TResult>> filters, IList<McpRequestFilter<TParams, TResult>> filters,
McpRequestFilter<TParams, TResult>? initialHandler = null) McpRequestFilter<TParams, TResult>? initialHandler = null)
{ {
var current = baseHandler; var current = baseHandler;
@ -921,7 +921,7 @@ internal sealed partial class McpServerImpl : McpServer
return current; return current;
} }
private JsonRpcMessageFilter BuildMessageFilterPipeline(List<McpMessageFilter> filters) private JsonRpcMessageFilter BuildMessageFilterPipeline(IList<McpMessageFilter> filters)
{ {
if (filters.Count == 0) if (filters.Count == 0)
{ {

View file

@ -94,29 +94,21 @@ public sealed class McpServerOptions
/// </remarks> /// </remarks>
public ClientCapabilities? KnownClientCapabilities { get; set; } public ClientCapabilities? KnownClientCapabilities { get; set; }
/// <summary>
/// Gets or sets the container of handlers used by the server for processing protocol messages.
/// </summary>
public McpServerHandlers Handlers { get; } = new();
/// <summary> /// <summary>
/// Gets the filter collections for MCP server handlers. /// Gets the filter collections for MCP server handlers.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This property provides access to filter collections that can be used to modify the behavior /// This property provides access to filter collections that can be used to modify the behavior
/// of various MCP server handlers. Filters are applied in reverse order, so the last filter /// of various MCP server handlers. The first filter added is the outermost (first to execute),
/// added will be the outermost (first to execute). /// and each subsequent filter wraps closer to the handler.
/// </remarks> /// </remarks>
public McpServerFilters Filters { get; } = new(); public McpServerFilters Filters { get; } = new();
/// <summary>
/// Gets or sets the container of handlers used by the server for processing protocol messages.
/// </summary>
public McpServerHandlers Handlers
{
get => field ??= new();
set
{
Throw.IfNull(value);
field = value;
}
}
/// <summary> /// <summary>
/// Gets or sets a collection of tools served by the server. /// Gets or sets a collection of tools served by the server.
/// </summary> /// </summary>

View file

@ -14,7 +14,8 @@ namespace ModelContextProtocol.Server;
/// </para> /// </para>
/// <para> /// <para>
/// This type is typically received as a parameter in message filter delegates registered via /// This type is typically received as a parameter in message filter delegates registered via
/// <see cref="McpServerFilters.IncomingMessageFilters"/> or <see cref="McpServerFilters.OutgoingMessageFilters"/>. /// <see cref="McpServerFilters.Message"/>'s <see cref="McpMessageFilters.IncomingFilters"/> or
/// <see cref="McpMessageFilters.OutgoingFilters"/> collections.
/// </para> /// </para>
/// </remarks> /// </remarks>
public class MessageContext public class MessageContext

View file

@ -0,0 +1,6 @@
namespace Microsoft.Extensions.DependencyInjection;
internal sealed class DefaultMcpMessageFilterBuilder(IMcpServerBuilder serverBuilder) : IMcpMessageFilterBuilder
{
public IServiceCollection Services { get; } = serverBuilder.Services;
}

View file

@ -0,0 +1,6 @@
namespace Microsoft.Extensions.DependencyInjection;
internal sealed class DefaultMcpRequestFilterBuilder(IMcpServerBuilder serverBuilder) : IMcpRequestFilterBuilder
{
public IServiceCollection Services { get; } = serverBuilder.Services;
}

View file

@ -0,0 +1,14 @@
using ModelContextProtocol.Server;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Provides a builder for configuring message-level MCP server filters.
/// </summary>
public interface IMcpMessageFilterBuilder
{
/// <summary>
/// Gets the associated service collection.
/// </summary>
IServiceCollection Services { get; }
}

View file

@ -0,0 +1,14 @@
using ModelContextProtocol.Server;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Provides a builder for configuring request-specific MCP server filters.
/// </summary>
public interface IMcpRequestFilterBuilder
{
/// <summary>
/// Gets the associated service collection.
/// </summary>
IServiceCollection Services { get; }
}

View file

@ -0,0 +1,40 @@
using ModelContextProtocol;
using ModelContextProtocol.Server;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Provides extension methods for configuring message-level MCP server filters.
/// </summary>
public static class McpMessageFilterBuilderExtensions
{
/// <summary>
/// Adds a filter to intercept all incoming JSON-RPC messages.
/// </summary>
/// <param name="builder">The message filter builder instance.</param>
/// <param name="filter">The filter function that wraps the message handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpMessageFilterBuilder AddIncomingFilter(this IMcpMessageFilterBuilder builder, McpMessageFilter filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Message.IncomingFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to intercept all outgoing JSON-RPC messages.
/// </summary>
/// <param name="builder">The message filter builder instance.</param>
/// <param name="filter">The filter function that wraps the message handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpMessageFilterBuilder AddOutgoingFilter(this IMcpMessageFilterBuilder builder, McpMessageFilter filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Message.OutgoingFilters.Add(filter));
return builder;
}
}

View file

@ -0,0 +1,176 @@
using ModelContextProtocol;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Provides extension methods for configuring request-specific MCP server filters.
/// </summary>
public static class McpRequestFilterBuilderExtensions
{
/// <summary>
/// Adds a filter to the list resource templates handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddListResourceTemplatesFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<ListResourceTemplatesRequestParams, ListResourceTemplatesResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.ListResourceTemplatesFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the list tools handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddListToolsFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<ListToolsRequestParams, ListToolsResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.ListToolsFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the call tool handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddCallToolFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<CallToolRequestParams, CallToolResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.CallToolFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the list prompts handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddListPromptsFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<ListPromptsRequestParams, ListPromptsResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.ListPromptsFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the get prompt handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddGetPromptFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<GetPromptRequestParams, GetPromptResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.GetPromptFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the list resources handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddListResourcesFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<ListResourcesRequestParams, ListResourcesResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.ListResourcesFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the read resource handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddReadResourceFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<ReadResourceRequestParams, ReadResourceResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.ReadResourceFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the complete handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddCompleteFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<CompleteRequestParams, CompleteResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.CompleteFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the subscribe-to-resources handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddSubscribeToResourcesFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<SubscribeRequestParams, EmptyResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.SubscribeToResourcesFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the unsubscribe-from-resources handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddUnsubscribeFromResourcesFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<UnsubscribeRequestParams, EmptyResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.UnsubscribeFromResourcesFilters.Add(filter));
return builder;
}
/// <summary>
/// Adds a filter to the set logging level handler pipeline.
/// </summary>
/// <param name="builder">The request filter builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpRequestFilterBuilder AddSetLoggingLevelFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter<SetLevelRequestParams, EmptyResult> filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.Request.SetLoggingLevelFilters.Add(filter));
return builder;
}
}

View file

@ -893,28 +893,49 @@ public static partial class McpServerBuilderExtensions
#endregion #endregion
#region Filters #region Filters
/// <summary>
/// Configures message-level filters for the MCP server.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="configure">A callback used to register message filters.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
public static IMcpServerBuilder WithMessageFilters(this IMcpServerBuilder builder, Action<IMcpMessageFilterBuilder> configure)
{
Throw.IfNull(builder);
Throw.IfNull(configure);
configure(new DefaultMcpMessageFilterBuilder(builder));
return builder;
}
/// <summary>
/// Configures request-specific filters for the MCP server.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="configure">A callback used to register request-specific filters.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
public static IMcpServerBuilder WithRequestFilters(this IMcpServerBuilder builder, Action<IMcpRequestFilterBuilder> configure)
{
Throw.IfNull(builder);
Throw.IfNull(configure);
configure(new DefaultMcpRequestFilterBuilder(builder));
return builder;
}
#pragma warning disable CS0436 // ObsoleteAttribute polyfill conflicts with framework type on netstandard.
/// <summary> /// <summary>
/// Adds a filter to the list resource templates handler pipeline. /// Adds a filter to the list resource templates handler pipeline.
/// </summary> /// </summary>
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddListResourceTemplatesFilter(this IMcpServerBuilder builder, McpRequestFilter<ListResourceTemplatesRequestParams, ListResourceTemplatesResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddListResourceTemplatesFilter(filter));
/// This filter wraps handlers that return a list of available resource templates when requested by a client.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesTemplatesList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resource templates.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddListResourceTemplatesFilter(this IMcpServerBuilder builder, McpRequestFilter<ListResourceTemplatesRequestParams, ListResourceTemplatesResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.ListResourceTemplatesFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to the list tools handler pipeline. /// Adds a filter to the list tools handler pipeline.
@ -922,26 +943,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddListToolsFilter(this IMcpServerBuilder builder, McpRequestFilter<ListToolsRequestParams, ListToolsResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddListToolsFilter(filter));
/// This filter wraps handlers that return a list of available tools when requested by a client.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ToolsList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more tools.
/// </para>
/// <para>
/// This filter works alongside any tools defined in the <see cref="McpServerTool"/> collection.
/// Tools from both sources will be combined when returning results to clients.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddListToolsFilter(this IMcpServerBuilder builder, McpRequestFilter<ListToolsRequestParams, ListToolsResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.ListToolsFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to the call tool handler pipeline. /// Adds a filter to the call tool handler pipeline.
@ -949,21 +953,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddCallToolFilter(this IMcpServerBuilder builder, McpRequestFilter<CallToolRequestParams, CallToolResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddCallToolFilter(filter));
/// This filter wraps handlers that are invoked when a client makes a call to a tool that isn't found in the <see cref="McpServerTool"/> collection.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ToolsCall"/> requests. The handler should implement logic to execute the requested tool and return appropriate results.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddCallToolFilter(this IMcpServerBuilder builder, McpRequestFilter<CallToolRequestParams, CallToolResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.CallToolFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to the list prompts handler pipeline. /// Adds a filter to the list prompts handler pipeline.
@ -971,26 +963,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddListPromptsFilter(this IMcpServerBuilder builder, McpRequestFilter<ListPromptsRequestParams, ListPromptsResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddListPromptsFilter(filter));
/// This filter wraps handlers that return a list of available prompts when requested by a client.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.PromptsList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more prompts.
/// </para>
/// <para>
/// This filter works alongside any prompts defined in the <see cref="McpServerPrompt"/> collection.
/// Prompts from both sources will be combined when returning results to clients.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddListPromptsFilter(this IMcpServerBuilder builder, McpRequestFilter<ListPromptsRequestParams, ListPromptsResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.ListPromptsFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to the get prompt handler pipeline. /// Adds a filter to the get prompt handler pipeline.
@ -998,21 +973,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddGetPromptFilter(this IMcpServerBuilder builder, McpRequestFilter<GetPromptRequestParams, GetPromptResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddGetPromptFilter(filter));
/// This filter wraps handlers that are invoked when a client requests details for a specific prompt that isn't found in the <see cref="McpServerPrompt"/> collection.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.PromptsGet"/> requests. The handler should implement logic to fetch or generate the requested prompt and return appropriate results.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddGetPromptFilter(this IMcpServerBuilder builder, McpRequestFilter<GetPromptRequestParams, GetPromptResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.GetPromptFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to the list resources handler pipeline. /// Adds a filter to the list resources handler pipeline.
@ -1020,22 +983,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddListResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter<ListResourcesRequestParams, ListResourcesResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddListResourcesFilter(filter));
/// This filter wraps handlers that return a list of available resources when requested by a client.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resources.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddListResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter<ListResourcesRequestParams, ListResourcesResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.ListResourcesFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to the read resource handler pipeline. /// Adds a filter to the read resource handler pipeline.
@ -1043,21 +993,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddReadResourceFilter(this IMcpServerBuilder builder, McpRequestFilter<ReadResourceRequestParams, ReadResourceResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddReadResourceFilter(filter));
/// This filter wraps handlers that are invoked when a client requests the content of a specific resource identified by its URI.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesRead"/> requests. The handler should implement logic to locate and retrieve the requested resource.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddReadResourceFilter(this IMcpServerBuilder builder, McpRequestFilter<ReadResourceRequestParams, ReadResourceResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.ReadResourceFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to the complete handler pipeline. /// Adds a filter to the complete handler pipeline.
@ -1065,22 +1003,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddCompleteFilter(this IMcpServerBuilder builder, McpRequestFilter<CompleteRequestParams, CompleteResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddCompleteFilter(filter));
/// This filter wraps handlers that provide auto-completion suggestions for prompt arguments or resource references in the Model Context Protocol.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.CompletionComplete"/> requests. The handler processes auto-completion requests, returning a list of suggestions based on the
/// reference type and current argument value.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddCompleteFilter(this IMcpServerBuilder builder, McpRequestFilter<CompleteRequestParams, CompleteResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.CompleteFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to the subscribe-to-resources handler pipeline. /// Adds a filter to the subscribe-to-resources handler pipeline.
@ -1088,26 +1013,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddSubscribeToResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter<SubscribeRequestParams, EmptyResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddSubscribeToResourcesFilter(filter));
/// This filter wraps handlers that are invoked when a client wants to receive notifications about changes to specific resources or resource patterns.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesSubscribe"/> requests. The handler should implement logic to register the client's interest in the specified resources
/// and set up the necessary infrastructure to send notifications when those resources change.
/// </para>
/// <para>
/// After a successful subscription, the server should send resource change notifications to the client
/// whenever a relevant resource is created, updated, or deleted.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddSubscribeToResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter<SubscribeRequestParams, EmptyResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.SubscribeToResourcesFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to the unsubscribe-from-resources handler pipeline. /// Adds a filter to the unsubscribe-from-resources handler pipeline.
@ -1115,26 +1023,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddUnsubscribeFromResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter<UnsubscribeRequestParams, EmptyResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddUnsubscribeFromResourcesFilter(filter));
/// This filter wraps handlers that are invoked when a client wants to stop receiving notifications about previously subscribed resources.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesUnsubscribe"/> requests. The handler should implement logic to remove the client's subscriptions to the specified resources
/// and clean up any associated resources.
/// </para>
/// <para>
/// After a successful unsubscription, the server should no longer send resource change notifications
/// to the client for the specified resources.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddUnsubscribeFromResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter<UnsubscribeRequestParams, EmptyResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.UnsubscribeFromResourcesFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to the set logging level handler pipeline. /// Adds a filter to the set logging level handler pipeline.
@ -1142,26 +1033,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the handler.</param> /// <param name="filter">The filter function that wraps the handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddSetLoggingLevelFilter(this IMcpServerBuilder builder, McpRequestFilter<SetLevelRequestParams, EmptyResult> filter) =>
/// <para> builder.WithRequestFilters(filters => filters.AddSetLoggingLevelFilter(filter));
/// This filter wraps handlers that process <see cref="RequestMethods.LoggingSetLevel"/> requests from clients. When set, it enables
/// clients to control which log messages they receive by specifying a minimum severity threshold.
/// The filter can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.LoggingSetLevel"/> requests.
/// </para>
/// <para>
/// After handling a level change request, the server typically begins sending log messages
/// at or above the specified level to the client as notifications or message notifications.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddSetLoggingLevelFilter(this IMcpServerBuilder builder, McpRequestFilter<SetLevelRequestParams, EmptyResult> filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.SetLoggingLevelFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to intercept all incoming JSON-RPC messages. /// Adds a filter to intercept all incoming JSON-RPC messages.
@ -1169,36 +1043,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the message handler.</param> /// <param name="filter">The filter function that wraps the message handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.MessageFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddIncomingMessageFilter(this IMcpServerBuilder builder, McpMessageFilter filter) =>
/// <para> builder.WithMessageFilters(filters => filters.AddIncomingFilter(filter));
/// This filter intercepts all incoming JSON-RPC messages before they are processed by the server,
/// including requests, notifications, responses, and errors. The filter can perform logging,
/// authentication, rate limiting, or other cross-cutting concerns that apply to all message types.
/// </para>
/// <para>
/// Message filters are applied before request-specific filters. If a message filter does not call
/// the next handler in the pipeline, the default handlers will not be executed.
/// </para>
/// <para>
/// Filters are applied in the order they are registered, with the first registered filter being the outermost.
/// Each filter receives the next handler in the pipeline and can choose to:
/// <list type="bullet">
/// <item><description>Call the next handler to continue processing: <c>await next(context, cancellationToken)</c></description></item>
/// <item><description>Skip the default handlers entirely by not calling next</description></item>
/// <item><description>Perform operations before and/or after calling next</description></item>
/// <item><description>Catch and handle exceptions from inner handlers</description></item>
/// </list>
/// </para>
/// </remarks>
public static IMcpServerBuilder AddIncomingMessageFilter(this IMcpServerBuilder builder, McpMessageFilter filter)
{
Throw.IfNull(builder);
Throw.IfNull(filter);
builder.Services.Configure<McpServerOptions>(options => options.Filters.IncomingMessageFilters.Add(filter));
return builder;
}
/// <summary> /// <summary>
/// Adds a filter to intercept all outgoing JSON-RPC messages. /// Adds a filter to intercept all outgoing JSON-RPC messages.
@ -1206,26 +1053,11 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param> /// <param name="builder">The builder instance.</param>
/// <param name="filter">The filter function that wraps the message handler.</param> /// <param name="filter">The filter function that wraps the message handler.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns> /// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> [Obsolete(Obsoletions.MessageFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)]
/// <remarks> public static IMcpServerBuilder AddOutgoingMessageFilter(this IMcpServerBuilder builder, McpMessageFilter filter) =>
/// <para> builder.WithMessageFilters(filters => filters.AddOutgoingFilter(filter));
/// This filter intercepts all outgoing JSON-RPC messages before they are sent to the client,
/// including responses, notifications, and errors. The filter can perform logging, redaction,
/// auditing, or other cross-cutting concerns that apply to all message types.
/// </para>
/// <para>
/// If a message filter does not call the next handler in the pipeline, the message will not be sent.
/// Filters may also call the next handler multiple times with different messages to emit additional
/// server-to-client messages.
/// </para>
/// </remarks>
public static IMcpServerBuilder AddOutgoingMessageFilter(this IMcpServerBuilder builder, McpMessageFilter filter)
{
Throw.IfNull(builder);
builder.Services.Configure<McpServerOptions>(options => options.Filters.OutgoingMessageFilters.Add(filter)); #pragma warning restore CS0436
return builder;
}
#endregion #endregion
#region Transports #region Transports

View file

@ -18,6 +18,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="..\Common\Throw.cs" Link="Throw.cs" /> <Compile Include="..\Common\Throw.cs" Link="Throw.cs" />
<Compile Include="..\Common\Obsoletions.cs" Link="Obsoletions.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -38,7 +38,7 @@ public class Program
}) })
.WithTools<ConformanceTools>() .WithTools<ConformanceTools>()
.WithTools([ConformanceTools.CreateJsonSchema202012Tool()]) .WithTools([ConformanceTools.CreateJsonSchema202012Tool()])
.AddCallToolFilter(next => async (request, cancellationToken) => .WithRequestFilters(filters => filters.AddCallToolFilter(next => async (request, cancellationToken) =>
{ {
var result = await next(request, cancellationToken); var result = await next(request, cancellationToken);
@ -51,7 +51,7 @@ public class Program
} }
return result; return result;
}) }))
.WithPrompts<ConformancePrompts>() .WithPrompts<ConformancePrompts>()
.WithResources<ConformanceResources>() .WithResources<ConformanceResources>()
.WithSubscribeToResourcesHandler(async (ctx, ct) => .WithSubscribeToResourcesHandler(async (ctx, ct) =>

View file

@ -90,9 +90,7 @@ public class Program
} }
const int pageSize = 10; const int pageSize = 10;
options.Handlers = new() options.Handlers.ListToolsHandler = async (request, cancellationToken) =>
{
ListToolsHandler = async (request, cancellationToken) =>
{ {
return new ListToolsResult return new ListToolsResult
{ {
@ -169,13 +167,15 @@ public class Program
} }
] ]
}; };
}, };
CallToolHandler = async (request, cancellationToken) =>
options.Handlers.CallToolHandler = async (request, cancellationToken) =>
{ {
if (request.Params is null) if (request.Params is null)
{ {
throw new McpProtocolException("Missing required parameter 'name'", McpErrorCode.InvalidParams); throw new McpProtocolException("Missing required parameter 'name'", McpErrorCode.InvalidParams);
} }
if (request.Params.Name == "echo") if (request.Params.Name == "echo")
{ {
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message)) if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message))
@ -227,10 +227,10 @@ public class Program
{ {
throw new McpProtocolException($"Unknown tool: '{request.Params.Name}'", McpErrorCode.InvalidParams); throw new McpProtocolException($"Unknown tool: '{request.Params.Name}'", McpErrorCode.InvalidParams);
} }
}, };
ListResourceTemplatesHandler = async (request, cancellationToken) =>
{
options.Handlers.ListResourceTemplatesHandler = async (request, cancellationToken) =>
{
return new ListResourceTemplatesResult return new ListResourceTemplatesResult
{ {
ResourceTemplates = [ ResourceTemplates = [
@ -241,11 +241,13 @@ public class Program
} }
] ]
}; };
}, };
ListResourcesHandler = async (request, cancellationToken) =>
options.Handlers.ListResourcesHandler = async (request, cancellationToken) =>
{ {
int startIndex = 0; int startIndex = 0;
var requestParams = request.Params ?? new(); var requestParams = request.Params ?? new();
if (requestParams.Cursor is not null) if (requestParams.Cursor is not null)
{ {
try try
@ -272,8 +274,9 @@ public class Program
NextCursor = nextCursor, NextCursor = nextCursor,
Resources = resources.GetRange(startIndex, endIndex - startIndex) Resources = resources.GetRange(startIndex, endIndex - startIndex)
}; };
}, };
ReadResourceHandler = async (request, cancellationToken) =>
options.Handlers.ReadResourceHandler = async (request, cancellationToken) =>
{ {
if (request.Params?.Uri is null) if (request.Params?.Uri is null)
{ {
@ -308,8 +311,9 @@ public class Program
{ {
Contents = [contents] Contents = [contents]
}; };
}, };
ListPromptsHandler = async (request, cancellationToken) =>
options.Handlers.ListPromptsHandler = async (request, cancellationToken) =>
{ {
return new ListPromptsResult return new ListPromptsResult
{ {
@ -341,14 +345,17 @@ public class Program
} }
] ]
}; };
}, };
GetPromptHandler = async (request, cancellationToken) =>
options.Handlers.GetPromptHandler = async (request, cancellationToken) =>
{ {
if (request.Params is null) if (request.Params is null)
{ {
throw new McpProtocolException("Missing required parameter 'name'", McpErrorCode.InvalidParams); throw new McpProtocolException("Missing required parameter 'name'", McpErrorCode.InvalidParams);
} }
List<PromptMessage> messages = []; List<PromptMessage> messages = [];
if (request.Params.Name == "simple_prompt") if (request.Params.Name == "simple_prompt")
{ {
messages.Add(new PromptMessage messages.Add(new PromptMessage
@ -390,7 +397,6 @@ public class Program
{ {
Messages = messages Messages = messages
}; };
}
}; };
} }

View file

@ -23,7 +23,9 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
List<string> messageTypes = []; List<string> messageTypes = [];
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters =>
{
filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
var logger = GetLogger(context.Services, "MessageFilter1"); var logger = GetLogger(context.Services, "MessageFilter1");
logger.LogInformation("MessageFilter1 before"); logger.LogInformation("MessageFilter1 before");
@ -34,13 +36,15 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
await next(context, cancellationToken); await next(context, cancellationToken);
logger.LogInformation("MessageFilter1 after"); logger.LogInformation("MessageFilter1 after");
}) });
.AddIncomingMessageFilter((next) => async (context, cancellationToken) =>
filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
var logger = GetLogger(context.Services, "MessageFilter2"); var logger = GetLogger(context.Services, "MessageFilter2");
logger.LogInformation("MessageFilter2 before"); logger.LogInformation("MessageFilter2 before");
await next(context, cancellationToken); await next(context, cancellationToken);
logger.LogInformation("MessageFilter2 after"); logger.LogInformation("MessageFilter2 after");
});
}) })
.WithTools<TestTool>() .WithTools<TestTool>()
.WithPrompts<TestPrompt>() .WithPrompts<TestPrompt>()
@ -69,12 +73,12 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
List<string> messageTypes = []; List<string> messageTypes = [];
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
var messageTypeName = context.JsonRpcMessage.GetType().Name; var messageTypeName = context.JsonRpcMessage.GetType().Name;
messageTypes.Add(messageTypeName); messageTypes.Add(messageTypeName);
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.WithTools<TestTool>(); .WithTools<TestTool>();
StartServer(); StartServer();
@ -91,19 +95,23 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
public async Task AddIncomingMessageFilter_Multiple_Filters_Execute_In_Order() public async Task AddIncomingMessageFilter_Multiple_Filters_Execute_In_Order()
{ {
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters =>
{
filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
var logger = GetLogger(context.Services, "MessageFilter1"); var logger = GetLogger(context.Services, "MessageFilter1");
logger.LogInformation("MessageFilter1 before"); logger.LogInformation("MessageFilter1 before");
await next(context, cancellationToken); await next(context, cancellationToken);
logger.LogInformation("MessageFilter1 after"); logger.LogInformation("MessageFilter1 after");
}) });
.AddIncomingMessageFilter((next) => async (context, cancellationToken) =>
filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
var logger = GetLogger(context.Services, "MessageFilter2"); var logger = GetLogger(context.Services, "MessageFilter2");
logger.LogInformation("MessageFilter2 before"); logger.LogInformation("MessageFilter2 before");
await next(context, cancellationToken); await next(context, cancellationToken);
logger.LogInformation("MessageFilter2 after"); logger.LogInformation("MessageFilter2 after");
});
}) })
.WithTools<TestTool>(); .WithTools<TestTool>();
@ -142,11 +150,11 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
McpServer? capturedServer = null; McpServer? capturedServer = null;
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
capturedServer = context.Server; capturedServer = context.Server;
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.WithTools<TestTool>(); .WithTools<TestTool>();
StartServer(); StartServer();
@ -166,18 +174,22 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
string? capturedValue = null; string? capturedValue = null;
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters =>
{
filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
context.Items["testKey"] = "testValue"; context.Items["testKey"] = "testValue";
await next(context, cancellationToken); await next(context, cancellationToken);
}) });
.AddIncomingMessageFilter((next) => async (context, cancellationToken) =>
filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
if (context.Items.TryGetValue("testKey", out var value)) if (context.Items.TryGetValue("testKey", out var value))
{ {
capturedValue = value as string; capturedValue = value as string;
} }
await next(context, cancellationToken); await next(context, cancellationToken);
});
}) })
.WithTools<TestTool>(); .WithTools<TestTool>();
@ -196,14 +208,14 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
string? capturedMethod = null; string? capturedMethod = null;
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList)
{ {
capturedMethod = request.Method; capturedMethod = request.Method;
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.WithTools<TestTool>(); .WithTools<TestTool>();
StartServer(); StartServer();
@ -219,7 +231,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
public async Task AddIncomingMessageFilter_Exception_Propagates_Properly() public async Task AddIncomingMessageFilter_Exception_Propagates_Properly()
{ {
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
// Only throw for tools/list, not for initialize/initialized // Only throw for tools/list, not for initialize/initialized
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList)
@ -227,7 +239,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
throw new InvalidOperationException("Filter exception"); throw new InvalidOperationException("Filter exception");
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.WithTools<TestTool>(); .WithTools<TestTool>();
StartServer(); StartServer();
@ -248,19 +260,19 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
var executionOrder = new List<string>(); var executionOrder = new List<string>();
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList)
{ {
executionOrder.Add("MessageFilter"); executionOrder.Add("MessageFilter");
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.AddListToolsFilter((next) => async (request, cancellationToken) => .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) =>
{ {
executionOrder.Add("ListToolsFilter"); executionOrder.Add("ListToolsFilter");
return await next(request, cancellationToken); return await next(request, cancellationToken);
}) }))
.WithTools<TestTool>(); .WithTools<TestTool>();
StartServer(); StartServer();
@ -279,7 +291,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
public async Task AddIncomingMessageFilter_Can_Skip_Default_Handlers() public async Task AddIncomingMessageFilter_Can_Skip_Default_Handlers()
{ {
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
// Skip calling next for tools/list // Skip calling next for tools/list
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList)
@ -288,7 +300,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
return; return;
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.WithTools<TestTool>(); .WithTools<TestTool>();
StartServer(); StartServer();
@ -310,7 +322,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
var observedMessages = new List<string>(); var observedMessages = new List<string>();
McpServerBuilder McpServerBuilder
.AddOutgoingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddOutgoingFilter((next) => async (context, cancellationToken) =>
{ {
switch (context.JsonRpcMessage) switch (context.JsonRpcMessage)
{ {
@ -330,7 +342,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.WithTools<ProgressTool>(); .WithTools<ProgressTool>();
StartServer(); StartServer();
@ -353,7 +365,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
public async Task AddOutgoingMessageFilter_Can_Skip_Sending_Messages() public async Task AddOutgoingMessageFilter_Can_Skip_Sending_Messages()
{ {
McpServerBuilder McpServerBuilder
.AddOutgoingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddOutgoingFilter((next) => async (context, cancellationToken) =>
{ {
if (context.JsonRpcMessage is JsonRpcResponse response && response.Result is JsonObject result && result.ContainsKey("tools")) if (context.JsonRpcMessage is JsonRpcResponse response && response.Result is JsonObject result && result.ContainsKey("tools"))
{ {
@ -361,7 +373,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.WithTools<TestTool>(); .WithTools<TestTool>();
StartServer(); StartServer();
@ -379,7 +391,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
public async Task AddOutgoingMessageFilter_Can_Send_Additional_Messages() public async Task AddOutgoingMessageFilter_Can_Send_Additional_Messages()
{ {
McpServerBuilder McpServerBuilder
.AddOutgoingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddOutgoingFilter((next) => async (context, cancellationToken) =>
{ {
if (context.JsonRpcMessage is JsonRpcResponse response && response.Result is JsonObject result && result.ContainsKey("tools")) if (context.JsonRpcMessage is JsonRpcResponse response && response.Result is JsonObject result && result.ContainsKey("tools"))
{ {
@ -394,7 +406,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.WithTools<TestTool>(); .WithTools<TestTool>();
StartServer(); StartServer();
@ -422,7 +434,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
string? capturedValue = null; string? capturedValue = null;
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
// Set an item in the message filter // Set an item in the message filter
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList)
@ -430,8 +442,8 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
context.Items["messageFilterKey"] = "messageFilterValue"; context.Items["messageFilterKey"] = "messageFilterValue";
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.AddListToolsFilter((next) => async (request, cancellationToken) => .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) =>
{ {
// Read the item in the request-specific filter // Read the item in the request-specific filter
if (request.Items.TryGetValue("messageFilterKey", out var value)) if (request.Items.TryGetValue("messageFilterKey", out var value))
@ -439,7 +451,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
capturedValue = value as string; capturedValue = value as string;
} }
return await next(request, cancellationToken); return await next(request, cancellationToken);
}) }))
.WithTools<TestTool>(); .WithTools<TestTool>();
StartServer(); StartServer();
@ -457,7 +469,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
object? capturedValue = null; object? capturedValue = null;
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
// Set an item in the message filter for CallTool requests // Set an item in the message filter for CallTool requests
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsCall) if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsCall)
@ -465,8 +477,8 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
context.Items["toolContextKey"] = 42; context.Items["toolContextKey"] = 42;
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.AddCallToolFilter((next) => async (request, cancellationToken) => .WithRequestFilters(filters => filters.AddCallToolFilter((next) => async (request, cancellationToken) =>
{ {
// Read the item in the call tool filter // Read the item in the call tool filter
if (request.Items.TryGetValue("toolContextKey", out var value)) if (request.Items.TryGetValue("toolContextKey", out var value))
@ -474,7 +486,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
capturedValue = value; capturedValue = value;
} }
return await next(request, cancellationToken); return await next(request, cancellationToken);
}) }))
.WithTools<SimpleTool>(); .WithTools<SimpleTool>();
StartServer(); StartServer();
@ -492,7 +504,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
ClaimsPrincipal? capturedUser = null; ClaimsPrincipal? capturedUser = null;
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
// Set a custom user in the message filter for CallTool requests // Set a custom user in the message filter for CallTool requests
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsCall) if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsCall)
@ -502,13 +514,13 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
context.User = new ClaimsPrincipal(identity); context.User = new ClaimsPrincipal(identity);
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.AddCallToolFilter((next) => async (request, cancellationToken) => .WithRequestFilters(filters => filters.AddCallToolFilter((next) => async (request, cancellationToken) =>
{ {
// Read the user in the call tool filter // Read the user in the call tool filter
capturedUser = request.User; capturedUser = request.User;
return await next(request, cancellationToken); return await next(request, cancellationToken);
}) }))
.WithTools<SimpleTool>(); .WithTools<SimpleTool>();
StartServer(); StartServer();
@ -529,7 +541,9 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
object? secondFilterValue = null; object? secondFilterValue = null;
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters =>
{
filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
// First filter sets an item // First filter sets an item
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList)
@ -537,8 +551,9 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
context.Items["firstFilterKey"] = "firstFilterValue"; context.Items["firstFilterKey"] = "firstFilterValue";
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) });
.AddIncomingMessageFilter((next) => async (context, cancellationToken) =>
filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
// Second filter creates a new context with a new JsonRpcRequest and adds an item // Second filter creates a new context with a new JsonRpcRequest and adds an item
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList)
@ -558,14 +573,15 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
return; return;
} }
await next(context, cancellationToken); await next(context, cancellationToken);
});
}) })
.AddListToolsFilter((next) => async (request, cancellationToken) => .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) =>
{ {
// Request filter should see items from message filters // Request filter should see items from message filters
request.Items.TryGetValue("firstFilterKey", out firstFilterValue); request.Items.TryGetValue("firstFilterKey", out firstFilterValue);
request.Items.TryGetValue("secondFilterKey", out secondFilterValue); request.Items.TryGetValue("secondFilterKey", out secondFilterValue);
return await next(request, cancellationToken); return await next(request, cancellationToken);
}) }))
.WithTools<TestTool>(); .WithTools<TestTool>();
StartServer(); StartServer();
@ -584,15 +600,17 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
var observedValues = new List<string>(); var observedValues = new List<string>();
McpServerBuilder McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) => .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{ {
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList)
{ {
context.Items["sharedKey"] = "fromMessageFilter"; context.Items["sharedKey"] = "fromMessageFilter";
} }
await next(context, cancellationToken); await next(context, cancellationToken);
}) }))
.AddListToolsFilter((next) => async (request, cancellationToken) => .WithRequestFilters(filters =>
{
filters.AddListToolsFilter((next) => async (request, cancellationToken) =>
{ {
// First request filter reads and modifies // First request filter reads and modifies
if (request.Items.TryGetValue("sharedKey", out var value)) if (request.Items.TryGetValue("sharedKey", out var value))
@ -601,8 +619,9 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
request.Items["sharedKey"] = "modifiedByFilter1"; request.Items["sharedKey"] = "modifiedByFilter1";
} }
return await next(request, cancellationToken); return await next(request, cancellationToken);
}) });
.AddListToolsFilter((next) => async (request, cancellationToken) =>
filters.AddListToolsFilter((next) => async (request, cancellationToken) =>
{ {
// Second request filter should see modified value // Second request filter should see modified value
if (request.Items.TryGetValue("sharedKey", out var value)) if (request.Items.TryGetValue("sharedKey", out var value))
@ -610,6 +629,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
observedValues.Add((string)value!); observedValues.Add((string)value!);
} }
return await next(request, cancellationToken); return await next(request, cancellationToken);
});
}) })
.WithTools<TestTool>(); .WithTools<TestTool>();

Some files were not shown because too many files have changed in this diff Show more