Split configuration of request and message filters (#1308)

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

View file

@ -23,24 +23,28 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
List<string> 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<TestTool>()
.WithPrompts<TestPrompt>()
@ -69,12 +73,12 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
List<string> 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<TestTool>();
StartServer();
@ -91,19 +95,23 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
public async Task AddIncomingMessageFilter_Multiple_Filters_Execute_In_Order()
{
McpServerBuilder
.AddIncomingMessageFilter((next) => async (context, cancellationToken) =>
.WithMessageFilters(filters =>
{
var logger = GetLogger(context.Services, "MessageFilter1");
logger.LogInformation("MessageFilter1 before");
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");
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<TestTool>();
@ -142,11 +150,11 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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<TestTool>();
StartServer();
@ -166,18 +174,22 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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<TestTool>();
@ -196,14 +208,14 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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<TestTool>();
StartServer();
@ -219,7 +231,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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 class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
throw new InvalidOperationException("Filter exception");
}
await next(context, cancellationToken);
})
}))
.WithTools<TestTool>();
StartServer();
@ -248,19 +260,19 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
var executionOrder = new List<string>();
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<TestTool>();
StartServer();
@ -279,7 +291,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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 class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
return;
}
await next(context, cancellationToken);
})
}))
.WithTools<TestTool>();
StartServer();
@ -310,7 +322,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
var observedMessages = new List<string>();
McpServerBuilder
.AddOutgoingMessageFilter((next) => async (context, cancellationToken) =>
.WithMessageFilters(filters => filters.AddOutgoingFilter((next) => async (context, cancellationToken) =>
{
switch (context.JsonRpcMessage)
{
@ -330,7 +342,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
}
await next(context, cancellationToken);
})
}))
.WithTools<ProgressTool>();
StartServer();
@ -353,7 +365,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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 class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
}
await next(context, cancellationToken);
})
}))
.WithTools<TestTool>();
StartServer();
@ -379,7 +391,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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 class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
}
await next(context, cancellationToken);
})
}))
.WithTools<TestTool>();
StartServer();
@ -422,7 +434,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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 class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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 class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
capturedValue = value as string;
}
return await next(request, cancellationToken);
})
}))
.WithTools<TestTool>();
StartServer();
@ -457,7 +469,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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 class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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 class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
capturedValue = value;
}
return await next(request, cancellationToken);
})
}))
.WithTools<SimpleTool>();
StartServer();
@ -492,7 +504,7 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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 class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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<SimpleTool>();
StartServer();
@ -529,43 +541,47 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
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";
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 },
};
await next(newContext, cancellationToken);
return;
}
await next(context, cancellationToken);
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<TestTool>();
StartServer();
@ -584,32 +600,36 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
var observedValues = new List<string>();
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<TestTool>();

View file

