Skip to content

Add exec_in_pod tool for command execution in Kubernetes pods #128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { deletePod, deletePodSchema } from "./tools/delete_pod.js";
import { describePod, describePodSchema } from "./tools/describe_pod.js";
import { getLogs, getLogsSchema } from "./tools/get_logs.js";
import { getEvents, getEventsSchema } from "./tools/get_events.js";
import { execInPod, execInPodSchema } from "./tools/exec_in_pod.js";
import { getResourceHandlers } from "./resources/handlers.js";
import {
ListResourcesRequestSchema,
Expand Down Expand Up @@ -165,6 +166,7 @@ const allTools = [
DeleteCronJobSchema,
CreateConfigMapSchema,
updateServiceSchema,
execInPodSchema,
];

const k8sManager = new KubernetesManager();
Expand Down Expand Up @@ -664,6 +666,18 @@ server.setRequestHandler(
);
}

case "exec_in_pod": {
return await execInPod(
k8sManager,
input as {
name: string;
namespace?: string;
command: string | string[];
container?: string;
}
);
}

default:
throw new McpError(ErrorCode.InvalidRequest, `Unknown tool: ${name}`);
}
Expand Down
4 changes: 4 additions & 0 deletions src/models/response-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,7 @@ export const SetCurrentContextResponseSchema = z.object({
export const DescribeNodeResponseSchema = z.object({
content: z.array(ToolResponseContent),
});

export const ExecInPodResponseSchema = z.object({
content: z.array(ToolResponseContent),
});
203 changes: 203 additions & 0 deletions src/tools/exec_in_pod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/**
* Tool: exec_in_pod
* Execute a command in a Kubernetes pod or container and return the output.
* Uses the official Kubernetes client-node Exec API for native execution.
* Supports both string and array command formats, and optional container targeting.
*/

import * as k8s from "@kubernetes/client-node";
import { KubernetesManager } from "../types.js";
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import { Writable } from "stream";

/**
* Schema for exec_in_pod tool.
* - name: Pod name
* - namespace: Namespace (default: "default")
* - command: Command to execute (string or array of args)
* - container: (Optional) Container name
*/
export const execInPodSchema = {
name: "exec_in_pod",
description: "Execute a command in a Kubernetes pod or container and return the output",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the pod to execute the command in",
},
namespace: {
type: "string",
description: "Kubernetes namespace where the pod is located",
default: "default",
},
command: {
anyOf: [
{ type: "string" },
{ type: "array", items: { type: "string" } }
],
description: "Command to execute in the pod (string or array of args)",
},
container: {
type: "string",
description: "Container name (required when pod has multiple containers)",
optional: true,
},
shell: {
type: "string",
description: "Shell to use for command execution (e.g. '/bin/sh', '/bin/bash'). If not provided, will use command as-is.",
optional: true,
},
timeout: {
type: "number",
description: "Timeout for command - 60000 milliseconds if not specified",
optional: true,
},
},
required: ["name", "command"],
},
};

/**
* Execute a command in a Kubernetes pod or container using the Kubernetes client-node Exec API.
* Returns the stdout output as a text response.
* Throws McpError on failure.
*/
export async function execInPod(
k8sManager: KubernetesManager,
input: {
name: string;
namespace?: string;
command: string | string[];
container?: string;
shell?: string;
timeout?: number;
}
): Promise<{ content: { type: string; text: string }[] }> {
const namespace = input.namespace || "default";
// Convert command to array of strings for the Exec API
let commandArr: string[];
if (Array.isArray(input.command)) {
commandArr = input.command;
} else {
// Always wrap string commands in a shell for correct parsing
const shell = input.shell || "/bin/sh";
commandArr = [shell, "-c", input.command];
console.log("[exec_in_pod] Using shell:", shell, "Command array:", commandArr);
}

// Prepare buffers to capture stdout and stderr
let stdout = "";
let stderr = "";

// Use Node.js Writable streams to collect output
const stdoutStream = new Writable({
write(chunk, _encoding, callback) {
stdout += chunk.toString();
callback();
}
});
const stderrStream = new Writable({
write(chunk, _encoding, callback) {
stderr += chunk.toString();
callback();
}
});
// Add a dummy stdin stream
const stdinStream = new Writable({
write(_chunk, _encoding, callback) {
callback();
}
});

try {
// Use the Kubernetes client-node Exec API for native exec
const kc = k8sManager.getKubeConfig();
const exec = new k8s.Exec(kc);

// Add a timeout to avoid hanging forever if exec never returns
await new Promise<void>((resolve, reject) => {
let finished = false;
const timeoutMs = input.timeout || 60000;
const timeout = setTimeout(() => {
if (!finished) {
finished = true;
reject(
new McpError(
ErrorCode.InternalError,
"Exec operation timed out (possible networking, RBAC, or cluster issue)"
)
);
}
}, timeoutMs);

console.log("[exec_in_pod] Calling exec.exec with params:", {
namespace,
pod: input.name,
container: input.container ?? "",
commandArr,
stdoutStreamType: typeof stdoutStream,
stderrStreamType: typeof stderrStream,
});

exec.exec(
namespace,
input.name,
input.container ?? "",
commandArr,
stdoutStream as any,
stderrStream as any,
stdinStream as any, // use dummy stdin
true, // set tty to true
(status: any) => {
console.log("[exec_in_pod] exec.exec callback called. Status:", status);
if (finished) return;
finished = true;
clearTimeout(timeout);
// Always resolve; handle errors based on stderr or thrown errors
resolve();
}
).catch((err: any) => {
console.log("[exec_in_pod] exec.exec threw error:", err);
if (!finished) {
finished = true;
clearTimeout(timeout);
reject(
new McpError(
ErrorCode.InternalError,
`Exec threw error: ${err?.message || err}`
)
);
}
});
});

// Return the collected stdout as the result
// If there is stderr output or no output at all, treat as error
if (stderr || (!stdout && !stderr)) {
throw new McpError(
ErrorCode.InternalError,
`Failed to execute command in pod: ${stderr || "No output"}`
);
}
return {
content: [
{
type: "text",
text: stdout,
},
],
};
} catch (error: any) {
// Collect error message and stderr output if available
let message = error.message || "Unknown error";
if (stderr) {
message += "\n" + stderr;
}
throw new McpError(
ErrorCode.InternalError,
`Failed to execute command in pod: ${message}`
);
}
}
Loading