UNPKG

mcp-server-kubernetes

Version:

MCP server for interacting with Kubernetes clusters via kubectl

180 lines (179 loc) 7.92 kB
import { trace, SpanStatusCode } from "@opentelemetry/api"; import { serverConfig } from "../config/server-config.js"; import { getTelemetryConfig } from "../config/telemetry-config.js"; /** * Telemetry middleware for MCP tool call tracing * Wraps tool handlers with OpenTelemetry spans to provide automatic instrumentation */ // Get tracer instance const tracer = trace.getTracer(serverConfig.name, serverConfig.version); /** * Wrap a tool call handler with OpenTelemetry tracing * Creates a span for each tool invocation with detailed attributes * * @param handler - The original tool call handler function * @returns Wrapped handler with tracing instrumentation */ export function withTelemetry(handler) { return async (request) => { const { name: toolName, arguments: args } = request.params; // Create span for this tool call return await tracer.startActiveSpan(`tools/call ${toolName}`, { attributes: { "mcp.method.name": "tools/call", "gen_ai.tool.name": toolName, "gen_ai.operation.name": "execute_tool", "network.transport": "pipe", // STDIO mode }, }, async (span) => { const startTime = Date.now(); try { // Add argument metadata (safely, without exposing sensitive data) if (args) { const argKeys = Object.keys(args); span.setAttribute("tool.argument_count", argKeys.length); span.setAttribute("tool.argument_keys", argKeys.join(",")); // Add specific attributes for common arguments if (args.context) { span.setAttribute("k8s.context", args.context); } if (args.namespace) { span.setAttribute("k8s.namespace", args.namespace); } if (args.resourceType) { span.setAttribute("k8s.resource_type", args.resourceType); } } // Execute the actual tool handler const result = await handler(request); // Record success const duration = Date.now() - startTime; span.setAttribute("tool.duration_ms", duration); span.setStatus({ code: SpanStatusCode.OK }); // Capture response metadata (not the actual data) // Can be disabled with OTEL_CAPTURE_RESPONSE_METADATA=false for privacy const telemetryConfig = getTelemetryConfig(); if (result && telemetryConfig.captureResponseMetadata) { // Check if result has content array (MCP response format) if (result.content && Array.isArray(result.content)) { span.setAttribute("response.content_items", result.content.length); // Get the first content item to analyze if (result.content.length > 0) { const firstItem = result.content[0]; span.setAttribute("response.content_type", firstItem.type || "unknown"); // If it's text content, capture size and maybe a snippet if (firstItem.type === "text" && firstItem.text) { const textSize = firstItem.text.length; span.setAttribute("response.text_size_bytes", textSize); // Try to parse JSON and get item count try { const parsed = JSON.parse(firstItem.text); // Check for Kubernetes list response if (parsed.items && Array.isArray(parsed.items)) { span.setAttribute("response.k8s_items_count", parsed.items.length); span.setAttribute("response.k8s_kind", parsed.kind || "unknown"); } // Check for MCP list response if (Array.isArray(parsed)) { span.setAttribute("response.items_count", parsed.length); } // Capture if response indicates success if (parsed.success !== undefined) { span.setAttribute("response.success", parsed.success); } } catch (e) { // Not JSON, that's fine - just capture text size } } } } // Check for direct success indicators if (typeof result.success === "boolean") { span.setAttribute("response.success", result.success); } } return result; } catch (error) { // Record failure const duration = Date.now() - startTime; span.setAttribute("tool.duration_ms", duration); span.setAttribute("error.type", "tool_error"); if (error.message) { span.setAttribute("error.message", error.message); } if (error.code) { span.setAttribute("error.code", error.code); } span.setStatus({ code: SpanStatusCode.ERROR, message: error.message || "Tool execution failed", }); // Re-throw to maintain original error behavior throw error; } finally { span.end(); } }); }; } /** * Create a manual span for non-tool operations * Useful for tracing other server operations outside of tool calls * * @param name - Span name * @param fn - Function to execute within the span * @returns Result of the function */ export async function withSpan(name, attributes, fn) { return await tracer.startActiveSpan(name, { attributes }, async (span) => { try { const result = await fn(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setAttribute("error.type", "operation_error"); if (error.message) { span.setAttribute("error.message", error.message); } span.setStatus({ code: SpanStatusCode.ERROR, message: error.message || "Operation failed", }); throw error; } finally { span.end(); } }); } /** * Add custom attributes to the current active span * Useful for adding context during tool execution * * @param attributes - Key-value pairs to add to the span */ export function addSpanAttributes(attributes) { const currentSpan = trace.getActiveSpan(); if (currentSpan) { for (const [key, value] of Object.entries(attributes)) { currentSpan.setAttribute(key, value); } } } /** * Record an event on the current active span * Useful for tracking significant moments during tool execution * * @param name - Event name * @param attributes - Optional event attributes */ export function recordSpanEvent(name, attributes) { const currentSpan = trace.getActiveSpan(); if (currentSpan) { currentSpan.addEvent(name, attributes); } }