Re-implementation of C# interceptors SDK with clearer focus on server-side / gateway interceptors. Fixes a lot of issues with the existing implementation. Still more boilerplate than I'm happy with, but this is better than what was there. Will refine further in later PRs.
164 lines
6.6 KiB
C#
164 lines
6.6 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Text.Json.Nodes;
|
|
using ModelContextProtocol.Client;
|
|
using ModelContextProtocol.Interceptors.Client;
|
|
using ModelContextProtocol.Interceptors.Protocol;
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Interceptor Client Sample
|
|
//
|
|
// Demonstrates the client-side interceptor API:
|
|
// 1. Listing available interceptors
|
|
// 2. Invoking a single interceptor directly
|
|
// 3. Executing a full interceptor chain
|
|
//
|
|
// Uses the InterceptorServerSample as the backend.
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
Console.WriteLine("=== MCP Interceptors Client Sample ===");
|
|
Console.WriteLine();
|
|
|
|
// 1. Connect to the interceptor server
|
|
Console.WriteLine("[setup] Starting interceptor server...");
|
|
var interceptorServerPath = Path.Combine(GetSourceDir(), "..", "InterceptorServerSample");
|
|
await using var client = await McpClient.CreateAsync(
|
|
new StdioClientTransport(new()
|
|
{
|
|
Name = "InterceptorServer",
|
|
Command = "dotnet",
|
|
Arguments = ["run", "--project", interceptorServerPath],
|
|
}));
|
|
|
|
Console.WriteLine("[setup] Connected!");
|
|
Console.WriteLine();
|
|
|
|
// ── Demo 1: List all interceptors ────────────────────────────────────
|
|
Console.WriteLine("── Demo 1: List interceptors ──");
|
|
var listResult = await client.ListInterceptorsAsync();
|
|
foreach (var interceptor in listResult.Interceptors)
|
|
{
|
|
var hookSummary = string.Join("; ", interceptor.Hooks.Select(h => $"{h.Phase}:[{string.Join(",", h.Events)}]"));
|
|
Console.WriteLine($" {interceptor.Name,-20} type={interceptor.Type,-15} hooks={hookSummary}");
|
|
if (interceptor.Description is not null)
|
|
{
|
|
Console.WriteLine($" {"",20} {interceptor.Description}");
|
|
}
|
|
}
|
|
|
|
// ── Demo 2: Invoke a single interceptor ──────────────────────────────
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Demo 2: Invoke email-redactor directly ──");
|
|
|
|
var cleanPayload = JsonNode.Parse("""{"name":"echo","arguments":{"message":"Contact alice@example.com"}}""")!;
|
|
var invokeResult = await client.InvokeInterceptorAsync(new InvokeInterceptorRequestParams
|
|
{
|
|
Name = "email-redactor",
|
|
Event = InterceptionEvents.ToolsCall,
|
|
Phase = InterceptorPhase.Request,
|
|
Payload = cleanPayload,
|
|
});
|
|
|
|
if (invokeResult is MutationInterceptorResult mutation)
|
|
{
|
|
Console.WriteLine($" Modified: {mutation.Modified}");
|
|
Console.WriteLine($" Payload: {mutation.Payload}");
|
|
}
|
|
|
|
// ── Demo 3: Invoke pii-validator with clean data ─────────────────────
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Demo 3: Invoke pii-validator (clean data) ──");
|
|
|
|
var safePayload = JsonNode.Parse("""{"name":"echo","arguments":{"message":"Hello world"}}""")!;
|
|
var validationResult = await client.InvokeInterceptorAsync(new InvokeInterceptorRequestParams
|
|
{
|
|
Name = "pii-validator",
|
|
Event = InterceptionEvents.ToolsCall,
|
|
Phase = InterceptorPhase.Request,
|
|
Payload = safePayload,
|
|
});
|
|
|
|
if (validationResult is ValidationInterceptorResult validation)
|
|
{
|
|
Console.WriteLine($" Valid: {validation.Valid}");
|
|
}
|
|
|
|
// ── Demo 4: Invoke pii-validator with PII ────────────────────────────
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Demo 4: Invoke pii-validator (with PII) ──");
|
|
|
|
var piiPayload = JsonNode.Parse("""{"name":"echo","arguments":{"message":"My SSN is 123-45-6789"}}""")!;
|
|
var piiResult = await client.InvokeInterceptorAsync(new InvokeInterceptorRequestParams
|
|
{
|
|
Name = "pii-validator",
|
|
Event = InterceptionEvents.ToolsCall,
|
|
Phase = InterceptorPhase.Request,
|
|
Payload = piiPayload,
|
|
});
|
|
|
|
if (piiResult is ValidationInterceptorResult piiValidation)
|
|
{
|
|
Console.WriteLine($" Valid: {piiValidation.Valid}");
|
|
Console.WriteLine($" Severity: {piiValidation.Severity}");
|
|
foreach (var msg in piiValidation.Messages ?? [])
|
|
{
|
|
Console.WriteLine($" Message: [{msg.Severity}] {msg.Message} (path: {msg.Path})");
|
|
}
|
|
}
|
|
|
|
// ── Demo 5: Execute full chain ───────────────────────────────────────
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Demo 5: Execute chain (email + PII check) ──");
|
|
|
|
var chainPayload = JsonNode.Parse("""{"name":"echo","arguments":{"message":"Email bob@corp.com about SSN"}}""")!;
|
|
var chainResult = await client.ExecuteChainAsync(new ExecuteChainRequestParams
|
|
{
|
|
Event = InterceptionEvents.ToolsCall,
|
|
Phase = InterceptorPhase.Request,
|
|
Payload = chainPayload,
|
|
Context = new InvokeInterceptorContext
|
|
{
|
|
TraceId = Guid.NewGuid().ToString("N"),
|
|
},
|
|
});
|
|
|
|
Console.WriteLine($" Status: {chainResult.Status}");
|
|
Console.WriteLine($" Duration: {chainResult.TotalDurationMs:F1}ms");
|
|
Console.WriteLine($" Results: {chainResult.Results?.Count ?? 0} interceptor(s) ran");
|
|
|
|
if (chainResult.AbortedAt is { } abort)
|
|
{
|
|
Console.WriteLine($" Aborted at: {abort.Interceptor} ({abort.Reason})");
|
|
}
|
|
|
|
foreach (var r in chainResult.Results ?? [])
|
|
{
|
|
var typeName = r switch
|
|
{
|
|
ValidationInterceptorResult => "validation",
|
|
MutationInterceptorResult => "mutation",
|
|
SinkInterceptorResult => "sink",
|
|
_ => "unknown",
|
|
};
|
|
Console.WriteLine($" {r.InterceptorName,-20} type={typeName,-15} duration={r.DurationMs:F1}ms");
|
|
}
|
|
|
|
// ── Demo 6: Execute chain with clean data ────────────────────────────
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Demo 6: Execute chain (clean data, should pass) ──");
|
|
|
|
var cleanChainPayload = JsonNode.Parse("""{"name":"echo","arguments":{"message":"Contact bob@corp.com please"}}""")!;
|
|
var cleanChainResult = await client.ExecuteChainAsync(new ExecuteChainRequestParams
|
|
{
|
|
Event = InterceptionEvents.ToolsCall,
|
|
Phase = InterceptorPhase.Request,
|
|
Payload = cleanChainPayload,
|
|
});
|
|
|
|
Console.WriteLine($" Status: {cleanChainResult.Status}");
|
|
Console.WriteLine($" Final payload: {cleanChainResult.FinalPayload}");
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("=== Done ===");
|
|
|
|
static string GetSourceDir([CallerFilePath] string? path = null) =>
|
|
Path.GetDirectoryName(path) ?? throw new InvalidOperationException();
|