mcp-server-kubernetes
Version:
MCP server for interacting with Kubernetes clusters via kubectl
151 lines (150 loc) • 6.26 kB
JavaScript
import { execFileSyncSafe } from "../security/kubectl-flags.js";
import { isRemoteTransport } from "../security/transport.js";
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
import { contextParameter, dryRunParameter, namespaceParameter, } from "../models/common-parameters.js";
export const kubectlPatchSchema = {
name: "kubectl_patch",
description: "Update field(s) of a resource using strategic merge patch, JSON merge patch, or JSON patch",
annotations: {
destructiveHint: true,
},
inputSchema: {
type: "object",
properties: {
resourceType: {
type: "string",
description: "Type of resource to patch (e.g., pods, deployments, services)",
},
name: {
type: "string",
description: "Name of the resource to patch",
},
namespace: namespaceParameter,
patchType: {
type: "string",
description: "Type of patch to apply",
enum: ["strategic", "merge", "json"],
default: "strategic",
},
patchData: {
type: "object",
description: "Patch data as a JSON object",
},
patchFile: {
type: "string",
description: "Path to a file containing the patch data (alternative to patchData). The path is read on the machine running the MCP server, so it is rejected when the server runs over a remote (SSE/Streamable HTTP) transport; use 'patchData' to pass the patch contents instead.",
},
dryRun: dryRunParameter,
context: contextParameter,
},
required: ["resourceType", "name"],
},
};
export async function kubectlPatch(k8sManager, input) {
try {
if (!input.patchData && !input.patchFile) {
throw new McpError(ErrorCode.InvalidRequest, "Either patchData or patchFile must be provided");
}
// Reject server-side filesystem reads on remote transports. Over SSE /
// Streamable HTTP the path resolves on the MCP server host, not the
// client, so `patchFile` (--patch-file) would let any client that can
// reach the endpoint read arbitrary server files (kubeconfig,
// service-account token, /proc/self/environ, etc.) via kubectl's parse
// errors, or merge their contents into a resource that is then readable
// through kubectl_get. Clients on these transports must pass the patch
// inline via `patchData` instead.
if (isRemoteTransport() && input.patchFile) {
throw new McpError(ErrorCode.InvalidRequest, "The 'patchFile' parameter reads a file from the MCP server's filesystem and is disabled on remote (SSE/Streamable HTTP) transports. Pass the patch contents via 'patchData' instead.");
}
const namespace = input.namespace || "default";
const patchType = input.patchType || "strategic";
const dryRun = input.dryRun || false;
const context = input.context || "";
let tempFile = null;
const command = "kubectl";
const args = ["patch", input.resourceType, input.name, "-n", namespace];
// Add patch type flag
switch (patchType) {
case "strategic":
args.push("--type", "strategic");
break;
case "merge":
args.push("--type", "merge");
break;
case "json":
args.push("--type", "json");
break;
default:
args.push("--type", "strategic");
}
// Handle patch data
if (input.patchData) {
if (input.patchData === null || typeof input.patchData !== "object") {
throw new McpError(ErrorCode.InvalidRequest, "patchData must be a valid JSON object, not a string.");
}
// Create a temporary file for the patch data
const tmpDir = os.tmpdir();
tempFile = path.join(tmpDir, `patch-${Date.now()}.json`);
fs.writeFileSync(tempFile, JSON.stringify(input.patchData));
args.push("--patch-file", tempFile);
}
else if (input.patchFile) {
args.push("--patch-file", input.patchFile);
}
// Add dry-run flag if requested
if (dryRun) {
args.push("--dry-run=client");
}
// Add context if provided
if (context) {
args.push("--context", context);
}
// Execute the command
try {
const result = execFileSyncSafe(command, args, {
encoding: "utf8",
maxBuffer: getSpawnMaxBuffer(),
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
});
// Clean up temp file if created
if (tempFile) {
try {
fs.unlinkSync(tempFile);
}
catch (err) {
console.warn(`Failed to delete temporary file ${tempFile}: ${err}`);
}
}
return {
content: [
{
type: "text",
text: result,
},
],
};
}
catch (error) {
// Clean up temp file if created, even if command failed
if (tempFile) {
try {
fs.unlinkSync(tempFile);
}
catch (err) {
console.warn(`Failed to delete temporary file ${tempFile}: ${err}`);
}
}
throw new McpError(ErrorCode.InternalError, `Failed to patch resource: ${error.message}`);
}
}
catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Failed to execute kubectl patch command: ${error.message}`);
}
}