mcp-server-kubernetes 2.4.7 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export declare function getSpawnMaxBuffer(): number;
@@ -0,0 +1,3 @@
1
+ export function getSpawnMaxBuffer() {
2
+ return parseInt(process.env.SPAWN_MAX_BUFFER || "1048577", 10);
3
+ }
package/dist/index.js CHANGED
@@ -24,8 +24,20 @@ import { kubectlPatch, kubectlPatchSchema } from "./tools/kubectl-patch.js";
24
24
  import { kubectlRollout, kubectlRolloutSchema, } from "./tools/kubectl-rollout.js";
25
25
  import { registerPromptHandlers } from "./prompts/index.js";
26
26
  import { ping, pingSchema } from "./tools/ping.js";
27
- // Check if non-destructive tools only mode is enabled
27
+ // Check environment variables for tool filtering
28
+ const allowOnlyReadonlyTools = process.env.ALLOW_ONLY_READONLY_TOOLS === "true";
29
+ const allowedToolsEnv = process.env.ALLOWED_TOOLS;
28
30
  const nonDestructiveTools = process.env.ALLOW_ONLY_NON_DESTRUCTIVE_TOOLS === "true";
31
+ // Define readonly tools
32
+ const readonlyTools = [
33
+ kubectlGetSchema,
34
+ kubectlDescribeSchema,
35
+ kubectlLogsSchema,
36
+ kubectlContextSchema,
37
+ explainResourceSchema,
38
+ listApiResourcesSchema,
39
+ pingSchema,
40
+ ];
29
41
  // Define destructive tools (delete and uninstall operations)
