UNPKG

mcp-server-kubernetes

Version:

MCP server for interacting with Kubernetes clusters via kubectl

139 lines (138 loc) • 6.13 kB
import { assertNotFlagLike, execFileSyncSafe, } from "../security/kubectl-flags.js"; import { isRemoteTransport } from "../security/transport.js"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { getSpawnMaxBuffer } from "../config/max-buffer.js"; import { removeTempFile, writeTempFile } from "../utils/temp-file.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", "array"], description: "Patch data: a JSON object for 'strategic' and 'merge' patches, or an array of JSON Patch operations for patchType 'json'", }, 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; // Positional operands: a leading "-" would make kubectl parse them as // flags (e.g. --filename=<server path>). assertNotFlagLike(input.resourceType, "resourceType"); assertNotFlagLike(input.name, "name"); 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 tempFile = writeTempFile("patch.json", 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) removeTempFile(tempFile); return { content: [ { type: "text", text: result, }, ], }; } catch (error) { // Clean up temp file if created, even if command failed if (tempFile) removeTempFile(tempFile); 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}`); } }