@ -0,0 +1,350 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
namespace ModelContextProtocol.Tests.Configuration;
public class McpServerBuilderExtensionsRequestFilterTests(ITestOutputHelper testOutputHelper) : ClientServerTestBase(testOutputHelper)
{
private static ILogger GetLogger(IServiceProvider? services, string categoryName)
{
var loggerFactory = services?.GetRequiredService<ILoggerFactory>() ?? throw new InvalidOperationException("LoggerFactory not available");
return loggerFactory.CreateLogger(categoryName);
}
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
mcpServerBuilder
.WithRequestFilters(filters =>
{
filters.AddListResourceTemplatesFilter((next) => async (request, cancellationToken) =>
{
var logger = GetLogger(request.Services, "ListResourceTemplatesFilter");
logger.LogInformation("ListResourceTemplatesFilter executed");
return await next(request, cancellationToken);
});
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) =>
{
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
{
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<TestTool>()
.WithPrompts<TestPrompt>()
.WithResources<TestResource>()
.WithSetLoggingLevelHandler(async (request, cancellationToken) => new EmptyResult())
.WithListResourceTemplatesHandler(async (request, cancellationToken) => new ListResourceTemplatesResult
{
ResourceTemplates = [new() { Name = "test", UriTemplate = "test://resource/{id}" }]
})
.WithCompleteHandler(async (request, cancellationToken) => new CompleteResult
{
Completion = new() { Values = ["test"] }
});
}
[Fact]
public async Task AddListResourceTemplatesFilter_Logs_When_ListResourceTemplates_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "ListResourceTemplatesFilter executed");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("ListResourceTemplatesFilter", logMessage.Category);
}
[Fact]
public async Task AddListToolsFilter_Logs_When_ListTools_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "ListToolsFilter executed");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("ListToolsFilter", logMessage.Category);
}
[Fact]
public async Task AddCallToolFilter_Logs_When_CallTool_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await client.CallToolAsync("test_tool_method", cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "CallToolFilter executed for tool: test_tool_method");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("CallToolFilter", logMessage.Category);
}
[Fact]
public async Task AddCallToolFilter_Catches_Exception_From_Tool()
{
await using McpClient client = await CreateMcpClientForServer();
var result = await client.CallToolAsync("throwing_tool_method", cancellationToken: TestContext.Current.CancellationToken);
Assert.True(result.IsError);
Assert.NotNull(result.Content);
var textContent = Assert.Single(result.Content);
var textBlock = Assert.IsType<TextContentBlock>(textContent);
Assert.Equal("Error from filter: This tool always throws an exception", textBlock.Text);
}
[Fact]
public async Task AddListPromptsFilter_Logs_When_ListPrompts_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "ListPromptsFilter executed");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("ListPromptsFilter", logMessage.Category);
}
[Fact]
public async Task AddGetPromptFilter_Logs_When_GetPrompt_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await client.GetPromptAsync("test_prompt_method", cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "GetPromptFilter executed for prompt: test_prompt_method");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("GetPromptFilter", logMessage.Category);
}
[Fact]
public async Task AddListResourcesFilter_Logs_When_ListResources_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "ListResourcesFilter executed");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("ListResourcesFilter", logMessage.Category);
}
[Fact]
public async Task AddReadResourceFilter_Logs_When_ReadResource_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await client.ReadResourceAsync("test://resource/123", cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "ReadResourceFilter executed for resource: test://resource/{id}");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("ReadResourceFilter", logMessage.Category);
}
[Fact]
public async Task AddCompleteFilter_Logs_When_Complete_Called()
{
await using McpClient client = await CreateMcpClientForServer();
var reference = new PromptReference { Name = "test_prompt_method" };
await client.CompleteAsync(reference, "argument", "value", cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "CompleteFilter executed");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("CompleteFilter", logMessage.Category);
}
[Fact]
public async Task AddSubscribeToResourcesFilter_Logs_When_SubscribeToResources_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await client.SubscribeToResourceAsync("test://resource/123", cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "SubscribeToResourcesFilter executed");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("SubscribeToResourcesFilter", logMessage.Category);
}
[Fact]
public async Task AddUnsubscribeFromResourcesFilter_Logs_When_UnsubscribeFromResources_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await client.UnsubscribeFromResourceAsync("test://resource/123", cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "UnsubscribeFromResourcesFilter executed");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("UnsubscribeFromResourcesFilter", logMessage.Category);
}
[Fact]
public async Task AddSetLoggingLevelFilter_Logs_When_SetLoggingLevel_Called()
{
await using McpClient client = await CreateMcpClientForServer();
await client.SetLoggingLevelAsync(LoggingLevel.Info, cancellationToken: TestContext.Current.CancellationToken);
var logMessage = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "SetLoggingLevelFilter executed");
Assert.Equal(LogLevel.Information, logMessage.LogLevel);
Assert.Equal("SetLoggingLevelFilter", logMessage.Category);
}
[Fact]
public async Task AddListToolsFilter_Multiple_Filters_Log_In_Expected_Order()
{
await using McpClient client = await CreateMcpClientForServer();
await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
var logMessages = MockLoggerProvider.LogMessages
.Where(m => m.Category.StartsWith("ListToolsOrder"))
.Select(m => m.Message);
Assert.Collection(logMessages,
m => Assert.Equal("ListToolsOrder1 before", m),
m => Assert.Equal("ListToolsOrder2 before", m),
m => Assert.Equal("ListToolsOrder2 after", m),
m => Assert.Equal("ListToolsOrder1 after", m)
);
}
[McpServerToolType]
public sealed class TestTool
{
[McpServerTool]
public static string TestToolMethod()
{
return "test result";
}
[McpServerTool]
public static string ThrowingToolMethod()
{
throw new InvalidOperationException("This tool always throws an exception");
}
}
[McpServerPromptType]
public sealed class TestPrompt
{
[McpServerPrompt]
public static Task<GetPromptResult> TestPromptMethod()
{
return Task.FromResult(new GetPromptResult
{
Description = "Test prompt",
Messages = [new() { Role = Role.User, Content = new TextContentBlock { Text = "Test" } }]
});
}
}
[McpServerResourceType]
public sealed class TestResource
{
[McpServerResource(UriTemplate = "test://resource/{id}")]
public static string TestResourceMethod(string id)
{
return $"Test resource for ID: {id}";
}
}
}