using ModelContextProtocol.Interceptors;
using ModelContextProtocol.Interceptors.Client;
using ModelContextProtocol.Protocol;
using System.Text.Json;
using System.Text.Json.Nodes;
///
/// Sample mutation interceptors for MCP client operations.
/// These interceptors demonstrate argument transformation and response filtering.
///
[McpClientInterceptorType]
public class ClientMutationInterceptors
{
///
/// Normalizes tool arguments by trimming whitespace and converting empty strings to null.
///
[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();
}
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();
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);
}
///
/// Adds a timestamp to all outgoing tool call requests for audit purposes.
///
[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);
}
///
/// Redacts sensitive information from tool responses before returning to caller.
///
[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();
}
catch (JsonException)
{
return MutationInterceptorResult.Unchanged(payload);
}
if (result?.Content is null)
{
return MutationInterceptorResult.Unchanged(payload);
}
bool modified = false;
var newContent = new List();
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");
}
}