mcp-server-kubernetes
Version:
MCP server for interacting with Kubernetes clusters via kubectl
322 lines (321 loc) • 13.8 kB
JavaScript
import { execSync } from "child_process";
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
export const kubectlCreateSchema = {
name: "kubectl_create",
description: "Create Kubernetes resources using various methods (from file or using subcommands)",
inputSchema: {
type: "object",
properties: {
// General options
dryRun: {
type: "boolean",
description: "If true, only validate the resource, don't actually create it",
default: false
},
output: {
type: "string",
enum: ["json", "yaml", "name", "go-template", "go-template-file", "template", "templatefile", "jsonpath", "jsonpath-as-json", "jsonpath-file"],
description: "Output format. One of: json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-as-json|jsonpath-file",
default: "yaml"
},
validate: {
type: "boolean",
description: "If true, validate resource schema against server schema",
default: true
},
// Create from file method
manifest: {
type: "string",
description: "YAML manifest to create resources from"
},
filename: {
type: "string",
description: "Path to a YAML file to create resources from"
},
// Resource type to create (determines which subcommand to use)
resourceType: {
type: "string",
description: "Type of resource to create (namespace, configmap, deployment, service, etc.)"
},
// Common parameters for most resource types
name: {
type: "string",
description: "Name of the resource to create"
},
namespace: {
type: "string",
description: "Namespace to create the resource in",
default: "default"
},
// ConfigMap specific parameters
fromLiteral: {
type: "array",
items: { type: "string" },
description: "Key-value pair for creating configmap (e.g. [\"key1=value1\", \"key2=value2\"])"
},
fromFile: {
type: "array",
items: { type: "string" },
description: "Path to file for creating configmap (e.g. [\"key1=/path/to/file1\", \"key2=/path/to/file2\"])"
},
// Namespace specific parameters
// No special parameters for namespace, just name is needed
// Secret specific parameters
secretType: {
type: "string",
enum: ["generic", "docker-registry", "tls"],
description: "Type of secret to create (generic, docker-registry, tls)"
},
// Service specific parameters
serviceType: {
type: "string",
enum: ["clusterip", "nodeport", "loadbalancer", "externalname"],
description: "Type of service to create (clusterip, nodeport, loadbalancer, externalname)"
},
tcpPort: {
type: "array",
items: { type: "string" },
description: "Port pairs for tcp service (e.g. [\"80:8080\", \"443:8443\"])"
},
// Deployment specific parameters
image: {
type: "string",
description: "Image to use for the containers in the deployment"
},
replicas: {
type: "number",
description: "Number of replicas to create for the deployment",
default: 1
},
port: {
type: "number",
description: "Port that the container exposes"
},
// CronJob specific parameters
schedule: {
type: "string",
description: "Cron schedule expression for the CronJob (e.g. \"*/5 * * * *\")"
},
suspend: {
type: "boolean",
description: "Whether to suspend the CronJob",
default: false
},
// Job specific parameters
command: {
type: "array",
items: { type: "string" },
description: "Command to run in the container"
},
// Additional common parameters
labels: {
type: "array",
items: { type: "string" },
description: "Labels to apply to the resource (e.g. [\"key1=value1\", \"key2=value2\"])"
},
annotations: {
type: "array",
items: { type: "string" },
description: "Annotations to apply to the resource (e.g. [\"key1=value1\", \"key2=value2\"])"
}
},
required: [],
},
};
export async function kubectlCreate(k8sManager, input) {
try {
// Check if we have enough information to proceed
if (!input.manifest && !input.filename && !input.resourceType) {
throw new McpError(ErrorCode.InvalidRequest, "Either manifest, filename, or resourceType must be provided");
}
// If resourceType is provided, check if name is provided for most resource types
if (input.resourceType && !input.name && input.resourceType !== "namespace") {
throw new McpError(ErrorCode.InvalidRequest, `Name is required when creating a ${input.resourceType}`);
}
// Set up common parameters
const namespace = input.namespace || "default";
const dryRun = input.dryRun || false;
const validate = input.validate ?? true;
const output = input.output || "yaml";
let command = "kubectl create";
let tempFile = null;
// Process manifest content if provided (file-based creation)
if (input.manifest || input.filename) {
if (input.manifest) {
// Create temporary file for the manifest
const tmpDir = os.tmpdir();
tempFile = path.join(tmpDir, `create-manifest-${Date.now()}.yaml`);
fs.writeFileSync(tempFile, input.manifest);
command += ` -f ${tempFile}`;
}
else if (input.filename) {
command += ` -f ${input.filename}`;
}
}
else {
// Process subcommand-based creation
switch (input.resourceType?.toLowerCase()) {
case "namespace":
command += ` namespace ${input.name}`;
break;
case "configmap":
command += ` configmap ${input.name}`;
// Add --from-literal arguments
if (input.fromLiteral && input.fromLiteral.length > 0) {
input.fromLiteral.forEach(literal => {
command += ` --from-literal=${literal}`;
});
}
// Add --from-file arguments
if (input.fromFile && input.fromFile.length > 0) {
input.fromFile.forEach(file => {
command += ` --from-file=${file}`;
});
}
break;
case "secret":
if (!input.secretType) {
throw new McpError(ErrorCode.InvalidRequest, "secretType is required when creating a secret");
}
command += ` secret ${input.secretType} ${input.name}`;
// Add --from-literal arguments
if (input.fromLiteral && input.fromLiteral.length > 0) {
input.fromLiteral.forEach(literal => {
command += ` --from-literal=${literal}`;
});
}
// Add --from-file arguments
if (input.fromFile && input.fromFile.length > 0) {
input.fromFile.forEach(file => {
command += ` --from-file=${file}`;
});
}
break;
case "service":
if (!input.serviceType) {
// Default to clusterip if not specified
input.serviceType = "clusterip";
}
command += ` service ${input.serviceType} ${input.name}`;
// Add --tcp arguments for ports
if (input.tcpPort && input.tcpPort.length > 0) {
input.tcpPort.forEach(port => {
command += ` --tcp=${port}`;
});
}
break;
case "cronjob":
if (!input.image) {
throw new McpError(ErrorCode.InvalidRequest, "image is required when creating a cronjob");
}
if (!input.schedule) {
throw new McpError(ErrorCode.InvalidRequest, "schedule is required when creating a cronjob");
}
command += ` cronjob ${input.name} --image=${input.image} --schedule="${input.schedule}"`;
// Add command if specified
if (input.command && input.command.length > 0) {
command += ` -- ${input.command.join(" ")}`;
}
// Add suspend flag if specified
if (input.suspend === true) {
command += ` --suspend`;
}
break;
case "deployment":
if (!input.image) {
throw new McpError(ErrorCode.InvalidRequest, "image is required when creating a deployment");
}
command += ` deployment ${input.name} --image=${input.image}`;
// Add replicas if specified
if (input.replicas) {
command += ` --replicas=${input.replicas}`;
}
// Add port if specified
if (input.port) {
command += ` --port=${input.port}`;
}
break;
case "job":
if (!input.image) {
throw new McpError(ErrorCode.InvalidRequest, "image is required when creating a job");
}
command += ` job ${input.name} --image=${input.image}`;
// Add command if specified
if (input.command && input.command.length > 0) {
command += ` -- ${input.command.join(" ")}`;
}
break;
default:
throw new McpError(ErrorCode.InvalidRequest, `Unsupported resource type: ${input.resourceType}`);
}
}
// Add namespace if not creating a namespace itself
if (input.resourceType !== "namespace") {
command += ` -n ${namespace}`;
}
// Add labels if specified
if (input.labels && input.labels.length > 0) {
input.labels.forEach(label => {
command += ` -l ${label}`;
});
}
// Add annotations if specified
if (input.annotations && input.annotations.length > 0) {
input.annotations.forEach(annotation => {
command += ` --annotation=${annotation}`;
});
}
// Add dry-run flag if requested
if (dryRun) {
command += " --dry-run=client";
}
// Add validate flag if needed
if (!validate) {
command += " --validate=false";
}
// Add output format
command += ` -o ${output}`;
// Execute the command
try {
const result = execSync(command, { encoding: "utf8", 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 create resource: ${error.message}`);
}
}
catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Failed to execute kubectl create command: ${error.message}`);
}
}