C# Implementation (#2)

C# implementation of SEP-1763

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Peder Holdgaard Pedersen 2026-01-28 00:53:44 +01:00 • committed by GitHub
commit 45f9e0ef27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 13862 additions and 21 deletions

View file

@ -0,0 +1,504 @@
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<string>();
var interceptors = new List<McpClientInterceptor>
{
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<string>();
var interceptors = new List<McpClientInterceptor>
{
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<McpClientInterceptor>
{
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<string>();
var interceptors = new List<McpClientInterceptor>
{
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<McpClientInterceptor>
{
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<McpClientInterceptor>
{
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<McpClientInterceptor>
{
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<McpClientInterceptor>
{
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<bool>());
Assert.True(final["step2"]!.GetValue<bool>());
Assert.True(final["step3"]!.GetValue<bool>());
}
[Fact]
public async Task ValidationWarning_DoesNotBlockExecution()
{
// Arrange
var mutationExecuted = false;
var interceptors = new List<McpClientInterceptor>
{
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<JsonNode?, MutationInterceptorResult> handler)
{
return new TestMutationInterceptor(name, priority, handler);
}
private static McpClientInterceptor CreateAsyncMutationInterceptor(
string name,
int priority,
Func<JsonNode?, Task<MutationInterceptorResult>> handler)
{
return new TestAsyncMutationInterceptor(name, priority, handler);
}
private static McpClientInterceptor CreateCancellableAsyncMutationInterceptor(
string name,
int priority,
Func<JsonNode?, CancellationToken, Task<MutationInterceptorResult>> handler)
{
return new TestCancellableAsyncMutationInterceptor(name, priority, handler);
}
private static McpClientInterceptor CreateValidationInterceptor(
string name,
Func<JsonNode?, ValidationInterceptorResult> handler)
{
return new TestValidationInterceptor(name, handler);
}
private static McpClientInterceptor CreateObservabilityInterceptor(
string name,
Action<JsonNode?> handler)
{
return new TestObservabilityInterceptor(name, handler);
}
}
// Test interceptor implementations
file class TestMutationInterceptor : McpClientInterceptor
{
private readonly Func<JsonNode?, MutationInterceptorResult> _handler;
private readonly Interceptor _protocolInterceptor;
private readonly IReadOnlyList<object> _metadata = [];
public TestMutationInterceptor(string name, int priority, Func<JsonNode?, MutationInterceptorResult> 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<object> Metadata => _metadata;
public override ValueTask<InterceptorResult> InvokeAsync(
ClientInterceptorContext<InvokeInterceptorRequestParams> context,
CancellationToken cancellationToken = default)
{
var result = _handler(context.Params?.Payload);
result.Interceptor = ProtocolInterceptor.Name;
return new ValueTask<InterceptorResult>(result);
}
}
file class TestAsyncMutationInterceptor : McpClientInterceptor
{
private readonly Func<JsonNode?, Task<MutationInterceptorResult>> _handler;
private readonly Interceptor _protocolInterceptor;
private readonly IReadOnlyList<object> _metadata = [];
public TestAsyncMutationInterceptor(string name, int priority, Func<JsonNode?, Task<MutationInterceptorResult>> 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<object> Metadata => _metadata;
public override async ValueTask<InterceptorResult> InvokeAsync(
ClientInterceptorContext<InvokeInterceptorRequestParams> context,
CancellationToken cancellationToken = default)
{
var result = await _handler(context.Params?.Payload);
result.Interceptor = ProtocolInterceptor.Name;
return result;
}
}
file class TestCancellableAsyncMutationInterceptor : McpClientInterceptor
{
private readonly Func<JsonNode?, CancellationToken, Task<MutationInterceptorResult>> _handler;
private readonly Interceptor _protocolInterceptor;
private readonly IReadOnlyList<object> _metadata = [];
public TestCancellableAsyncMutationInterceptor(string name, int priority, Func<JsonNode?, CancellationToken, Task<MutationInterceptorResult>> 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<object> Metadata => _metadata;
public override async ValueTask<InterceptorResult> InvokeAsync(
ClientInterceptorContext<InvokeInterceptorRequestParams> 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<JsonNode?, ValidationInterceptorResult> _handler;
private readonly Interceptor _protocolInterceptor;
private readonly IReadOnlyList<object> _metadata = [];
public TestValidationInterceptor(string name, Func<JsonNode?, ValidationInterceptorResult> 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<object> Metadata => _metadata;
public override ValueTask<InterceptorResult> InvokeAsync(
ClientInterceptorContext<InvokeInterceptorRequestParams> context,
CancellationToken cancellationToken = default)
{
var result = _handler(context.Params?.Payload);
result.Interceptor = ProtocolInterceptor.Name;
result.Phase = context.Params?.Phase ?? InterceptorPhase.Request;
return new ValueTask<InterceptorResult>(result);
}
}
file class TestObservabilityInterceptor : McpClientInterceptor
{
private readonly Action<JsonNode?> _handler;
private readonly Interceptor _protocolInterceptor;
private readonly IReadOnlyList<object> _metadata = [];
public TestObservabilityInterceptor(string name, Action<JsonNode?> 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<object> Metadata => _metadata;
public override ValueTask<InterceptorResult> InvokeAsync(
ClientInterceptorContext<InvokeInterceptorRequestParams> context,
CancellationToken cancellationToken = default)
{
_handler(context.Params?.Payload);
return new ValueTask<InterceptorResult>(new ObservabilityInterceptorResult
{
Interceptor = ProtocolInterceptor.Name,
Observed = true
});
}
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,328 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using ModelContextProtocol.Interceptors;
using ModelContextProtocol.Interceptors.Protocol.Llm;
namespace ModelContextProtocol.Interceptors.Tests;
public class ProtocolTypesTests
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
#region InterceptorType Serialization
[Theory]
[InlineData(InterceptorType.Validation, "\"validation\"")]
[InlineData(InterceptorType.Mutation, "\"mutation\"")]
[InlineData(InterceptorType.Observability, "\"observability\"")]
public void InterceptorType_SerializesToCorrectJsonString(InterceptorType type, string expected)
{
var json = JsonSerializer.Serialize(type, JsonOptions);
Assert.Equal(expected, json);
}
[Theory]
[InlineData("\"validation\"", InterceptorType.Validation)]
[InlineData("\"mutation\"", InterceptorType.Mutation)]
[InlineData("\"observability\"", InterceptorType.Observability)]
public void InterceptorType_DeserializesFromJsonString(string json, InterceptorType expected)
{
var type = JsonSerializer.Deserialize<InterceptorType>(json, JsonOptions);
Assert.Equal(expected, type);
}
#endregion
#region InterceptorPhase Serialization
[Theory]
[InlineData(InterceptorPhase.Request, "\"request\"")]
[InlineData(InterceptorPhase.Response, "\"response\"")]
[InlineData(InterceptorPhase.Both, "\"both\"")]
public void InterceptorPhase_SerializesToCorrectJsonString(InterceptorPhase phase, string expected)
{
var json = JsonSerializer.Serialize(phase, JsonOptions);
Assert.Equal(expected, json);
}
#endregion
#region InterceptorPriorityHint Serialization
[Fact]
public void InterceptorPriorityHint_SerializesAsNumber_WhenBothPhasesEqual()
{
var hint = new InterceptorPriorityHint(100);
var json = JsonSerializer.Serialize(hint, JsonOptions);
Assert.Equal("100", json);
}
[Fact]
public void InterceptorPriorityHint_SerializesAsObject_WhenPhasesDiffer()
{
var hint = new InterceptorPriorityHint(-100, 50);
var json = JsonSerializer.Serialize(hint, JsonOptions);
var obj = JsonSerializer.Deserialize<JsonObject>(json);
Assert.NotNull(obj);
Assert.Equal(-100, obj["request"]!.GetValue<int>());
Assert.Equal(50, obj["response"]!.GetValue<int>());
}
[Fact]
public void InterceptorPriorityHint_DeserializesFromNumber()
{
var hint = JsonSerializer.Deserialize<InterceptorPriorityHint>("100", JsonOptions);
Assert.Equal(100, hint.GetPriorityForPhase(InterceptorPhase.Request));
Assert.Equal(100, hint.GetPriorityForPhase(InterceptorPhase.Response));
}
[Fact]
public void InterceptorPriorityHint_DeserializesFromObject()
{
var json = """{"request": -100, "response": 50}""";
var hint = JsonSerializer.Deserialize<InterceptorPriorityHint>(json, JsonOptions);
Assert.Equal(-100, hint.GetPriorityForPhase(InterceptorPhase.Request));
Assert.Equal(50, hint.GetPriorityForPhase(InterceptorPhase.Response));
}
#endregion
#region Interceptor Serialization
[Fact]
public void Interceptor_SerializesWithAllFields()
{
var interceptor = new Interceptor
{
Name = "test-interceptor",
Version = "1.0.0",
Description = "A test interceptor",
Events = [InterceptorEvents.ToolsCall, InterceptorEvents.LlmCompletion],
Type = InterceptorType.Validation,
Phase = InterceptorPhase.Request,
PriorityHint = new InterceptorPriorityHint(100)
};
var json = JsonSerializer.Serialize(interceptor, JsonOptions);
var deserialized = JsonSerializer.Deserialize<Interceptor>(json, JsonOptions);
Assert.NotNull(deserialized);
Assert.Equal("test-interceptor", deserialized.Name);
Assert.Equal("1.0.0", deserialized.Version);
Assert.Equal("A test interceptor", deserialized.Description);
Assert.Contains(InterceptorEvents.ToolsCall, deserialized.Events);
Assert.Contains(InterceptorEvents.LlmCompletion, deserialized.Events);
Assert.Equal(InterceptorType.Validation, deserialized.Type);
Assert.Equal(InterceptorPhase.Request, deserialized.Phase);
Assert.NotNull(deserialized.PriorityHint);
Assert.Equal(100, deserialized.PriorityHint.Value.GetPriorityForPhase(InterceptorPhase.Request));
}
#endregion
#region ValidationInterceptorResult Serialization
[Fact]
public void ValidationInterceptorResult_SerializesCorrectly()
{
var result = new ValidationInterceptorResult
{
Valid = false,
Severity = ValidationSeverity.Error,
Messages =
[
new() { Path = "$.name", Message = "Name is required", Severity = ValidationSeverity.Error }
],
Suggestions =
[
new() { Path = "$.name", Value = JsonNode.Parse("\"default\"") }
]
};
var json = JsonSerializer.Serialize(result, JsonOptions);
var deserialized = JsonSerializer.Deserialize<ValidationInterceptorResult>(json, JsonOptions);
Assert.NotNull(deserialized);
Assert.False(deserialized.Valid);
Assert.Equal(ValidationSeverity.Error, deserialized.Severity);
Assert.Single(deserialized.Messages!);
Assert.Equal("$.name", deserialized.Messages![0].Path);
Assert.Single(deserialized.Suggestions!);
}
#endregion
#region MutationInterceptorResult Serialization
[Fact]
public void MutationInterceptorResult_SerializesCorrectly()
{
var result = MutationInterceptorResult.Mutated(JsonNode.Parse("{\"modified\": true}"));
var json = JsonSerializer.Serialize(result, JsonOptions);
var deserialized = JsonSerializer.Deserialize<MutationInterceptorResult>(json, JsonOptions);
Assert.NotNull(deserialized);
Assert.True(deserialized.Modified);
Assert.NotNull(deserialized.Payload);
Assert.True(deserialized.Payload["modified"]!.GetValue<bool>());
}
#endregion
#region InterceptorChainResult Serialization
[Fact]
public void InterceptorChainStatus_SerializesCorrectly()
{
// Test chain status enum serialization
Assert.Equal("\"success\"", JsonSerializer.Serialize(InterceptorChainStatus.Success, JsonOptions));
Assert.Equal("\"validation_failed\"", JsonSerializer.Serialize(InterceptorChainStatus.ValidationFailed, JsonOptions));
Assert.Equal("\"mutation_failed\"", JsonSerializer.Serialize(InterceptorChainStatus.MutationFailed, JsonOptions));
Assert.Equal("\"timeout\"", JsonSerializer.Serialize(InterceptorChainStatus.Timeout, JsonOptions));
}
[Fact]
public void ValidationSummary_SerializesCorrectly()
{
var summary = new ValidationSummary { Errors = 1, Warnings = 2, Infos = 3 };
var json = JsonSerializer.Serialize(summary, JsonOptions);
var deserialized = JsonSerializer.Deserialize<ValidationSummary>(json, JsonOptions);
Assert.NotNull(deserialized);
Assert.Equal(1, deserialized.Errors);
Assert.Equal(2, deserialized.Warnings);
Assert.Equal(3, deserialized.Infos);
}
[Fact]
public void ChainAbortInfo_SerializesCorrectly()
{
var abortInfo = new ChainAbortInfo
{
Interceptor = "pii-validator",
Reason = "PII detected in request",
Type = "validation"
};
var json = JsonSerializer.Serialize(abortInfo, JsonOptions);
var deserialized = JsonSerializer.Deserialize<ChainAbortInfo>(json, JsonOptions);
Assert.NotNull(deserialized);
Assert.Equal("pii-validator", deserialized.Interceptor);
Assert.Equal("PII detected in request", deserialized.Reason);
Assert.Equal("validation", deserialized.Type);
}
#endregion
#region LlmCompletionRequest Serialization
[Fact]
public void LlmCompletionRequest_SerializesCorrectly()
{
var request = new LlmCompletionRequest
{
Model = "gpt-4",
Messages =
[
LlmMessage.System("You are a helpful assistant."),
LlmMessage.User("Hello!")
],
Temperature = 0.7,
MaxTokens = 1000,
TopP = 0.9
};
var json = JsonSerializer.Serialize(request, JsonOptions);
var deserialized = JsonSerializer.Deserialize<LlmCompletionRequest>(json, JsonOptions);
Assert.NotNull(deserialized);
Assert.Equal("gpt-4", deserialized.Model);
Assert.Equal(2, deserialized.Messages.Count);
Assert.Equal(LlmMessageRole.System, deserialized.Messages[0].Role);
Assert.Equal("You are a helpful assistant.", deserialized.Messages[0].Content);
Assert.Equal(LlmMessageRole.User, deserialized.Messages[1].Role);
Assert.Equal(0.7, deserialized.Temperature);
Assert.Equal(1000, deserialized.MaxTokens);
}
[Fact]
public void LlmMessage_ToolMessage_SerializesCorrectly()
{
var message = LlmMessage.Tool("call_abc123", "{\"result\": \"success\"}", "get_weather");
var json = JsonSerializer.Serialize(message, JsonOptions);
var deserialized = JsonSerializer.Deserialize<LlmMessage>(json, JsonOptions);
Assert.NotNull(deserialized);
Assert.Equal(LlmMessageRole.Tool, deserialized.Role);
Assert.Equal("call_abc123", deserialized.ToolCallId);
Assert.Equal("{\"result\": \"success\"}", deserialized.Content);
Assert.Equal("get_weather", deserialized.Name);
}
[Fact]
public void LlmMessage_AssistantWithToolCalls_SerializesCorrectly()
{
var message = LlmMessage.Assistant(null, [
new LlmToolCall
{
Id = "call_abc123",
Type = "function",
Function = new LlmFunctionCall
{
Name = "get_weather",
Arguments = "{\"location\": \"NYC\"}"
}
}
]);
var json = JsonSerializer.Serialize(message, JsonOptions);
var deserialized = JsonSerializer.Deserialize<LlmMessage>(json, JsonOptions);
Assert.NotNull(deserialized);
Assert.Equal(LlmMessageRole.Assistant, deserialized.Role);
Assert.NotNull(deserialized.ToolCalls);
Assert.Single(deserialized.ToolCalls);
Assert.Equal("call_abc123", deserialized.ToolCalls[0].Id);
Assert.Equal("get_weather", deserialized.ToolCalls[0].Function!.Name);
}
#endregion
#region InterceptorEvents Constants
[Fact]
public void InterceptorEvents_HasAllRequiredEvents()
{
// Server features
Assert.Equal("tools/list", InterceptorEvents.ToolsList);
Assert.Equal("tools/call", InterceptorEvents.ToolsCall);
Assert.Equal("prompts/list", InterceptorEvents.PromptsList);
Assert.Equal("prompts/get", InterceptorEvents.PromptsGet);
Assert.Equal("resources/list", InterceptorEvents.ResourcesList);
Assert.Equal("resources/read", InterceptorEvents.ResourcesRead);
Assert.Equal("resources/subscribe", InterceptorEvents.ResourcesSubscribe);
// Client features
Assert.Equal("sampling/createMessage", InterceptorEvents.SamplingCreateMessage);
Assert.Equal("elicitation/create", InterceptorEvents.ElicitationCreate);
Assert.Equal("roots/list", InterceptorEvents.RootsList);
// LLM interactions
Assert.Equal("llm/completion", InterceptorEvents.LlmCompletion);
// Wildcards
Assert.Equal("*/request", InterceptorEvents.AllRequests);
Assert.Equal("*/response", InterceptorEvents.AllResponses);
Assert.Equal("*", InterceptorEvents.All);
}
#endregion
}