mcp-server-kubernetes 4.0.1 → 4.0.3

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.
package/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Flux159/mcp-server-kubernetes/pulls)
11
11
  [![Last Commit](https://img.shields.io/github/last-commit/Flux159/mcp-server-kubernetes)](https://github.com/Flux159/mcp-server-kubernetes/commits/main)
12
12
  [![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/Flux159/mcp-server-kubernetes)](https://archestra.ai/mcp-catalog/flux159__mcp-server-kubernetes)
13
+ [![HVTrust](https://hvtracker.net/badge/kubernetes-mcp-server.svg)](https://hvtracker.net/agents/kubernetes-mcp-server/)
13
14
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Flux159/mcp-server-kubernetes)
14
15
 
15
16
  <p align="center">
@@ -1,6 +1,6 @@
1
1
  export declare const serverConfig: {
2
2
  readonly name: "kubernetes";
3
- readonly version: "4.0.1";
3
+ readonly version: "4.0.3";
4
4
  readonly capabilities: {
5
5
  readonly resources: {};
6
6
  readonly tools: {};
@@ -1,6 +1,6 @@
1
1
  export const serverConfig = {
2
2
  name: "kubernetes",
3
- version: "4.0.1",
3
+ version: "4.0.3",
4
4
  capabilities: {
5
5
  resources: {},
6
6
  tools: {},
package/dist/index.d.ts CHANGED
@@ -616,7 +616,7 @@ declare const allTools: ({
616
616
  };
617
617
  readonly filename: {
618
618
  readonly type: "string";
619
- readonly description: "Path to a YAML file to create resources from";
619
+ readonly description: "Path to a YAML file to create resources from. 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 'manifest' to pass the file's contents instead.";
620
620
  };
621
621
  readonly resourceType: {
622
622
  readonly type: "string";
@@ -643,7 +643,26 @@ declare const allTools: ({
643
643
  readonly items: {
644
644
  readonly type: "string";
645
645
  };
646
- readonly description: "Path to file for creating configmap (e.g. [\"key1=/path/to/file1\", \"key2=/path/to/file2\"])";
646
+ readonly description: "Path to file for creating configmap/secret (e.g. [\"key1=/path/to/file1\", \"key2=/path/to/file2\"]). 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 \"fromFileContent\" to pass file contents directly instead.";
647
+ };
648
+ readonly fromFileContent: {
649
+ readonly type: "array";
650
+ readonly items: {
651
+ readonly type: "object";
652
+ readonly properties: {
653
+ readonly key: {
654
+ readonly type: "string";
655
+ readonly description: "Key to store the content under in the configmap/secret";
656
+ };
657
+ readonly content: {
658
+ readonly type: "string";
659
+ readonly description: "The file content to store";
660
+ };
661
+ };
662
+ readonly required: readonly ["key", "content"];
663
+ readonly additionalProperties: false;
664
+ };
665
+ readonly description: "Inline file contents for creating a configmap/secret, provided by the client instead of a server-side path (e.g. [{\"key\": \"app.conf\", \"content\": \"...\"}]). Safe on all transports; use this instead of \"fromFile\" on remote (SSE/Streamable HTTP) servers.";
647
666
  };
648
667
  readonly secretType: {
649
668
  readonly type: "string";
@@ -0,0 +1 @@
1
+ export declare function isRemoteTransport(): boolean;
@@ -0,0 +1,17 @@
1
+ // Transport-mode detection for security decisions.
2
+ //
3
+ // The server selects its transport at startup based on these env vars (see
4
+ // src/index.ts): if either is set the server listens over HTTP (SSE or
5
+ // Streamable HTTP) and serves remote clients; otherwise it uses stdio and is
6
+ // driven by a local process that already runs as the operator.
7
+ //
8
+ // Some tool inputs are only safe under the stdio trust model. A server-side
9
+ // filesystem path (e.g. `--from-file` / `-f`) is read on the machine running
10
+ // the server: over stdio that machine is the operator's own, but over HTTP it
11
+ // is the server host, so any client that can reach the endpoint could read
12
+ // arbitrary files (kubeconfig, service-account token, /proc/self/environ).
13
+ // Guards use this helper to reject path-based reads on remote transports.
14
+ export function isRemoteTransport() {
15
+ return Boolean(process.env.ENABLE_UNSAFE_SSE_TRANSPORT ||
16
+ process.env.ENABLE_UNSAFE_STREAMABLE_HTTP_TRANSPORT);
17
+ }
@@ -27,7 +27,7 @@ export declare const kubectlCreateSchema: {
27
27
  };
28
28
  readonly filename: {
29
29
  readonly type: "string";
30
- readonly description: "Path to a YAML file to create resources from";
30
+ readonly description: "Path to a YAML file to create resources from. 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 'manifest' to pass the file's contents instead.";
31
31
  };
32
32
  readonly resourceType: {
33
33
  readonly type: "string";
@@ -54,7 +54,26 @@ export declare const kubectlCreateSchema: {
54
54
  readonly items: {
55
55
  readonly type: "string";
56
56
  };
57
- readonly description: "Path to file for creating configmap (e.g. [\"key1=/path/to/file1\", \"key2=/path/to/file2\"])";
57
+ readonly description: "Path to file for creating configmap/secret (e.g. [\"key1=/path/to/file1\", \"key2=/path/to/file2\"]). 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 \"fromFileContent\" to pass file contents directly instead.";
58
+ };
59
+ readonly fromFileContent: {
60
+ readonly type: "array";
61
+ readonly items: {
62
+ readonly type: "object";
63
+ readonly properties: {
64
+ readonly key: {
65
+ readonly type: "string";
66
+ readonly description: "Key to store the content under in the configmap/secret";
67
+ };
68
+ readonly content: {
69
+ readonly type: "string";
70
+ readonly description: "The file content to store";
71
+ };
72
+ };
73
+ readonly required: readonly ["key", "content"];
74
+ readonly additionalProperties: false;
75
+ };
76
+ readonly description: "Inline file contents for creating a configmap/secret, provided by the client instead of a server-side path (e.g. [{\"key\": \"app.conf\", \"content\": \"...\"}]). Safe on all transports; use this instead of \"fromFile\" on remote (SSE/Streamable HTTP) servers.";
58
77
  };
59
78
  readonly secretType: {
60
79
  readonly type: "string";
@@ -136,6 +155,10 @@ export declare function kubectlCreate(k8sManager: KubernetesManager, input: {
136
155
  namespace?: string;
137
156
  fromLiteral?: string[];
138
157
  fromFile?: string[];
158
+ fromFileContent?: {
159
+ key: string;
160
+ content: string;
161
+ }[];
139
162
  secretType?: "generic" | "docker-registry" | "tls";
140
163
  serviceType?: "clusterip" | "nodeport" | "loadbalancer" | "externalname";
141
164
  tcpPort?: string[];
@@ -5,6 +5,7 @@ import * as path from "path";
5
5
  import * as os from "os";
6
6
  import { getSpawnMaxBuffer } from "../config/max-buffer.js";
7
7
  import { contextParameter, namespaceParameter, dryRunParameter } from "../models/common-parameters.js";
8
+ import { isRemoteTransport } from "../security/transport.js";
8
9
  export const kubectlCreateSchema = {
9
10
  name: "kubectl_create",
10
11
  description: "Create Kubernetes resources using various methods (from file or using subcommands)",
@@ -42,7 +43,7 @@ export const kubectlCreateSchema = {
42
43
  },
43
44
  filename: {
44
45
  type: "string",
45
- description: "Path to a YAML file to create resources from",
46
+ description: "Path to a YAML file to create resources from. 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 'manifest' to pass the file's contents instead.",
46
47
  },
47
48
  // Resource type to create (determines which subcommand to use)
48
49
  resourceType: {
@@ -64,7 +65,26 @@ export const kubectlCreateSchema = {
64
65
  fromFile: {
65
66
  type: "array",
66
67
  items: { type: "string" },
67
- description: 'Path to file for creating configmap (e.g. ["key1=/path/to/file1", "key2=/path/to/file2"])',
68
+ description: 'Path to file for creating configmap/secret (e.g. ["key1=/path/to/file1", "key2=/path/to/file2"]). 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 "fromFileContent" to pass file contents directly instead.',
69
+ },
70
+ fromFileContent: {
71
+ type: "array",
72
+ items: {
73
+ type: "object",
74
+ properties: {
75
+ key: {
76
+ type: "string",
77
+ description: "Key to store the content under in the configmap/secret",
78
+ },
79
+ content: {
80
+ type: "string",
81
+ description: "The file content to store",
82
+ },
83
+ },
84
+ required: ["key", "content"],
85
+ additionalProperties: false,
86
+ },
87
+ description: 'Inline file contents for creating a configmap/secret, provided by the client instead of a server-side path (e.g. [{"key": "app.conf", "content": "..."}]). Safe on all transports; use this instead of "fromFile" on remote (SSE/Streamable HTTP) servers.',
68
88
  },
69
89
  // Namespace specific parameters
70
90
  // No special parameters for namespace, just name is needed
@@ -143,6 +163,21 @@ export async function kubectlCreate(k8sManager, input) {
143
163
  input.resourceType !== "namespace") {
144
164
  throw new McpError(ErrorCode.InvalidRequest, `Name is required when creating a ${input.resourceType}`);
145
165
  }
166
+ // Reject server-side filesystem reads on remote transports. Over SSE /
167
+ // Streamable HTTP the path resolves on the MCP server host, not the
168
+ // client, so `filename` (-f) and `fromFile` (--from-file) would let any
169
+ // client that can reach the endpoint read arbitrary server files
170
+ // (kubeconfig, service-account token, /proc/self/environ, etc.). See
171
+ // GHSA-m67f-jxm9-cvx8. Clients on these transports must pass content
172
+ // inline via `manifest` or `fromFileContent` instead.
173
+ if (isRemoteTransport()) {
174
+ if (input.filename) {
175
+ throw new McpError(ErrorCode.InvalidRequest, "The 'filename' parameter reads a file from the MCP server's filesystem and is disabled on remote (SSE/Streamable HTTP) transports. Pass the file contents via 'manifest' instead.");
176
+ }
177
+ if (input.fromFile && input.fromFile.length > 0) {
178
+ throw new McpError(ErrorCode.InvalidRequest, "The 'fromFile' parameter reads files from the MCP server's filesystem and is disabled on remote (SSE/Streamable HTTP) transports. Pass the file contents via 'fromFileContent' instead.");
179
+ }
180
+ }
146
181
  // Set up common parameters
147
182
  const namespace = input.namespace || "default";
148
183
  const dryRun = input.dryRun || false;
@@ -151,15 +186,33 @@ export async function kubectlCreate(k8sManager, input) {
151
186
  const context = input.context || "";
152
187
  const command = "kubectl";
153
188
  const args = ["create"];
154
- let tempFile = null;
189
+ const tempFiles = [];
190
+ // Write client-provided content to a private temp file and return its
191
+ // path. The file is tracked for cleanup after kubectl runs.
192
+ const writeTempFile = (contents, label, ext = "") => {
193
+ const tmpDir = os.tmpdir();
194
+ const tempFile = path.join(tmpDir, `create-${label}-${Date.now()}-${tempFiles.length}${ext}`);
195
+ fs.writeFileSync(tempFile, contents, { mode: 0o600 });
196
+ tempFiles.push(tempFile);
197
+ return tempFile;
198
+ };
199
+ // Emit `--from-file=<key>=<tempfile>` args for inline content supplied by
200
+ // the client. Each entry's content is written to a server-side temp file
201
+ // (never a client-controlled path), so this is safe on all transports.
202
+ const pushFromFileContent = (entries) => {
203
+ entries.forEach((entry) => {
204
+ if (!entry.key) {
205
+ throw new McpError(ErrorCode.InvalidRequest, "Each fromFileContent entry requires a non-empty 'key'");
206
+ }
207
+ const tempFile = writeTempFile(entry.content ?? "", "fromfile");
208
+ args.push(`--from-file=${entry.key}=${tempFile}`);
209
+ });
210
+ };
155
211
  // Process manifest content if provided (file-based creation)
156
212
  if (input.manifest || input.filename) {
157
213
  if (input.manifest) {
158
214
  // Create temporary file for the manifest
159
- const tmpDir = os.tmpdir();
160
- tempFile = path.join(tmpDir, `create-manifest-${Date.now()}.yaml`);
161
- fs.writeFileSync(tempFile, input.manifest);
162
- args.push("-f", tempFile);
215
+ args.push("-f", writeTempFile(input.manifest, "manifest", ".yaml"));
163
216
  }
164
217
  else if (input.filename) {
165
218
  args.push("-f", input.filename);
@@ -179,12 +232,17 @@ export async function kubectlCreate(k8sManager, input) {
179
232
  args.push(`--from-literal=${literal}`);
180
233
  });
181
234
  }
182
- // Add --from-file arguments
235
+ // Add --from-file arguments (server-side paths; blocked on remote
236
+ // transports by the guard above)
183
237
  if (input.fromFile && input.fromFile.length > 0) {
184
238
  input.fromFile.forEach((file) => {
185
239
  args.push(`--from-file=${file}`);
186
240
  });
187
241
  }
242
+ // Add inline file contents (safe on all transports)
243
+ if (input.fromFileContent && input.fromFileContent.length > 0) {
244
+ pushFromFileContent(input.fromFileContent);
245
+ }
188
246
  break;
189
247
  case "secret":
190
248
  if (!input.secretType) {
@@ -197,12 +255,17 @@ export async function kubectlCreate(k8sManager, input) {
197
255
  args.push(`--from-literal=${literal}`);
198
256
  });
199
257
  }
200
- // Add --from-file arguments
258
+ // Add --from-file arguments (server-side paths; blocked on remote
259
+ // transports by the guard above)
201
260
  if (input.fromFile && input.fromFile.length > 0) {
202
261
  input.fromFile.forEach((file) => {
203
262
  args.push(`--from-file=${file}`);
204
263
  });
205
264
  }
265
+ // Add inline file contents (safe on all transports)
266
+ if (input.fromFileContent && input.fromFileContent.length > 0) {
267
+ pushFromFileContent(input.fromFileContent);
268
+ }
206
269
  break;
207
270
  case "service":
208
271
  if (!input.serviceType) {
@@ -292,6 +355,17 @@ export async function kubectlCreate(k8sManager, input) {
292
355
  if (context) {
293
356
  args.push("--context", context);
294
357
  }
358
+ // Remove any temp files created for inline content.
359
+ const cleanupTempFiles = () => {
360
+ tempFiles.forEach((tempFile) => {
361
+ try {
362
+ fs.unlinkSync(tempFile);
363
+ }
364
+ catch (err) {
365
+ console.warn(`Failed to delete temporary file ${tempFile}: ${err}`);
366
+ }
367
+ });
368
+ };
295
369
  // Execute the command
296
370
  try {
297
371
  const result = execFileSyncSafe(command, args, {
@@ -299,15 +373,8 @@ export async function kubectlCreate(k8sManager, input) {
299
373
  maxBuffer: getSpawnMaxBuffer(),
300
374
  env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
301
375
  });
302
- // Clean up temp file if created
303
- if (tempFile) {
304
- try {
305
- fs.unlinkSync(tempFile);
306
- }
307
- catch (err) {
308
- console.warn(`Failed to delete temporary file ${tempFile}: ${err}`);
309
- }
310
- }
376
+ // Clean up temp files if created
377
+ cleanupTempFiles();
311
378
  return {
312
379
  content: [
313
380
  {
@@ -318,15 +385,8 @@ export async function kubectlCreate(k8sManager, input) {
318
385
  };
319
386
  }
320
387
  catch (error) {
321
- // Clean up temp file if created, even if command failed
322
- if (tempFile) {
323
- try {
324
- fs.unlinkSync(tempFile);
325
- }
326
- catch (err) {
327
- console.warn(`Failed to delete temporary file ${tempFile}: ${err}`);
328
- }
329
- }
388
+ // Clean up temp files if created, even if command failed
389
+ cleanupTempFiles();
330
390
  throw new McpError(ErrorCode.InternalError, `Failed to create resource: ${error.message}`);
331
391
  }
332
392
  }
@@ -113,10 +113,10 @@ export async function kubectlGet(k8sManager, input) {
113
113
  }
114
114
  else if (output === "custom") {
115
115
  if (resourceType === "events") {
116
- args.push("-o", "'custom-columns=LASTSEEN:.lastTimestamp,TYPE:.type,REASON:.reason,OBJECT:.involvedObject.name,MESSAGE:.message'");
116
+ args.push("-o", "custom-columns=LASTSEEN:.lastTimestamp,TYPE:.type,REASON:.reason,OBJECT:.involvedObject.name,MESSAGE:.message");
117
117
  }
118
118
  else {
119
- args.push("-o", "'custom-columns=NAME:.metadata.name,NAMESPACE:.metadata.namespace,STATUS:.status.phase,AGE:.metadata.creationTimestamp'");
119
+ args.push("-o", "custom-columns=NAME:.metadata.name,NAMESPACE:.metadata.namespace,STATUS:.status.phase,AGE:.metadata.creationTimestamp");
120
120
  }
121
121
  }
122
122
  // Execute the command
@@ -63,7 +63,7 @@ export const kubectlLogsSchema = {
63
63
  };
64
64
  export async function kubectlLogs(k8sManager, input) {
65
65
  try {
66
- const resourceType = input.resourceType.toLowerCase();
66
+ const resourceType = (input.resourceType ?? "pod").toLowerCase();
67
67
  const name = input.name;
68
68
  const namespace = input.namespace || "default";
69
69
  const context = input.context || "";
@@ -108,7 +108,7 @@ export async function kubectlLogs(k8sManager, input) {
108
108
  "deployment",
109
109
  name,
110
110
  "-o",
111
- "jsonpath='{.spec.selector.matchLabels}'",
111
+ "jsonpath={.spec.selector.matchLabels}",
112
112
  ];
113
113
  }
114
114
  else if (resourceType === "job") {
@@ -124,7 +124,7 @@ export async function kubectlLogs(k8sManager, input) {
124
124
  "jobs",
125
125
  "--selector=job-name=" + name,
126
126
  "-o",
127
- "jsonpath='{.items[*].metadata.name}'",
127
+ "jsonpath={.items[*].metadata.name}",
128
128
  ];
129
129
  try {
130
130
  const jobs = execFileSyncSafe(command, jobsArgs, {
@@ -182,7 +182,7 @@ export async function kubectlLogs(k8sManager, input) {
182
182
  maxBuffer: getSpawnMaxBuffer(),
183
183
  env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
184
184
  }).trim();
185
- const selector = JSON.parse(selectorJson.replace(/'/g, '"'));
185
+ const selector = JSON.parse(selectorJson);
186
186
  // Convert to label selector format
187
187
  const labelSelector = Object.entries(selector)
188
188
  .map(([key, value]) => `${key}=${value}`)
@@ -258,7 +258,7 @@ async function getLabelSelectorLogs(labelSelector, namespace, input) {
258
258
  "pods",
259
259
  `--selector=${labelSelector}`,
260
260
  "-o",
261
- "jsonpath='{.items[*].metadata.name}'",
261
+ "jsonpath={.items[*].metadata.name}",
262
262
  ];
263
263
  const pods = execFileSyncSafe(command, podsArgs, {
264
264
  encoding: "utf8",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-kubernetes",
3
- "version": "4.0.1",
3
+ "version": "4.0.3",
4
4
  "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -57,6 +57,6 @@
57
57
  "@types/node": "22.9.3",
58
58
  "shx": "0.3.4",
59
59
  "typescript": "5.6.2",
60
- "vitest": "2.1.9"
60
+ "vitest": "3.2.6"
61
61
  }
62
62
  }