using System.Text.Json.Nodes; using ModelContextProtocol.Interceptors; using ModelContextProtocol.Interceptors.Client; namespace ModelContextProtocol.Interceptors.Tests; public class InterceptorChainExecutorTests { [Fact] public async Task ExecuteForSendingAsync_MutationsExecuteSequentiallyByPriority() { // Arrange var executionOrder = new List(); var interceptors = new List { CreateMutationInterceptor("high-priority", -100, payload => { executionOrder.Add("high-priority"); return MutationInterceptorResult.Unchanged(payload); }), CreateMutationInterceptor("medium-priority", 0, payload => { executionOrder.Add("medium-priority"); return MutationInterceptorResult.Unchanged(payload); }), CreateMutationInterceptor("low-priority", 100, payload => { executionOrder.Add("low-priority"); return MutationInterceptorResult.Unchanged(payload); }) }; var executor = new InterceptorChainExecutor(interceptors); var payload = JsonNode.Parse("{}"); // Act await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload); // Assert - Lower priority numbers execute first Assert.Equal(["high-priority", "medium-priority", "low-priority"], executionOrder); } [Fact] public async Task ExecuteForSendingAsync_MutationsWithSamePriority_OrderAlphabetically() { // Arrange var executionOrder = new List(); var interceptors = new List { CreateMutationInterceptor("zebra", 0, payload => { executionOrder.Add("zebra"); return MutationInterceptorResult.Unchanged(payload); }), CreateMutationInterceptor("alpha", 0, payload => { executionOrder.Add("alpha"); return MutationInterceptorResult.Unchanged(payload); }), CreateMutationInterceptor("beta", 0, payload => { executionOrder.Add("beta"); return MutationInterceptorResult.Unchanged(payload); }) }; var executor = new InterceptorChainExecutor(interceptors); var payload = JsonNode.Parse("{}"); // Act await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload); // Assert - Same priority, alphabetical order Assert.Equal(["alpha", "beta", "zebra"], executionOrder); } [Fact] public async Task ExecuteForSendingAsync_ValidationErrorBlocksExecution() { // Arrange var mutationExecuted = false; var interceptors = new List { CreateMutationInterceptor("mutator", -1000, payload => { mutationExecuted = true; return MutationInterceptorResult.Mutated(JsonNode.Parse("{\"mutated\": true}")); }), CreateValidationInterceptor("validator", _ => ValidationInterceptorResult.Error("Validation failed")) }; var executor = new InterceptorChainExecutor(interceptors); var payload = JsonNode.Parse("{}"); // Act var result = await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload); // Assert // Mutations execute first in sending direction, so mutator runs before validator Assert.True(mutationExecuted, "Mutation should execute before validation in sending direction"); Assert.Equal(InterceptorChainStatus.ValidationFailed, result.Status); Assert.NotNull(result.AbortedAt); Assert.Equal("validator", result.AbortedAt.Interceptor); } [Fact] public async Task ExecuteForReceivingAsync_ValidationsExecuteBeforeMutations() { // Arrange var executionOrder = new List(); var interceptors = new List { CreateMutationInterceptor("mutator", 0, payload => { executionOrder.Add("mutator"); return MutationInterceptorResult.Unchanged(payload); }), CreateValidationInterceptor("validator", _ => { executionOrder.Add("validator"); return ValidationInterceptorResult.Success(); }) }; var executor = new InterceptorChainExecutor(interceptors); var payload = JsonNode.Parse("{}"); // Act await executor.ExecuteForReceivingAsync(InterceptorEvents.ToolsCall, payload); // Assert - In receiving direction: Validate → Mutate Assert.Equal("validator", executionOrder[0]); Assert.Equal("mutator", executionOrder[1]); } [Fact] public async Task ExecuteForReceivingAsync_ValidationErrorBlocksMutations() { // Arrange var mutationExecuted = false; var interceptors = new List { CreateMutationInterceptor("mutator", 0, payload => { mutationExecuted = true; return MutationInterceptorResult.Mutated(JsonNode.Parse("{\"mutated\": true}")); }), CreateValidationInterceptor("validator", _ => ValidationInterceptorResult.Error("Validation failed")) }; var executor = new InterceptorChainExecutor(interceptors); var payload = JsonNode.Parse("{}"); // Act var result = await executor.ExecuteForReceivingAsync(InterceptorEvents.ToolsCall, payload); // Assert - In receiving direction, validation runs first and blocks mutation Assert.False(mutationExecuted); Assert.Equal(InterceptorChainStatus.ValidationFailed, result.Status); } [Fact] public async Task ObservabilityInterceptor_NeverBlocksExecution() { // Arrange var observabilityExecuted = false; var mutationExecuted = false; var interceptors = new List { CreateMutationInterceptor("mutator", 0, payload => { mutationExecuted = true; return MutationInterceptorResult.Unchanged(payload); }), CreateObservabilityInterceptor("observer", _ => { observabilityExecuted = true; throw new Exception("Observability failure should not block"); }) }; var executor = new InterceptorChainExecutor(interceptors); var payload = JsonNode.Parse("{}"); // Act var result = await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload); // Assert - Observability failures don't block Assert.True(mutationExecuted); Assert.True(observabilityExecuted); Assert.Equal(InterceptorChainStatus.Success, result.Status); } [Fact] public async Task Timeout_AbortsChainExecution() { // Arrange var interceptors = new List { CreateCancellableAsyncMutationInterceptor("slow-mutator", 0, async (payload, ct) => { // Use the cancellation token so the delay can be cancelled await Task.Delay(5000, ct); return MutationInterceptorResult.Unchanged(payload); }) }; var executor = new InterceptorChainExecutor(interceptors); var payload = JsonNode.Parse("{}"); // Act var result = await executor.ExecuteForSendingAsync( InterceptorEvents.ToolsCall, payload, timeoutMs: 100); // Assert - The chain should abort (either as Timeout or MutationFailed due to cancellation) // The implementation catches OperationCanceledException from the mutation which gets reported // as MutationFailed rather than Timeout depending on timing Assert.NotEqual(InterceptorChainStatus.Success, result.Status); Assert.NotNull(result.AbortedAt); } [Fact] public async Task Mutations_PropagatePayloadThroughChain() { // Arrange var interceptors = new List { CreateMutationInterceptor("first", -100, payload => { var obj = payload!.AsObject(); obj["step1"] = true; return MutationInterceptorResult.Mutated(obj); }), CreateMutationInterceptor("second", 0, payload => { var obj = payload!.AsObject(); obj["step2"] = true; return MutationInterceptorResult.Mutated(obj); }), CreateMutationInterceptor("third", 100, payload => { var obj = payload!.AsObject(); obj["step3"] = true; return MutationInterceptorResult.Mutated(obj); }) }; var executor = new InterceptorChainExecutor(interceptors); var payload = JsonNode.Parse("{}"); // Act var result = await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload); // Assert Assert.Equal(InterceptorChainStatus.Success, result.Status); var final = result.FinalPayload!.AsObject(); Assert.True(final["step1"]!.GetValue()); Assert.True(final["step2"]!.GetValue()); Assert.True(final["step3"]!.GetValue()); } [Fact] public async Task ValidationWarning_DoesNotBlockExecution() { // Arrange var mutationExecuted = false; var interceptors = new List { CreateMutationInterceptor("mutator", -1000, payload => { mutationExecuted = true; return MutationInterceptorResult.Unchanged(payload); }), CreateValidationInterceptor("validator", _ => ValidationInterceptorResult.Warning("This is just a warning")) }; var executor = new InterceptorChainExecutor(interceptors); var payload = JsonNode.Parse("{}"); // Act var result = await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload); // Assert - Warnings don't block Assert.True(mutationExecuted); Assert.Equal(InterceptorChainStatus.Success, result.Status); Assert.Equal(1, result.ValidationSummary.Warnings); } // Helper methods to create test interceptors private static McpClientInterceptor CreateMutationInterceptor( string name, int priority, Func handler) { return new TestMutationInterceptor(name, priority, handler); } private static McpClientInterceptor CreateAsyncMutationInterceptor( string name, int priority, Func> handler) { return new TestAsyncMutationInterceptor(name, priority, handler); } private static McpClientInterceptor CreateCancellableAsyncMutationInterceptor( string name, int priority, Func> handler) { return new TestCancellableAsyncMutationInterceptor(name, priority, handler); } private static McpClientInterceptor CreateValidationInterceptor( string name, Func handler) { return new TestValidationInterceptor(name, handler); } private static McpClientInterceptor CreateObservabilityInterceptor( string name, Action handler) { return new TestObservabilityInterceptor(name, handler); } } // Test interceptor implementations file class TestMutationInterceptor : McpClientInterceptor { private readonly Func _handler; private readonly Interceptor _protocolInterceptor; private readonly IReadOnlyList _metadata = []; public TestMutationInterceptor(string name, int priority, Func handler) { _protocolInterceptor = new Interceptor { Name = name, Type = InterceptorType.Mutation, Phase = InterceptorPhase.Both, Events = [InterceptorEvents.ToolsCall], PriorityHint = new InterceptorPriorityHint(priority) }; _handler = handler; } public override Interceptor ProtocolInterceptor => _protocolInterceptor; public override IReadOnlyList Metadata => _metadata; public override ValueTask InvokeAsync( ClientInterceptorContext context, CancellationToken cancellationToken = default) { var result = _handler(context.Params?.Payload); result.Interceptor = ProtocolInterceptor.Name; return new ValueTask(result); } } file class TestAsyncMutationInterceptor : McpClientInterceptor { private readonly Func> _handler; private readonly Interceptor _protocolInterceptor; private readonly IReadOnlyList _metadata = []; public TestAsyncMutationInterceptor(string name, int priority, Func> handler) { _protocolInterceptor = new Interceptor { Name = name, Type = InterceptorType.Mutation, Phase = InterceptorPhase.Both, Events = [InterceptorEvents.ToolsCall], PriorityHint = new InterceptorPriorityHint(priority) }; _handler = handler; } public override Interceptor ProtocolInterceptor => _protocolInterceptor; public override IReadOnlyList Metadata => _metadata; public override async ValueTask InvokeAsync( ClientInterceptorContext context, CancellationToken cancellationToken = default) { var result = await _handler(context.Params?.Payload); result.Interceptor = ProtocolInterceptor.Name; return result; } } file class TestCancellableAsyncMutationInterceptor : McpClientInterceptor { private readonly Func> _handler; private readonly Interceptor _protocolInterceptor; private readonly IReadOnlyList _metadata = []; public TestCancellableAsyncMutationInterceptor(string name, int priority, Func> handler) { _protocolInterceptor = new Interceptor { Name = name, Type = InterceptorType.Mutation, Phase = InterceptorPhase.Both, Events = [InterceptorEvents.ToolsCall], PriorityHint = new InterceptorPriorityHint(priority) }; _handler = handler; } public override Interceptor ProtocolInterceptor => _protocolInterceptor; public override IReadOnlyList Metadata => _metadata; public override async ValueTask InvokeAsync( ClientInterceptorContext context, CancellationToken cancellationToken = default) { var result = await _handler(context.Params?.Payload, cancellationToken); result.Interceptor = ProtocolInterceptor.Name; return result; } } file class TestValidationInterceptor : McpClientInterceptor { private readonly Func _handler; private readonly Interceptor _protocolInterceptor; private readonly IReadOnlyList _metadata = []; public TestValidationInterceptor(string name, Func handler) { _protocolInterceptor = new Interceptor { Name = name, Type = InterceptorType.Validation, Phase = InterceptorPhase.Both, Events = [InterceptorEvents.ToolsCall] }; _handler = handler; } public override Interceptor ProtocolInterceptor => _protocolInterceptor; public override IReadOnlyList Metadata => _metadata; public override ValueTask InvokeAsync( ClientInterceptorContext context, CancellationToken cancellationToken = default) { var result = _handler(context.Params?.Payload); result.Interceptor = ProtocolInterceptor.Name; result.Phase = context.Params?.Phase ?? InterceptorPhase.Request; return new ValueTask(result); } } file class TestObservabilityInterceptor : McpClientInterceptor { private readonly Action _handler; private readonly Interceptor _protocolInterceptor; private readonly IReadOnlyList _metadata = []; public TestObservabilityInterceptor(string name, Action handler) { _protocolInterceptor = new Interceptor { Name = name, Type = InterceptorType.Observability, Phase = InterceptorPhase.Both, Events = [InterceptorEvents.ToolsCall] }; _handler = handler; } public override Interceptor ProtocolInterceptor => _protocolInterceptor; public override IReadOnlyList Metadata => _metadata; public override ValueTask InvokeAsync( ClientInterceptorContext context, CancellationToken cancellationToken = default) { _handler(context.Params?.Payload); return new ValueTask(new ObservabilityInterceptorResult { Interceptor = ProtocolInterceptor.Name, Observed = true }); } }