Split configuration of request and message filters (#1308)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
27a3196c7f
commit
d21aa9e5a2
2 changed files with 1288 additions and 1078 deletions
|
|
@ -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>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue