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.
@@ -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 kubectlGenericSchema = {
4
5
  name: "kubectl_generic",
5
6
  description: "Execute any kubectl command with the provided arguments and flags",
@@ -8,48 +9,49 @@ export const kubectlGenericSchema = {
8
9
  properties: {
9
10
  command: {
10
11
  type: "string",
11
- description: "The kubectl command to execute (e.g. patch, rollout, top)"
12
+ description: "The kubectl command to execute (e.g. patch, rollout, top)",
12
13
  },
13
14
  subCommand: {
14
15
  type: "string",
15
- description: "Subcommand if applicable (e.g. 'history' for rollout)"
16
+ description: "Subcommand if applicable (e.g. 'history' for rollout)",
16
17
  },
17
18
  resourceType: {
18
19
  type: "string",
19
- description: "Resource type (e.g. pod, deployment)"
20
+ description: "Resource type (e.g. pod, deployment)",
20
21
  },
21
22
  name: {
22
23
  type: "string",
23
- description: "Resource name"
24
+ description: "Resource name",
24
25
  },
25
26
  namespace: {
26
27
  type: "string",
27
28
  description: "Namespace",
28
- default: "default"
29
+ default: "default",
29
30
  },
30
31
  outputFormat: {
31
32
  type: "string",
32
33
  description: "Output format (e.g. json, yaml, wide)",
33
- enum: ["json", "yaml", "wide", "name", "custom"]
34
+ enum: ["json", "yaml", "wide", "name", "custom"],
34
35
  },
35
36
  flags: {
36
37
  type: "object",
37
38
  description: "Command flags as key-value pairs",
38
- additionalProperties: true
39
+ additionalProperties: true,
39
40
  },
40
41
  args: {
41
42
  type: "array",
42
43
  items: { type: "string" },
43
- description: "Additional command arguments"
44
- }
44
+ description: "Additional command arguments",
45
+ },
45
46
  },
46
- required: ["command"]
47
- }
47
+ required: ["command"],
48
+ },
48
49
  };
