C# Implementation (#2)
C# implementation of SEP-1763 --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
46d96779d6
commit
45f9e0ef27
5 changed files with 13862 additions and 21 deletions
|
|
@ -0,0 +1,225 @@
|
|||
using ModelContextProtocol.Interceptors;
|
||||
using ModelContextProtocol.Interceptors.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
/// <summary>
|
||||
/// Sample mutation interceptors for MCP client operations.
|
||||
/// These interceptors demonstrate argument transformation and response filtering.
|
||||
/// </summary>
|
||||
[McpClientInterceptorType]
|
||||
public class ClientMutationInterceptors
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes tool arguments by trimming whitespace and converting empty strings to null.
|
||||
/// </summary>
|
||||
[McpClientInterceptor(
|
||||
Name = "argument-normalizer",
|
||||
Description = "Normalizes tool arguments (trim whitespace, handle empty strings)",
|
||||
Type = InterceptorType.Mutation,
|
||||
Events = [InterceptorEvents.ToolsCall],
|
||||
Phase = InterceptorPhase.Request,
|
||||
PriorityHint = -100)] // Normalization runs before validation
|
||||
public MutationInterceptorResult NormalizeArguments(JsonNode? payload)
|
||||
{
|
||||
if (payload is null)
|
||||
{
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}
|
||||
|
||||
// Parse the tool call
|
||||
CallToolRequestParams? toolCall;
|
||||
try
|
||||
{
|
||||
toolCall = payload.Deserialize<CallToolRequestParams>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}
|
||||
|
||||
if (toolCall?.Arguments is null || toolCall.Arguments.Count == 0)
|
||||
{
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}
|
||||
|
||||
bool modified = false;
|
||||
var normalizedArgs = new Dictionary<string, JsonElement>();
|
||||
|
||||
foreach (var arg in toolCall.Arguments)
|
||||
{
|
||||
var value = arg.Value;
|
||||
|
||||
// Trim string values
|
||||
if (value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var stringValue = value.GetString();
|
||||
if (stringValue is not null)
|
||||
{
|
||||
var trimmed = stringValue.Trim();
|
||||
if (trimmed != stringValue)
|
||||
{
|
||||
modified = true;
|
||||
// Convert empty strings to null
|
||||
if (string.IsNullOrEmpty(trimmed))
|
||||
{
|
||||
normalizedArgs[arg.Key] = JsonDocument.Parse("null").RootElement;
|
||||
}
|
||||
else
|
||||
{
|
||||
normalizedArgs[arg.Key] = JsonDocument.Parse($"\"{EscapeJsonString(trimmed)}\"").RootElement;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
normalizedArgs[arg.Key] = value;
|
||||
}
|
||||
|
||||
if (!modified)
|
||||
{
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}
|
||||
|
||||
// Rebuild the payload with normalized arguments
|
||||
var mutatedPayload = new JsonObject
|
||||
{
|
||||
["name"] = toolCall.Name,
|
||||
["arguments"] = JsonSerializer.SerializeToNode(normalizedArgs)
|
||||
};
|
||||
|
||||
if (toolCall.Meta is not null)
|
||||
{
|
||||
mutatedPayload["_meta"] = JsonSerializer.SerializeToNode(toolCall.Meta);
|
||||
}
|
||||
|
||||
return MutationInterceptorResult.Mutated(mutatedPayload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a timestamp to all outgoing tool call requests for audit purposes.
|
||||
/// </summary>
|
||||
[McpClientInterceptor(
|
||||
Name = "request-timestamp",
|
||||
Description = "Adds timestamp metadata to tool call requests",
|
||||
Type = InterceptorType.Mutation,
|
||||
Events = [InterceptorEvents.ToolsCall],
|
||||
Phase = InterceptorPhase.Request,
|
||||
PriorityHint = 100)] // Runs after normalization/validation
|
||||
public MutationInterceptorResult AddTimestamp(JsonNode? payload)
|
||||
{
|
||||
if (payload is not JsonObject obj)
|
||||
{
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}
|
||||
|
||||
// Create or update _meta with timestamp
|
||||
var meta = obj["_meta"]?.AsObject() ?? new JsonObject();
|
||||
meta["clientTimestamp"] = DateTimeOffset.UtcNow.ToString("o");
|
||||
meta["clientVersion"] = "1.0.0";
|
||||
|
||||
obj["_meta"] = meta;
|
||||
|
||||
return MutationInterceptorResult.Mutated(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redacts sensitive information from tool responses before returning to caller.
|
||||
/// </summary>
|
||||
[McpClientInterceptor(
|
||||
Name = "response-redactor",
|
||||
Description = "Redacts sensitive patterns from tool responses",
|
||||
Type = InterceptorType.Mutation,
|
||||
Events = [InterceptorEvents.ToolsCall],
|
||||
Phase = InterceptorPhase.Response,
|
||||
PriorityHint = 50)]
|
||||
public MutationInterceptorResult RedactResponse(JsonNode? payload)
|
||||
{
|
||||
if (payload is null)
|
||||
{
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}
|
||||
|
||||
// Parse the result
|
||||
CallToolResult? result;
|
||||
try
|
||||
{
|
||||
result = payload.Deserialize<CallToolResult>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}
|
||||
|
||||
if (result?.Content is null)
|
||||
{
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}
|
||||
|
||||
bool modified = false;
|
||||
var newContent = new List<ContentBlock>();
|
||||
|
||||
foreach (var content in result.Content)
|
||||
{
|
||||
if (content is TextContentBlock textBlock)
|
||||
{
|
||||
var redactedText = RedactSensitivePatterns(textBlock.Text);
|
||||
if (redactedText != textBlock.Text)
|
||||
{
|
||||
modified = true;
|
||||
newContent.Add(new TextContentBlock { Text = redactedText });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
newContent.Add(content);
|
||||
}
|
||||
|
||||
if (!modified)
|
||||
{
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}
|
||||
|
||||
// Rebuild result with redacted content
|
||||
var mutatedResult = new JsonObject
|
||||
{
|
||||
["content"] = JsonSerializer.SerializeToNode(newContent),
|
||||
["isError"] = result.IsError
|
||||
};
|
||||
|
||||
return MutationInterceptorResult.Mutated(mutatedResult);
|
||||
}
|
||||
|
||||
private static string RedactSensitivePatterns(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return text ?? string.Empty;
|
||||
}
|
||||
|
||||
// Redact API keys (common patterns)
|
||||
text = System.Text.RegularExpressions.Regex.Replace(
|
||||
text,
|
||||
@"(?i)(api[_-]?key|apikey|secret|password|token)[""']?\s*[:=]\s*[""']?[\w\-\.]+[""']?",
|
||||
"$1=***REDACTED***");
|
||||
|
||||
// Redact bearer tokens
|
||||
text = System.Text.RegularExpressions.Regex.Replace(
|
||||
text,
|
||||
@"(?i)bearer\s+[\w\-\.]+",
|
||||
"Bearer ***REDACTED***");
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private static string EscapeJsonString(string value)
|
||||
{
|
||||
return value
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("\"", "\\\"")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\t", "\\t");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
using ModelContextProtocol.Interceptors;
|
||||
using ModelContextProtocol.Interceptors.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
/// <summary>
|
||||
/// Sample observability interceptors for MCP client operations.
|
||||
/// These interceptors demonstrate logging, metrics collection, and tracing.
|
||||
/// </summary>
|
||||
[McpClientInterceptorType]
|
||||
public class ClientObservabilityInterceptors
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs all outgoing tool call requests for audit purposes.
|
||||
/// </summary>
|
||||
[McpClientInterceptor(
|
||||
Name = "request-logger",
|
||||
Description = "Logs outgoing tool call requests",
|
||||
Type = InterceptorType.Observability,
|
||||
Events = [InterceptorEvents.ToolsCall],
|
||||
Phase = InterceptorPhase.Request)]
|
||||
public ObservabilityInterceptorResult LogRequest(JsonNode? payload, string @event)
|
||||
{
|
||||
if (payload is null)
|
||||
{
|
||||
Console.WriteLine($"[Observability] Request for '{@event}' - no payload");
|
||||
return ObservabilityInterceptorResult.Success();
|
||||
}
|
||||
|
||||
// Extract tool name for logging
|
||||
string? toolName = null;
|
||||
try
|
||||
{
|
||||
var toolCall = payload.Deserialize<CallToolRequestParams>();
|
||||
toolName = toolCall?.Name;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore deserialization errors
|
||||
}
|
||||
|
||||
var timestamp = DateTimeOffset.UtcNow.ToString("HH:mm:ss.fff");
|
||||
Console.WriteLine($"[{timestamp}] [REQUEST] Tool: {toolName ?? "unknown"} | Event: {@event}");
|
||||
|
||||
// Log argument keys (not values for privacy)
|
||||
if (payload["arguments"] is JsonObject args)
|
||||
{
|
||||
var argKeys = string.Join(", ", args.Select(a => a.Key));
|
||||
Console.WriteLine($" Arguments: [{argKeys}]");
|
||||
}
|
||||
|
||||
return new ObservabilityInterceptorResult
|
||||
{
|
||||
Observed = true,
|
||||
Info = new JsonObject
|
||||
{
|
||||
["loggedAt"] = DateTimeOffset.UtcNow.ToString("o"),
|
||||
["toolName"] = toolName
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs all incoming tool call responses for audit purposes.
|
||||
/// </summary>
|
||||
[McpClientInterceptor(
|
||||
Name = "response-logger",
|
||||
Description = "Logs incoming tool call responses",
|
||||
Type = InterceptorType.Observability,
|
||||
Events = [InterceptorEvents.ToolsCall],
|
||||
Phase = InterceptorPhase.Response)]
|
||||
public ObservabilityInterceptorResult LogResponse(JsonNode? payload, string @event)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToString("HH:mm:ss.fff");
|
||||
|
||||
if (payload is null)
|
||||
{
|
||||
Console.WriteLine($"[{timestamp}] [RESPONSE] Event: {@event} - no payload");
|
||||
return ObservabilityInterceptorResult.Success();
|
||||
}
|
||||
|
||||
// Extract result info for logging
|
||||
bool? isError = null;
|
||||
int contentCount = 0;
|
||||
try
|
||||
{
|
||||
var result = payload.Deserialize<CallToolResult>();
|
||||
isError = result?.IsError;
|
||||
contentCount = result?.Content?.Count ?? 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore deserialization errors
|
||||
}
|
||||
|
||||
var status = isError == true ? "ERROR" : "SUCCESS";
|
||||
Console.WriteLine($"[{timestamp}] [RESPONSE] Status: {status} | Content blocks: {contentCount}");
|
||||
|
||||
return new ObservabilityInterceptorResult
|
||||
{
|
||||
Observed = true,
|
||||
Info = new JsonObject
|
||||
{
|
||||
["loggedAt"] = DateTimeOffset.UtcNow.ToString("o"),
|
||||
["isError"] = isError,
|
||||
["contentCount"] = contentCount
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects metrics about tool list operations.
|
||||
/// </summary>
|
||||
[McpClientInterceptor(
|
||||
Name = "tools-list-metrics",
|
||||
Description = "Collects metrics about tools list operations",
|
||||
Type = InterceptorType.Observability,
|
||||
Events = [InterceptorEvents.ToolsList],
|
||||
Phase = InterceptorPhase.Response)]
|
||||
public ObservabilityInterceptorResult CollectToolsMetrics(JsonNode? payload)
|
||||
{
|
||||
if (payload is null)
|
||||
{
|
||||
return ObservabilityInterceptorResult.Success();
|
||||
}
|
||||
|
||||
// Count tools in response
|
||||
int toolCount = 0;
|
||||
try
|
||||
{
|
||||
if (payload["tools"] is JsonArray tools)
|
||||
{
|
||||
toolCount = tools.Count;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
var timestamp = DateTimeOffset.UtcNow.ToString("HH:mm:ss.fff");
|
||||
Console.WriteLine($"[{timestamp}] [METRICS] Tools discovered: {toolCount}");
|
||||
|
||||
return new ObservabilityInterceptorResult
|
||||
{
|
||||
Observed = true,
|
||||
Info = new JsonObject
|
||||
{
|
||||
["toolCount"] = toolCount,
|
||||
["collectedAt"] = DateTimeOffset.UtcNow.ToString("o")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
using ModelContextProtocol.Interceptors;
|
||||
using ModelContextProtocol.Interceptors.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// <summary>
|
||||
/// Sample validation interceptors for MCP client operations.
|
||||
/// These interceptors demonstrate PII detection and request validation.
|
||||
/// </summary>
|
||||
[McpClientInterceptorType]
|
||||
public partial class ClientValidationInterceptors
|
||||
{
|
||||
// Common PII patterns
|
||||
private static readonly Regex SsnPattern = SsnRegex();
|
||||
private static readonly Regex EmailPattern = EmailRegex();
|
||||
private static readonly Regex CreditCardPattern = CreditCardRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Validates outgoing tool call arguments for PII (Personally Identifiable Information).
|
||||
/// Blocks requests containing SSN, email addresses, or credit card numbers.
|
||||
/// </summary>
|
||||
[McpClientInterceptor(
|
||||
Name = "pii-validator",
|
||||
Description = "Validates tool call arguments for PII leakage",
|
||||
Events = [InterceptorEvents.ToolsCall],
|
||||
Phase = InterceptorPhase.Request,
|
||||
PriorityHint = -1000)] // Security interceptors run early
|
||||
public ValidationInterceptorResult ValidatePii(JsonNode? payload)
|
||||
{
|
||||
if (payload is null)
|
||||
{
|
||||
return ValidationInterceptorResult.Success();
|
||||
}
|
||||
|
||||
var messages = new List<ValidationMessage>();
|
||||
|
||||
// Parse as tool call request to inspect arguments
|
||||
CallToolRequestParams? toolCall;
|
||||
try
|
||||
{
|
||||
toolCall = payload.Deserialize<CallToolRequestParams>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return ValidationInterceptorResult.Success(); // Can't parse, let other validators handle
|
||||
}
|
||||
|
||||
if (toolCall?.Arguments is null)
|
||||
{
|
||||
return ValidationInterceptorResult.Success();
|
||||
}
|
||||
|
||||
// Check each argument for PII
|
||||
foreach (var arg in toolCall.Arguments)
|
||||
{
|
||||
var value = arg.Value.ToString() ?? string.Empty;
|
||||
|
||||
if (SsnPattern.IsMatch(value))
|
||||
{
|
||||
messages.Add(new ValidationMessage
|
||||
{
|
||||
Path = $"arguments.{arg.Key}",
|
||||
Message = "Social Security Number detected - PII not allowed in tool arguments",
|
||||
Severity = ValidationSeverity.Error
|
||||
});
|
||||
}
|
||||
|
||||
if (CreditCardPattern.IsMatch(value))
|
||||
{
|
||||
messages.Add(new ValidationMessage
|
||||
{
|
||||
Path = $"arguments.{arg.Key}",
|
||||
Message = "Credit card number detected - PII not allowed in tool arguments",
|
||||
Severity = ValidationSeverity.Error
|
||||
});
|
||||
}
|
||||
|
||||
// Email is a warning, not an error
|
||||
if (EmailPattern.IsMatch(value))
|
||||
{
|
||||
messages.Add(new ValidationMessage
|
||||
{
|
||||
Path = $"arguments.{arg.Key}",
|
||||
Message = "Email address detected - consider if PII is necessary",
|
||||
Severity = ValidationSeverity.Warn
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.Count > 0)
|
||||
{
|
||||
var maxSeverity = messages.Max(m => m.Severity);
|
||||
return new ValidationInterceptorResult
|
||||
{
|
||||
Valid = maxSeverity != ValidationSeverity.Error,
|
||||
Severity = maxSeverity,
|
||||
Messages = messages
|
||||
};
|
||||
}
|
||||
|
||||
return ValidationInterceptorResult.Success();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that tool responses don't contain error indicators that should be handled.
|
||||
/// </summary>
|
||||
[McpClientInterceptor(
|
||||
Name = "response-validator",
|
||||
Description = "Validates tool call responses for errors",
|
||||
Events = [InterceptorEvents.ToolsCall],
|
||||
Phase = InterceptorPhase.Response)]
|
||||
public ValidationInterceptorResult ValidateResponse(JsonNode? payload)
|
||||
{
|
||||
if (payload is null)
|
||||
{
|
||||
return ValidationInterceptorResult.Success();
|
||||
}
|
||||
|
||||
// Parse as tool call result
|
||||
CallToolResult? result;
|
||||
try
|
||||
{
|
||||
result = payload.Deserialize<CallToolResult>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return ValidationInterceptorResult.Success();
|
||||
}
|
||||
|
||||
if (result?.IsError == true)
|
||||
{
|
||||
// Extract error message from content if available
|
||||
var errorMessage = result.Content?.FirstOrDefault() switch
|
||||
{
|
||||
TextContentBlock text => text.Text,
|
||||
_ => "Tool execution failed"
|
||||
};
|
||||
|
||||
return new ValidationInterceptorResult
|
||||
{
|
||||
Valid = false,
|
||||
Severity = ValidationSeverity.Warn, // Warning so it doesn't block, but is logged
|
||||
Messages = [new() { Message = $"Tool returned error: {errorMessage}", Severity = ValidationSeverity.Warn }]
|
||||
};
|
||||
}
|
||||
|
||||
return ValidationInterceptorResult.Success();
|
||||
}
|
||||
|
||||
#if NET
|
||||
[GeneratedRegex(@"\b\d{3}-\d{2}-\d{4}\b")]
|
||||
private static partial Regex SsnRegex();
|
||||
|
||||
[GeneratedRegex(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")]
|
||||
private static partial Regex EmailRegex();
|
||||
|
||||
[GeneratedRegex(@"\b(?:\d{4}[-\s]?){3}\d{4}\b")]
|
||||
private static partial Regex CreditCardRegex();
|
||||
#else
|
||||
private static Regex SsnRegex() => new(@"\b\d{3}-\d{2}-\d{4}\b", RegexOptions.Compiled);
|
||||
private static Regex EmailRegex() => new(@"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b", RegexOptions.Compiled);
|
||||
private static Regex CreditCardRegex() => new(@"\b(?:\d{4}[-\s]?){3}\d{4}\b", RegexOptions.Compiled);
|
||||
#endif
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
225
csharp/sdk/samples/InterceptorClientSample/Program.cs
Normal file
225
csharp/sdk/samples/InterceptorClientSample/Program.cs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
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();
|
||||
}
|
||||
Loading…
Reference in a new issue