using ModelContextProtocol.Interceptors; using ModelContextProtocol.Interceptors.Server; using ModelContextProtocol.Protocol; using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Nodes; /// /// Server-side observability interceptors for MCP operations. /// These interceptors log, monitor, and audit MCP messages without modifying them. /// Useful for centralized logging, metrics collection, and compliance auditing. /// [McpServerInterceptorType] public class ServerObservabilityInterceptors { // In-memory metrics storage (in production, use a proper metrics service) private static readonly ConcurrentDictionary ToolCallCounts = new(); private static readonly ConcurrentDictionary ResourceReadCounts = new(); private static readonly ConcurrentDictionary> ToolCallDurations = new(); /// /// Logs tool call requests for monitoring and debugging. /// [McpServerInterceptor( Name = "tool-request-logger", Description = "Logs tool call request details", Events = [InterceptorEvents.ToolsCall], Phase = InterceptorPhase.Request)] public ObservabilityInterceptorResult LogToolRequest(JsonNode? payload) { if (payload is null) { return new ObservabilityInterceptorResult { Observed = false }; } CallToolRequestParams? toolCall; try { toolCall = payload.Deserialize(); } catch (JsonException) { return new ObservabilityInterceptorResult { Observed = false }; } if (toolCall is null) { return new ObservabilityInterceptorResult { Observed = false }; } // Track tool call counts var toolName = toolCall.Name ?? "unknown"; ToolCallCounts.AddOrUpdate(toolName, 1, (_, count) => count + 1); var argumentCount = toolCall.Arguments?.Count ?? 0; var argumentNames = toolCall.Arguments?.Keys.ToArray() ?? []; return new ObservabilityInterceptorResult { Observed = true, Info = new JsonObject { ["event"] = "tool_request", ["tool"] = toolName, ["argumentCount"] = argumentCount, ["argumentNames"] = JsonNode.Parse(JsonSerializer.Serialize(argumentNames)), ["timestamp"] = DateTimeOffset.UtcNow.ToString("O"), ["totalCallsToTool"] = ToolCallCounts.GetValueOrDefault(toolName, 0) } }; } /// /// Logs tool call responses including success/failure status. /// [McpServerInterceptor( Name = "tool-response-logger", Description = "Logs tool call response details", Events = [InterceptorEvents.ToolsCall], Phase = InterceptorPhase.Response)] public ObservabilityInterceptorResult LogToolResponse(JsonNode? payload) { if (payload is null) { return new ObservabilityInterceptorResult { Observed = false }; } CallToolResult? result; try { result = payload.Deserialize(); } catch (JsonException) { return new ObservabilityInterceptorResult { Observed = false }; } if (result is null) { return new ObservabilityInterceptorResult { Observed = false }; } var contentCount = result.Content?.Count ?? 0; var contentTypes = result.Content?.Select(c => c.GetType().Name).Distinct().ToArray() ?? []; return new ObservabilityInterceptorResult { Observed = true, Metrics = new Dictionary { ["content_count"] = contentCount, ["is_error"] = (result.IsError ?? false) ? 1.0 : 0.0 }, Info = new JsonObject { ["event"] = "tool_response", ["isError"] = result.IsError, ["contentCount"] = contentCount, ["contentTypes"] = JsonNode.Parse(JsonSerializer.Serialize(contentTypes)), ["timestamp"] = DateTimeOffset.UtcNow.ToString("O") } }; } /// /// Logs resource read requests. /// [McpServerInterceptor( Name = "resource-request-logger", Description = "Logs resource read request details", Events = [InterceptorEvents.ResourcesRead], Phase = InterceptorPhase.Request)] public ObservabilityInterceptorResult LogResourceRequest(JsonNode? payload) { if (payload is null) { return new ObservabilityInterceptorResult { Observed = false }; } var uri = payload["uri"]?.GetValue() ?? "unknown"; // Track resource read counts by URI pattern var uriPattern = ExtractUriPattern(uri); ResourceReadCounts.AddOrUpdate(uriPattern, 1, (_, count) => count + 1); return new ObservabilityInterceptorResult { Observed = true, Info = new JsonObject { ["event"] = "resource_request", ["uri"] = uri, ["uriPattern"] = uriPattern, ["timestamp"] = DateTimeOffset.UtcNow.ToString("O"), ["totalReadsToPattern"] = ResourceReadCounts.GetValueOrDefault(uriPattern, 0) } }; } /// /// Logs resource read responses including content size. /// [McpServerInterceptor( Name = "resource-response-logger", Description = "Logs resource read response details", Events = [InterceptorEvents.ResourcesRead], Phase = InterceptorPhase.Response)] public ObservabilityInterceptorResult LogResourceResponse(JsonNode? payload) { if (payload is null) { return new ObservabilityInterceptorResult { Observed = false }; } ReadResourceResult? result; try { result = payload.Deserialize(); } catch (JsonException) { return new ObservabilityInterceptorResult { Observed = false }; } if (result is null) { return new ObservabilityInterceptorResult { Observed = false }; } var contentCount = result.Contents?.Count ?? 0; long totalSize = 0; if (result.Contents is not null) { foreach (var content in result.Contents) { if (content is TextResourceContents textContent) { totalSize += textContent.Text?.Length ?? 0; } else if (content is BlobResourceContents blobContent) { totalSize += blobContent.Blob?.Length ?? 0; } } } return new ObservabilityInterceptorResult { Observed = true, Metrics = new Dictionary { ["content_count"] = contentCount, ["total_size_bytes"] = totalSize }, Info = new JsonObject { ["event"] = "resource_response", ["contentCount"] = contentCount, ["totalSizeBytes"] = totalSize, ["timestamp"] = DateTimeOffset.UtcNow.ToString("O") } }; } /// /// Tracks prompt list requests for analytics. /// [McpServerInterceptor( Name = "prompt-request-logger", Description = "Logs prompt list and get request details", Events = [InterceptorEvents.PromptsList, InterceptorEvents.PromptsGet], Phase = InterceptorPhase.Request)] public ObservabilityInterceptorResult LogPromptRequest(JsonNode? payload, string @event) { return new ObservabilityInterceptorResult { Observed = true, Info = new JsonObject { ["event"] = @event, ["payload"] = payload?.ToJsonString() ?? "null", ["timestamp"] = DateTimeOffset.UtcNow.ToString("O") } }; } /// /// Collects aggregate metrics for dashboards. /// [McpServerInterceptor( Name = "metrics-collector", Description = "Collects aggregate metrics across all operations", Events = [InterceptorEvents.ToolsCall, InterceptorEvents.ResourcesRead], Phase = InterceptorPhase.Response)] public ObservabilityInterceptorResult CollectMetrics(JsonNode? payload, string @event) { // Calculate aggregate metrics var totalToolCalls = ToolCallCounts.Values.Sum(); var totalResourceReads = ResourceReadCounts.Values.Sum(); var uniqueTools = ToolCallCounts.Count; var uniqueResourcePatterns = ResourceReadCounts.Count; return new ObservabilityInterceptorResult { Observed = true, Metrics = new Dictionary { ["total_tool_calls"] = totalToolCalls, ["total_resource_reads"] = totalResourceReads, ["unique_tools_used"] = uniqueTools, ["unique_resource_patterns"] = uniqueResourcePatterns }, Info = new JsonObject { ["event"] = "metrics_snapshot", ["triggeringEvent"] = @event, ["timestamp"] = DateTimeOffset.UtcNow.ToString("O") } }; } /// /// Generates alerts for suspicious activity patterns. /// [McpServerInterceptor( Name = "anomaly-detector", Description = "Detects anomalous patterns and generates alerts", Events = [InterceptorEvents.ToolsCall], Phase = InterceptorPhase.Request)] public ObservabilityInterceptorResult DetectAnomalies(JsonNode? payload) { if (payload is null) { return new ObservabilityInterceptorResult { Observed = false }; } CallToolRequestParams? toolCall; try { toolCall = payload.Deserialize(); } catch (JsonException) { return new ObservabilityInterceptorResult { Observed = false }; } var alerts = new List(); // Check for rapid-fire requests to the same tool var toolName = toolCall?.Name ?? "unknown"; var callCount = ToolCallCounts.GetValueOrDefault(toolName, 0); if (callCount > 100) { alerts.Add(new ObservabilityAlert { Level = "warning", Message = $"High volume of calls to tool '{toolName}': {callCount} calls", Tags = ["rate_limiting", "abuse_prevention"] }); } // Check for unusually large payloads var payloadSize = payload.ToJsonString().Length; if (payloadSize > 10000) { alerts.Add(new ObservabilityAlert { Level = "info", Message = $"Large payload detected: {payloadSize} bytes", Tags = ["performance", "payload_size"] }); } return new ObservabilityInterceptorResult { Observed = true, Alerts = alerts.Count > 0 ? alerts : null, Info = new JsonObject { ["event"] = "anomaly_check", ["tool"] = toolName, ["payloadSize"] = payloadSize, ["alertCount"] = alerts.Count, ["timestamp"] = DateTimeOffset.UtcNow.ToString("O") } }; } /// /// Audit logger for compliance purposes. /// [McpServerInterceptor( Name = "audit-logger", Description = "Creates audit trail for compliance requirements", Events = [InterceptorEvents.ToolsCall, InterceptorEvents.ResourcesRead, InterceptorEvents.PromptsList, InterceptorEvents.PromptsGet], Phase = InterceptorPhase.Request)] public ObservabilityInterceptorResult CreateAuditEntry(JsonNode? payload, string @event) { // In production, this would write to a secure audit log var auditEntry = new JsonObject { ["auditId"] = Guid.NewGuid().ToString(), ["event"] = @event, ["timestamp"] = DateTimeOffset.UtcNow.ToString("O"), ["payloadHash"] = ComputePayloadHash(payload), ["sourceIp"] = "127.0.0.1", // Would be extracted from context in production ["userId"] = "system" // Would be extracted from authentication context }; return new ObservabilityInterceptorResult { Observed = true, Info = new JsonObject { ["event"] = "audit_entry_created", ["auditEntry"] = auditEntry } }; } private static string ExtractUriPattern(string uri) { // Extract pattern from URI (e.g., file:///path/to/file.txt -> file:///**/*) try { var uriObj = new Uri(uri); return $"{uriObj.Scheme}://{uriObj.Host}/**"; } catch { return uri.Split('/').FirstOrDefault() ?? "unknown"; } } private static string ComputePayloadHash(JsonNode? payload) { if (payload is null) { return "null"; } var json = payload.ToJsonString(); using var sha256 = System.Security.Cryptography.SHA256.Create(); var hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(json)); return Convert.ToHexString(hashBytes)[..16]; // First 16 chars of hash } }