49
50
  export async function kubectlGeneric(k8sManager, input) {
50
51
  try {
51
52
  // Start building the kubectl command
52
- let cmdArgs = ["kubectl", input.command];
53
+ const command = "kubectl";
54
+ const cmdArgs = [input.command];
53
55
  // Add subcommand if provided
54
56
  if (input.subCommand) {
55
57
  cmdArgs.push(input.subCommand);
@@ -87,11 +89,14 @@ export async function kubectlGeneric(k8sManager, input) {
87
89
  if (input.args && input.args.length > 0) {
88
90
  cmdArgs.push(...input.args);
89
91
  }
90
- // Execute the command (join all args except the first "kubectl" which is used in execSync)
91
- const command = cmdArgs.slice(1).join(' ');
92
+ // Execute the command
92
93
  try {
93
- console.error(`Executing: kubectl ${command}`);
94
- const result = execSync(`kubectl ${command}`, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
94
+ console.error(`Executing: kubectl ${cmdArgs.join(" ")}`);
95
+ const result = execFileSync(command, cmdArgs, {
96
+ encoding: "utf8",
97
+ maxBuffer: getSpawnMaxBuffer(),
98
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
99
+ });
95
100
  return {
96
101
  content: [
97
102
  {
@@ -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 kubectlGetSchema = {
4
5
  name: "kubectl_get",
5
6
  description: "Get or list Kubernetes resources by resource type, name, and optionally namespace",
@@ -32,15 +33,15 @@ export const kubectlGetSchema = {
32
33
  },
33
34
  labelSelector: {
34
35
  type: "string",
35
- description: "Filter resources by label selector (e.g. 'app=nginx')"
36
+ description: "Filter resources by label selector (e.g. 'app=nginx')",
36
37
  },
37
38
  fieldSelector: {
38
39
  type: "string",
39
- description: "Filter resources by field selector (e.g. 'metadata.name=my-pod')"
40
+ description: "Filter resources by field selector (e.g. 'metadata.name=my-pod')",
40
41
  },
41
42
  sortBy: {
42
43
  type: "string",
43
- description: "Sort events by a field (default: lastTimestamp). Only applicable for events."
44
+ description: "Sort events by a field (default: lastTimestamp). Only applicable for events.",
44
45
  },
45
46
  },
46
47
  required: ["resourceType", "name", "namespace"],
@@ -57,12 +58,11 @@ export async function kubectlGet(k8sManager, input) {
57
58
  const fieldSelector = input.fieldSelector || "";
58
59
  const sortBy = input.sortBy;
59
60
  // Build the kubectl command
60
- let command = "kubectl get ";
61
- // Add resource type
62
- command += resourceType;
61
+ const command = "kubectl";
62
+ const args = ["get", resourceType];
63
63
  // Add name if provided
64
64
  if (name) {
65
- command += ` ${name}`;
65
+ args.push(name);
66
66
  }
67
67
  // For events, default to all namespaces unless explicitly specified
68
68
  const shouldShowAllNamespaces = resourceType === "events"
@@ -72,51 +72,52 @@ export async function kubectlGet(k8sManager, input) {
72
72
  : allNamespaces;
73
73
  // Add namespace flag unless all namespaces is specified
74
74
  if (shouldShowAllNamespaces) {
75
- command += " --all-namespaces";
75
+ args.push("--all-namespaces");
76
76
  }
77
77
  else if (namespace && !isNonNamespacedResource(resourceType)) {
78
- command += ` -n ${namespace}`;
78
+ args.push("-n", namespace);
79
79
  }
80
80
  // Add label selector if provided
81
81
  if (labelSelector) {
82
- command += ` -l ${labelSelector}`;
82
+ args.push("-l", labelSelector);
83
83
  }
84
84
  // Add field selector if provided
85
85
  if (fieldSelector) {
86
- command += ` --field-selector=${fieldSelector}`;
86
+ args.push(`--field-selector=${fieldSelector}`);
87
87
  }
88
88
  // Add sort-by for events
89
89
  if (resourceType === "events" && sortBy) {
90
- command += ` --sort-by=.${sortBy}`;
90
+ args.push(`--sort-by=.${sortBy}`);
91
91
  }
92
92
  else if (resourceType === "events") {
93
- command += ` --sort-by=.lastTimestamp`;
93
+ args.push(`--sort-by=.lastTimestamp`);
94
94
  }
95
95
  // Add output format
96
96
  if (output === "json") {
97
- command += " -o json";
97
+ args.push("-o", "json");
98
98
  }
99
99
  else if (output === "yaml") {
100
- command += " -o yaml";
100
+ args.push("-o", "yaml");
101
101
  }
102
102
  else if (output === "wide") {
103
- command += " -o wide";
103
+ args.push("-o", "wide");
104
104
  }
105
105
  else if (output === "name") {
106
- command += " -o name";
106
+ args.push("-o", "name");
107
107
  }
108
108
  else if (output === "custom") {
109
109
  if (resourceType === "events") {
110
- command += ` -o 'custom-columns=LAST SEEN:.lastTimestamp,TYPE:.type,REASON:.reason,OBJECT:.involvedObject.kind/.involvedObject.name,MESSAGE:.message'`;
110
+ args.push("-o", "'custom-columns=LASTSEEN:.lastTimestamp,TYPE:.type,REASON:.reason,OBJECT:.involvedObject.name,MESSAGE:.message'");
111
111
  }
112
112
  else {
113
- command += ` -o 'custom-columns=NAME:.metadata.name,NAMESPACE:.metadata.namespace,STATUS:.status.phase,AGE:.metadata.creationTimestamp'`;
113
+ args.push("-o", "'custom-columns=NAME:.metadata.name,NAMESPACE:.metadata.namespace,STATUS:.status.phase,AGE:.metadata.creationTimestamp'");
114
114
  }
115
115
  }
116
116
  // Execute the command
117
117
  try {
118
- const result = execSync(command, {
118
+ const result = execFileSync(command, args, {
119
119
  encoding: "utf8",
120
+ maxBuffer: getSpawnMaxBuffer(),
120
121
  env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
121
122
  });
122
123
  // Format the results for better readability
@@ -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 kubectlLogsSchema = {
4
5
  name: "kubectl_logs",
5
6
  description: "Get logs from Kubernetes resources like pods, deployments, or jobs",
@@ -22,19 +23,19 @@ export const kubectlLogsSchema = {
22
23
  },
23
24
  container: {
24
25
  type: "string",
25
- description: "Container name (required when pod has multiple containers)"
26
+ description: "Container name (required when pod has multiple containers)",
26
27
  },
27
28
  tail: {
28
29
  type: "number",
29
- description: "Number of lines to show from end of logs"
30
+ description: "Number of lines to show from end of logs",
30
31
  },
31
32
  since: {
32
33
  type: "string",
33
- description: "Show logs since relative time (e.g. '5s', '2m', '3h')"
34
+ description: "Show logs since relative time (e.g. '5s', '2m', '3h')",
34
35
  },
35
36
  sinceTime: {
36
37
  type: "string",
37
- description: "Show logs since absolute time (RFC3339)"
38
+ description: "Show logs since absolute time (RFC3339)",
38
39
  },
39
40
  timestamps: {
40
41
  type: "boolean",
@@ -53,8 +54,8 @@ export const kubectlLogsSchema = {
53
54
  },
54
55
  labelSelector: {
55
56
  type: "string",
56
- description: "Filter resources by label selector"
57
- }
57
+ description: "Filter resources by label selector",
58
+ },
58
59
  },
59
60
  required: ["resourceType", "name", "namespace"],
60
61
  },
@@ -64,32 +65,45 @@ export async function kubectlLogs(k8sManager, input) {
64
65
  const resourceType = input.resourceType.toLowerCase();
65
66
  const name = input.name;
66
67
  const namespace = input.namespace || "default";
67
- // Build the kubectl command base
68
- let baseCommand = `kubectl -n ${namespace}`;
68
+ const command = "kubectl";
69
69
  // Handle different resource types
70
70
  if (resourceType === "pod") {
71
71
  // Direct pod logs
72
- baseCommand += ` logs ${name}`;
72
+ let args = ["-n", namespace, "logs", name];
73
73
  // If container is specified, add it
74
74
  if (input.container) {
75
- baseCommand += ` -c ${input.container}`;
75
+ args.push(`-c`, input.container);
76
76
  }
77
77
  // Add options
78
- baseCommand = addLogOptions(baseCommand, input);
78
+ args = addLogOptions(args, input);
79
79
  // Execute the command
80
80
  try {
81
- const result = execSync(baseCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
81
+ const result = execFileSync(command, args, {
82
+ encoding: "utf8",
83
+ maxBuffer: getSpawnMaxBuffer(),
84
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
85
+ });
82
86
  return formatLogOutput(name, result);
83
87
  }
84
88
  catch (error) {
85
89
  return handleCommandError(error, `pod ${name}`);
86
90
  }
87
91
  }
88
- else if (resourceType === "deployment" || resourceType === "job" || resourceType === "cronjob") {
92
+ else if (resourceType === "deployment" ||
93
+ resourceType === "job" ||
94
+ resourceType === "cronjob") {
89
95
  // For deployments, jobs and cronjobs we need to find the pods first
90
- let selectorCommand;
96
+ let selectorArgs;
91
97
  if (resourceType === "deployment") {
92
- selectorCommand = `kubectl -n ${namespace} get deployment ${name} -o jsonpath='{.spec.selector.matchLabels}'`;
98
+ selectorArgs = [
99
+ "-n",
100
+ namespace,
101
+ "get",
102
+ "deployment",
103
+ name,
104
+ "-o",
105
+ "jsonpath='{.spec.selector.matchLabels}'",
106
+ ];
93
107
  }
94
108
  else if (resourceType === "job") {
95
109
  // For jobs, we use the job-name label
@@ -97,10 +111,24 @@ export async function kubectlLogs(k8sManager, input) {
97
111
  }
98
112
  else if (resourceType === "cronjob") {
99
113
  // For cronjobs, it's more complex - need to find the job first
100
- const jobsCommand = `kubectl -n ${namespace} get jobs --selector=job-name=${name} -o jsonpath='{.items[*].metadata.name}'`;
114
+ const jobsArgs = [
115
+ "-n",
116
+ namespace,
117
+ "get",
118
+ "jobs",
119
+ "--selector=job-name=" + name,
120
+ "-o",
121
+ "jsonpath='{.items[*].metadata.name}'",
122
+ ];
101
123
  try {
102
- const jobs = execSync(jobsCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim().split(' ');
103
- if (jobs.length === 0 || (jobs.length === 1 && jobs[0] === '')) {
124
+ const jobs = execFileSync(command, jobsArgs, {
125
+ encoding: "utf8",
126
+ maxBuffer: getSpawnMaxBuffer(),
127
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
128
+ })
129
+ .trim()
130
+ .split(" ");
131
+ if (jobs.length === 0 || (jobs.length === 1 && jobs[0] === "")) {
104
132
  return {
105
133
  content: [
106
134
  {
@@ -140,15 +168,19 @@ export async function kubectlLogs(k8sManager, input) {
140
168
  try {
141
169
  if (resourceType === "deployment") {
142
170
  // Get the deployment's selector
143
- if (!selectorCommand) {
171
+ if (!selectorArgs) {
144
172
  throw new Error("Selector command is undefined");
145
173
  }
146
- const selectorJson = execSync(selectorCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim();
174
+ const selectorJson = execFileSync(command, selectorArgs, {
175
+ encoding: "utf8",
176
+ maxBuffer: getSpawnMaxBuffer(),
177
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
178
+ }).trim();
147
179
  const selector = JSON.parse(selectorJson.replace(/'/g, '"'));
148
180
  // Convert to label selector format
149
181
  const labelSelector = Object.entries(selector)
150
182
  .map(([key, value]) => `${key}=${value}`)
151
- .join(',');
183
+ .join(",");
152
184
  return getLabelSelectorLogs(labelSelector, namespace, input);
153
185
  }
154
186
  // For jobs and cronjobs, the logic is handled above
@@ -183,36 +215,50 @@ export async function kubectlLogs(k8sManager, input) {
183
215
  }
184
216
  }
185
217
  // Helper function to add log options to the kubectl command
186
- function addLogOptions(baseCommand, input) {
187
- let command = baseCommand;
218
+ function addLogOptions(args, input) {
188
219
  // Add options based on inputs
189
220
  if (input.tail !== undefined) {
190
- command += ` --tail=${input.tail}`;
221
+ args.push(`--tail=${input.tail}`);
191
222
  }
192
223
  if (input.since) {
193
- command += ` --since=${input.since}`;
224
+ args.push(`--since=${input.since}`);
194
225
  }
195
226
  if (input.sinceTime) {
196
- command += ` --since-time=${input.sinceTime}`;
227
+ args.push(`--since-time=${input.sinceTime}`);
197
228
  }
198
229
  if (input.timestamps) {
199
- command += ` --timestamps`;
230
+ args.push(`--timestamps`);
200
231
  }
201
232
  if (input.previous) {
202
- command += ` --previous`;
233
+ args.push(`--previous`);
203
234
  }
204
235
  if (input.follow) {
205
- command += ` --follow`;
236
+ args.push(`--follow`);
206
237
  }
207
- return command;
238
+ return args;
208
239
  }
209
240
  // Helper function to get logs for resources selected by labels
210
241
  async function getLabelSelectorLogs(labelSelector, namespace, input) {
211
242
  try {
243
+ const command = "kubectl";
212
244
  // First, find all pods matching the label selector
213
- const podsCommand = `kubectl -n ${namespace} get pods --selector=${labelSelector} -o jsonpath='{.items[*].metadata.name}'`;
214
- const pods = execSync(podsCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim().split(' ');
215
- if (pods.length === 0 || (pods.length === 1 && pods[0] === '')) {
245
+ const podsArgs = [
246
+ "-n",
247
+ namespace,
248
+ "get",
249
+ "pods",
250
+ `--selector=${labelSelector}`,
251
+ "-o",
252
+ "jsonpath='{.items[*].metadata.name}'",
253
+ ];
254
+ const pods = execFileSync(command, podsArgs, {
255
+ encoding: "utf8",
256
+ maxBuffer: getSpawnMaxBuffer(),
257
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
258
+ })
259
+ .trim()
260
+ .split(" ");
261
+ if (pods.length === 0 || (pods.length === 1 && pods[0] === "")) {
216
262
  return {
217
263
  content: [
218
264
  {
@@ -230,15 +276,19 @@ async function getLabelSelectorLogs(labelSelector, namespace, input) {
230
276
  // Skip empty pod names
231
277
  if (!pod)
232
278
  continue;
233
- let podCommand = `kubectl -n ${namespace} logs ${pod}`;
279
+ let podArgs = ["-n", namespace, "logs", pod];
234
280
  // Add container if specified
235
281
  if (input.container) {
236
- podCommand += ` -c ${input.container}`;
282
+ podArgs.push(`-c`, input.container);
237
283
  }
238
284
  // Add other options
239
- podCommand = addLogOptions(podCommand, input);
285
+ podArgs = addLogOptions(podArgs, input);
240
286
  try {
241
- const logs = execSync(podCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
287
+ const logs = execFileSync(command, podArgs, {
288
+ encoding: "utf8",
289
+ maxBuffer: getSpawnMaxBuffer(),
290
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
291
+ });
242
292
  logsMap[pod] = logs;
243
293
  }
244
294
  catch (error) {
@@ -299,9 +349,13 @@ function handleCommandError(error, resourceDescription) {
299
349
  const podNameMatch = error.message.match(/for pod ([^,]+)/);
300
350
  const containersMatch = error.message.match(/choose one of: \[([^\]]+)\]/);
301
351
  const initContainersMatch = error.message.match(/or one of the init containers: \[([^\]]+)\]/);
302
- const podName = podNameMatch ? podNameMatch[1] : 'unknown';
303
- const containers = containersMatch ? containersMatch[1].split(' ').map((c) => c.trim()) : [];
304
- const initContainers = initContainersMatch ? initContainersMatch[1].split(' ').map((c) => c.trim()) : [];
352
+ const podName = podNameMatch ? podNameMatch[1] : "unknown";
353
+ const containers = containersMatch
354
+ ? containersMatch[1].split(" ").map((c) => c.trim())
355
+ : [];
356
+ const initContainers = initContainersMatch
357
+ ? initContainersMatch[1].split(" ").map((c) => c.trim())
358
+ : [];
305
359
  // Generate structured context for the MCP client to make decisions
306
360
  const context = {
307
361
  error: "Multi-container pod requires container specification",
@@ -309,7 +363,9 @@ function handleCommandError(error, resourceDescription) {
309
363
  pod_name: podName,
310
364
  available_containers: containers,
311
365
  init_containers: initContainers,
312
- suggestion: `Please specify a container name using the 'container' parameter. Available containers: ${containers.join(', ')}${initContainers.length > 0 ? `. Init containers: ${initContainers.join(', ')}` : ''}`
366
+ suggestion: `Please specify a container name using the 'container' parameter. Available containers: ${containers.join(", ")}${initContainers.length > 0
367
+ ? `. Init containers: ${initContainers.join(", ")}`
368
+ : ""}`,
313
369
  };
314
370
  return {
315
371
  content: [
@@ -328,7 +384,7 @@ function handleCommandError(error, resourceDescription) {
328
384
  text: JSON.stringify({
329
385
  error: `Failed to get logs for ${resourceDescription}: ${error.message}`,
330
386
  status: "general_error",
331
- original_error: error.message
387
+ original_error: error.message,
332
388
  }, null, 2),
333
389
  },
334
390
  ],
@@ -1,4 +1,5 @@
1
- import { execSync } from "child_process";
1
+ import { execFileSync } from "child_process";
2
+ import { getSpawnMaxBuffer } from "../config/max-buffer.js";
2
3
  export const explainResourceSchema = {
3
4
  name: "explain_resource",
4
5
  description: "Get documentation for a Kubernetes resource or field",
@@ -58,9 +59,13 @@ export const listApiResourcesSchema = {
58
59
  },
59
60
  },
60
61
  };
61
- const executeKubectlCommand = (command) => {
62
+ const executeKubectlCommand = (command, args) => {
62
63
  try {
63
- return execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
64
+ return execFileSync(command, args, {
65
+ encoding: "utf8",
66
+ maxBuffer: getSpawnMaxBuffer(),
67
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
68
+ });
64
69
  }
65
70
  catch (error) {
66
71
  throw new Error(`Kubectl command failed: ${error.message}`);
@@ -68,18 +73,19 @@ const executeKubectlCommand = (command) => {
68
73
  };
69
74
  export async function explainResource(params) {
70
75
  try {
71
- let command = "kubectl explain";
76
+ const command = "kubectl";
77
+ const args = ["explain"];
72
78
  if (params.apiVersion) {
73
- command += ` --api-version=${params.apiVersion}`;
79
+ args.push(`--api-version=${params.apiVersion}`);
74
80
  }
75
81
  if (params.recursive) {
76
- command += " --recursive";
82
+ args.push("--recursive");
77
83
  }
78
84
  if (params.output) {
79
- command += ` --output=${params.output}`;
85
+ args.push(`--output=${params.output}`);
80
86
  }
81
- command += ` ${params.resource}`;
82
- const result = executeKubectlCommand(command);
87
+ args.push(params.resource);
88
+ const result = executeKubectlCommand(command, args);
83
89
  return {
84
90
  content: [
85
91
  {
@@ -95,20 +101,21 @@ export async function explainResource(params) {
95
101
  }
96
102
  export async function listApiResources(params) {
97
103
  try {
98
- let command = "kubectl api-resources";
104
+ const command = "kubectl";
105
+ const args = ["api-resources"];
99
106
  if (params.apiGroup) {
100
- command += ` --api-group=${params.apiGroup}`;
107
+ args.push(`--api-group=${params.apiGroup}`);
101
108
  }
102
109
  if (params.namespaced !== undefined) {
103
- command += ` --namespaced=${params.namespaced}`;
110
+ args.push(`--namespaced=${params.namespaced}`);
104
111
  }
105
112
  if (params.verbs && params.verbs.length > 0) {
106
- command += ` --verbs=${params.verbs.join(",")}`;
113
+ args.push(`--verbs=${params.verbs.join(",")}`);
107
114
  }
108
115
  if (params.output) {
109
- command += ` -o ${params.output}`;
116
+ args.push(`-o`, params.output);
110
117
  }
111
- const result = executeKubectlCommand(command);
118
+ const result = executeKubectlCommand(command, args);
112
119
  return {
113
120
  content: [
114
121
  {
@@ -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 kubectlPatchSchema = {
7
8
  name: "kubectl_patch",
8
9
  description: "Update field(s) of a resource using strategic merge patch, JSON merge patch, or JSON patch",
@@ -11,39 +12,39 @@ export const kubectlPatchSchema = {
11
12
  properties: {
12
13
  resourceType: {
13
14
  type: "string",
14
- description: "Type of resource to patch (e.g., pods, deployments, services)"
15
+ description: "Type of resource to patch (e.g., pods, deployments, services)",
15
16
  },
16
17
  name: {
17
18
  type: "string",
18
- description: "Name of the resource to patch"
19
+ description: "Name of the resource to patch",
19
20
  },
20
21
  namespace: {
21
22
  type: "string",
22
23
  description: "Namespace of the resource",
23
- default: "default"
24
+ default: "default",
24
25
  },
25
26
  patchType: {
26
27
  type: "string",
27
28
  description: "Type of patch to apply",
28
29
  enum: ["strategic", "merge", "json"],
29
- default: "strategic"
30
+ default: "strategic",
30
31
  },
31
32
  patchData: {
32
33
  type: "object",
33
- description: "Patch data as a JSON object"
34
+ description: "Patch data as a JSON object",
34
35
  },
35
36
  patchFile: {
36
37
  type: "string",
37
- description: "Path to a file containing the patch data (alternative to patchData)"
38
+ description: "Path to a file containing the patch data (alternative to patchData)",
38
39
  },
39
40
  dryRun: {
40
41
  type: "boolean",
41
42
  description: "If true, only print the object that would be sent, without sending it",
42
- default: false
43
- }
43
+ default: false,
44
+ },
44
45
  },
45
46
  required: ["resourceType", "name"],
46
- }
47
+ },
47
48
  };
48
49
  export async function kubectlPatch(k8sManager, input) {
49
50
  try {
@@ -54,21 +55,21 @@ export async function kubectlPatch(k8sManager, input) {
54
55
  const patchType = input.patchType || "strategic";
55
56
  const dryRun = input.dryRun || false;
56
57
  let tempFile = null;
57
- // Build the kubectl patch command
58
- let command = `kubectl patch ${input.resourceType} ${input.name} -n ${namespace}`;
58
+ const command = "kubectl";
59
+ const args = ["patch", input.resourceType, input.name, "-n", namespace];
59
60
  // Add patch type flag
60
61
  switch (patchType) {
61
62
  case "strategic":
62
- command += " --type strategic";
63
+ args.push("--type", "strategic");
63
64
  break;
64
65
  case "merge":
65
- command += " --type merge";
66
+ args.push("--type", "merge");
66
67
  break;
67
68
  case "json":
68
- command += " --type json";
69
+ args.push("--type", "json");
69
70
  break;
70
71
  default:
71
- command += " --type strategic";
72
+ args.push("--type", "strategic");
72
73
  }
73
74
  // Handle patch data
74
75
  if (input.patchData) {
@@ -76,18 +77,22 @@ export async function kubectlPatch(k8sManager, input) {
76
77
  const tmpDir = os.tmpdir();
77
78
  tempFile = path.join(tmpDir, `patch-${Date.now()}.json`);
78
79
  fs.writeFileSync(tempFile, JSON.stringify(input.patchData));
79
- command += ` --patch-file ${tempFile}`;
80
+ args.push("--patch-file", tempFile);
80
81
  }
81
82
  else if (input.patchFile) {
82
- command += ` --patch-file ${input.patchFile}`;
83
+ args.push("--patch-file", input.patchFile);
83
84
  }
84
85
  // Add dry-run flag if requested
85
86
  if (dryRun) {
86
- command += " --dry-run=client";
87
+ args.push("--dry-run=client");
87
88
  }
88
89
  // Execute the command
89
90
  try {
90
- const result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
91
+ const result = execFileSync(command, args, {
92
+ encoding: "utf8",
93
+ maxBuffer: getSpawnMaxBuffer(),
94
+ env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
95
+ });
91
96
  // Clean up temp file if created
92
97
  if (tempFile) {
93
98
  try {