UNPKG

mcp-server-kubernetes

Version:

MCP server for interacting with Kubernetes clusters via kubectl

308 lines (307 loc) 12 kB
import { execSync } from "child_process"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; export const kubectlLogsSchema = { name: "kubectl_logs", description: "Get logs from Kubernetes resources like pods, deployments, or jobs", inputSchema: { type: "object", properties: { resourceType: { type: "string", enum: ["pod", "deployment", "job", "cronjob"], description: "Type of resource to get logs from", }, name: { type: "string", description: "Name of the resource", }, namespace: { type: "string", description: "Namespace of the resource", default: "default", }, container: { type: "string", description: "Container name (required when pod has multiple containers)" }, tail: { type: "number", description: "Number of lines to show from end of logs" }, since: { type: "string", description: "Show logs since relative time (e.g. '5s', '2m', '3h')" }, sinceTime: { type: "string", description: "Show logs since absolute time (RFC3339)" }, timestamps: { type: "boolean", description: "Include timestamps in logs", default: false, }, previous: { type: "boolean", description: "Include logs from previously terminated containers", default: false, }, follow: { type: "boolean", description: "Follow logs output (not recommended, may cause timeouts)", default: false, }, labelSelector: { type: "string", description: "Filter resources by label selector" } }, required: ["resourceType", "name", "namespace"], }, }; export async function kubectlLogs(k8sManager, input) { try { const resourceType = input.resourceType.toLowerCase(); const name = input.name; const namespace = input.namespace || "default"; // Build the kubectl command base let baseCommand = `kubectl -n ${namespace}`; // Handle different resource types if (resourceType === "pod") { // Direct pod logs baseCommand += ` logs ${name}`; // If container is specified, add it if (input.container) { baseCommand += ` -c ${input.container}`; } // Add options baseCommand = addLogOptions(baseCommand, input); // Execute the command try { const result = execSync(baseCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }); return formatLogOutput(name, result); } catch (error) { return handleCommandError(error, `pod ${name}`); } } else if (resourceType === "deployment" || resourceType === "job" || resourceType === "cronjob") { // For deployments, jobs and cronjobs we need to find the pods first let selectorCommand; if (resourceType === "deployment") { selectorCommand = `kubectl -n ${namespace} get deployment ${name} -o jsonpath='{.spec.selector.matchLabels}'`; } else if (resourceType === "job") { // For jobs, we use the job-name label return getLabelSelectorLogs(`job-name=${name}`, namespace, input); } else if (resourceType === "cronjob") { // For cronjobs, it's more complex - need to find the job first const jobsCommand = `kubectl -n ${namespace} get jobs --selector=job-name=${name} -o jsonpath='{.items[*].metadata.name}'`; try { const jobs = execSync(jobsCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim().split(' '); if (jobs.length === 0 || (jobs.length === 1 && jobs[0] === '')) { return { content: [ { type: "text", text: JSON.stringify({ message: `No jobs found for cronjob ${name} in namespace ${namespace}`, }, null, 2), }, ], }; } // Get logs for all jobs const allJobLogs = {}; for (const job of jobs) { // Get logs for pods from this job const result = await getLabelSelectorLogs(`job-name=${job}`, namespace, input); const jobLog = JSON.parse(result.content[0].text); allJobLogs[job] = jobLog.logs; } return { content: [ { type: "text", text: JSON.stringify({ cronjob: name, namespace: namespace, jobs: allJobLogs, }, null, 2), }, ], }; } catch (error) { return handleCommandError(error, `cronjob ${name}`); } } try { if (resourceType === "deployment") { // Get the deployment's selector if (!selectorCommand) { throw new Error("Selector command is undefined"); } const selectorJson = execSync(selectorCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim(); const selector = JSON.parse(selectorJson.replace(/'/g, '"')); // Convert to label selector format const labelSelector = Object.entries(selector) .map(([key, value]) => `${key}=${value}`) .join(','); return getLabelSelectorLogs(labelSelector, namespace, input); } // For jobs and cronjobs, the logic is handled above return { content: [ { type: "text", text: JSON.stringify({ error: `Unexpected resource type: ${resourceType}`, }, null, 2), }, ], isError: true, }; } catch (error) { return handleCommandError(error, `${resourceType} ${name}`); } } else if (input.labelSelector) { // Handle logs by label selector return getLabelSelectorLogs(input.labelSelector, namespace, input); } else { throw new McpError(ErrorCode.InvalidRequest, `Unsupported resource type: ${resourceType}`); } } catch (error) { if (error instanceof McpError) throw error; throw new McpError(ErrorCode.InternalError, `Failed to get logs: ${error.message}`); } } // Helper function to add log options to the kubectl command function addLogOptions(baseCommand, input) { let command = baseCommand; // Add options based on inputs if (input.tail !== undefined) { command += ` --tail=${input.tail}`; } if (input.since) { command += ` --since=${input.since}`; } if (input.sinceTime) { command += ` --since-time=${input.sinceTime}`; } if (input.timestamps) { command += ` --timestamps`; } if (input.previous) { command += ` --previous`; } if (input.follow) { command += ` --follow`; } return command; } // Helper function to get logs for resources selected by labels async function getLabelSelectorLogs(labelSelector, namespace, input) { try { // First, find all pods matching the label selector const podsCommand = `kubectl -n ${namespace} get pods --selector=${labelSelector} -o jsonpath='{.items[*].metadata.name}'`; const pods = execSync(podsCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim().split(' '); if (pods.length === 0 || (pods.length === 1 && pods[0] === '')) { return { content: [ { type: "text", text: JSON.stringify({ message: `No pods found with label selector "${labelSelector}" in namespace ${namespace}`, }, null, 2), }, ], }; } // Get logs for each pod const logsMap = {}; for (const pod of pods) { // Skip empty pod names if (!pod) continue; let podCommand = `kubectl -n ${namespace} logs ${pod}`; // Add container if specified if (input.container) { podCommand += ` -c ${input.container}`; } // Add other options podCommand = addLogOptions(podCommand, input); try { const logs = execSync(podCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }); logsMap[pod] = logs; } catch (error) { logsMap[pod] = `Error: ${error.message}`; } } return { content: [ { type: "text", text: JSON.stringify({ selector: labelSelector, namespace: namespace, logs: logsMap, }, null, 2), }, ], }; } catch (error) { return handleCommandError(error, `pods with selector "${labelSelector}"`); } } // Helper function to format log output function formatLogOutput(resourceName, logOutput) { return { content: [ { type: "text", text: JSON.stringify({ name: resourceName, logs: logOutput, }, null, 2), }, ], }; } // Helper function to handle command errors function handleCommandError(error, resourceDescription) { console.error(`Error getting logs for ${resourceDescription}:`, error); if (error.status === 404 || error.message.includes("not found")) { return { content: [ { type: "text", text: JSON.stringify({ error: `Resource ${resourceDescription} not found`, status: "not_found", }, null, 2), }, ], isError: true, }; } return { content: [ { type: "text", text: JSON.stringify({ error: `Failed to get logs for ${resourceDescription}: ${error.message}`, }, null, 2), }, ], isError: true, }; }