Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 213 additions & 2 deletions docs/concepts/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ uid: filters

# MCP Server Handler Filters

For each handler type in the MCP Server, there are corresponding `AddXXXFilter` methods in `McpServerBuilderExtensions.cs` that allow you to add filters to the handler pipeline. The filters are stored in `McpServerOptions.Filters` and applied during server configuration.
The MCP Server provides two levels of filters for intercepting and modifying request processing:

## Available Filter Methods
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

The filters are stored in `McpServerOptions.Filters` and applied during server configuration.

## Available Request-Specific Filter Methods

The following filter methods are available:

Expand All @@ -25,6 +30,212 @@ The following filter methods are available:
- `AddUnsubscribeFromResourcesFilter` - Filter for resource unsubscription handlers
- `AddSetLoggingLevelFilter` - Filter for logging level handlers

## 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:

- `AddIncomingMessageFilter` - Filter for all incoming JSON-RPC messages (requests and notifications)
- `AddOutgoingMessageFilter` - Filter for all outgoing JSON-RPC messages (responses and notifications)

### When to Use Message Filters

Message filters operate at a lower level than request-specific filters and are useful when you need to:

- Intercept all messages regardless of type
- Implement custom protocol extensions or handle custom JSON-RPC methods
- Log or monitor all traffic between client and server
- Modify or skip messages before they reach handlers
- Send additional messages in response to specific events

### Incoming Message Filter

`AddIncomingMessageFilter` intercepts all incoming JSON-RPC messages before they are dispatched to request-specific handlers:

```csharp
services.AddMcpServer()
.AddIncomingMessageFilter(next => async (context, cancellationToken) =>
{
var logger = context.Services?.GetService<ILogger<Program>>();

// Access the raw JSON-RPC message
if (context.JsonRpcMessage is JsonRpcRequest request)
{
logger?.LogInformation($"Incoming request: {request.Method}");
}

// Call next to continue processing
await next(context, cancellationToken);
})
.WithTools<MyTools>();
```

#### MessageContext Properties

Inside an incoming message filter, you have access to:

- `context.JsonRpcMessage` - The incoming `JsonRpcMessage` (can be `JsonRpcRequest` or `JsonRpcNotification`)
- `context.Server` - The `McpServer` instance for sending responses or notifications
- `context.Services` - The request's service provider
- `context.Items` - A dictionary for passing data between filters

#### Skipping Default Handlers

You can skip the default handler by not calling `next`. This is useful for implementing custom protocol methods:

```csharp
.AddIncomingMessageFilter(next => async (context, cancellationToken) =>
{
if (context.JsonRpcMessage is JsonRpcRequest request && request.Method == "custom/myMethod")
{
// 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);
})
```

### Outgoing Message Filter

`AddOutgoingMessageFilter` intercepts all outgoing JSON-RPC messages before they are sent to the client:

```csharp
services.AddMcpServer()
.AddOutgoingMessageFilter(next => async (context, cancellationToken) =>
{
var logger = context.Services?.GetService<ILogger<Program>>();

// Inspect outgoing messages
switch (context.JsonRpcMessage)
{
case JsonRpcResponse response:
logger?.LogInformation($"Sending response for request {response.Id}");
break;
case JsonRpcNotification notification:
logger?.LogInformation($"Sending notification: {notification.Method}");
break;
}

await next(context, cancellationToken);
})
.WithTools<MyTools>();
```

#### Skipping Outgoing Messages

You can suppress outgoing messages by not calling `next`:

```csharp
.AddOutgoingMessageFilter(next => async (context, cancellationToken) =>
{
// Suppress specific notifications
if (context.JsonRpcMessage is JsonRpcNotification notification &&
notification.Method == "notifications/progress")
{
return; // Don't send this notification
}

await next(context, cancellationToken);
})
```

#### Sending Additional Messages

Outgoing message filters can send additional messages by calling `next` with a new `MessageContext`:

