mcp-server-kubernetes
Version:
MCP server for interacting with Kubernetes clusters via kubectl
147 lines (146 loc) • 5.22 kB
JavaScript
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}`);
}
}