experimental-ext-interceptors/csharp/sdk/samples/InterceptorClientSample/Program.cs
Peder Holdgaard Pedersen 45f9e0ef27
C# Implementation (#2)
C# implementation of SEP-1763

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 23:53:44 +00:00

225 lines
9.5 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Interceptors;
using ModelContextProtocol.Interceptors.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.IO.Pipelines;
using System.Text.Json.Nodes;
// =============================================================================
// MCP Interceptors Client Sample
// =============================================================================
// This sample demonstrates how to use the InterceptingMcpClient to automatically
// execute interceptor chains for MCP tool operations.
//
// The sample:
// 1. Creates an in-memory MCP server with sample tools
// 2. Creates an MCP client with interceptors using InterceptingMcpClient
// 3. Demonstrates validation, mutation, and observability interceptors
// 4. Shows error handling with McpInterceptorValidationException
// =============================================================================
Console.WriteLine("=== MCP Interceptors Client Sample ===\n");
// Set up in-memory transport using pipes
Pipe clientToServerPipe = new(), serverToClientPipe = new();
// Create server with sample tools
await using McpServer server = McpServer.Create(
new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()),
new McpServerOptions
{
ServerInfo = new() { Name = "SampleServer", Version = "1.0.0" },
ToolCollection =
[
// Echo tool - returns what you send
McpServerTool.Create(
(string message) => $"Echo: {message}",
new() { Name = "echo", Description = "Echoes the message back" }),
// Greet tool - creates a greeting
McpServerTool.Create(
(string name, string? title = null) =>
title is not null ? $"Hello, {title} {name}!" : $"Hello, {name}!",
new() { Name = "greet", Description = "Creates a greeting" }),
// Search tool - simulates a search operation
McpServerTool.Create(
(string query) => $"Search results for: {query}\n- Result 1\n- Result 2\n- Result 3",
new() { Name = "search", Description = "Searches for content" }),
// Sensitive tool - returns data that should be redacted
McpServerTool.Create(
() => "API Response: api_key=sk_live_abc123 and token=Bearer xyz789",
new() { Name = "get_config", Description = "Gets configuration (contains sensitive data)" }),
]
});
// Start server in background
_ = server.RunAsync();
// Create the base MCP client
await using McpClient client = await McpClient.CreateAsync(
new StreamClientTransport(clientToServerPipe.Writer.AsStream(), serverToClientPipe.Reader.AsStream()));
Console.WriteLine($"Connected to server: {client.ServerInfo.Name} v{client.ServerInfo.Version}\n");
// =============================================================================
// Create interceptors using both attribute-based classes and inline delegates
// =============================================================================
// Collect interceptors from attributed classes
var attributeBasedInterceptors = new List<McpClientInterceptor>();
attributeBasedInterceptors.AddRange(McpClientInterceptorExtensions.WithInterceptors<ClientValidationInterceptors>());
attributeBasedInterceptors.AddRange(McpClientInterceptorExtensions.WithInterceptors<ClientMutationInterceptors>());
attributeBasedInterceptors.AddRange(McpClientInterceptorExtensions.WithInterceptors<ClientObservabilityInterceptors>());
Console.WriteLine($"Loaded {attributeBasedInterceptors.Count} attribute-based interceptors:");
foreach (var interceptor in attributeBasedInterceptors)
{
var proto = interceptor.ProtocolInterceptor;
var priority = proto.PriorityHint?.Request ?? proto.PriorityHint?.Response ?? 0;
Console.WriteLine($" - {proto.Name} ({proto.Type}, priority: {priority})");
}
Console.WriteLine();
// =============================================================================
// Wrap the client with interceptors
// =============================================================================
var interceptedClient = client.WithInterceptors(new InterceptingMcpClientOptions
{
Interceptors = attributeBasedInterceptors,
DefaultTimeoutMs = 10000,
ThrowOnValidationError = true,
InterceptResponses = true
});
// =============================================================================
// Demo 1: List tools (shows observability interceptor metrics)
// =============================================================================
Console.WriteLine("--- Demo 1: List Tools ---");
var tools = await interceptedClient.ListToolsAsync();
Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}\n");
// =============================================================================
// Demo 2: Normal tool call (shows request/response logging)
// =============================================================================
Console.WriteLine("--- Demo 2: Normal Tool Call ---");
var echoResult = await interceptedClient.CallToolAsync("echo", new Dictionary<string, object?>
{
["message"] = "Hello from intercepted client!"
});
PrintResult(echoResult);
// =============================================================================
// Demo 3: Tool call with argument normalization (mutation)
// =============================================================================
Console.WriteLine("--- Demo 3: Argument Normalization (Mutation) ---");
var greetResult = await interceptedClient.CallToolAsync("greet", new Dictionary<string, object?>
{
["name"] = " John Doe ", // Will be trimmed
["title"] = " " // Will be converted to null (empty after trim)
});
PrintResult(greetResult);
// =============================================================================
// Demo 4: Response redaction (mutation on response)
// =============================================================================
Console.WriteLine("--- Demo 4: Response Redaction ---");
var configResult = await interceptedClient.CallToolAsync("get_config", new Dictionary<string, object?>());
PrintResult(configResult);
// =============================================================================
// Demo 5: PII Detection - Email Warning (validation with warning)
// =============================================================================
Console.WriteLine("--- Demo 5: PII Warning (Email) ---");
var searchWithEmail = await interceptedClient.CallToolAsync("search", new Dictionary<string, object?>
{
["query"] = "contact john@example.com for details"
});
PrintResult(searchWithEmail);
// =============================================================================
// Demo 6: PII Detection - SSN Error (validation blocks request)
// =============================================================================
Console.WriteLine("--- Demo 6: PII Error (SSN - Blocked) ---");
try
{
var searchWithSsn = await interceptedClient.CallToolAsync("search", new Dictionary<string, object?>
{
["query"] = "user SSN is 123-45-6789"
});
PrintResult(searchWithSsn);
}
catch (McpInterceptorValidationException ex)
{
Console.WriteLine($" REQUEST BLOCKED!");
Console.WriteLine($" Blocked by: {ex.AbortedAt?.Interceptor}");
Console.WriteLine($" Reason: {ex.AbortedAt?.Reason}");
Console.WriteLine($" Chain status: {ex.ChainResult.Status}");
Console.WriteLine();
// Show detailed validation messages
Console.WriteLine(" Validation details:");
Console.WriteLine(ex.GetDetailedMessage());
}
// =============================================================================
// Demo 7: Using non-throwing mode
// =============================================================================
Console.WriteLine("--- Demo 7: Non-Throwing Mode ---");
var nonThrowingClient = client.WithInterceptors(new InterceptingMcpClientOptions
{
Interceptors = attributeBasedInterceptors,
ThrowOnValidationError = false // Don't throw, return error result instead
});
var blockedResult = await nonThrowingClient.CallToolAsync("search", new Dictionary<string, object?>
{
["query"] = "credit card 4111-1111-1111-1111"
});
Console.WriteLine($" IsError: {blockedResult.IsError}");
if (blockedResult.Content?.FirstOrDefault() is TextContentBlock text)
{
Console.WriteLine($" Message: {text.Text}");
}
Console.WriteLine();
// =============================================================================
// Demo 8: Accessing inner client for non-intercepted operations
// =============================================================================
Console.WriteLine("--- Demo 8: Accessing Inner Client ---");
Console.WriteLine($" Server capabilities via inner client:");
Console.WriteLine($" Tools: {interceptedClient.ServerCapabilities.Tools is not null}");
Console.WriteLine($" Server info: {interceptedClient.ServerInfo.Name} v{interceptedClient.ServerInfo.Version}");
Console.WriteLine($" Inner client type: {interceptedClient.Inner.GetType().Name}");
Console.WriteLine();
Console.WriteLine("=== Sample Complete ===");
// Helper to print results
static void PrintResult(CallToolResult result)
{
Console.WriteLine($" IsError: {result.IsError}");
if (result.Content is not null)
{
foreach (var content in result.Content)
{
if (content is TextContentBlock textBlock)
{
Console.WriteLine($" Content: {textBlock.Text}");
}
}
}
Console.WriteLine();
}