UNPKG

mcp-server-kubernetes

Version:

MCP server for interacting with Kubernetes clusters via kubectl

147 lines (146 loc) 5.22 kB
import { spawn } from "child_process"; import { McpError } from "@modelcontextprotocol/sdk/types.js"; import { assertSafeArgv } from "../security/kubectl-flags.js"; // Use spawn instead of exec because port-forward is a long-running process async function executePortForward(args) { // port_forward uses spawn (long-running process) rather than the shared // execFileSyncSafe wrapper, so it must run the argv guard itself. Otherwise // user-supplied values pushed into positional slots (e.g. resourceType) can // smuggle credential/target-redirecting flags such as --server. assertSafeArgv(args); return new Promise((resolve, reject) => { const process = spawn("kubectl", args); let output = ""; let errorOutput = ""; process.stdout.on("data", (data) => { output += data.toString(); if (output.includes("Forwarding from")) { resolve({ success: true, message: "port-forwarding was successful", pid: process.pid, }); } }); process.stderr.on("data", (data) => { errorOutput += data.toString(); }); process.on("error", (error) => { reject(new Error(`Failed to execute port-forward: ${error.message}`)); }); process.on("close", (code) => { if (code !== 0) { reject(new Error(`Port-forward process exited with code ${code}. Error: ${errorOutput}`)); } }); // Set a timeout to reject if we don't see the success message setTimeout(() => { if (!output.includes("Forwarding from")) { reject(new Error("port-forwarding failed - no success message received")); } }, 5000); }); } export const PortForwardSchema = { name: "port_forward", description: "Forward a local port to a port on a Kubernetes resource", annotations: { title: "Port Forward", }, inputSchema: { type: "object", properties: { resourceType: { type: "string" }, resourceName: { type: "string" }, localPort: { type: "number" }, targetPort: { type: "number" }, namespace: { type: "string" }, }, required: ["resourceType", "resourceName", "localPort", "targetPort"], }, }; export async function startPortForward(k8sManager, input) { const args = ["port-forward"]; if (input.namespace) { args.push("-n", input.namespace); } args.push(`${input.resourceType}/${input.resourceName}`); args.push(`${input.localPort}:${input.targetPort}`); try { const result = await executePortForward(args); // Track the port-forward process k8sManager.trackPortForward({ id: `${input.resourceType}-${input.resourceName}-${input.localPort}`, server: { stop: async () => { try { process.kill(result.pid); } catch (error) { console.error(`Failed to stop port-forward process ${result.pid}:`, error); } }, }, resourceType: input.resourceType, name: input.resourceName, namespace: input.namespace || "default", ports: [{ local: input.localPort, remote: input.targetPort }], }); return { content: [ { type: "text", text: JSON.stringify({ success: result.success, message: result.message, }), }, ], }; } catch (error) { // Preserve McpError (e.g. the argv safety guard's InvalidParams rejection) // so the client sees the real reason instead of a generic InternalError. if (error instanceof McpError) throw error; throw new Error(`Failed to execute port-forward: ${error.message}`); } } export const StopPortForwardSchema = { name: "stop_port_forward", description: "Stop a port-forward process", annotations: { title: "Stop Port Forward", }, inputSchema: { type: "object", properties: { id: { type: "string" }, }, required: ["id"], }, }; export async function stopPortForward(k8sManager, input) { const portForward = k8sManager.getPortForward(input.id); if (!portForward) { throw new Error(`Port-forward with id ${input.id} not found`); } try { await portForward.server.stop(); k8sManager.removePortForward(input.id); return { content: [ { type: "text", text: JSON.stringify({ success: true, message: "port-forward stopped successfully", }), }, ], }; } catch (error) { throw new Error(`Failed to stop port-forward: ${error.message}`); } }