30
42
  const destructiveTools = [
31
43
  kubectlDeleteSchema, // This replaces all individual delete operations
@@ -85,10 +97,20 @@ server.setRequestHandler(ReadResourceRequestSchema, resourceHandlers.readResourc
85
97
  registerPromptHandlers(server, k8sManager);
86
98
  // Tools handlers
87
99
  server.setRequestHandler(ListToolsRequestSchema, async () => {
88
- // Filter out destructive tools if ALLOW_ONLY_NON_DESTRUCTIVE_TOOLS is set to 'true'
89
- const tools = nonDestructiveTools
90
- ? allTools.filter((tool) => !destructiveTools.some((dt) => dt.name === tool.name))
91
- : allTools;
100
+ let tools;
101
+ if (allowedToolsEnv) {
102
+ const allowedToolNames = allowedToolsEnv.split(",").map((t) => t.trim());
103
+ tools = allTools.filter((tool) => allowedToolNames.includes(tool.name));
104
+ }
105
+ else if (allowOnlyReadonlyTools) {
106
+ tools = readonlyTools;
107
+ }
108
+ else if (nonDestructiveTools) {
109
+ tools = allTools.filter((tool) => !destructiveTools.some((dt) => dt.name === tool.name));
110
+ }
111
+ else {
112
+ tools = allTools;
113
+ }
92
114
  return { tools };
93
115
  });
94
116
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -42,17 +42,14 @@ export declare const execInPodSchema: {
42
42
  container: {
43
43
  type: string;
44
44
  description: string;
45
- optional: boolean;
46
45
  };
47
46
  shell: {
48
47
  type: string;
49
48
  description: string;
50
- optional: boolean;
51
49
  };
52
50
  timeout: {
53
51
  type: string;
54
52
  description: string;
55
- optional: boolean;
56
53
  };
57
54
  };
58
55
  required: string[];
@@ -39,17 +39,14 @@ export const execInPodSchema = {
39
39
  container: {
40
40
  type: "string",
41
41
  description: "Container name (required when pod has multiple containers)",
42
- optional: true,
43
42
  },
44
43
  shell: {
45
44
  type: "string",
46
45
  description: "Shell to use for command execution (e.g. '/bin/sh', '/bin/bash'). If not provided, will use command as-is.",
47
- optional: true,
48
46
  },
49
47
  timeout: {
50
48
  type: "number",
51
49
  description: "Timeout for command - 60000 milliseconds if not specified",
52
- optional: true,
53
50
  },
54
51
  },
55
52
  required: ["name", "command"],
@@ -1,6 +1,7 @@
1
- import { execSync } from "child_process";
1
+ import { execFileSync } from "child_process";
2
2
  import { writeFileSync, unlinkSync } from "fs";
3
3
  import yaml from "yaml";
4
+ import { getSpawnMaxBuffer } from "../config/max-buffer.js";
4
5
  export const installHelmChartSchema = {
5
6
  name: "install_helm_chart",
6
7
  description: "Install a Helm chart",
@@ -83,13 +84,14 @@ export const uninstallHelmChartSchema = {
83
84
  required: ["name", "namespace"],
84
85
  },
85
86
  };
86
- const executeHelmCommand = (command) => {
87
+ const executeHelmCommand = (command, args) => {
87
88
  try {
88
89
  // Add a generous timeout of 60 seconds for Helm operations
89
- return execSync(command, {
90
+ return execFileSync(command, args, {
90
91
  encoding: "utf8",
91
92
  timeout: 60000, // 60 seconds timeout
92
- env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG }
93
+ maxBuffer: getSpawnMaxBuffer(),
94
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
93
95
  });
94
96
  }
95
97
  catch (error) {
@@ -106,16 +108,24 @@ export async function installHelmChart(params) {
106
108
  // Add helm repository if provided
107
109
  if (params.repo) {
108
110
  const repoName = params.chart.split("/")[0];
109
- executeHelmCommand(`helm repo add ${repoName} ${params.repo}`);
110
- executeHelmCommand("helm repo update");
111
+ executeHelmCommand("helm", ["repo", "add", repoName, params.repo]);
112
+ executeHelmCommand("helm", ["repo", "update"]);
111
113
  }
112
- let command = `helm install ${params.name} ${params.chart} --namespace ${params.namespace} --create-namespace`;
114
+ let command = "helm";
115
+ let args = [
116
+ "install",
117
+ params.name,
118
+ params.chart,
119
+ "--namespace",
120
+ params.namespace,
121
+ "--create-namespace",
122
+ ];
113
123
  // Handle values if provided
114
124
  if (params.values) {
115
125
  const valuesFile = writeValuesFile(params.name, params.values);
116
- command += ` -f ${valuesFile}`;
126
+ args.push("-f", valuesFile);
117
127
  try {
118
- executeHelmCommand(command);
128
+ executeHelmCommand(command, args);
119
129
  }
120
130
  finally {
121
131
  // Cleanup values file
@@ -123,7 +133,7 @@ export async function installHelmChart(params) {
123
133
  }
124
134
  }
125
135
  else {
126
- executeHelmCommand(command);
136
+ executeHelmCommand(command, args);
127
137
  }
128
138
  const response = {
129
139
  status: "installed",
@@ -147,16 +157,23 @@ export async function upgradeHelmChart(params) {
147
157
  // Add helm repository if provided
148
158
  if (params.repo) {
149
159
  const repoName = params.chart.split("/")[0];
150
- executeHelmCommand(`helm repo add ${repoName} ${params.repo}`);
151
- executeHelmCommand("helm repo update");
160
+ executeHelmCommand("helm", ["repo", "add", repoName, params.repo]);
161
+ executeHelmCommand("helm", ["repo", "update"]);
152
162
  }
153
- let command = `helm upgrade ${params.name} ${params.chart} --namespace ${params.namespace}`;
163
+ let command = "helm";
164
+ let args = [
165
+ "upgrade",
166
+ params.name,
167
+ params.chart,
168
+ "--namespace",
169
+ params.namespace,
170
+ ];
154
171
  // Handle values if provided
155
172
  if (params.values) {
156
173
  const valuesFile = writeValuesFile(params.name, params.values);
157
- command += ` -f ${valuesFile}`;
174
+ args.push("-f", valuesFile);
158
175
  try {
159
- executeHelmCommand(command);
176
+ executeHelmCommand(command, args);
160
177
  }
161
178
  finally {
162
179
  // Cleanup values file
@@ -164,7 +181,7 @@ export async function upgradeHelmChart(params) {
164
181
  }
165
182
  }
166
183
  else {
167
- executeHelmCommand(command);
184
+ executeHelmCommand(command, args);
168
185
  }
169
186
  const response = {
170
187
  status: "upgraded",
@@ -185,7 +202,12 @@ export async function upgradeHelmChart(params) {
185
202
  }
186
203
  export async function uninstallHelmChart(params) {
187
204
  try {
188
- executeHelmCommand(`helm uninstall ${params.name} --namespace ${params.namespace}`);
205
+ executeHelmCommand("helm", [
206
+ "uninstall",
207
+ params.name,
208
+ "--namespace",
209
+ params.namespace,
210
+ ]);
189
211
  const response = {
190
212
  status: "uninstalled",
191
213
  message: `Successfully uninstalled ${params.name}`,
@@ -1,8 +1,9 @@
1
- import { execSync } from "child_process";
1
+ import { execFileSync } from "child_process";
2
2
  import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import * as os from "os";
6
+ import { getSpawnMaxBuffer } from "../config/max-buffer.js";
6
7
  export const kubectlApplySchema = {
7
8
  name: "kubectl_apply",
8
9
  description: "Apply a Kubernetes YAML manifest from a string or file",
@@ -11,27 +12,27 @@ export const kubectlApplySchema = {
11
12
  properties: {
12
13
  manifest: {
13
14
  type: "string",
14
- description: "YAML manifest to apply"
15
+ description: "YAML manifest to apply",
15
16
  },
16
17
  filename: {
17
18
  type: "string",
18
- description: "Path to a YAML file to apply (optional - use either manifest or filename)"
19
+ description: "Path to a YAML file to apply (optional - use either manifest or filename)",
19
20
  },
20
21
  namespace: {
21
22
  type: "string",
22
23
  description: "Namespace to apply the resource to (optional)",
23
- default: "default"
24
+ default: "default",
24
25
  },
25
26
  dryRun: {
26
27
  type: "boolean",
27
28
  description: "If true, only validate the resource, don't apply it",
28
- default: false
29
+ default: false,
29
30
  },
30
31
  force: {
31
32
  type: "boolean",
32
33
  description: "If true, immediately remove resources from API and bypass graceful deletion",
33
- default: false
34
- }
34
+ default: false,
35
+ },
35
36
  },
36
37
  required: [],
37
38
  },
@@ -44,7 +45,8 @@ export async function kubectlApply(k8sManager, input) {
44
45
  const namespace = input.namespace || "default";
45
46
  const dryRun = input.dryRun || false;
46
47
  const force = input.force || false;
47
- let command = "kubectl apply";
48
+ let command = "kubectl";
49
+ let args = ["apply"];
48
50
  let tempFile = null;
49
51
  // Process manifest content if provided
50
52
  if (input.manifest) {
@@ -52,24 +54,28 @@ export async function kubectlApply(k8sManager, input) {
52
54
  const tmpDir = os.tmpdir();
53
55
  tempFile = path.join(tmpDir, `manifest-${Date.now()}.yaml`);
54
56
  fs.writeFileSync(tempFile, input.manifest);
55
- command += ` -f ${tempFile}`;
57
+ args.push("-f", tempFile);
56
58
  }
57
59
  else if (input.filename) {
58
- command += ` -f ${input.filename}`;
60
+ args.push("-f", input.filename);
59
61
  }
60
62
  // Add namespace
61
- command += ` -n ${namespace}`;
63
+ args.push("-n", namespace);
62
64
  // Add dry-run flag if requested
63
65
  if (dryRun) {
64
- command += " --dry-run=client";
66
+ args.push("--dry-run=client");
65
67
  }
66
68
  // Add force flag if requested
67
69
  if (force) {
68
- command += " --force";
70
+ args.push("--force");
69
71
  }
70
72
  // Execute the command
71
73
  try {
72
- const result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
74
+ const result = execFileSync(command, args, {
75
+ encoding: "utf8",
76
+ maxBuffer: getSpawnMaxBuffer(),
77
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
78
+ });
73
79
  // Clean up temp file if created
74
80
  if (tempFile) {
75
81
  try {
@@ -1,5 +1,6 @@
1
- import { execSync } from "child_process";
1
+ import { execFileSync } from "child_process";
2
2
  import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
3
+ import { getSpawnMaxBuffer } from "../config/max-buffer.js";
3
4
  export const kubectlContextSchema = {
4
5
  name: "kubectl_context",
5
6
  description: "Manage Kubernetes contexts - list, get, or set the current context",
@@ -10,28 +11,28 @@ export const kubectlContextSchema = {
10
11
  type: "string",
11
12
  enum: ["list", "get", "set"],
12
13
  description: "Operation to perform: list contexts, get current context, or set current context",
13
- default: "list"
14
+ default: "list",
14
15
  },
15
16
  name: {
16
17
  type: "string",
17
- description: "Name of the context to set as current (required for set operation)"
18
+ description: "Name of the context to set as current (required for set operation)",
18
19
  },
19
20
  showCurrent: {
20
21
  type: "boolean",
21
22
  description: "When listing contexts, highlight which one is currently active",
22
- default: true
23
+ default: true,
23
24
  },
24
25
  detailed: {
25
26
  type: "boolean",
26
27
  description: "Include detailed information about the context",
27
- default: false
28
+ default: false,
28
29
  },
29
30
  output: {
30
31
  type: "string",
31
32
  enum: ["json", "yaml", "name", "custom"],
32
33
  description: "Output format",
33
- default: "json"
34
- }
34
+ default: "json",
35
+ },
35
36
  },
36
37
  required: ["operation"],
37
38
  },
@@ -41,18 +42,22 @@ export async function kubectlContext(k8sManager, input) {
41
42
  const { operation, name, output = "json" } = input;
42
43
  const showCurrent = input.showCurrent !== false; // Default to true if not specified
43
44
  const detailed = input.detailed === true; // Default to false if not specified
44
- let command = "";
45
+ const command = "kubectl";
45
46
  let result = "";
46
47
  switch (operation) {
47
48
  case "list":
48
49
  // Build command to list contexts
49
- command = "kubectl config get-contexts";
50
+ let listArgs = ["config", "get-contexts"];
50
51
  if (output === "name") {
51
- command += " -o name";
52
+ listArgs.push("-o", "name");
52
53
  }
53
54
  else if (output === "custom" || output === "json") {
54
55
  // For custom or JSON output, we'll format it ourselves
55
- const rawResult = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
56
+ const rawResult = execFileSync(command, listArgs, {
57
+ encoding: "utf8",
58
+ maxBuffer: getSpawnMaxBuffer(),
59
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
60
+ });
56
61
  // Parse the tabular output from kubectl
57
62
  const lines = rawResult.trim().split("\n");
58
63
  const headers = lines[0].trim().split(/\s+/);
@@ -70,7 +75,7 @@ export async function kubectlContext(k8sManager, input) {
70
75
  cluster: columns[clusterIndex]?.trim(),
71
76
  user: columns[authInfoIndex]?.trim(),
72
77
  namespace: columns[namespaceIndex]?.trim() || "default",
73
- isCurrent: isCurrent
78
+ isCurrent: isCurrent,
74
79
  });
75
80
  }
76
81
  return {
@@ -83,17 +88,29 @@ export async function kubectlContext(k8sManager, input) {
83
88
  };
84
89
  }
85
90
  // Execute the command for non-json outputs
86
- result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
91
+ result = execFileSync(command, listArgs, {
92
+ encoding: "utf8",
93
+ maxBuffer: getSpawnMaxBuffer(),
94
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
95
+ });
87
96
  break;
88
97
  case "get":
89
98
  // Build command to get current context
90
- command = "kubectl config current-context";
99
+ const getArgs = ["config", "current-context"];
91
100
  // Execute the command
92
101
  try {
93
- const currentContext = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim();
102
+ const currentContext = execFileSync(command, getArgs, {
103
+ encoding: "utf8",
104
+ maxBuffer: getSpawnMaxBuffer(),
105
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
106
+ }).trim();
94
107
  if (detailed) {
95
108
  // For detailed context info, we need to use get-contexts and filter
96
- const allContextsOutput = execSync("kubectl config get-contexts", { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
109
+ const allContextsOutput = execFileSync(command, ["config", "get-contexts"], {
110
+ encoding: "utf8",
111
+ maxBuffer: getSpawnMaxBuffer(),
112
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
113
+ });
97
114
  // Parse the tabular output from kubectl
98
115
  const lines = allContextsOutput.trim().split("\n");
99
116
  const headers = lines[0].trim().split(/\s+/);
@@ -105,7 +122,7 @@ export async function kubectlContext(k8sManager, input) {
105
122
  name: currentContext,
106
123
  cluster: "",
107
124
  user: "",
108
- namespace: "default"
125
+ namespace: "default",
109
126
  };
110
127
  // Find the current context in the output
111
128
  for (let i = 1; i < lines.length; i++) {
@@ -117,7 +134,7 @@ export async function kubectlContext(k8sManager, input) {
117
134
  name: currentContext,
118
135
  cluster: columns[clusterIndex]?.trim() || "",
119
136
  user: columns[authInfoIndex]?.trim() || "",
120
- namespace: columns[namespaceIndex]?.trim() || "default"
137
+ namespace: columns[namespaceIndex]?.trim() || "default",
121
138
  };
122
139
  break;
123
140
  }
@@ -155,7 +172,10 @@ export async function kubectlContext(k8sManager, input) {
155
172
  content: [
156
173
  {
157
174
  type: "text",
158
- text: JSON.stringify({ currentContext: null, error: "No current context is set" }, null, 2),
175
+ text: JSON.stringify({
176
+ currentContext: null,
177
+ error: "No current context is set",
178
+ }, null, 2),
159
179
  },
160
180
  ],
161
181
  };
@@ -169,7 +189,11 @@ export async function kubectlContext(k8sManager, input) {
169
189
  }
170
190
  // First check if the context exists
171
191
  try {
172
- const allContextsOutput = execSync("kubectl config get-contexts -o name", { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
192
+ const allContextsOutput = execFileSync(command, ["config", "get-contexts", "-o", "name"], {
193
+ encoding: "utf8",
194
+ maxBuffer: getSpawnMaxBuffer(),
195
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
196
+ });
173
197
  const availableContexts = allContextsOutput.trim().split("\n");
174
198
  // Extract the short name from the ARN if needed
175
199
  let contextName = name;
@@ -180,13 +204,18 @@ export async function kubectlContext(k8sManager, input) {
180
204
  }
181
205
  }
182
206
  // Check if the context exists
183
- if (!availableContexts.includes(contextName) && !availableContexts.includes(name)) {
207
+ if (!availableContexts.includes(contextName) &&
208
+ !availableContexts.includes(name)) {
184
209
  throw new McpError(ErrorCode.InvalidParams, `Context '${name}' not found`);
185
210
  }
186
211
  // Build command to set context
187
- command = `kubectl config use-context "${contextName}"`;
212
+ const setArgs = ["config", "use-context", contextName];
188
213
  // Execute the command
189
- result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
214
+ result = execFileSync(command, setArgs, {
215
+ encoding: "utf8",
216
+ maxBuffer: getSpawnMaxBuffer(),
217
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
218
+ });
190
219
  // For tests to pass, we need to return the original name format that was passed in
191
220
  return {
192
221
  content: [
@@ -195,7 +224,7 @@ export async function kubectlContext(k8sManager, input) {
195
224
  text: JSON.stringify({
196
225
  success: true,
197
226
  message: `Current context set to '${name}'`,
198
- context: name
227
+ context: name,
199
228
  }, null, 2),
200
229
  },
201
230
  ],