From 7202e4a69fcfcb9eebed41aa8a44470d3925b7bc Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Wed, 18 Feb 2026 17:11:33 -0800 Subject: [PATCH 01/11] Split configuration of request and message filters This adds WithMessageFilters and WithRequestFilters extension methods to IMcpServerBuilder which expose an IMcpMessageFilterBuilder and IMcpRequestFilterBuilder to their respective callbacks. This avoids having to many similar looking, poorly sorted extension methods on IMcpServerBuilder. This also keeps incoming and outgoing message filters more closely associated. --- docs/concepts/filters.md | 366 ++++++++++-------- .../AuthorizationFilterSetup.cs | 28 +- .../Server/McpMessageFilters.cs | 42 ++ .../Server/McpRequestFilters.cs | 157 ++++++++ .../Server/McpServerFilters.cs | 179 +-------- .../Server/McpServerImpl.cs | 26 +- .../Server/McpServerOptions.cs | 4 +- .../Server/MessageContext.cs | 3 +- .../DefaultMcpMessageFilterBuilder.cs | 6 + .../DefaultMcpRequestFilterBuilder.cs | 6 + .../IMcpMessageFilterBuilder.cs | 14 + .../IMcpRequestFilterBuilder.cs | 14 + .../McpMessageFilterBuilderExtensions.cs | 40 ++ .../McpRequestFilterBuilderExtensions.cs | 176 +++++++++ .../McpServerBuilderExtensions.cs | 307 ++++----------- .../Program.cs | 4 +- .../McpServerBuilderExtensionsFilterTests.cs | 52 +-- ...rverBuilderExtensionsMessageFilterTests.cs | 112 +++--- 18 files changed, 856 insertions(+), 680 deletions(-) create mode 100644 src/ModelContextProtocol.Core/Server/McpMessageFilters.cs create mode 100644 src/ModelContextProtocol.Core/Server/McpRequestFilters.cs create mode 100644 src/ModelContextProtocol/DefaultMcpMessageFilterBuilder.cs create mode 100644 src/ModelContextProtocol/DefaultMcpRequestFilterBuilder.cs create mode 100644 src/ModelContextProtocol/IMcpMessageFilterBuilder.cs create mode 100644 src/ModelContextProtocol/IMcpRequestFilterBuilder.cs create mode 100644 src/ModelContextProtocol/McpMessageFilterBuilderExtensions.cs create mode 100644 src/ModelContextProtocol/McpRequestFilterBuilderExtensions.cs diff --git a/docs/concepts/filters.md b/docs/concepts/filters.md index fbf7b6d6b..100b5d23c 100644 --- a/docs/concepts/filters.md +++ b/docs/concepts/filters.md @@ -1,22 +1,22 @@ --- title: Filters author: halter73 -description: MCP Server Handler Filters +description: MCP Server Filters uid: filters --- -# MCP Server Handler Filters +# MCP Server Filters The MCP Server provides two levels of filters for intercepting and modifying request processing: -1. **Message Filters** - Low-level filters (`AddIncomingMessageFilter`, `AddOutgoingMessageFilter`) that intercept all JSON-RPC messages before routing -2. **Request-Specific Filters** - Handler-level filters (e.g., `AddListToolsFilter`, `AddCallToolFilter`) that target specific MCP operations +1. **Message Filters** - Low-level filters (`AddIncomingFilter`, `AddOutgoingFilter`) configured via `WithMessageFilters(...)`. +2. **Request-Specific Filters** - Handler-level filters (e.g., `AddListToolsFilter`, `AddCallToolFilter`) configured via `WithRequestFilters(...)`. The filters are stored in `McpServerOptions.Filters` and applied during server configuration. ## Available Request-Specific Filter Methods -The following filter methods are available: +The following request filter methods are available on `IMcpRequestFilterBuilder` inside `WithRequestFilters(...)`: - `AddListResourceTemplatesFilter` - Filter for list resource templates handlers - `AddListToolsFilter` - Filter for list tools handlers @@ -32,10 +32,11 @@ The following filter methods are available: ## Message Filters -In addition to the request-specific filters above, there are low-level message filters that intercept all JSON-RPC messages before they are routed to specific handlers: +In addition to the request-specific filters above, there are low-level message filters that intercept all JSON-RPC messages before they are routed to specific handlers. +Configure these on `IMcpMessageFilterBuilder` inside `WithMessageFilters(...)`: -- `AddIncomingMessageFilter` - Filter for all incoming JSON-RPC messages (requests and notifications) -- `AddOutgoingMessageFilter` - Filter for all outgoing JSON-RPC messages (responses and notifications) +- `AddIncomingFilter` - Filter for all incoming JSON-RPC messages (requests and notifications) +- `AddOutgoingFilter` - Filter for all outgoing JSON-RPC messages (responses and notifications) ### When to Use Message Filters @@ -49,22 +50,25 @@ Message filters operate at a lower level than request-specific filters and are u ### Incoming Message Filter -`AddIncomingMessageFilter` intercepts all incoming JSON-RPC messages before they are dispatched to request-specific handlers: +`AddIncomingFilter` intercepts all incoming JSON-RPC messages before they are dispatched to request-specific handlers: ```csharp services.AddMcpServer() - .AddIncomingMessageFilter(next => async (context, cancellationToken) => + .WithMessageFilters(messageFilters => { - var logger = context.Services?.GetService>(); - - // Access the raw JSON-RPC message - if (context.JsonRpcMessage is JsonRpcRequest request) + messageFilters.AddIncomingFilter(next => async (context, cancellationToken) => { - logger?.LogInformation($"Incoming request: {request.Method}"); - } + var logger = context.Services?.GetService>(); - // Call next to continue processing - await next(context, cancellationToken); + // Access the raw JSON-RPC message + if (context.JsonRpcMessage is JsonRpcRequest request) + { + logger?.LogInformation($"Incoming request: {request.Method}"); + } + + // Call next to continue processing + await next(context, cancellationToken); + }); }) .WithTools(); ``` @@ -83,46 +87,52 @@ Inside an incoming message filter, you have access to: You can skip the default handler by not calling `next`. This is useful for implementing custom protocol methods: ```csharp -.AddIncomingMessageFilter(next => async (context, cancellationToken) => +.WithMessageFilters(messageFilters => { - if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == "custom/myMethod") + messageFilters.AddIncomingFilter(next => async (context, cancellationToken) => { - // Handle the custom method directly - var response = new JsonRpcResponse + if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == "custom/myMethod") { - Id = request.Id, - Result = JsonSerializer.SerializeToNode(new { message = "Custom response" }) - }; - await context.Server.SendMessageAsync(response, cancellationToken); - return; // Don't call next - we handled it - } + // Handle the custom method directly + var response = new JsonRpcResponse + { + Id = request.Id, + Result = JsonSerializer.SerializeToNode(new { message = "Custom response" }) + }; + await context.Server.SendMessageAsync(response, cancellationToken); + return; // Don't call next - we handled it + } - await next(context, cancellationToken); + await next(context, cancellationToken); + }); }) ``` ### Outgoing Message Filter -`AddOutgoingMessageFilter` intercepts all outgoing JSON-RPC messages before they are sent to the client: +`AddOutgoingFilter` intercepts all outgoing JSON-RPC messages before they are sent to the client: ```csharp services.AddMcpServer() - .AddOutgoingMessageFilter(next => async (context, cancellationToken) => + .WithMessageFilters(messageFilters => { - var logger = context.Services?.GetService>(); - - // Inspect outgoing messages - switch (context.JsonRpcMessage) + messageFilters.AddOutgoingFilter(next => async (context, cancellationToken) => { - case JsonRpcResponse response: - logger?.LogInformation($"Sending response for request {response.Id}"); - break; - case JsonRpcNotification notification: - logger?.LogInformation($"Sending notification: {notification.Method}"); - break; - } + var logger = context.Services?.GetService>(); - await next(context, cancellationToken); + // Inspect outgoing messages + switch (context.JsonRpcMessage) + { + case JsonRpcResponse response: + logger?.LogInformation($"Sending response for request {response.Id}"); + break; + case JsonRpcNotification notification: + logger?.LogInformation($"Sending notification: {notification.Method}"); + break; + } + + await next(context, cancellationToken); + }); }) .WithTools(); ``` @@ -132,16 +142,19 @@ services.AddMcpServer() You can suppress outgoing messages by not calling `next`: ```csharp -.AddOutgoingMessageFilter(next => async (context, cancellationToken) => +.WithMessageFilters(messageFilters => { - // Suppress specific notifications - if (context.JsonRpcMessage is JsonRpcNotification notification && - notification.Method == "notifications/progress") + messageFilters.AddOutgoingFilter(next => async (context, cancellationToken) => { - return; // Don't send this notification - } + // Suppress specific notifications + if (context.JsonRpcMessage is JsonRpcNotification notification && + notification.Method == "notifications/progress") + { + return; // Don't send this notification + } - await next(context, cancellationToken); + await next(context, cancellationToken); + }); }) ``` @@ -150,26 +163,29 @@ You can suppress outgoing messages by not calling `next`: Outgoing message filters can send additional messages by calling `next` with a new `MessageContext`: ```csharp -.AddOutgoingMessageFilter(next => async (context, cancellationToken) => +.WithMessageFilters(messageFilters => { - // Send an extra notification before certain responses - if (context.JsonRpcMessage is JsonRpcResponse response && - response.Result is JsonObject result && - result.ContainsKey("tools")) + messageFilters.AddOutgoingFilter(next => async (context, cancellationToken) => { - var notification = new JsonRpcNotification + // Send an extra notification before certain responses + if (context.JsonRpcMessage is JsonRpcResponse response && + response.Result is JsonObject result && + result.ContainsKey("tools")) { - Method = "custom/toolsListed", - Params = new JsonObject { ["timestamp"] = DateTime.UtcNow.ToString("O") }, - Context = new JsonRpcMessageContext + var notification = new JsonRpcNotification { - RelatedTransport = context.JsonRpcMessage.Context?.RelatedTransport - } - }; - await next(new MessageContext(context.Server, notification), cancellationToken); - } + Method = "custom/toolsListed", + Params = new JsonObject { ["timestamp"] = DateTime.UtcNow.ToString("O") }, + Context = new JsonRpcMessageContext + { + RelatedTransport = context.JsonRpcMessage.Context?.RelatedTransport + } + }; + await next(new MessageContext(context.Server, notification), cancellationToken); + } - await next(context, cancellationToken); + await next(context, cancellationToken); + }); }) ``` @@ -179,11 +195,17 @@ Message filters execute in registration order, with the first registered filter ```csharp services.AddMcpServer() - .AddIncomingMessageFilter(incomingFilter1) // Incoming: executes first (outermost) - .AddIncomingMessageFilter(incomingFilter2) // Incoming: executes second - .AddOutgoingMessageFilter(outgoingFilter1) // Outgoing: executes first (outermost) - .AddOutgoingMessageFilter(outgoingFilter2) // Outgoing: executes second - .AddListToolsFilter(toolsFilter) // Request-specific filter + .WithMessageFilters(messageFilters => + { + messageFilters.AddIncomingFilter(incomingFilter1); // Incoming: executes first (outermost) + messageFilters.AddIncomingFilter(incomingFilter2); // Incoming: executes second + messageFilters.AddOutgoingFilter(outgoingFilter1); // Outgoing: executes first (outermost) + messageFilters.AddOutgoingFilter(outgoingFilter2); // Outgoing: executes second + }) + .WithRequestFilters(requestFilters => + { + requestFilters.AddListToolsFilter(toolsFilter); // Request-specific filter + }) .WithTools(); ``` @@ -218,21 +240,25 @@ OutgoingFilter1 (after next) The `Items` dictionary allows you to pass data between filters processing the same message: ```csharp -.AddIncomingMessageFilter(next => async (context, cancellationToken) => +.WithMessageFilters(messageFilters => { - context.Items["requestStartTime"] = DateTime.UtcNow; - await next(context, cancellationToken); -}) -.AddIncomingMessageFilter(next => async (context, cancellationToken) => -{ - await next(context, cancellationToken); + messageFilters.AddIncomingFilter(next => async (context, cancellationToken) => + { + context.Items["requestStartTime"] = DateTime.UtcNow; + await next(context, cancellationToken); + }); - if (context.Items.TryGetValue("requestStartTime", out var startTime)) + messageFilters.AddIncomingFilter(next => async (context, cancellationToken) => { - var elapsed = DateTime.UtcNow - (DateTime)startTime; - var logger = context.Services?.GetService>(); - logger?.LogInformation($"Request processed in {elapsed.TotalMilliseconds}ms"); - } + await next(context, cancellationToken); + + if (context.Items.TryGetValue("requestStartTime", out var startTime)) + { + var elapsed = DateTime.UtcNow - (DateTime)startTime; + var logger = context.Services?.GetService>(); + logger?.LogInformation($"Request processed in {elapsed.TotalMilliseconds}ms"); + } + }); }) ``` @@ -247,18 +273,21 @@ services.AddMcpServer() // Your base handler logic return new ListToolsResult { Tools = GetTools() }; }) - .AddListToolsFilter(next => async (context, cancellationToken) => + .WithRequestFilters(requestFilters => { - var logger = context.Services?.GetService>(); + requestFilters.AddListToolsFilter(next => async (context, cancellationToken) => + { + var logger = context.Services?.GetService>(); - // Pre-processing logic - logger?.LogInformation("Before handler execution"); + // Pre-processing logic + logger?.LogInformation("Before handler execution"); - var result = await next(context, cancellationToken); + var result = await next(context, cancellationToken); - // Post-processing logic - logger?.LogInformation("After handler execution"); - return result; + // Post-processing logic + logger?.LogInformation("After handler execution"); + return result; + }); }); ``` @@ -267,9 +296,12 @@ services.AddMcpServer() ```csharp services.AddMcpServer() .WithListToolsHandler(baseHandler) - .AddListToolsFilter(filter1) // Executes first (outermost) - .AddListToolsFilter(filter2) // Executes second - .AddListToolsFilter(filter3); // Executes third (closest to handler) + .WithRequestFilters(requestFilters => + { + requestFilters.AddListToolsFilter(filter1); // Executes first (outermost) + requestFilters.AddListToolsFilter(filter2); // Executes second + requestFilters.AddListToolsFilter(filter3); // Executes third (closest to handler) + }); ``` Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filter2 -> filter1` @@ -279,76 +311,88 @@ Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filt ### Logging ```csharp -.AddListToolsFilter(next => async (context, cancellationToken) => +.WithRequestFilters(requestFilters => { - var logger = context.Services?.GetService>(); + requestFilters.AddListToolsFilter(next => async (context, cancellationToken) => + { + var logger = context.Services?.GetService>(); - logger?.LogInformation($"Processing request from {context.Meta.ProgressToken}"); - var result = await next(context, cancellationToken); - logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools"); - return result; + logger?.LogInformation($"Processing request from {context.Meta.ProgressToken}"); + var result = await next(context, cancellationToken); + logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools"); + return result; + }); }); ``` ### Error Handling ```csharp -.AddCallToolFilter(next => async (context, cancellationToken) => +.WithRequestFilters(requestFilters => { - try - { - return await next(context, cancellationToken); - } - catch (Exception ex) + requestFilters.AddCallToolFilter(next => async (context, cancellationToken) => { - return new CallToolResult + try { - Content = new[] { new TextContent { Type = "text", Text = $"Error: {ex.Message}" } }, - IsError = true - }; - } + return await next(context, cancellationToken); + } + catch (Exception ex) + { + return new CallToolResult + { + Content = new[] { new TextContent { Type = "text", Text = $"Error: {ex.Message}" } }, + IsError = true + }; + } + }); }); ``` ### Performance Monitoring ```csharp -.AddListToolsFilter(next => async (context, cancellationToken) => +.WithRequestFilters(requestFilters => { - var logger = context.Services?.GetService>(); + requestFilters.AddListToolsFilter(next => async (context, cancellationToken) => + { + var logger = context.Services?.GetService>(); - var stopwatch = Stopwatch.StartNew(); - var result = await next(context, cancellationToken); - stopwatch.Stop(); - logger?.LogInformation($"Handler took {stopwatch.ElapsedMilliseconds}ms"); - return result; + var stopwatch = Stopwatch.StartNew(); + var result = await next(context, cancellationToken); + stopwatch.Stop(); + logger?.LogInformation($"Handler took {stopwatch.ElapsedMilliseconds}ms"); + return result; + }); }); ``` ### Caching ```csharp -.AddListResourcesFilter(next => async (context, cancellationToken) => +.WithRequestFilters(requestFilters => { - var cache = context.Services!.GetRequiredService(); - - var cacheKey = $"resources:{context.Params.Cursor}"; - if (cache.TryGetValue(cacheKey, out var cached)) + requestFilters.AddListResourcesFilter(next => async (context, cancellationToken) => { - return (ListResourcesResult)cached; - } + var cache = context.Services!.GetRequiredService(); + + var cacheKey = $"resources:{context.Params.Cursor}"; + if (cache.TryGetValue(cacheKey, out var cached)) + { + return (ListResourcesResult)cached; + } - var result = await next(context, cancellationToken); - cache.Set(cacheKey, result, TimeSpan.FromMinutes(5)); - return result; + var result = await next(context, cancellationToken); + cache.Set(cacheKey, result, TimeSpan.FromMinutes(5)); + return result; + }); }); ``` -## Built-in Authorization Filters +## Built-in Authorization Request Filters When using the ASP.NET Core integration (`ModelContextProtocol.AspNetCore`), you can add authorization filters to support `[Authorize]` and `[AllowAnonymous]` attributes on MCP server tools, prompts, and resources by calling `AddAuthorizationFilters()` on your MCP server builder. -### Enabling Authorization Filters +### Enabling Authorization Request Filters To enable authorization support, call `AddAuthorizationFilters()` when configuring your MCP server: @@ -455,25 +499,31 @@ This allows you to implement logging, metrics, or other cross-cutting concerns t ```csharp services.AddMcpServer() .WithHttpTransport() - .AddListToolsFilter(next => async (context, cancellationToken) => + .WithRequestFilters(requestFilters => { - var logger = context.Services?.GetService>(); - - // This filter runs BEFORE authorization - sees all tools - logger?.LogInformation("Request for tools list - will see all tools"); - var result = await next(context, cancellationToken); - logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools after authorization"); - return result; + requestFilters.AddListToolsFilter(next => async (context, cancellationToken) => + { + var logger = context.Services?.GetService>(); + + // This filter runs BEFORE authorization - sees all tools + logger?.LogInformation("Request for tools list - will see all tools"); + var result = await next(context, cancellationToken); + logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools after authorization"); + return result; + }); }) .AddAuthorizationFilters() // Authorization filtering happens here - .AddListToolsFilter(next => async (context, cancellationToken) => + .WithRequestFilters(requestFilters => { - var logger = context.Services?.GetService>(); + requestFilters.AddListToolsFilter(next => async (context, cancellationToken) => + { + var logger = context.Services?.GetService>(); - // This filter runs AFTER authorization - only sees authorized tools - var result = await next(context, cancellationToken); - logger?.LogInformation($"Post-auth filter sees {result.Tools?.Count ?? 0} authorized tools"); - return result; + // This filter runs AFTER authorization - only sees authorized tools + var result = await next(context, cancellationToken); + logger?.LogInformation($"Post-auth filter sees {result.Tools?.Count ?? 0} authorized tools"); + return result; + }); }) .WithTools(); ``` @@ -494,10 +544,13 @@ builder.Services.AddMcpServer() .WithHttpTransport() .AddAuthorizationFilters() // Required for authorization support .WithTools() - .AddCallToolFilter(next => async (context, cancellationToken) => + .WithRequestFilters(requestFilters => { - // Custom call tool logic - return await next(context, cancellationToken); + requestFilters.AddCallToolFilter(next => async (context, cancellationToken) => + { + // Custom call tool logic + return await next(context, cancellationToken); + }); }); var app = builder.Build(); @@ -511,19 +564,22 @@ app.Run(); You can also create custom authorization filters using the filter methods: ```csharp -.AddCallToolFilter(next => async (context, cancellationToken) => +.WithRequestFilters(requestFilters => { - // Custom authorization logic - if (context.User?.Identity?.IsAuthenticated != true) + requestFilters.AddCallToolFilter(next => async (context, cancellationToken) => { - return new CallToolResult + // Custom authorization logic + if (context.User?.Identity?.IsAuthenticated != true) { - Content = [new TextContent { Text = "Custom: Authentication required" }], - IsError = true - }; - } + return new CallToolResult + { + Content = [new TextContent { Text = "Custom: Authentication required" }], + IsError = true + }; + } - return await next(context, cancellationToken); + return await next(context, cancellationToken); + }); }); ``` diff --git a/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs b/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs index ae5e42dd8..a549ec384 100644 --- a/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs +++ b/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs @@ -43,7 +43,7 @@ public void PostConfigure(string? name, 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; @@ -57,7 +57,7 @@ await FilterAuthorizedItemsAsync( 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); @@ -73,7 +73,7 @@ private static void CheckListToolsFilter(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); if (!authResult.Succeeded) @@ -89,7 +89,7 @@ private void ConfigureCallToolFilter(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) && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) @@ -103,7 +103,7 @@ private static void CheckCallToolFilter(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; @@ -117,7 +117,7 @@ await FilterAuthorizedItemsAsync( 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); @@ -133,7 +133,7 @@ private static void CheckListResourcesFilter(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; @@ -147,7 +147,7 @@ await FilterAuthorizedItemsAsync( 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); @@ -163,7 +163,7 @@ private static void CheckListResourceTemplatesFilter(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; @@ -179,7 +179,7 @@ private void ConfigureReadResourceFilter(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) && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) @@ -193,7 +193,7 @@ private static void CheckReadResourceFilter(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; @@ -207,7 +207,7 @@ await FilterAuthorizedItemsAsync( 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); @@ -223,7 +223,7 @@ private static void CheckListPromptsFilter(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; @@ -239,7 +239,7 @@ private void ConfigureGetPromptFilter(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) && !context.Items.ContainsKey(AuthorizationFilterInvokedKey)) diff --git a/src/ModelContextProtocol.Core/Server/McpMessageFilters.cs b/src/ModelContextProtocol.Core/Server/McpMessageFilters.cs new file mode 100644 index 000000000..00a94bb19 --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpMessageFilters.cs @@ -0,0 +1,42 @@ +using ModelContextProtocol.Protocol; + +namespace ModelContextProtocol.Server; + +/// +/// Provides grouped message filter collections. +/// +public sealed class McpMessageFilters +{ + /// + /// Gets the filters for all incoming JSON-RPC messages. + /// + /// + /// + /// 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. + /// + /// + /// 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. + /// + /// + public List IncomingFilters { get; } = []; + + /// + /// Gets the filters for all outgoing JSON-RPC messages. + /// + /// + /// + /// 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. + /// + /// + /// 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. + /// + /// + public List OutgoingFilters { get; } = []; +} diff --git a/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs b/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs new file mode 100644 index 000000000..923c1fab6 --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs @@ -0,0 +1,157 @@ +using ModelContextProtocol.Protocol; + +namespace ModelContextProtocol.Server; + +/// +/// Provides grouped request-specific filter collections. +/// +public sealed class McpRequestFilters +{ + /// + /// Gets the filters for the list-tools handler pipeline. + /// + /// + /// + /// 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 + /// 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. + /// + /// + /// These filters work alongside any tools defined in the collection. + /// Tools from both sources will be combined when returning results to clients. + /// + /// + public List> ListToolsFilters { get; } = []; + + /// + /// Gets the filters for the call-tool handler pipeline. + /// + /// + /// These filters wrap handlers that are invoked when a client makes a call to a tool that isn't found in the collection. + /// The filters can modify, log, or perform additional operations on requests and responses for + /// requests. The handler should implement logic to execute the requested tool and return appropriate results. + /// + public List> CallToolFilters { get; } = []; + + /// + /// Gets the filters for the list-prompts handler pipeline. + /// + /// + /// + /// 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 + /// 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. + /// + /// + /// These filters work alongside any prompts defined in the collection. + /// Prompts from both sources will be combined when returning results to clients. + /// + /// + public List> ListPromptsFilters { get; } = []; + + /// + /// Gets the filters for the get-prompt handler pipeline. + /// + /// + /// These filters wrap handlers that are invoked when a client requests details for a specific prompt that isn't found in the collection. + /// The filters can modify, log, or perform additional operations on requests and responses for + /// requests. The handler should implement logic to fetch or generate the requested prompt and return appropriate results. + /// + public List> GetPromptFilters { get; } = []; + + /// + /// Gets the filters for the list-resource-templates handler pipeline. + /// + /// + /// 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 + /// 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. + /// + public List> ListResourceTemplatesFilters { get; } = []; + + /// + /// Gets the filters for the list-resources handler pipeline. + /// + /// + /// 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 + /// 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. + /// + public List> ListResourcesFilters { get; } = []; + + /// + /// Gets the filters for the read-resource handler pipeline. + /// + /// + /// 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 + /// requests. The handler should implement logic to locate and retrieve the requested resource. + /// + public List> ReadResourceFilters { get; } = []; + + /// + /// Gets the filters for the complete-handler pipeline. + /// + /// + /// 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 + /// requests. The handler processes auto-completion requests, returning a list of suggestions based on the + /// reference type and current argument value. + /// + public List> CompleteFilters { get; } = []; + + /// + /// Gets the filters for the subscribe-to-resources handler pipeline. + /// + /// + /// + /// 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 + /// 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. + /// + /// + /// After a successful subscription, the server should send resource change notifications to the client + /// whenever a relevant resource is created, updated, or deleted. + /// + /// + public List> SubscribeToResourcesFilters { get; } = []; + + /// + /// Gets the filters for the unsubscribe-from-resources handler pipeline. + /// + /// + /// + /// 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 + /// requests. The handler should implement logic to remove the client's subscriptions to the specified resources + /// and clean up any associated resources. + /// + /// + /// After a successful unsubscription, the server should no longer send resource change notifications + /// to the client for the specified resources. + /// + /// + public List> UnsubscribeFromResourcesFilters { get; } = []; + + /// + /// Gets the filters for the set-logging-level handler pipeline. + /// + /// + /// + /// These filters wrap handlers that process 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 + /// requests. + /// + /// + /// 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. + /// + /// + public List> SetLoggingLevelFilters { get; } = []; +} diff --git a/src/ModelContextProtocol.Core/Server/McpServerFilters.cs b/src/ModelContextProtocol.Core/Server/McpServerFilters.cs index f2fe320f1..39fa1d6a7 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerFilters.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerFilters.cs @@ -12,183 +12,12 @@ namespace ModelContextProtocol.Server; public sealed class McpServerFilters { /// - /// Gets the filters for all incoming JSON-RPC messages. + /// Gets the filters for incoming and outgoing JSON-RPC messages. /// - /// - /// - /// 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. - /// - /// - /// 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. - /// - /// - public List IncomingMessageFilters { get; } = []; + public McpMessageFilters Message { get; } = new(); /// - /// Gets the filters for all outgoing JSON-RPC messages. + /// Gets the filters for request-specific MCP handler pipelines. /// - /// - /// - /// 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. - /// - /// - /// 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. - /// - /// - public List OutgoingMessageFilters { get; } = []; - - /// - /// Gets the filters for the list-tools handler pipeline. - /// - /// - /// - /// 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 - /// 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. - /// - /// - /// These filters work alongside any tools defined in the collection. - /// Tools from both sources will be combined when returning results to clients. - /// - /// - public List> ListToolsFilters { get; } = []; - - /// - /// Gets the filters for the call-tool handler pipeline. - /// - /// - /// These filters wrap handlers that are invoked when a client makes a call to a tool that isn't found in the collection. - /// The filters can modify, log, or perform additional operations on requests and responses for - /// requests. The handler should implement logic to execute the requested tool and return appropriate results. - /// - public List> CallToolFilters { get; } = []; - - /// - /// Gets the filters for the list-prompts handler pipeline. - /// - /// - /// - /// 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 - /// 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. - /// - /// - /// These filters work alongside any prompts defined in the collection. - /// Prompts from both sources will be combined when returning results to clients. - /// - /// - public List> ListPromptsFilters { get; } = []; - - /// - /// Gets the filters for the get-prompt handler pipeline. - /// - /// - /// These filters wrap handlers that are invoked when a client requests details for a specific prompt that isn't found in the collection. - /// The filters can modify, log, or perform additional operations on requests and responses for - /// requests. The handler should implement logic to fetch or generate the requested prompt and return appropriate results. - /// - public List> GetPromptFilters { get; } = []; - - /// - /// Gets the filters for the list-resource-templates handler pipeline. - /// - /// - /// 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 - /// 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. - /// - public List> ListResourceTemplatesFilters { get; } = []; - - /// - /// Gets the filters for the list-resources handler pipeline. - /// - /// - /// 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 - /// 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. - /// - public List> ListResourcesFilters { get; } = []; - - /// - /// Gets the filters for the read-resource handler pipeline. - /// - /// - /// 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 - /// requests. The handler should implement logic to locate and retrieve the requested resource. - /// - public List> ReadResourceFilters { get; } = []; - - /// - /// Gets the filters for the complete-handler pipeline. - /// - /// - /// 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 - /// requests. The handler processes auto-completion requests, returning a list of suggestions based on the - /// reference type and current argument value. - /// - public List> CompleteFilters { get; } = []; - - /// - /// Gets the filters for the subscribe-to-resources handler pipeline. - /// - /// - /// - /// 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 - /// 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. - /// - /// - /// After a successful subscription, the server should send resource change notifications to the client - /// whenever a relevant resource is created, updated, or deleted. - /// - /// - public List> SubscribeToResourcesFilters { get; } = []; - - /// - /// Gets the filters for the unsubscribe-from-resources handler pipeline. - /// - /// - /// - /// 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 - /// requests. The handler should implement logic to remove the client's subscriptions to the specified resources - /// and clean up any associated resources. - /// - /// - /// After a successful unsubscription, the server should no longer send resource change notifications - /// to the client for the specified resources. - /// - /// - public List> UnsubscribeFromResourcesFilters { get; } = []; - - /// - /// Gets the filters for the set-logging-level handler pipeline. - /// - /// - /// - /// These filters wrap handlers that process 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 - /// requests. - /// - /// - /// 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. - /// - /// - public List> SetLoggingLevelFilters { get; } = []; + public McpRequestFilters Request { get; } = new(); } diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 7dfc0f09c..2e14b2712 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -118,8 +118,8 @@ void Register(McpServerPrimitiveCollection? collection, } // And initialize the session. - var incomingMessageFilter = BuildMessageFilterPipeline(options.Filters.IncomingMessageFilters); - var outgoingMessageFilter = BuildMessageFilterPipeline(options.Filters.OutgoingMessageFilters); + var incomingMessageFilter = BuildMessageFilterPipeline(options.Filters.Message.IncomingFilters); + var outgoingMessageFilter = BuildMessageFilterPipeline(options.Filters.Message.OutgoingFilters); _sessionHandler = new McpSessionHandler( isServer: true, _sessionTransport, @@ -259,7 +259,7 @@ private void ConfigureCompletion(McpServerOptions options) } completeHandler ??= (static async (_, __) => new CompleteResult()); - completeHandler = BuildFilterPipeline(completeHandler, options.Filters.CompleteFilters); + completeHandler = BuildFilterPipeline(completeHandler, options.Filters.Request.CompleteFilters); ServerCapabilities.Completions = new(); @@ -365,9 +365,9 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure // subscribe = true; } - listResourcesHandler = BuildFilterPipeline(listResourcesHandler, options.Filters.ListResourcesFilters); - listResourceTemplatesHandler = BuildFilterPipeline(listResourceTemplatesHandler, options.Filters.ListResourceTemplatesFilters); - readResourceHandler = BuildFilterPipeline(readResourceHandler, options.Filters.ReadResourceFilters, handler => + listResourcesHandler = BuildFilterPipeline(listResourcesHandler, options.Filters.Request.ListResourcesFilters); + listResourceTemplatesHandler = BuildFilterPipeline(listResourceTemplatesHandler, options.Filters.Request.ListResourceTemplatesFilters); + readResourceHandler = BuildFilterPipeline(readResourceHandler, options.Filters.Request.ReadResourceFilters, handler => async (request, cancellationToken) => { // Initial handler that sets MatchedPrimitive @@ -404,8 +404,8 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure throw; } }); - subscribeHandler = BuildFilterPipeline(subscribeHandler, options.Filters.SubscribeToResourcesFilters); - unsubscribeHandler = BuildFilterPipeline(unsubscribeHandler, options.Filters.UnsubscribeFromResourcesFilters); + subscribeHandler = BuildFilterPipeline(subscribeHandler, options.Filters.Request.SubscribeToResourcesFilters); + unsubscribeHandler = BuildFilterPipeline(unsubscribeHandler, options.Filters.Request.UnsubscribeFromResourcesFilters); ServerCapabilities.Resources.ListChanged = listChanged; ServerCapabilities.Resources.Subscribe = subscribe; @@ -495,8 +495,8 @@ await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(fals listChanged = true; } - listPromptsHandler = BuildFilterPipeline(listPromptsHandler, options.Filters.ListPromptsFilters); - getPromptHandler = BuildFilterPipeline(getPromptHandler, options.Filters.GetPromptFilters, handler => + listPromptsHandler = BuildFilterPipeline(listPromptsHandler, options.Filters.Request.ListPromptsFilters); + getPromptHandler = BuildFilterPipeline(getPromptHandler, options.Filters.Request.GetPromptFilters, handler => async (request, cancellationToken) => { // Initial handler that sets MatchedPrimitive @@ -617,8 +617,8 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) listChanged = true; } - listToolsHandler = BuildFilterPipeline(listToolsHandler, options.Filters.ListToolsFilters); - callToolHandler = BuildFilterPipeline(callToolHandler, options.Filters.CallToolFilters, handler => + listToolsHandler = BuildFilterPipeline(listToolsHandler, options.Filters.Request.ListToolsFilters); + callToolHandler = BuildFilterPipeline(callToolHandler, options.Filters.Request.CallToolFilters, handler => async (request, cancellationToken) => { // Initial handler that sets MatchedPrimitive @@ -818,7 +818,7 @@ private void ConfigureLogging(McpServerOptions options) // Apply filters to the handler if (setLoggingLevelHandler is not null) { - setLoggingLevelHandler = BuildFilterPipeline(setLoggingLevelHandler, options.Filters.SetLoggingLevelFilters); + setLoggingLevelHandler = BuildFilterPipeline(setLoggingLevelHandler, options.Filters.Request.SetLoggingLevelFilters); } ServerCapabilities.Logging = new(); diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs index 9d30c720a..4e26f5731 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs @@ -87,8 +87,8 @@ public sealed class McpServerOptions /// /// /// 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 - /// added will be the outermost (first to execute). + /// of various MCP server handlers. The first filter added is the outermost (first to execute), + /// and each subsequent filter wraps closer to the handler. /// public McpServerFilters Filters { get; } = new(); diff --git a/src/ModelContextProtocol.Core/Server/MessageContext.cs b/src/ModelContextProtocol.Core/Server/MessageContext.cs index 9426532f5..af5b26e90 100644 --- a/src/ModelContextProtocol.Core/Server/MessageContext.cs +++ b/src/ModelContextProtocol.Core/Server/MessageContext.cs @@ -14,7 +14,8 @@ namespace ModelContextProtocol.Server; /// /// /// This type is typically received as a parameter in message filter delegates registered via -/// or . +/// 's or +/// collections. /// /// public class MessageContext diff --git a/src/ModelContextProtocol/DefaultMcpMessageFilterBuilder.cs b/src/ModelContextProtocol/DefaultMcpMessageFilterBuilder.cs new file mode 100644 index 000000000..2b2b66a37 --- /dev/null +++ b/src/ModelContextProtocol/DefaultMcpMessageFilterBuilder.cs @@ -0,0 +1,6 @@ +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class DefaultMcpMessageFilterBuilder(IMcpServerBuilder serverBuilder) : IMcpMessageFilterBuilder +{ + public IServiceCollection Services { get; } = serverBuilder.Services; +} diff --git a/src/ModelContextProtocol/DefaultMcpRequestFilterBuilder.cs b/src/ModelContextProtocol/DefaultMcpRequestFilterBuilder.cs new file mode 100644 index 000000000..5fe3a1086 --- /dev/null +++ b/src/ModelContextProtocol/DefaultMcpRequestFilterBuilder.cs @@ -0,0 +1,6 @@ +namespace Microsoft.Extensions.DependencyInjection; + +internal sealed class DefaultMcpRequestFilterBuilder(IMcpServerBuilder serverBuilder) : IMcpRequestFilterBuilder +{ + public IServiceCollection Services { get; } = serverBuilder.Services; +} diff --git a/src/ModelContextProtocol/IMcpMessageFilterBuilder.cs b/src/ModelContextProtocol/IMcpMessageFilterBuilder.cs new file mode 100644 index 000000000..595f8080f --- /dev/null +++ b/src/ModelContextProtocol/IMcpMessageFilterBuilder.cs @@ -0,0 +1,14 @@ +using ModelContextProtocol.Server; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Provides a builder for configuring message-level MCP server filters. +/// +public interface IMcpMessageFilterBuilder +{ + /// + /// Gets the associated service collection. + /// + IServiceCollection Services { get; } +} diff --git a/src/ModelContextProtocol/IMcpRequestFilterBuilder.cs b/src/ModelContextProtocol/IMcpRequestFilterBuilder.cs new file mode 100644 index 000000000..199ee8999 --- /dev/null +++ b/src/ModelContextProtocol/IMcpRequestFilterBuilder.cs @@ -0,0 +1,14 @@ +using ModelContextProtocol.Server; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Provides a builder for configuring request-specific MCP server filters. +/// +public interface IMcpRequestFilterBuilder +{ + /// + /// Gets the associated service collection. + /// + IServiceCollection Services { get; } +} diff --git a/src/ModelContextProtocol/McpMessageFilterBuilderExtensions.cs b/src/ModelContextProtocol/McpMessageFilterBuilderExtensions.cs new file mode 100644 index 000000000..a79f877a3 --- /dev/null +++ b/src/ModelContextProtocol/McpMessageFilterBuilderExtensions.cs @@ -0,0 +1,40 @@ +using ModelContextProtocol; +using ModelContextProtocol.Server; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Provides extension methods for configuring message-level MCP server filters. +/// +public static class McpMessageFilterBuilderExtensions +{ + /// + /// Adds a filter to intercept all incoming JSON-RPC messages. + /// + /// The message filter builder instance. + /// The filter function that wraps the message handler. + /// The builder provided in . + public static IMcpMessageFilterBuilder AddIncomingFilter(this IMcpMessageFilterBuilder builder, McpMessageFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Message.IncomingFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to intercept all outgoing JSON-RPC messages. + /// + /// The message filter builder instance. + /// The filter function that wraps the message handler. + /// The builder provided in . + public static IMcpMessageFilterBuilder AddOutgoingFilter(this IMcpMessageFilterBuilder builder, McpMessageFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Message.OutgoingFilters.Add(filter)); + return builder; + } +} diff --git a/src/ModelContextProtocol/McpRequestFilterBuilderExtensions.cs b/src/ModelContextProtocol/McpRequestFilterBuilderExtensions.cs new file mode 100644 index 000000000..8ee7fb064 --- /dev/null +++ b/src/ModelContextProtocol/McpRequestFilterBuilderExtensions.cs @@ -0,0 +1,176 @@ +using ModelContextProtocol; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Provides extension methods for configuring request-specific MCP server filters. +/// +public static class McpRequestFilterBuilderExtensions +{ + /// + /// Adds a filter to the list resource templates handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddListResourceTemplatesFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.ListResourceTemplatesFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to the list tools handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddListToolsFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.ListToolsFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to the call tool handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddCallToolFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.CallToolFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to the list prompts handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddListPromptsFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.ListPromptsFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to the get prompt handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddGetPromptFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.GetPromptFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to the list resources handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddListResourcesFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.ListResourcesFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to the read resource handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddReadResourceFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.ReadResourceFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to the complete handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddCompleteFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.CompleteFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to the subscribe-to-resources handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddSubscribeToResourcesFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.SubscribeToResourcesFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to the unsubscribe-from-resources handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddUnsubscribeFromResourcesFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.UnsubscribeFromResourcesFilters.Add(filter)); + return builder; + } + + /// + /// Adds a filter to the set logging level handler pipeline. + /// + /// The request filter builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + public static IMcpRequestFilterBuilder AddSetLoggingLevelFilter(this IMcpRequestFilterBuilder builder, McpRequestFilter filter) + { + Throw.IfNull(builder); + Throw.IfNull(filter); + + builder.Services.Configure(options => options.Filters.Request.SetLoggingLevelFilters.Add(filter)); + return builder; + } +} diff --git a/src/ModelContextProtocol/McpServerBuilderExtensions.cs b/src/ModelContextProtocol/McpServerBuilderExtensions.cs index b77533574..5bb48334a 100644 --- a/src/ModelContextProtocol/McpServerBuilderExtensions.cs +++ b/src/ModelContextProtocol/McpServerBuilderExtensions.cs @@ -836,104 +836,82 @@ public static IMcpServerBuilder WithSetLoggingLevelHandler(this IMcpServerBuilde #endregion #region Filters + private const string MessageFilterObsoleteMessage = "Use WithMessageFilters() instead."; + private const string RequestFilterObsoleteMessage = "Use WithRequestFilters() instead."; + /// - /// Adds a filter to the list resource templates handler pipeline. + /// Configures message-level filters for the MCP server. /// /// The builder instance. - /// The filter function that wraps the handler. + /// A callback used to register message filters. /// The builder provided in . - /// is . - /// - /// - /// 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 - /// 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. - /// - /// - public static IMcpServerBuilder AddListResourceTemplatesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) + /// or is . + public static IMcpServerBuilder WithMessageFilters(this IMcpServerBuilder builder, Action configure) { Throw.IfNull(builder); + Throw.IfNull(configure); - builder.Services.Configure(options => options.Filters.ListResourceTemplatesFilters.Add(filter)); + configure(new DefaultMcpMessageFilterBuilder(builder)); return builder; } /// - /// Adds a filter to the list tools handler pipeline. + /// Configures request-specific filters for the MCP server. /// /// The builder instance. - /// The filter function that wraps the handler. + /// A callback used to register request-specific filters. /// The builder provided in . - /// is . - /// - /// - /// 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 - /// 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. - /// - /// - /// This filter works alongside any tools defined in the collection. - /// Tools from both sources will be combined when returning results to clients. - /// - /// - public static IMcpServerBuilder AddListToolsFilter(this IMcpServerBuilder builder, McpRequestFilter filter) + /// or is . + public static IMcpServerBuilder WithRequestFilters(this IMcpServerBuilder builder, Action configure) { Throw.IfNull(builder); + Throw.IfNull(configure); - builder.Services.Configure(options => options.Filters.ListToolsFilters.Add(filter)); + configure(new DefaultMcpRequestFilterBuilder(builder)); return builder; } + #pragma warning disable CS0436 // ObsoleteAttribute polyfill conflicts with framework type on netstandard. + /// - /// Adds a filter to the call tool handler pipeline. + /// Adds a filter to the list resource templates handler pipeline. /// /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - /// is . - /// - /// - /// This filter wraps handlers that are invoked when a client makes a call to a tool that isn't found in the collection. - /// The filter can modify, log, or perform additional operations on requests and responses for - /// requests. The handler should implement logic to execute the requested tool and return appropriate results. - /// - /// - public static IMcpServerBuilder AddCallToolFilter(this IMcpServerBuilder builder, McpRequestFilter filter) - { - Throw.IfNull(builder); + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddListResourceTemplatesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddListResourceTemplatesFilter(filter)); - builder.Services.Configure(options => options.Filters.CallToolFilters.Add(filter)); - return builder; - } + /// + /// Adds a filter to the list tools handler pipeline. + /// + /// The builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddListToolsFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddListToolsFilter(filter)); /// - /// Adds a filter to the list prompts handler pipeline. + /// Adds a filter to the call tool handler pipeline. /// /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - /// is . - /// - /// - /// 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 - /// 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. - /// - /// - /// This filter works alongside any prompts defined in the collection. - /// Prompts from both sources will be combined when returning results to clients. - /// - /// - public static IMcpServerBuilder AddListPromptsFilter(this IMcpServerBuilder builder, McpRequestFilter filter) - { - Throw.IfNull(builder); + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddCallToolFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddCallToolFilter(filter)); - builder.Services.Configure(options => options.Filters.ListPromptsFilters.Add(filter)); - return builder; - } + /// + /// Adds a filter to the list prompts handler pipeline. + /// + /// The builder instance. + /// The filter function that wraps the handler. + /// The builder provided in . + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddListPromptsFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddListPromptsFilter(filter)); /// /// Adds a filter to the get prompt handler pipeline. @@ -941,21 +919,9 @@ public static IMcpServerBuilder AddListPromptsFilter(this IMcpServerBuilder buil /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - /// is . - /// - /// - /// This filter wraps handlers that are invoked when a client requests details for a specific prompt that isn't found in the collection. - /// The filter can modify, log, or perform additional operations on requests and responses for - /// requests. The handler should implement logic to fetch or generate the requested prompt and return appropriate results. - /// - /// - public static IMcpServerBuilder AddGetPromptFilter(this IMcpServerBuilder builder, McpRequestFilter filter) - { - Throw.IfNull(builder); - - builder.Services.Configure(options => options.Filters.GetPromptFilters.Add(filter)); - return builder; - } + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddGetPromptFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddGetPromptFilter(filter)); /// /// Adds a filter to the list resources handler pipeline. @@ -963,22 +929,9 @@ public static IMcpServerBuilder AddGetPromptFilter(this IMcpServerBuilder builde /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - /// is . - /// - /// - /// 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 - /// 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. - /// - /// - public static IMcpServerBuilder AddListResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) - { - Throw.IfNull(builder); - - builder.Services.Configure(options => options.Filters.ListResourcesFilters.Add(filter)); - return builder; - } + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddListResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddListResourcesFilter(filter)); /// /// Adds a filter to the read resource handler pipeline. @@ -986,21 +939,9 @@ public static IMcpServerBuilder AddListResourcesFilter(this IMcpServerBuilder bu /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - /// is . - /// - /// - /// 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 - /// requests. The handler should implement logic to locate and retrieve the requested resource. - /// - /// - public static IMcpServerBuilder AddReadResourceFilter(this IMcpServerBuilder builder, McpRequestFilter filter) - { - Throw.IfNull(builder); - - builder.Services.Configure(options => options.Filters.ReadResourceFilters.Add(filter)); - return builder; - } + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddReadResourceFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddReadResourceFilter(filter)); /// /// Adds a filter to the complete handler pipeline. @@ -1008,22 +949,9 @@ public static IMcpServerBuilder AddReadResourceFilter(this IMcpServerBuilder bui /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - /// is . - /// - /// - /// 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 - /// requests. The handler processes auto-completion requests, returning a list of suggestions based on the - /// reference type and current argument value. - /// - /// - public static IMcpServerBuilder AddCompleteFilter(this IMcpServerBuilder builder, McpRequestFilter filter) - { - Throw.IfNull(builder); - - builder.Services.Configure(options => options.Filters.CompleteFilters.Add(filter)); - return builder; - } + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddCompleteFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddCompleteFilter(filter)); /// /// Adds a filter to the subscribe-to-resources handler pipeline. @@ -1031,26 +959,9 @@ public static IMcpServerBuilder AddCompleteFilter(this IMcpServerBuilder builder /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - /// is . - /// - /// - /// 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 - /// 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. - /// - /// - /// After a successful subscription, the server should send resource change notifications to the client - /// whenever a relevant resource is created, updated, or deleted. - /// - /// - public static IMcpServerBuilder AddSubscribeToResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) - { - Throw.IfNull(builder); - - builder.Services.Configure(options => options.Filters.SubscribeToResourcesFilters.Add(filter)); - return builder; - } + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddSubscribeToResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddSubscribeToResourcesFilter(filter)); /// /// Adds a filter to the unsubscribe-from-resources handler pipeline. @@ -1058,26 +969,9 @@ public static IMcpServerBuilder AddSubscribeToResourcesFilter(this IMcpServerBui /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - /// is . - /// - /// - /// 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 - /// requests. The handler should implement logic to remove the client's subscriptions to the specified resources - /// and clean up any associated resources. - /// - /// - /// After a successful unsubscription, the server should no longer send resource change notifications - /// to the client for the specified resources. - /// - /// - public static IMcpServerBuilder AddUnsubscribeFromResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) - { - Throw.IfNull(builder); - - builder.Services.Configure(options => options.Filters.UnsubscribeFromResourcesFilters.Add(filter)); - return builder; - } + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddUnsubscribeFromResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddUnsubscribeFromResourcesFilter(filter)); /// /// Adds a filter to the set logging level handler pipeline. @@ -1085,26 +979,9 @@ public static IMcpServerBuilder AddUnsubscribeFromResourcesFilter(this IMcpServe /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - /// is . - /// - /// - /// This filter wraps handlers that process 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 - /// requests. - /// - /// - /// 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. - /// - /// - public static IMcpServerBuilder AddSetLoggingLevelFilter(this IMcpServerBuilder builder, McpRequestFilter filter) - { - Throw.IfNull(builder); - - builder.Services.Configure(options => options.Filters.SetLoggingLevelFilters.Add(filter)); - return builder; - } + [Obsolete(RequestFilterObsoleteMessage)] + public static IMcpServerBuilder AddSetLoggingLevelFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => + builder.WithRequestFilters(filters => filters.AddSetLoggingLevelFilter(filter)); /// /// Adds a filter to intercept all incoming JSON-RPC messages. @@ -1112,36 +989,9 @@ public static IMcpServerBuilder AddSetLoggingLevelFilter(this IMcpServerBuilder /// The builder instance. /// The filter function that wraps the message handler. /// The builder provided in . - /// is . - /// - /// - /// 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. - /// - /// - /// 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. - /// - /// - /// 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: - /// - /// Call the next handler to continue processing: await next(context, cancellationToken) - /// Skip the default handlers entirely by not calling next - /// Perform operations before and/or after calling next - /// Catch and handle exceptions from inner handlers - /// - /// - /// - public static IMcpServerBuilder AddIncomingMessageFilter(this IMcpServerBuilder builder, McpMessageFilter filter) - { - Throw.IfNull(builder); - Throw.IfNull(filter); - - builder.Services.Configure(options => options.Filters.IncomingMessageFilters.Add(filter)); - return builder; - } + [Obsolete(MessageFilterObsoleteMessage)] + public static IMcpServerBuilder AddIncomingMessageFilter(this IMcpServerBuilder builder, McpMessageFilter filter) => + builder.WithMessageFilters(filters => filters.AddIncomingFilter(filter)); /// /// Adds a filter to intercept all outgoing JSON-RPC messages. @@ -1149,26 +999,11 @@ public static IMcpServerBuilder AddIncomingMessageFilter(this IMcpServerBuilder /// The builder instance. /// The filter function that wraps the message handler. /// The builder provided in . - /// is . - /// - /// - /// 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. - /// - /// - /// 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. - /// - /// - public static IMcpServerBuilder AddOutgoingMessageFilter(this IMcpServerBuilder builder, McpMessageFilter filter) - { - Throw.IfNull(builder); + [Obsolete(MessageFilterObsoleteMessage)] + public static IMcpServerBuilder AddOutgoingMessageFilter(this IMcpServerBuilder builder, McpMessageFilter filter) => + builder.WithMessageFilters(filters => filters.AddOutgoingFilter(filter)); - builder.Services.Configure(options => options.Filters.OutgoingMessageFilters.Add(filter)); - return builder; - } + #pragma warning restore CS0436 #endregion #region Transports diff --git a/tests/ModelContextProtocol.ConformanceServer/Program.cs b/tests/ModelContextProtocol.ConformanceServer/Program.cs index 980fa499b..f216a34a2 100644 --- a/tests/ModelContextProtocol.ConformanceServer/Program.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Program.cs @@ -38,7 +38,7 @@ public static async Task MainAsync(string[] args, ILoggerProvider? loggerProvide }) .WithTools() .WithTools([ConformanceTools.CreateJsonSchema202012Tool()]) - .AddCallToolFilter(next => async (request, cancellationToken) => + .WithRequestFilters(filters => filters.AddCallToolFilter(next => async (request, cancellationToken) => { var result = await next(request, cancellationToken); @@ -51,7 +51,7 @@ public static async Task MainAsync(string[] args, ILoggerProvider? loggerProvide } return result; - }) + })) .WithPrompts() .WithResources() .WithSubscribeToResourcesHandler(async (ctx, ct) => diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs index 9c9d24468..0c96a16a8 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs @@ -22,35 +22,35 @@ private static ILogger GetLogger(IServiceProvider? services, string categoryName protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) { mcpServerBuilder - .AddListResourceTemplatesFilter((next) => async (request, cancellationToken) => + .WithRequestFilters(filters => filters.AddListResourceTemplatesFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "ListResourceTemplatesFilter"); logger.LogInformation("ListResourceTemplatesFilter executed"); return await next(request, cancellationToken); - }) - .AddListToolsFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "ListToolsFilter"); logger.LogInformation("ListToolsFilter executed"); return await next(request, cancellationToken); - }) - .AddListToolsFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "ListToolsOrder1"); logger.LogInformation("ListToolsOrder1 before"); var result = await next(request, cancellationToken); logger.LogInformation("ListToolsOrder1 after"); return result; - }) - .AddListToolsFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "ListToolsOrder2"); logger.LogInformation("ListToolsOrder2 before"); var result = await next(request, cancellationToken); logger.LogInformation("ListToolsOrder2 after"); return result; - }) - .AddCallToolFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddCallToolFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "CallToolFilter"); var primitiveId = request.MatchedPrimitive?.Id ?? "unknown"; @@ -67,57 +67,57 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer IsError = true }; } - }) - .AddListPromptsFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddListPromptsFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "ListPromptsFilter"); logger.LogInformation("ListPromptsFilter executed"); return await next(request, cancellationToken); - }) - .AddGetPromptFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddGetPromptFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "GetPromptFilter"); var primitiveId = request.MatchedPrimitive?.Id ?? "unknown"; logger.LogInformation($"GetPromptFilter executed for prompt: {primitiveId}"); return await next(request, cancellationToken); - }) - .AddListResourcesFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddListResourcesFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "ListResourcesFilter"); logger.LogInformation("ListResourcesFilter executed"); return await next(request, cancellationToken); - }) - .AddReadResourceFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddReadResourceFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "ReadResourceFilter"); var primitiveId = request.MatchedPrimitive?.Id ?? "unknown"; logger.LogInformation($"ReadResourceFilter executed for resource: {primitiveId}"); return await next(request, cancellationToken); - }) - .AddCompleteFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddCompleteFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "CompleteFilter"); logger.LogInformation("CompleteFilter executed"); return await next(request, cancellationToken); - }) - .AddSubscribeToResourcesFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddSubscribeToResourcesFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "SubscribeToResourcesFilter"); logger.LogInformation("SubscribeToResourcesFilter executed"); return await next(request, cancellationToken); - }) - .AddUnsubscribeFromResourcesFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddUnsubscribeFromResourcesFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "UnsubscribeFromResourcesFilter"); logger.LogInformation("UnsubscribeFromResourcesFilter executed"); return await next(request, cancellationToken); - }) - .AddSetLoggingLevelFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddSetLoggingLevelFilter((next) => async (request, cancellationToken) => { var logger = GetLogger(request.Services, "SetLoggingLevelFilter"); logger.LogInformation("SetLoggingLevelFilter executed"); return await next(request, cancellationToken); - }) + })) .WithTools() .WithPrompts() .WithResources() diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs index 46929c0c1..87449fc7b 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs @@ -23,7 +23,7 @@ public async Task AddIncomingMessageFilter_Logs_For_Request() List messageTypes = []; McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { var logger = GetLogger(context.Services, "MessageFilter1"); logger.LogInformation("MessageFilter1 before"); @@ -34,14 +34,14 @@ public async Task AddIncomingMessageFilter_Logs_For_Request() await next(context, cancellationToken); logger.LogInformation("MessageFilter1 after"); - }) - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + })) + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { var logger = GetLogger(context.Services, "MessageFilter2"); logger.LogInformation("MessageFilter2 before"); await next(context, cancellationToken); logger.LogInformation("MessageFilter2 after"); - }) + })) .WithTools() .WithPrompts() .WithResources(); @@ -69,12 +69,12 @@ public async Task AddIncomingMessageFilter_Intercepts_Request_Messages() List messageTypes = []; McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { var messageTypeName = context.JsonRpcMessage.GetType().Name; messageTypes.Add(messageTypeName); await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -91,20 +91,20 @@ public async Task AddIncomingMessageFilter_Intercepts_Request_Messages() public async Task AddIncomingMessageFilter_Multiple_Filters_Execute_In_Order() { McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { var logger = GetLogger(context.Services, "MessageFilter1"); logger.LogInformation("MessageFilter1 before"); await next(context, cancellationToken); logger.LogInformation("MessageFilter1 after"); - }) - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + })) + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { var logger = GetLogger(context.Services, "MessageFilter2"); logger.LogInformation("MessageFilter2 before"); await next(context, cancellationToken); logger.LogInformation("MessageFilter2 after"); - }) + })) .WithTools(); StartServer(); @@ -142,11 +142,11 @@ public async Task AddIncomingMessageFilter_Has_Access_To_Server() McpServer? capturedServer = null; McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { capturedServer = context.Server; await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -166,19 +166,19 @@ public async Task AddIncomingMessageFilter_Items_Dictionary_Can_Be_Used() string? capturedValue = null; McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { context.Items["testKey"] = "testValue"; await next(context, cancellationToken); - }) - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + })) + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { if (context.Items.TryGetValue("testKey", out var value)) { capturedValue = value as string; } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -196,14 +196,14 @@ public async Task AddIncomingMessageFilter_Can_Access_JsonRpcMessage_Details() string? capturedMethod = null; McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) { capturedMethod = request.Method; } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -219,7 +219,7 @@ public async Task AddIncomingMessageFilter_Can_Access_JsonRpcMessage_Details() public async Task AddIncomingMessageFilter_Exception_Propagates_Properly() { McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { // Only throw for tools/list, not for initialize/initialized if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) @@ -227,7 +227,7 @@ public async Task AddIncomingMessageFilter_Exception_Propagates_Properly() throw new InvalidOperationException("Filter exception"); } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -248,19 +248,19 @@ public async Task AddIncomingMessageFilter_Runs_Before_Request_Specific_Filters( var executionOrder = new List(); McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) { executionOrder.Add("MessageFilter"); } await next(context, cancellationToken); - }) - .AddListToolsFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => { executionOrder.Add("ListToolsFilter"); return await next(request, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -279,7 +279,7 @@ public async Task AddIncomingMessageFilter_Runs_Before_Request_Specific_Filters( public async Task AddIncomingMessageFilter_Can_Skip_Default_Handlers() { McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { // Skip calling next for tools/list if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) @@ -288,7 +288,7 @@ public async Task AddIncomingMessageFilter_Can_Skip_Default_Handlers() return; } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -310,7 +310,7 @@ public async Task AddOutgoingMessageFilter_Sees_Initialize_Progress_And_Response var observedMessages = new List(); McpServerBuilder - .AddOutgoingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddOutgoingFilter((next) => async (context, cancellationToken) => { switch (context.JsonRpcMessage) { @@ -330,7 +330,7 @@ public async Task AddOutgoingMessageFilter_Sees_Initialize_Progress_And_Response } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -353,7 +353,7 @@ public async Task AddOutgoingMessageFilter_Sees_Initialize_Progress_And_Response public async Task AddOutgoingMessageFilter_Can_Skip_Sending_Messages() { 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")) { @@ -361,7 +361,7 @@ public async Task AddOutgoingMessageFilter_Can_Skip_Sending_Messages() } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -379,7 +379,7 @@ await Assert.ThrowsAnyAsync(async () => public async Task AddOutgoingMessageFilter_Can_Send_Additional_Messages() { 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")) { @@ -394,7 +394,7 @@ public async Task AddOutgoingMessageFilter_Can_Send_Additional_Messages() } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -422,7 +422,7 @@ public async Task AddIncomingMessageFilter_Items_Flow_To_Request_Filters() string? capturedValue = null; McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { // Set an item in the message filter if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) @@ -430,8 +430,8 @@ public async Task AddIncomingMessageFilter_Items_Flow_To_Request_Filters() context.Items["messageFilterKey"] = "messageFilterValue"; } 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 if (request.Items.TryGetValue("messageFilterKey", out var value)) @@ -439,7 +439,7 @@ public async Task AddIncomingMessageFilter_Items_Flow_To_Request_Filters() capturedValue = value as string; } return await next(request, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -457,7 +457,7 @@ public async Task AddIncomingMessageFilter_Items_Flow_To_CallTool_Handler() object? capturedValue = null; McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { // Set an item in the message filter for CallTool requests if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsCall) @@ -465,8 +465,8 @@ public async Task AddIncomingMessageFilter_Items_Flow_To_CallTool_Handler() context.Items["toolContextKey"] = 42; } 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 if (request.Items.TryGetValue("toolContextKey", out var value)) @@ -474,7 +474,7 @@ public async Task AddIncomingMessageFilter_Items_Flow_To_CallTool_Handler() capturedValue = value; } return await next(request, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -492,7 +492,7 @@ public async Task AddIncomingMessageFilter_User_Flows_To_CallTool_Handler() ClaimsPrincipal? capturedUser = null; 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 if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsCall) @@ -502,13 +502,13 @@ public async Task AddIncomingMessageFilter_User_Flows_To_CallTool_Handler() context.User = new ClaimsPrincipal(identity); } 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 capturedUser = request.User; return await next(request, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -529,7 +529,7 @@ public async Task AddIncomingMessageFilter_Items_Preserved_When_Context_Replaced object? secondFilterValue = null; McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { // First filter sets an item if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) @@ -537,8 +537,8 @@ public async Task AddIncomingMessageFilter_Items_Preserved_When_Context_Replaced context.Items["firstFilterKey"] = "firstFilterValue"; } await next(context, cancellationToken); - }) - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + })) + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { // Second filter creates a new context with a new JsonRpcRequest and adds an item if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) @@ -558,14 +558,14 @@ public async Task AddIncomingMessageFilter_Items_Preserved_When_Context_Replaced return; } 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.Items.TryGetValue("firstFilterKey", out firstFilterValue); request.Items.TryGetValue("secondFilterKey", out secondFilterValue); return await next(request, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -584,15 +584,15 @@ public async Task AddIncomingMessageFilter_Items_Flow_Through_Multiple_Request_F var observedValues = new List(); McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => { if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) { context.Items["sharedKey"] = "fromMessageFilter"; } await next(context, cancellationToken); - }) - .AddListToolsFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => { // First request filter reads and modifies if (request.Items.TryGetValue("sharedKey", out var value)) @@ -601,8 +601,8 @@ public async Task AddIncomingMessageFilter_Items_Flow_Through_Multiple_Request_F request.Items["sharedKey"] = "modifiedByFilter1"; } return await next(request, cancellationToken); - }) - .AddListToolsFilter((next) => async (request, cancellationToken) => + })) + .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => { // Second request filter should see modified value if (request.Items.TryGetValue("sharedKey", out var value)) @@ -610,7 +610,7 @@ public async Task AddIncomingMessageFilter_Items_Flow_Through_Multiple_Request_F observedValues.Add((string)value!); } return await next(request, cancellationToken); - }) + })) .WithTools(); StartServer(); From f93a06cb8fb5354fa096c422590dd9a8da04ca4a Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Wed, 18 Feb 2026 18:23:29 -0800 Subject: [PATCH 02/11] Update docs/concepts/filters.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/concepts/filters.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/concepts/filters.md b/docs/concepts/filters.md index 100b5d23c..5fb6a2255 100644 --- a/docs/concepts/filters.md +++ b/docs/concepts/filters.md @@ -338,9 +338,12 @@ Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filt } catch (Exception ex) { + var logger = context.Services?.GetService>(); + logger?.LogError(ex, "Error while processing CallTool request for {ProgressToken}", context.Meta.ProgressToken); + 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 }; } From fd9233b2d96229bfd9161e7c809f4277194ceb11 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Wed, 18 Feb 2026 18:34:28 -0800 Subject: [PATCH 03/11] Lazily construct McpServerFilters --- .../Server/McpServerOptions.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs index 4e26f5731..22835be7f 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs @@ -90,7 +90,15 @@ public sealed class McpServerOptions /// of various MCP server handlers. The first filter added is the outermost (first to execute), /// and each subsequent filter wraps closer to the handler. /// - public McpServerFilters Filters { get; } = new(); + public McpServerFilters Filters + { + get => field ??= new(); + set + { + Throw.IfNull(value); + field = value; + } + } /// /// Gets or sets the container of handlers used by the server for processing protocol messages. From c9b27e42c3dee64eb44cba5ed52a6c464d338381 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 19 Feb 2026 11:37:58 -0800 Subject: [PATCH 04/11] Add back intro detail to filters.md --- docs/concepts/filters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/concepts/filters.md b/docs/concepts/filters.md index 5fb6a2255..b14c64782 100644 --- a/docs/concepts/filters.md +++ b/docs/concepts/filters.md @@ -9,8 +9,8 @@ uid: filters The MCP Server provides two levels of filters for intercepting and modifying request processing: -1. **Message Filters** - Low-level filters (`AddIncomingFilter`, `AddOutgoingFilter`) configured via `WithMessageFilters(...)`. -2. **Request-Specific Filters** - Handler-level filters (e.g., `AddListToolsFilter`, `AddCallToolFilter`) configured via `WithRequestFilters(...)`. +1. **Message Filters** - Low-level filters (`AddIncomingFilter`, `AddOutgoingFilter`) configured via `WithMessageFilters(...)` that intercept all JSON-RPC messages before routing. +2. **Request-Specific Filters** - Handler-level filters (e.g., `AddListToolsFilter`, `AddCallToolFilter`) configured via `WithRequestFilters(...)` that target specific MCP operations. The filters are stored in `McpServerOptions.Filters` and applied during server configuration. From 138cf025533ac6c4e437cfa132343ccc93aac609 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 19 Feb 2026 11:41:29 -0800 Subject: [PATCH 05/11] Remove confusing phrase from filters.md --- docs/concepts/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/filters.md b/docs/concepts/filters.md index b14c64782..321c8193b 100644 --- a/docs/concepts/filters.md +++ b/docs/concepts/filters.md @@ -12,7 +12,7 @@ The MCP Server provides two levels of filters for intercepting and modifying req 1. **Message Filters** - Low-level filters (`AddIncomingFilter`, `AddOutgoingFilter`) configured via `WithMessageFilters(...)` that intercept all JSON-RPC messages before routing. 2. **Request-Specific Filters** - Handler-level filters (e.g., `AddListToolsFilter`, `AddCallToolFilter`) configured via `WithRequestFilters(...)` that target specific MCP operations. -The filters are stored in `McpServerOptions.Filters` and applied during server configuration. +The filters are stored in `McpServerOptions.Filters`. ## Available Request-Specific Filter Methods From 854776fbc0cf95e22cd353a70e208f20e1409d2a Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 19 Feb 2026 12:08:50 -0800 Subject: [PATCH 06/11] Use common obsoletion pattern for --- src/Common/Obsoletions.cs | 5 ++++ .../McpServerBuilderExtensions.cs | 29 +++++++++---------- .../ModelContextProtocol.csproj | 1 + 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Common/Obsoletions.cs b/src/Common/Obsoletions.cs index ae7581997..d75a2c841 100644 --- a/src/Common/Obsoletions.cs +++ b/src/Common/Obsoletions.cs @@ -22,4 +22,9 @@ internal static class Obsoletions 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_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."; } diff --git a/src/ModelContextProtocol/McpServerBuilderExtensions.cs b/src/ModelContextProtocol/McpServerBuilderExtensions.cs index 5bb48334a..33ecd5346 100644 --- a/src/ModelContextProtocol/McpServerBuilderExtensions.cs +++ b/src/ModelContextProtocol/McpServerBuilderExtensions.cs @@ -836,9 +836,6 @@ public static IMcpServerBuilder WithSetLoggingLevelHandler(this IMcpServerBuilde #endregion #region Filters - private const string MessageFilterObsoleteMessage = "Use WithMessageFilters() instead."; - private const string RequestFilterObsoleteMessage = "Use WithRequestFilters() instead."; - /// /// Configures message-level filters for the MCP server. /// @@ -879,7 +876,7 @@ public static IMcpServerBuilder WithRequestFilters(this IMcpServerBuilder builde /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddListResourceTemplatesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddListResourceTemplatesFilter(filter)); @@ -889,7 +886,7 @@ public static IMcpServerBuilder AddListResourceTemplatesFilter(this IMcpServerBu /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddListToolsFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddListToolsFilter(filter)); @@ -899,7 +896,7 @@ public static IMcpServerBuilder AddListToolsFilter(this IMcpServerBuilder builde /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddCallToolFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddCallToolFilter(filter)); @@ -909,7 +906,7 @@ public static IMcpServerBuilder AddCallToolFilter(this IMcpServerBuilder builder /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddListPromptsFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddListPromptsFilter(filter)); @@ -919,7 +916,7 @@ public static IMcpServerBuilder AddListPromptsFilter(this IMcpServerBuilder buil /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddGetPromptFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddGetPromptFilter(filter)); @@ -929,7 +926,7 @@ public static IMcpServerBuilder AddGetPromptFilter(this IMcpServerBuilder builde /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddListResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddListResourcesFilter(filter)); @@ -939,7 +936,7 @@ public static IMcpServerBuilder AddListResourcesFilter(this IMcpServerBuilder bu /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddReadResourceFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddReadResourceFilter(filter)); @@ -949,7 +946,7 @@ public static IMcpServerBuilder AddReadResourceFilter(this IMcpServerBuilder bui /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddCompleteFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddCompleteFilter(filter)); @@ -959,7 +956,7 @@ public static IMcpServerBuilder AddCompleteFilter(this IMcpServerBuilder builder /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddSubscribeToResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddSubscribeToResourcesFilter(filter)); @@ -969,7 +966,7 @@ public static IMcpServerBuilder AddSubscribeToResourcesFilter(this IMcpServerBui /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddUnsubscribeFromResourcesFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddUnsubscribeFromResourcesFilter(filter)); @@ -979,7 +976,7 @@ public static IMcpServerBuilder AddUnsubscribeFromResourcesFilter(this IMcpServe /// The builder instance. /// The filter function that wraps the handler. /// The builder provided in . - [Obsolete(RequestFilterObsoleteMessage)] + [Obsolete(Obsoletions.RequestFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddSetLoggingLevelFilter(this IMcpServerBuilder builder, McpRequestFilter filter) => builder.WithRequestFilters(filters => filters.AddSetLoggingLevelFilter(filter)); @@ -989,7 +986,7 @@ public static IMcpServerBuilder AddSetLoggingLevelFilter(this IMcpServerBuilder /// The builder instance. /// The filter function that wraps the message handler. /// The builder provided in . - [Obsolete(MessageFilterObsoleteMessage)] + [Obsolete(Obsoletions.MessageFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddIncomingMessageFilter(this IMcpServerBuilder builder, McpMessageFilter filter) => builder.WithMessageFilters(filters => filters.AddIncomingFilter(filter)); @@ -999,7 +996,7 @@ public static IMcpServerBuilder AddIncomingMessageFilter(this IMcpServerBuilder /// The builder instance. /// The filter function that wraps the message handler. /// The builder provided in . - [Obsolete(MessageFilterObsoleteMessage)] + [Obsolete(Obsoletions.MessageFilter_Message, DiagnosticId = Obsoletions.MessageAndRequestFilter_DiagnosticId, UrlFormat = Obsoletions.MessageAndRequestFilter_Url)] public static IMcpServerBuilder AddOutgoingMessageFilter(this IMcpServerBuilder builder, McpMessageFilter filter) => builder.WithMessageFilters(filters => filters.AddOutgoingFilter(filter)); diff --git a/src/ModelContextProtocol/ModelContextProtocol.csproj b/src/ModelContextProtocol/ModelContextProtocol.csproj index 0477ca55e..635123596 100644 --- a/src/ModelContextProtocol/ModelContextProtocol.csproj +++ b/src/ModelContextProtocol/ModelContextProtocol.csproj @@ -17,6 +17,7 @@ + From 85956c7d3b1db7b590172d9ab6c720036932e5c7 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 19 Feb 2026 12:21:21 -0800 Subject: [PATCH 07/11] Remove setters from McpServerOptions.Filters and Handlers - We can add internal properties later to lazily check if the getters have been used to avoid allocations if we care --- .../Server/McpServerOptions.cs | 30 +- .../Program.cs | 518 +++++++++--------- 2 files changed, 269 insertions(+), 279 deletions(-) diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs index 22835be7f..8b221f2fe 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs @@ -83,35 +83,19 @@ public sealed class McpServerOptions public Implementation? KnownClientInfo { get; set; } /// - /// Gets the filter collections for MCP server handlers. + /// Gets or sets the container of handlers used by the server for processing protocol messages. + /// + public McpServerHandlers Handlers { get; } = new(); + + /// + /// Gets the filter collections for incoming and outgoing messages and requests. /// /// /// This property provides access to filter collections that can be used to modify the behavior /// of various MCP server handlers. The first filter added is the outermost (first to execute), /// and each subsequent filter wraps closer to the handler. /// - public McpServerFilters Filters - { - get => field ??= new(); - set - { - Throw.IfNull(value); - field = value; - } - } - - /// - /// Gets or sets the container of handlers used by the server for processing protocol messages. - /// - public McpServerHandlers Handlers - { - get => field ??= new(); - set - { - Throw.IfNull(value); - field = value; - } - } + public McpServerFilters Filters { get; } = new(); /// /// Gets or sets a collection of tools served by the server. diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs index efd5b00bf..46528f2af 100644 --- a/tests/ModelContextProtocol.TestSseServer/Program.cs +++ b/tests/ModelContextProtocol.TestSseServer/Program.cs @@ -95,307 +95,313 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st } const int pageSize = 10; - options.Handlers = new() + options.Handlers.ListToolsHandler = async (request, cancellationToken) => { - ListToolsHandler = async (request, cancellationToken) => + return new ListToolsResult { - return new ListToolsResult - { - Tools = - [ - new Tool - { - Name = "echo", - Description = "Echoes the input back to the client.", - InputSchema = JsonElement.Parse(""" - { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "The input to echo back." - } - }, - "required": ["message"] - } - """), - }, - new Tool - { - Name = "echoSessionId", - Description = "Echoes the session id back to the client.", - InputSchema = JsonElement.Parse(""" - { - "type": "object" - } - """), - }, - new Tool - { - Name = "sampleLLM", - Description = "Samples from an LLM using MCP's sampling feature.", - InputSchema = JsonElement.Parse(""" - { - "type": "object", - "properties": { - "prompt": { - "type": "string", - "description": "The prompt to send to the LLM" - }, - "maxTokens": { - "type": "number", - "description": "Maximum number of tokens to generate" - } - }, - "required": ["prompt", "maxTokens"] - } - """), - }, - new Tool - { - Name = "longRunning", - Description = "Simulates a long-running operation that supports task-based execution.", - InputSchema = JsonElement.Parse(""" - { - "type": "object", - "properties": { - "durationMs": { - "type": "number", - "description": "Duration of the operation in milliseconds" - } + Tools = + [ + new Tool + { + Name = "echo", + Description = "Echoes the input back to the client.", + InputSchema = JsonElement.Parse(""" + { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "The input to echo back." + } + }, + "required": ["message"] + } + """), + }, + new Tool + { + Name = "echoSessionId", + Description = "Echoes the session id back to the client.", + InputSchema = JsonElement.Parse(""" + { + "type": "object" + } + """), + }, + new Tool + { + Name = "sampleLLM", + Description = "Samples from an LLM using MCP's sampling feature.", + InputSchema = JsonElement.Parse(""" + { + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "The prompt to send to the LLM" }, - "required": ["durationMs"] - } - """), - Execution = new ToolExecution + "maxTokens": { + "type": "number", + "description": "Maximum number of tokens to generate" + } + }, + "required": ["prompt", "maxTokens"] + } + """), + }, + new Tool + { + Name = "longRunning", + Description = "Simulates a long-running operation that supports task-based execution.", + InputSchema = JsonElement.Parse(""" { - TaskSupport = ToolTaskSupport.Optional + "type": "object", + "properties": { + "durationMs": { + "type": "number", + "description": "Duration of the operation in milliseconds" + } + }, + "required": ["durationMs"] } + """), + Execution = new ToolExecution + { + TaskSupport = ToolTaskSupport.Optional } - ] - }; - }, - CallToolHandler = async (request, cancellationToken) => + } + ] + }; + }; + + options.Handlers.CallToolHandler = async (request, cancellationToken) => + { + if (request.Params is null) + { + throw new McpProtocolException("Missing required parameter 'name'", McpErrorCode.InvalidParams); + } + + if (request.Params.Name == "echo") { - if (request.Params is null) + if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message)) { - throw new McpProtocolException("Missing required parameter 'name'", McpErrorCode.InvalidParams); + throw new McpProtocolException("Missing required argument 'message'", McpErrorCode.InvalidParams); } - if (request.Params.Name == "echo") + return new CallToolResult { - if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message)) - { - throw new McpProtocolException("Missing required argument 'message'", McpErrorCode.InvalidParams); - } - return new CallToolResult - { - Content = [new TextContentBlock { Text = $"Echo: {message}" }] - }; + Content = [new TextContentBlock { Text = $"Echo: {message}" }] + }; + } + else if (request.Params.Name == "echoSessionId") + { + return new CallToolResult + { + Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }] + }; + } + else if (request.Params.Name == "sampleLLM") + { + if (request.Params.Arguments is null || + !request.Params.Arguments.TryGetValue("prompt", out var prompt) || + !request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens)) + { + throw new McpProtocolException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams); } - else if (request.Params.Name == "echoSessionId") + var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())), + cancellationToken: cancellationToken); + + return new CallToolResult { - return new CallToolResult - { - Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }] - }; + Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}" }] + }; + } + else if (request.Params.Name == "longRunning") + { + if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("durationMs", out var durationMsValue)) + { + throw new McpProtocolException("Missing required argument 'durationMs'", McpErrorCode.InvalidParams); } - else if (request.Params.Name == "sampleLLM") + int durationMs = Convert.ToInt32(durationMsValue.ToString()); + await Task.Delay(durationMs, cancellationToken); + return new CallToolResult { - if (request.Params.Arguments is null || - !request.Params.Arguments.TryGetValue("prompt", out var prompt) || - !request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens)) - { - throw new McpProtocolException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams); - } - var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())), - cancellationToken: cancellationToken); + Content = [new TextContentBlock { Text = $"Long-running operation completed after {durationMs}ms" }] + }; + } + else + { + throw new McpProtocolException($"Unknown tool: '{request.Params.Name}'", McpErrorCode.InvalidParams); + } + }; - return new CallToolResult - { - Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}" }] - }; + options.Handlers.ListResourceTemplatesHandler = async (request, cancellationToken) => + { + return new ListResourceTemplatesResult + { + ResourceTemplates = [ + new ResourceTemplate + { + UriTemplate = "test://dynamic/resource/{id}", + Name = "Dynamic Resource", } - else if (request.Params.Name == "longRunning") + ] + }; + }; + + options.Handlers.ListResourcesHandler = async (request, cancellationToken) => + { + int startIndex = 0; + var requestParams = request.Params ?? new(); + + if (requestParams.Cursor is not null) + { + try { - if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("durationMs", out var durationMsValue)) - { - throw new McpProtocolException("Missing required argument 'durationMs'", McpErrorCode.InvalidParams); - } - int durationMs = Convert.ToInt32(durationMsValue.ToString()); - await Task.Delay(durationMs, cancellationToken); - return new CallToolResult - { - Content = [new TextContentBlock { Text = $"Long-running operation completed after {durationMs}ms" }] - }; + var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(requestParams.Cursor)); + startIndex = Convert.ToInt32(startIndexAsString); } - else + catch (Exception e) { - throw new McpProtocolException($"Unknown tool: '{request.Params.Name}'", McpErrorCode.InvalidParams); + throw new McpProtocolException($"Invalid cursor: '{requestParams.Cursor}'", e, McpErrorCode.InvalidParams); } - }, - ListResourceTemplatesHandler = async (request, cancellationToken) => + } + + int endIndex = Math.Min(startIndex + pageSize, resources.Count); + string? nextCursor = null; + + if (endIndex < resources.Count) { + nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString())); + } - return new ListResourceTemplatesResult - { - ResourceTemplates = [ - new ResourceTemplate - { - UriTemplate = "test://dynamic/resource/{id}", - Name = "Dynamic Resource", - } - ] - }; - }, - ListResourcesHandler = async (request, cancellationToken) => + return new ListResourcesResult { - int startIndex = 0; - var requestParams = request.Params ?? new(); - if (requestParams.Cursor is not null) - { - try - { - var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(requestParams.Cursor)); - startIndex = Convert.ToInt32(startIndexAsString); - } - catch (Exception e) - { - throw new McpProtocolException($"Invalid cursor: '{requestParams.Cursor}'", e, McpErrorCode.InvalidParams); - } - } + NextCursor = nextCursor, + Resources = resources.GetRange(startIndex, endIndex - startIndex) + }; + }; - int endIndex = Math.Min(startIndex + pageSize, resources.Count); - string? nextCursor = null; + options.Handlers.ReadResourceHandler = async (request, cancellationToken) => + { + if (request.Params?.Uri is null) + { + throw new McpProtocolException("Missing required argument 'uri'", McpErrorCode.InvalidParams); + } - if (endIndex < resources.Count) + if (request.Params.Uri.StartsWith("test://dynamic/resource/")) + { + var id = request.Params.Uri.Split('/').LastOrDefault(); + if (string.IsNullOrEmpty(id)) { - nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString())); + throw new McpProtocolException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams); } - return new ListResourcesResult + return new ReadResourceResult { - NextCursor = nextCursor, - Resources = resources.GetRange(startIndex, endIndex - startIndex) + Contents = [ + new TextResourceContents + { + Uri = request.Params.Uri, + MimeType = "text/plain", + Text = $"Dynamic resource {id}: This is a plaintext resource" + } + ] }; - }, - ReadResourceHandler = async (request, cancellationToken) => + } + + ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ?? + throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.ResourceNotFound); + + return new ReadResourceResult { - if (request.Params?.Uri is null) - { - throw new McpProtocolException("Missing required argument 'uri'", McpErrorCode.InvalidParams); - } + Contents = [contents] + }; + }; - if (request.Params.Uri.StartsWith("test://dynamic/resource/")) - { - var id = request.Params.Uri.Split('/').LastOrDefault(); - if (string.IsNullOrEmpty(id)) + options.Handlers.ListPromptsHandler = async (request, cancellationToken) => + { + return new ListPromptsResult + { + Prompts = [ + new Prompt + { + Name = "simple_prompt", + Description = "A prompt without arguments" + }, + new Prompt { - throw new McpProtocolException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams); + Name = "complex_prompt", + Description = "A prompt with arguments", + Arguments = + [ + new PromptArgument + { + Name = "temperature", + Description = "Temperature setting", + Required = true + }, + new PromptArgument + { + Name = "style", + Description = "Output style", + Required = false + } + ], } + ] + }; + }; - return new ReadResourceResult - { - Contents = [ - new TextResourceContents - { - Uri = request.Params.Uri, - MimeType = "text/plain", - Text = $"Dynamic resource {id}: This is a plaintext resource" - } - ] - }; - } + options.Handlers.GetPromptHandler = async (request, cancellationToken) => + { + if (request.Params is null) + { + throw new McpProtocolException("Missing required parameter 'name'", McpErrorCode.InvalidParams); + } - ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ?? - throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.ResourceNotFound); + List messages = []; - return new ReadResourceResult - { - Contents = [contents] - }; - }, - ListPromptsHandler = async (request, cancellationToken) => + if (request.Params.Name == "simple_prompt") { - return new ListPromptsResult + messages.Add(new PromptMessage { - Prompts = [ - new Prompt - { - Name = "simple_prompt", - Description = "A prompt without arguments" - }, - new Prompt - { - Name = "complex_prompt", - Description = "A prompt with arguments", - Arguments = - [ - new PromptArgument - { - Name = "temperature", - Description = "Temperature setting", - Required = true - }, - new PromptArgument - { - Name = "style", - Description = "Output style", - Required = false - } - ], - } - ] - }; - }, - GetPromptHandler = async (request, cancellationToken) => + Role = Role.User, + Content = new TextContentBlock { Text = "This is a simple prompt without arguments." }, + }); + } + else if (request.Params.Name == "complex_prompt") { - if (request.Params is null) + string temperature = request.Params.Arguments?["temperature"].ToString() ?? "unknown"; + string style = request.Params.Arguments?["style"].ToString() ?? "unknown"; + messages.Add(new PromptMessage { - throw new McpProtocolException("Missing required parameter 'name'", McpErrorCode.InvalidParams); - } - List messages = []; - if (request.Params.Name == "simple_prompt") + Role = Role.User, + Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, + }); + messages.Add(new PromptMessage { - messages.Add(new PromptMessage - { - Role = Role.User, - Content = new TextContentBlock { Text = "This is a simple prompt without arguments." }, - }); - } - else if (request.Params.Name == "complex_prompt") + Role = Role.User, + Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, + }); + messages.Add(new PromptMessage { - string temperature = request.Params.Arguments?["temperature"].ToString() ?? "unknown"; - string style = request.Params.Arguments?["style"].ToString() ?? "unknown"; - messages.Add(new PromptMessage - { - Role = Role.User, - Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, - }); - messages.Add(new PromptMessage - { - Role = Role.User, - Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, - }); - messages.Add(new PromptMessage + Role = Role.User, + Content = new ImageContentBlock { - Role = Role.User, - Content = new ImageContentBlock - { - Data = MCP_TINY_IMAGE, - MimeType = "image/png" - } - }); - } - else - { - throw new McpProtocolException($"Unknown prompt: {request.Params.Name}", McpErrorCode.InvalidParams); - } - - return new GetPromptResult - { - Messages = messages - }; + Data = MCP_TINY_IMAGE, + MimeType = "image/png" + } + }); + } + else + { + throw new McpProtocolException($"Unknown prompt: {request.Params.Name}", McpErrorCode.InvalidParams); } + + return new GetPromptResult + { + Messages = messages + }; }; } From 8b16ee4368c8bbd5eae0f889413a64050a46d27c Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 19 Feb 2026 14:44:02 -0800 Subject: [PATCH 08/11] Use IList for filter collections in options --- .../Server/McpMessageFilters.cs | 4 ++-- .../Server/McpRequestFilters.cs | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/ModelContextProtocol.Core/Server/McpMessageFilters.cs b/src/ModelContextProtocol.Core/Server/McpMessageFilters.cs index 00a94bb19..11923d411 100644 --- a/src/ModelContextProtocol.Core/Server/McpMessageFilters.cs +++ b/src/ModelContextProtocol.Core/Server/McpMessageFilters.cs @@ -21,7 +21,7 @@ public sealed class McpMessageFilters /// the next handler in the pipeline, the default handlers will not be executed. /// /// - public List IncomingFilters { get; } = []; + public IList IncomingFilters { get; } = []; /// /// Gets the filters for all outgoing JSON-RPC messages. @@ -38,5 +38,5 @@ public sealed class McpMessageFilters /// server-to-client messages. /// /// - public List OutgoingFilters { get; } = []; + public IList OutgoingFilters { get; } = []; } diff --git a/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs b/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs index 923c1fab6..530a30aa4 100644 --- a/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs +++ b/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs @@ -22,7 +22,7 @@ public sealed class McpRequestFilters /// Tools from both sources will be combined when returning results to clients. /// /// - public List> ListToolsFilters { get; } = []; + public IList> ListToolsFilters { get; } = []; /// /// Gets the filters for the call-tool handler pipeline. @@ -32,7 +32,7 @@ public sealed class McpRequestFilters /// The filters can modify, log, or perform additional operations on requests and responses for /// requests. The handler should implement logic to execute the requested tool and return appropriate results. /// - public List> CallToolFilters { get; } = []; + public IList> CallToolFilters { get; } = []; /// /// Gets the filters for the list-prompts handler pipeline. @@ -49,7 +49,7 @@ public sealed class McpRequestFilters /// Prompts from both sources will be combined when returning results to clients. /// /// - public List> ListPromptsFilters { get; } = []; + public IList> ListPromptsFilters { get; } = []; /// /// Gets the filters for the get-prompt handler pipeline. @@ -59,7 +59,7 @@ public sealed class McpRequestFilters /// The filters can modify, log, or perform additional operations on requests and responses for /// requests. The handler should implement logic to fetch or generate the requested prompt and return appropriate results. /// - public List> GetPromptFilters { get; } = []; + public IList> GetPromptFilters { get; } = []; /// /// Gets the filters for the list-resource-templates handler pipeline. @@ -70,7 +70,7 @@ public sealed class McpRequestFilters /// 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. /// - public List> ListResourceTemplatesFilters { get; } = []; + public IList> ListResourceTemplatesFilters { get; } = []; /// /// Gets the filters for the list-resources handler pipeline. @@ -81,7 +81,7 @@ public sealed class McpRequestFilters /// 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. /// - public List> ListResourcesFilters { get; } = []; + public IList> ListResourcesFilters { get; } = []; /// /// Gets the filters for the read-resource handler pipeline. @@ -91,7 +91,7 @@ public sealed class McpRequestFilters /// The filters can modify, log, or perform additional operations on requests and responses for /// requests. The handler should implement logic to locate and retrieve the requested resource. /// - public List> ReadResourceFilters { get; } = []; + public IList> ReadResourceFilters { get; } = []; /// /// Gets the filters for the complete-handler pipeline. @@ -102,7 +102,7 @@ public sealed class McpRequestFilters /// requests. The handler processes auto-completion requests, returning a list of suggestions based on the /// reference type and current argument value. /// - public List> CompleteFilters { get; } = []; + public IList> CompleteFilters { get; } = []; /// /// Gets the filters for the subscribe-to-resources handler pipeline. @@ -119,7 +119,7 @@ public sealed class McpRequestFilters /// whenever a relevant resource is created, updated, or deleted. /// /// - public List> SubscribeToResourcesFilters { get; } = []; + public IList> SubscribeToResourcesFilters { get; } = []; /// /// Gets the filters for the unsubscribe-from-resources handler pipeline. @@ -136,7 +136,7 @@ public sealed class McpRequestFilters /// to the client for the specified resources. /// /// - public List> UnsubscribeFromResourcesFilters { get; } = []; + public IList> UnsubscribeFromResourcesFilters { get; } = []; /// /// Gets the filters for the set-logging-level handler pipeline. @@ -153,5 +153,5 @@ public sealed class McpRequestFilters /// at or above the specified level to the client as notifications/message notifications. /// /// - public List> SetLoggingLevelFilters { get; } = []; + public IList> SetLoggingLevelFilters { get; } = []; } From 46bb1abce923604c2364146b2784b133cad7a2df Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 19 Feb 2026 14:45:01 -0800 Subject: [PATCH 09/11] Fix McpServerImpl to react to IList change --- src/ModelContextProtocol.Core/Server/McpServerImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 2e14b2712..74e2f0158 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -902,7 +902,7 @@ private void SetHandler( private static McpRequestHandler BuildFilterPipeline( McpRequestHandler baseHandler, - List> filters, + IList> filters, McpRequestFilter? initialHandler = null) { var current = baseHandler; @@ -920,7 +920,7 @@ private static McpRequestHandler BuildFilterPipeline filters) + private JsonRpcMessageFilter BuildMessageFilterPipeline(IList filters) { if (filters.Count == 0) { From f74855e713e0ac1e22f9d70cd31fca407cb79f9f Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 19 Feb 2026 14:59:44 -0800 Subject: [PATCH 10/11] Remove repetitive calls to WithRequestFilters and WithMessageFilters in tests --- ...rverBuilderExtensionsMessageFilterTests.cs | 182 +++++++++------- ...verBuilderExtensionsRequestFilterTests.cs} | 204 +++++++++--------- 2 files changed, 208 insertions(+), 178 deletions(-) rename tests/ModelContextProtocol.Tests/Configuration/{McpServerBuilderExtensionsFilterTests.cs => McpServerBuilderExtensionsRequestFilterTests.cs} (64%) diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs index 87449fc7b..ec396c5be 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs @@ -23,25 +23,29 @@ public async Task AddIncomingMessageFilter_Logs_For_Request() List messageTypes = []; McpServerBuilder - .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => { - var logger = GetLogger(context.Services, "MessageFilter1"); - logger.LogInformation("MessageFilter1 before"); + filters.AddIncomingFilter((next) => async (context, cancellationToken) => + { + var logger = GetLogger(context.Services, "MessageFilter1"); + logger.LogInformation("MessageFilter1 before"); - var messageTypeName = context.JsonRpcMessage.GetType().Name; - messageTypes.Add(messageTypeName); + var messageTypeName = context.JsonRpcMessage.GetType().Name; + messageTypes.Add(messageTypeName); - await next(context, cancellationToken); + await next(context, cancellationToken); - logger.LogInformation("MessageFilter1 after"); - })) - .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => - { - var logger = GetLogger(context.Services, "MessageFilter2"); - logger.LogInformation("MessageFilter2 before"); - await next(context, cancellationToken); - logger.LogInformation("MessageFilter2 after"); - })) + logger.LogInformation("MessageFilter1 after"); + }); + + filters.AddIncomingFilter((next) => async (context, cancellationToken) => + { + var logger = GetLogger(context.Services, "MessageFilter2"); + logger.LogInformation("MessageFilter2 before"); + await next(context, cancellationToken); + logger.LogInformation("MessageFilter2 after"); + }); + }) .WithTools() .WithPrompts() .WithResources(); @@ -91,20 +95,24 @@ public async Task AddIncomingMessageFilter_Intercepts_Request_Messages() public async Task AddIncomingMessageFilter_Multiple_Filters_Execute_In_Order() { McpServerBuilder - .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => - { - var logger = GetLogger(context.Services, "MessageFilter1"); - logger.LogInformation("MessageFilter1 before"); - await next(context, cancellationToken); - logger.LogInformation("MessageFilter1 after"); - })) - .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => { - var logger = GetLogger(context.Services, "MessageFilter2"); - logger.LogInformation("MessageFilter2 before"); - await next(context, cancellationToken); - logger.LogInformation("MessageFilter2 after"); - })) + filters.AddIncomingFilter((next) => async (context, cancellationToken) => + { + var logger = GetLogger(context.Services, "MessageFilter1"); + logger.LogInformation("MessageFilter1 before"); + await next(context, cancellationToken); + logger.LogInformation("MessageFilter1 after"); + }); + + filters.AddIncomingFilter((next) => async (context, cancellationToken) => + { + var logger = GetLogger(context.Services, "MessageFilter2"); + logger.LogInformation("MessageFilter2 before"); + await next(context, cancellationToken); + logger.LogInformation("MessageFilter2 after"); + }); + }) .WithTools(); StartServer(); @@ -166,19 +174,23 @@ public async Task AddIncomingMessageFilter_Items_Dictionary_Can_Be_Used() string? capturedValue = null; McpServerBuilder - .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => - { - context.Items["testKey"] = "testValue"; - await next(context, cancellationToken); - })) - .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => { - if (context.Items.TryGetValue("testKey", out var value)) + filters.AddIncomingFilter((next) => async (context, cancellationToken) => { - capturedValue = value as string; - } - await next(context, cancellationToken); - })) + context.Items["testKey"] = "testValue"; + await next(context, cancellationToken); + }); + + filters.AddIncomingFilter((next) => async (context, cancellationToken) => + { + if (context.Items.TryGetValue("testKey", out var value)) + { + capturedValue = value as string; + } + await next(context, cancellationToken); + }); + }) .WithTools(); StartServer(); @@ -529,36 +541,40 @@ public async Task AddIncomingMessageFilter_Items_Preserved_When_Context_Replaced object? secondFilterValue = null; McpServerBuilder - .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => - { - // First filter sets an item - if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) - { - context.Items["firstFilterKey"] = "firstFilterValue"; - } - await next(context, cancellationToken); - })) - .WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => { - // Second filter creates a new context with a new JsonRpcRequest and adds an item - if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) + filters.AddIncomingFilter((next) => async (context, cancellationToken) => { - var newRequest = new JsonRpcRequest + // First filter sets an item + if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) { - Id = request.Id, - Method = RequestMethods.ToolsList, - Params = request.Params, - Context = new JsonRpcMessageContext { RelatedTransport = request.Context?.RelatedTransport }, - }; + context.Items["firstFilterKey"] = "firstFilterValue"; + } + await next(context, cancellationToken); + }); - var newContext = new MessageContext(context.Server, newRequest); - newContext.Items["secondFilterKey"] = "secondFilterValue"; - - await next(newContext, cancellationToken); - return; - } - await next(context, cancellationToken); - })) + filters.AddIncomingFilter((next) => async (context, cancellationToken) => + { + // Second filter creates a new context with a new JsonRpcRequest and adds an item + if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) + { + var newRequest = new JsonRpcRequest + { + Id = request.Id, + Method = RequestMethods.ToolsList, + Params = request.Params, + Context = new JsonRpcMessageContext { RelatedTransport = request.Context?.RelatedTransport }, + }; + + var newContext = new MessageContext(context.Server, newRequest); + newContext.Items["secondFilterKey"] = "secondFilterValue"; + + await next(newContext, cancellationToken); + return; + } + await next(context, cancellationToken); + }); + }) .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => { // Request filter should see items from message filters @@ -592,25 +608,29 @@ public async Task AddIncomingMessageFilter_Items_Flow_Through_Multiple_Request_F } await next(context, cancellationToken); })) - .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => + .WithRequestFilters(filters => { - // First request filter reads and modifies - if (request.Items.TryGetValue("sharedKey", out var value)) + filters.AddListToolsFilter((next) => async (request, cancellationToken) => { - observedValues.Add((string)value!); - request.Items["sharedKey"] = "modifiedByFilter1"; - } - return await next(request, cancellationToken); - })) - .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => - { - // Second request filter should see modified value - if (request.Items.TryGetValue("sharedKey", out var value)) + // First request filter reads and modifies + if (request.Items.TryGetValue("sharedKey", out var value)) + { + observedValues.Add((string)value!); + request.Items["sharedKey"] = "modifiedByFilter1"; + } + return await next(request, cancellationToken); + }); + + filters.AddListToolsFilter((next) => async (request, cancellationToken) => { - observedValues.Add((string)value!); - } - return await next(request, cancellationToken); - })) + // Second request filter should see modified value + if (request.Items.TryGetValue("sharedKey", out var value)) + { + observedValues.Add((string)value!); + } + return await next(request, cancellationToken); + }); + }) .WithTools(); StartServer(); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsRequestFilterTests.cs similarity index 64% rename from tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs rename to tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsRequestFilterTests.cs index 0c96a16a8..0c4783b28 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsRequestFilterTests.cs @@ -6,13 +6,8 @@ namespace ModelContextProtocol.Tests.Configuration; -public class McpServerBuilderExtensionsFilterTests : ClientServerTestBase +public class McpServerBuilderExtensionsRequestFilterTests(ITestOutputHelper testOutputHelper) : ClientServerTestBase(testOutputHelper) { - public McpServerBuilderExtensionsFilterTests(ITestOutputHelper testOutputHelper) - : base(testOutputHelper) - { - } - private static ILogger GetLogger(IServiceProvider? services, string categoryName) { var loggerFactory = services?.GetRequiredService() ?? throw new InvalidOperationException("LoggerFactory not available"); @@ -22,102 +17,117 @@ private static ILogger GetLogger(IServiceProvider? services, string categoryName protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) { mcpServerBuilder - .WithRequestFilters(filters => filters.AddListResourceTemplatesFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "ListResourceTemplatesFilter"); - logger.LogInformation("ListResourceTemplatesFilter executed"); - return await next(request, cancellationToken); - })) - .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "ListToolsFilter"); - logger.LogInformation("ListToolsFilter executed"); - return await next(request, cancellationToken); - })) - .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "ListToolsOrder1"); - logger.LogInformation("ListToolsOrder1 before"); - var result = await next(request, cancellationToken); - logger.LogInformation("ListToolsOrder1 after"); - return result; - })) - .WithRequestFilters(filters => filters.AddListToolsFilter((next) => async (request, cancellationToken) => + .WithRequestFilters(filters => { - var logger = GetLogger(request.Services, "ListToolsOrder2"); - logger.LogInformation("ListToolsOrder2 before"); - var result = await next(request, cancellationToken); - logger.LogInformation("ListToolsOrder2 after"); - return result; - })) - .WithRequestFilters(filters => filters.AddCallToolFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "CallToolFilter"); - var primitiveId = request.MatchedPrimitive?.Id ?? "unknown"; - logger.LogInformation($"CallToolFilter executed for tool: {primitiveId}"); - try + filters.AddListResourceTemplatesFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "ListResourceTemplatesFilter"); + logger.LogInformation("ListResourceTemplatesFilter executed"); + return await next(request, cancellationToken); + }); + + filters.AddListToolsFilter((next) => async (request, cancellationToken) => { + var logger = GetLogger(request.Services, "ListToolsFilter"); + logger.LogInformation("ListToolsFilter executed"); return await next(request, cancellationToken); - } - catch (Exception ex) + }); + + filters.AddListToolsFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "ListToolsOrder1"); + logger.LogInformation("ListToolsOrder1 before"); + var result = await next(request, cancellationToken); + logger.LogInformation("ListToolsOrder1 after"); + return result; + }); + + filters.AddListToolsFilter((next) => async (request, cancellationToken) => { - return new CallToolResult + var logger = GetLogger(request.Services, "ListToolsOrder2"); + logger.LogInformation("ListToolsOrder2 before"); + var result = await next(request, cancellationToken); + logger.LogInformation("ListToolsOrder2 after"); + return result; + }); + + filters.AddCallToolFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "CallToolFilter"); + var primitiveId = request.MatchedPrimitive?.Id ?? "unknown"; + logger.LogInformation($"CallToolFilter executed for tool: {primitiveId}"); + try { - Content = [new TextContentBlock { Text = $"Error from filter: {ex.Message}" }], - IsError = true - }; - } - })) - .WithRequestFilters(filters => filters.AddListPromptsFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "ListPromptsFilter"); - logger.LogInformation("ListPromptsFilter executed"); - return await next(request, cancellationToken); - })) - .WithRequestFilters(filters => filters.AddGetPromptFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "GetPromptFilter"); - var primitiveId = request.MatchedPrimitive?.Id ?? "unknown"; - logger.LogInformation($"GetPromptFilter executed for prompt: {primitiveId}"); - return await next(request, cancellationToken); - })) - .WithRequestFilters(filters => filters.AddListResourcesFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "ListResourcesFilter"); - logger.LogInformation("ListResourcesFilter executed"); - return await next(request, cancellationToken); - })) - .WithRequestFilters(filters => filters.AddReadResourceFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "ReadResourceFilter"); - var primitiveId = request.MatchedPrimitive?.Id ?? "unknown"; - logger.LogInformation($"ReadResourceFilter executed for resource: {primitiveId}"); - return await next(request, cancellationToken); - })) - .WithRequestFilters(filters => filters.AddCompleteFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "CompleteFilter"); - logger.LogInformation("CompleteFilter executed"); - return await next(request, cancellationToken); - })) - .WithRequestFilters(filters => filters.AddSubscribeToResourcesFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "SubscribeToResourcesFilter"); - logger.LogInformation("SubscribeToResourcesFilter executed"); - return await next(request, cancellationToken); - })) - .WithRequestFilters(filters => filters.AddUnsubscribeFromResourcesFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "UnsubscribeFromResourcesFilter"); - logger.LogInformation("UnsubscribeFromResourcesFilter executed"); - return await next(request, cancellationToken); - })) - .WithRequestFilters(filters => filters.AddSetLoggingLevelFilter((next) => async (request, cancellationToken) => - { - var logger = GetLogger(request.Services, "SetLoggingLevelFilter"); - logger.LogInformation("SetLoggingLevelFilter executed"); - return await next(request, cancellationToken); - })) + return await next(request, cancellationToken); + } + catch (Exception ex) + { + return new CallToolResult + { + Content = [new TextContentBlock { Text = $"Error from filter: {ex.Message}" }], + IsError = true + }; + } + }); + + filters.AddListPromptsFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "ListPromptsFilter"); + logger.LogInformation("ListPromptsFilter executed"); + return await next(request, cancellationToken); + }); + + filters.AddGetPromptFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "GetPromptFilter"); + var primitiveId = request.MatchedPrimitive?.Id ?? "unknown"; + logger.LogInformation($"GetPromptFilter executed for prompt: {primitiveId}"); + return await next(request, cancellationToken); + }); + + filters.AddListResourcesFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "ListResourcesFilter"); + logger.LogInformation("ListResourcesFilter executed"); + return await next(request, cancellationToken); + }); + + filters.AddReadResourceFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "ReadResourceFilter"); + var primitiveId = request.MatchedPrimitive?.Id ?? "unknown"; + logger.LogInformation($"ReadResourceFilter executed for resource: {primitiveId}"); + return await next(request, cancellationToken); + }); + + filters.AddCompleteFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "CompleteFilter"); + logger.LogInformation("CompleteFilter executed"); + return await next(request, cancellationToken); + }); + + filters.AddSubscribeToResourcesFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "SubscribeToResourcesFilter"); + logger.LogInformation("SubscribeToResourcesFilter executed"); + return await next(request, cancellationToken); + }); + + filters.AddUnsubscribeFromResourcesFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "UnsubscribeFromResourcesFilter"); + logger.LogInformation("UnsubscribeFromResourcesFilter executed"); + return await next(request, cancellationToken); + }); + + filters.AddSetLoggingLevelFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "SetLoggingLevelFilter"); + logger.LogInformation("SetLoggingLevelFilter executed"); + return await next(request, cancellationToken); + }); + }) .WithTools() .WithPrompts() .WithResources() From cac77107bd1ec6ae0feb9777544d6666a141ed0a Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 19 Feb 2026 15:35:39 -0800 Subject: [PATCH 11/11] Fix bad merge --- src/ModelContextProtocol.Core/Server/McpServerOptions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs index 5654cfd63..b1a63ab0b 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs @@ -83,7 +83,6 @@ public sealed class McpServerOptions public Implementation? KnownClientInfo { get; set; } /// - /// Gets the filter collections for incoming and outgoing messages and requests. /// Gets or sets preexisting knowledge about the client's capabilities to support session migration /// scenarios where the client will not re-send the initialize request. ///