diff --git a/docs/concepts/filters.md b/docs/concepts/filters.md index fbf7b6d6b..321c8193b 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(...)` that intercept all JSON-RPC messages before routing. +2. **Request-Specific Filters** - Handler-level filters (e.g., `AddListToolsFilter`, `AddCallToolFilter`) configured via `WithRequestFilters(...)` that target specific MCP operations. -The filters are stored in `McpServerOptions.Filters` and applied during server configuration. +The filters are stored in `McpServerOptions.Filters`. ## Available Request-Specific Filter Methods -The following filter methods are available: +The following request filter methods are available on `IMcpRequestFilterBuilder` inside `WithRequestFilters(...)`: - `AddListResourceTemplatesFilter` - Filter for list resource templates handlers - `AddListToolsFilter` - Filter for list tools handlers @@ -32,10 +32,11 @@ The following filter methods are available: ## Message Filters -In addition to the request-specific filters above, there are low-level message filters that intercept all JSON-RPC messages before they are routed to specific handlers: +In addition to the request-specific filters above, there are low-level message filters that intercept all JSON-RPC messages before they are routed to specific handlers. +Configure these on `IMcpMessageFilterBuilder` inside `WithMessageFilters(...)`: -- `AddIncomingMessageFilter` - Filter for all incoming JSON-RPC messages (requests and notifications) -- `AddOutgoingMessageFilter` - Filter for all outgoing JSON-RPC messages (responses and notifications) +- `AddIncomingFilter` - Filter for all incoming JSON-RPC messages (requests and notifications) +- `AddOutgoingFilter` - Filter for all outgoing JSON-RPC messages (responses and notifications) ### When to Use Message Filters @@ -49,22 +50,25 @@ Message filters operate at a lower level than request-specific filters and are u ### Incoming Message Filter -`AddIncomingMessageFilter` intercepts all incoming JSON-RPC messages before they are dispatched to request-specific handlers: +`AddIncomingFilter` intercepts all incoming JSON-RPC messages before they are dispatched to request-specific handlers: ```csharp services.AddMcpServer() - .AddIncomingMessageFilter(next => async (context, cancellationToken) => + .WithMessageFilters(messageFilters => { - var logger = context.Services?.GetService>(); - - // 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,91 @@ 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) + { + 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 = "An unexpected error occurred while processing the tool call." } }, + IsError = true + }; + } + }); }); ``` ### Performance Monitoring ```csharp -.AddListToolsFilter(next => async (context, cancellationToken) => +.WithRequestFilters(requestFilters => { - var logger = context.Services?.GetService>(); + 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 +502,31 @@ This allows you to implement logging, metrics, or other cross-cutting concerns t ```csharp services.AddMcpServer() .WithHttpTransport() - .AddListToolsFilter(next => async (context, cancellationToken) => + .WithRequestFilters(requestFilters => { - var logger = context.Services?.GetService>(); - - // 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 +547,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 +567,22 @@ app.Run(); You can also create custom authorization filters using the filter methods: ```csharp -.AddCallToolFilter(next => async (context, cancellationToken) => +.WithRequestFilters(requestFilters => { - // Custom authorization logic - if (context.User?.Identity?.IsAuthenticated != true) + requestFilters.AddCallToolFilter(next => async (context, cancellationToken) => { - return new CallToolResult + // Custom authorization logic + if (context.User?.Identity?.IsAuthenticated != true) { - Content = [new TextContent { Text = "Custom: Authentication required" }], - IsError = true - }; - } + return new CallToolResult + { + Content = [new TextContent { Text = "Custom: Authentication required" }], + IsError = true + }; + } - return await next(context, cancellationToken); + return await next(context, cancellationToken); + }); }); ``` 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.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..11923d411 --- /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 IList 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 IList OutgoingFilters { get; } = []; +} diff --git a/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs b/src/ModelContextProtocol.Core/Server/McpRequestFilters.cs new file mode 100644 index 000000000..530a30aa4 --- /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 IList> 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 IList> 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 IList> 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 IList> 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 IList> 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 IList> 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 IList> 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 IList> 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 IList> 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 IList> 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 IList> 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 c2ae2af53..8bf2b11fb 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -119,8 +119,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, @@ -260,7 +260,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(); @@ -366,9 +366,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 @@ -405,8 +405,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; @@ -496,8 +496,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 @@ -618,8 +618,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 @@ -819,7 +819,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(); @@ -903,7 +903,7 @@ private void SetHandler( private static McpRequestHandler BuildFilterPipeline( McpRequestHandler baseHandler, - List> filters, + IList> filters, McpRequestFilter? initialHandler = null) { var current = baseHandler; @@ -921,7 +921,7 @@ private static McpRequestHandler BuildFilterPipeline filters) + private JsonRpcMessageFilter BuildMessageFilterPipeline(IList filters) { if (filters.Count == 0) { diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs index 6bc2b24a0..b1a63ab0b 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs @@ -94,29 +94,21 @@ public sealed class McpServerOptions /// public ClientCapabilities? KnownClientCapabilities { get; set; } + /// + /// 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 MCP server handlers. /// /// /// 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(); - /// - /// 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; - } - } - /// /// Gets or sets a collection of tools served by the server. /// 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 8369256b7..6ed498748 100644 --- a/src/ModelContextProtocol/McpServerBuilderExtensions.cs +++ b/src/ModelContextProtocol/McpServerBuilderExtensions.cs @@ -894,103 +894,78 @@ public static IMcpServerBuilder WithSetLoggingLevelHandler(this IMcpServerBuilde #region Filters /// - /// 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(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)); - 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(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)); /// - /// 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(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)); - 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(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)); /// /// Adds a filter to the get prompt handler pipeline. @@ -998,21 +973,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(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)); /// /// Adds a filter to the list resources handler pipeline. @@ -1020,22 +983,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(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)); /// /// Adds a filter to the read resource handler pipeline. @@ -1043,21 +993,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(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)); /// /// Adds a filter to the complete handler pipeline. @@ -1065,22 +1003,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(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)); /// /// Adds a filter to the subscribe-to-resources handler pipeline. @@ -1088,26 +1013,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(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)); /// /// Adds a filter to the unsubscribe-from-resources handler pipeline. @@ -1115,26 +1023,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(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)); /// /// Adds a filter to the set logging level handler pipeline. @@ -1142,26 +1033,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(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)); /// /// Adds a filter to intercept all incoming JSON-RPC messages. @@ -1169,36 +1043,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(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)); /// /// Adds a filter to intercept all outgoing JSON-RPC messages. @@ -1206,26 +1053,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(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)); - builder.Services.Configure(options => options.Filters.OutgoingMessageFilters.Add(filter)); - return builder; - } + #pragma warning restore CS0436 #endregion #region Transports diff --git a/src/ModelContextProtocol/ModelContextProtocol.csproj b/src/ModelContextProtocol/ModelContextProtocol.csproj index 2cbf513ba..ede6c0517 100644 --- a/src/ModelContextProtocol/ModelContextProtocol.csproj +++ b/src/ModelContextProtocol/ModelContextProtocol.csproj @@ -18,6 +18,7 @@ + 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.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs index 0a5516905..2222f5b5d 100644 --- a/tests/ModelContextProtocol.TestSseServer/Program.cs +++ b/tests/ModelContextProtocol.TestSseServer/Program.cs @@ -90,307 +90,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 = System.Text.Encoding.UTF8.GetBytes(MCP_TINY_IMAGE), - MimeType = "image/png" - } - }); - } - else - { - throw new McpProtocolException($"Unknown prompt: {request.Params.Name}", McpErrorCode.InvalidParams); - } - - return new GetPromptResult - { - Messages = messages - }; + Data = System.Text.Encoding.UTF8.GetBytes(MCP_TINY_IMAGE), + MimeType = "image/png" + } + }); + } + else + { + throw new McpProtocolException($"Unknown prompt: {request.Params.Name}", McpErrorCode.InvalidParams); } + + return new GetPromptResult + { + Messages = messages + }; }; } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs index 46929c0c1..ec396c5be 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsMessageFilterTests.cs @@ -23,24 +23,28 @@ public async Task AddIncomingMessageFilter_Logs_For_Request() List messageTypes = []; McpServerBuilder - .AddIncomingMessageFilter((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"); - }) - .AddIncomingMessageFilter((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() @@ -69,12 +73,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,19 +95,23 @@ public async Task AddIncomingMessageFilter_Intercepts_Request_Messages() public async Task AddIncomingMessageFilter_Multiple_Filters_Execute_In_Order() { McpServerBuilder - .AddIncomingMessageFilter((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 => { - 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(); @@ -142,11 +150,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,18 +174,22 @@ public async Task AddIncomingMessageFilter_Items_Dictionary_Can_Be_Used() string? capturedValue = null; McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => { - context.Items["testKey"] = "testValue"; - await next(context, cancellationToken); - }) - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => - { - 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(); @@ -196,14 +208,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 +231,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 +239,7 @@ public async Task AddIncomingMessageFilter_Exception_Propagates_Properly() throw new InvalidOperationException("Filter exception"); } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -248,19 +260,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 +291,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 +300,7 @@ public async Task AddIncomingMessageFilter_Can_Skip_Default_Handlers() return; } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -310,7 +322,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 +342,7 @@ public async Task AddOutgoingMessageFilter_Sees_Initialize_Progress_And_Response } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -353,7 +365,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 +373,7 @@ public async Task AddOutgoingMessageFilter_Can_Skip_Sending_Messages() } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -379,7 +391,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 +406,7 @@ public async Task AddOutgoingMessageFilter_Can_Send_Additional_Messages() } await next(context, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -422,7 +434,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 +442,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 +451,7 @@ public async Task AddIncomingMessageFilter_Items_Flow_To_Request_Filters() capturedValue = value as string; } return await next(request, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -457,7 +469,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 +477,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 +486,7 @@ public async Task AddIncomingMessageFilter_Items_Flow_To_CallTool_Handler() capturedValue = value; } return await next(request, cancellationToken); - }) + })) .WithTools(); StartServer(); @@ -492,7 +504,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 +514,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,43 +541,47 @@ public async Task AddIncomingMessageFilter_Items_Preserved_When_Context_Replaced object? secondFilterValue = null; McpServerBuilder - .AddIncomingMessageFilter((next) => async (context, cancellationToken) => + .WithMessageFilters(filters => { - // First filter sets an item - if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == RequestMethods.ToolsList) + filters.AddIncomingFilter((next) => async (context, cancellationToken) => { - context.Items["firstFilterKey"] = "firstFilterValue"; - } - await next(context, cancellationToken); - }) - .AddIncomingMessageFilter((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 + // 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); + }); }) - .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,32 +600,36 @@ 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 => { - // 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); - }) - .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(); 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 9c9d24468..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,101 +17,116 @@ private static ILogger GetLogger(IServiceProvider? services, string categoryName protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) { mcpServerBuilder - .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) => - { - var logger = GetLogger(request.Services, "ListToolsFilter"); - logger.LogInformation("ListToolsFilter executed"); - return await next(request, cancellationToken); - }) - .AddListToolsFilter((next) => async (request, cancellationToken) => + .WithRequestFilters(filters => { - 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) => - { - 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) => - { - 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); - } - catch (Exception ex) + }); + + filters.AddListToolsFilter((next) => async (request, cancellationToken) => + { + var logger = GetLogger(request.Services, "ListToolsFilter"); + logger.LogInformation("ListToolsFilter executed"); + return await next(request, cancellationToken); + }); + + filters.AddListToolsFilter((next) => async (request, cancellationToken) => { - return new CallToolResult + 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) => + { + 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 - }; - } - }) - .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) => - { - 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) => - { - var logger = GetLogger(request.Services, "ListResourcesFilter"); - logger.LogInformation("ListResourcesFilter executed"); - return await next(request, cancellationToken); - }) - .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) => - { - var logger = GetLogger(request.Services, "CompleteFilter"); - logger.LogInformation("CompleteFilter executed"); - return await next(request, cancellationToken); - }) - .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) => - { - var logger = GetLogger(request.Services, "UnsubscribeFromResourcesFilter"); - logger.LogInformation("UnsubscribeFromResourcesFilter executed"); - return await next(request, cancellationToken); - }) - .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()