```csharp
.AddOutgoingMessageFilter(next => async (context, cancellationToken) =>
{
// Send an extra notification before certain responses
if (context.JsonRpcMessage is JsonRpcResponse response &&
response.Result is JsonObject result &&
result.ContainsKey("tools"))
{
var notification = new JsonRpcNotification
{
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);
})
```

### Message Filter Execution Order

Message filters execute in registration order, with the first registered filter being the outermost:

```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
.WithTools<MyTools>();
```

**Important**: Incoming message filters always run before request-specific filters, and outgoing message filters run when responses or notifications are sent. The complete execution flow for a request/response cycle is:

```
Request arrives
IncomingFilter1 (before next)
IncomingFilter2 (before next)
Request Routing → ListToolsFilter → Handler
IncomingFilter2 (after next)
IncomingFilter1 (after next)
Response sent via OutgoingFilter1 (before next)
OutgoingFilter2 (before next)
Transport sends message
OutgoingFilter2 (after next)
OutgoingFilter1 (after next)
```

### Passing Data Between Filters

The `Items` dictionary allows you to pass data between filters processing the same message:

```csharp
.AddIncomingMessageFilter(next => async (context, cancellationToken) =>
{
context.Items["requestStartTime"] = DateTime.UtcNow;
await next(context, cancellationToken);
})
.AddIncomingMessageFilter(next => async (context, cancellationToken) =>
{
await next(context, cancellationToken);

if (context.Items.TryGetValue("requestStartTime", out var startTime))
{
var elapsed = DateTime.UtcNow - (DateTime)startTime;
var logger = context.Services?.GetService<ILogger<Program>>();
logger?.LogInformation($"Request processed in {elapsed.TotalMilliseconds}ms");
}
})
```

## Usage

Filters are functions that take a handler and return a new handler, allowing you to wrap the original handler with additional functionality:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,6 @@ private async Task<string> GetAccessTokenAsync(HttpResponseMessage response, boo
// Get auth server metadata
var authServerMetadata = await GetAuthServerMetadataAsync(selectedAuthServer, cancellationToken).ConfigureAwait(false);

// Store auth server metadata for future refresh operations
_authServerMetadata = authServerMetadata;

// The existing access token must be invalid to have resulted in a 401 response, but refresh might still work.
var resourceUri = GetRequiredResourceUri(protectedResourceMetadata);

Expand Down Expand Up @@ -296,6 +293,9 @@ await _tokenCache.GetTokensAsync(cancellationToken).ConfigureAwait(false) is { R
}
}

// Store auth server metadata for future refresh operations
_authServerMetadata = authServerMetadata;

// Perform the OAuth flow
return await InitiateAuthorizationCodeFlowAsync(protectedResourceMetadata, authServerMetadata, cancellationToken).ConfigureAwait(false);
}
Expand Down
10 changes: 9 additions & 1 deletion src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,15 @@ internal McpClientImpl(ITransport transport, string endpointName, McpClientOptio

RegisterHandlers(options, notificationHandlers, requestHandlers);

_sessionHandler = new McpSessionHandler(isServer: false, transport, endpointName, requestHandlers, notificationHandlers, _logger);
_sessionHandler = new McpSessionHandler(
isServer: false,
transport,
endpointName,
requestHandlers,
notificationHandlers,
incomingMessageFilter: null,
outgoingMessageFilter: null,
_logger);
}

private void RegisterHandlers(McpClientOptions options, NotificationHandlers notificationHandlers, RequestHandlers requestHandlers)
Expand Down
10 changes: 10 additions & 0 deletions src/ModelContextProtocol.Core/JsonRpcMessageFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using ModelContextProtocol.Protocol;

namespace ModelContextProtocol;

/// <summary>
/// Represents a filter that wraps the processing of incoming JSON-RPC messages.
/// </summary>
/// <param name="next">The next handler in the pipeline.</param>
/// <returns>A wrapped handler that processes messages and optionally delegates to the next handler.</returns>
internal delegate Func<JsonRpcMessage, CancellationToken, Task> JsonRpcMessageFilter(Func<JsonRpcMessage, CancellationToken, Task> next);
Loading
Loading