mcp-server-kubernetes 4.0.4 → 4.0.6

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
@@ -425,7 +425,12 @@ Adding clusters to kubectx.
425
425
 
426
426
  ## Star History
427
427
 
428
- [![Star History Chart](https://api.star-history.com/svg?repos=Flux159/mcp-server-kubernetes&type=Date)](https://www.star-history.com/#Flux159/mcp-server-kubernetes&Date)
428
+ <a href="https://github.com/Flux159/rust-star-history">
429
+ <picture>
430
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Flux159/mcp-server-kubernetes/star-history/star-history-dark.svg">
431
+ <img alt="Star History Chart" src="https://raw.githubusercontent.com/Flux159/mcp-server-kubernetes/star-history/star-history.svg">
432
+ </picture>
433
+ </a>
429
434
 
430
435
  ## 🖊️ Cite
431
436
 
@@ -1,6 +1,6 @@
1
1
  export declare const serverConfig: {
2
2
  readonly name: "kubernetes";
3
- readonly version: "4.0.4";
3
+ readonly version: "4.0.6";
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.4",
3
+ version: "4.0.6",
4
4
  capabilities: {
5
5
  resources: {},
6
6
  tools: {},
package/dist/index.d.ts CHANGED
@@ -123,7 +123,7 @@ declare const destructiveTools: ({
123
123
  };
124
124
  readonly filename: {
125
125
  readonly type: "string";
126
- readonly description: "Path to a YAML file to delete resources from (optional)";
126
+ readonly description: "Path to a YAML file to delete resources from (optional). 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.";
127
127
  };
128
128
  readonly allNamespaces: {
129
129
  readonly type: "boolean";
@@ -507,7 +507,7 @@ declare const allTools: ({
507
507
  };
508
508
  readonly filename: {
509
509
  readonly type: "string";
510
- readonly description: "Path to a YAML file to apply (optional - use either manifest or filename)";
510
+ readonly description: "Path to a YAML file to apply (optional - use either manifest or filename). 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.";
511
511
  };
512
512
  readonly namespace: {
513
513
  type: "string";
@@ -564,7 +564,7 @@ declare const allTools: ({
564
564
  };
565
565
  readonly filename: {
566
566
  readonly type: "string";
567
- readonly description: "Path to a YAML file to delete resources from (optional)";
567
+ readonly description: "Path to a YAML file to delete resources from (optional). 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.";
568
568
  };
569
569
  readonly allNamespaces: {
570
570
  readonly type: "boolean";
@@ -71,14 +71,35 @@ function normalizeFlagName(raw) {
71
71
  name = name.slice(0, eq);
72
72
  return name.toLowerCase();
73
73
  }
74
+ // Extract the short-flag letter from a single-dash token, or null if the token
75
+ // is not a single-dash short flag. pflag lets a short flag carry its value
76
+ // attached with no separator ("-shttp://x" == "--server http://x"), so the
77
+ // letter that matters is always the first character after the dash; the rest of
78
+ // the token is the attached value (or a boolean-flag cluster). Long "--" flags
79
+ // never attach a value without "=", so normalizeFlagName already handles them.
80
+ function shortFlagLetter(raw) {
81
+ if (!raw.startsWith("-") || raw.startsWith("--"))
82
+ return null;
83
+ const body = raw.slice(1);
84
+ if (body.length === 0)
85
+ return null;
86
+ return body[0].toLowerCase();
87
+ }
74
88
  function isDangerousFlagName(rawName, fromArgs) {
75
89
  const name = normalizeFlagName(rawName);
76
90
  if (DANGEROUS_FLAGS.has(name))
77
91
  return true;
78
92
  // Short aliases (-s) are only meaningful when they appear as a CLI token,
79
- // not as a key in the `flags` object.
80
- if (fromArgs && SHORT_ALIASES.has(name))
81
- return true;
93
+ // not as a key in the `flags` object. Match both the bare/split forms
94
+ // (normalizeFlagName -> "s") and the attached form "-sURL" (whose first
95
+ // post-dash character is the flag pflag actually parses).
96
+ if (fromArgs) {
97
+ if (SHORT_ALIASES.has(name))
98
+ return true;
99
+ const short = shortFlagLetter(rawName);
100
+ if (short !== null && SHORT_ALIASES.has(short))
101
+ return true;
102
+ }
82
103
  return false;
83
104
  }
84
105
  function reject(flag) {
@@ -139,6 +160,11 @@ export function assertSafeArgv(args) {
139
160
  const name = normalizeFlagName(tok);
140
161
  if (ARGV_DANGEROUS_FLAGS.has(name) || SHORT_ALIASES.has(name))
141
162
  reject(tok);
163
+ // Attached short-flag form ("-sURL"): match the first post-dash character,
164
+ // which is the flag pflag parses regardless of the trailing value.
165
+ const short = shortFlagLetter(tok);
166
+ if (short !== null && SHORT_ALIASES.has(short))
167
+ reject(tok);
142
168
  }
143
169
  }
144
170
  /**
@@ -4,9 +4,11 @@
4
4
  * Template mode bypasses authentication issues and kubeconfig API version mismatches.
5
5
  * Supports local chart paths, remote repositories, and custom values.
6
6
  */
7
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
7
8
  import { execFileSyncSafe } from "../security/kubectl-flags.js";
8
9
  import { writeFileSync, unlinkSync } from "fs";
9
10
  import { dump } from "js-yaml";
11
+ import { isRemoteTransport } from "../security/transport.js";
10
12
  import { getSpawnMaxBuffer } from "../config/max-buffer.js";
11
13
  import { contextParameter, namespaceParameter, } from "../models/common-parameters.js";
12
14
  /**
@@ -49,7 +51,7 @@ export const installHelmChartSchema = {
49
51
  },
50
52
  valuesFile: {
51
53
  type: "string",
52
- description: "Path to values file (alternative to values object)",
54
+ description: "Path to values file (alternative to values object). 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 'values' to pass the values inline instead.",
53
55
  },
54
56
  useTemplate: {
55
57
  type: "boolean",
@@ -103,7 +105,7 @@ export const upgradeHelmChartSchema = {
103
105
  },
104
106
  valuesFile: {
105
107
  type: "string",
106
- description: "Path to values file (alternative to values object)",
108
+ description: "Path to values file (alternative to values object). 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 'values' to pass the values inline instead.",
107
109
  },
108
110
  },
109
111
  required: ["name", "chart", "namespace"],
@@ -153,6 +155,17 @@ const executeCommand = (command, args) => {
153
155
  throw new Error(`${command} command failed: ${error.message}`);
154
156
  }
155
157
  };
158
+ // Reject server-side filesystem reads on remote transports. Over SSE /
159
+ // Streamable HTTP the path resolves on the MCP server host, not the client,
160
+ // so `valuesFile` (-f) would let any client that can reach the endpoint read
161
+ // arbitrary server files (kubeconfig, service-account token,
162
+ // /proc/self/environ, etc.) via helm's parse errors. Clients on these
163
+ // transports must pass values inline via `values` instead.
164
+ const rejectValuesFileOnRemoteTransport = (valuesFile) => {
165
+ if (valuesFile && isRemoteTransport()) {
166
+ throw new McpError(ErrorCode.InvalidRequest, "The 'valuesFile' parameter reads a file from the MCP server's filesystem and is disabled on remote (SSE/Streamable HTTP) transports. Pass the values inline via 'values' instead.");
167
+ }
168
+ };
156
169
  /**
157
170
  * Install a Helm chart using template mode (helm template + kubectl apply).
158
171
  * This mode bypasses authentication issues and kubeconfig API version mismatches.
@@ -179,17 +192,7 @@ async function installHelmChartTemplate(params) {
179
192
  }
180
193
  steps.push(`Namespace ${params.namespace} already exists`);
181
194
  }
182
- // Step 3: Prepare values
183
- let valuesContent = "";
184
- if (params.valuesFile) {
185
- steps.push(`Using values file: ${params.valuesFile}`);
186
- valuesContent = executeCommand("cat", [params.valuesFile]);
187
- }
188
- else if (params.values) {
189
- steps.push("Using provided values object");
190
- valuesContent = dump(params.values);
191
- }
192
- // Step 4: Generate YAML using helm template
195
+ // Step 3: Generate YAML using helm template
193
196
  steps.push("Generating YAML using helm template");
194
197
  const templateArgs = [
195
198
  "template",
@@ -201,40 +204,40 @@ async function installHelmChartTemplate(params) {
201
204
  if (params.repo) {
202
205
  templateArgs.push("--repo", params.repo);
203
206
  }
204
- if (valuesContent) {
205
- const tempValuesFile = `/tmp/values-${Date.now()}.yaml`;
206
- writeFileSync(tempValuesFile, valuesContent);
207
+ let tempValuesFile = null;
208
+ if (params.valuesFile) {
209
+ // Hand the path to helm directly rather than reading the file's
210
+ // contents into this process.
211
+ steps.push(`Using values file: ${params.valuesFile}`);
212
+ templateArgs.push("-f", params.valuesFile);
213
+ }
214
+ else if (params.values) {
215
+ steps.push("Using provided values object");
216
+ tempValuesFile = `/tmp/values-${Date.now()}.yaml`;
217
+ writeFileSync(tempValuesFile, dump(params.values));
207
218
  templateArgs.push("-f", tempValuesFile);
208
- const yamlOutput = executeCommand("helm", templateArgs);
219
+ }
220
+ let yamlOutput;
221
+ try {
222
+ yamlOutput = executeCommand("helm", templateArgs);
223
+ }
224
+ finally {
209
225
  // Clean up temp file
210
- unlinkSync(tempValuesFile);
211
- // Step 5: Apply YAML using kubectl
212
- steps.push("Applying YAML using kubectl");
213
- const tempYamlFile = `/tmp/helm-template-${Date.now()}.yaml`;
214
- writeFileSync(tempYamlFile, yamlOutput);
215
- try {
216
- executeCommand("kubectl", ["apply", "-f", tempYamlFile]);
217
- steps.push("Helm chart installed successfully using template mode");
218
- }
219
- finally {
220
- // Clean up temp file
221
- unlinkSync(tempYamlFile);
226
+ if (tempValuesFile) {
227
+ unlinkSync(tempValuesFile);
222
228
  }
223
229
  }
224
- else {
225
- const yamlOutput = executeCommand("helm", templateArgs);
226
- // Step 5: Apply YAML using kubectl
227
- steps.push("Applying YAML using kubectl");
228
- const tempYamlFile = `/tmp/helm-template-${Date.now()}.yaml`;
229
- writeFileSync(tempYamlFile, yamlOutput);
230
- try {
231
- executeCommand("kubectl", ["apply", "-f", tempYamlFile]);
232
- steps.push("Helm chart installed successfully using template mode");
233
- }
234
- finally {
235
- // Clean up temp file
236
- unlinkSync(tempYamlFile);
237
- }
230
+ // Step 4: Apply YAML using kubectl
231
+ steps.push("Applying YAML using kubectl");
232
+ const tempYamlFile = `/tmp/helm-template-${Date.now()}.yaml`;
233
+ writeFileSync(tempYamlFile, yamlOutput);
234
+ try {
235
+ executeCommand("kubectl", ["apply", "-f", tempYamlFile]);
236
+ steps.push("Helm chart installed successfully using template mode");
237
+ }
238
+ finally {
239
+ // Clean up temp file
240
+ unlinkSync(tempYamlFile);
238
241
  }
239
242
  return {
240
243
  content: [
@@ -270,6 +273,7 @@ async function installHelmChartTemplate(params) {
270
273
  * @returns Promise with installation result
271
274
  */
272
275
  export async function installHelmChart(params) {
276
+ rejectValuesFileOnRemoteTransport(params.valuesFile);
273
277
  // Use template mode if requested
274
278
  if (params.useTemplate) {
275
279
  return installHelmChartTemplate(params);
@@ -344,6 +348,7 @@ export async function installHelmChart(params) {
344
348
  * @returns Promise with upgrade result
345
349
  */
346
350
  export async function upgradeHelmChart(params) {
351
+ rejectValuesFileOnRemoteTransport(params.valuesFile);
347
352
  try {
348
353
  // Add repository if provided
349
354
  if (params.repo) {
@@ -14,7 +14,7 @@ export declare const kubectlApplySchema: {
14
14
  };
15
15
  readonly filename: {
16
16
  readonly type: "string";
17
- readonly description: "Path to a YAML file to apply (optional - use either manifest or filename)";
17
+ readonly description: "Path to a YAML file to apply (optional - use either manifest or filename). 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.";
18
18
  };
19
19
  readonly namespace: {
20
20
  type: "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 kubectlApplySchema = {
9
10
  name: "kubectl_apply",
10
11
  description: "Apply a Kubernetes YAML manifest from a string or file",
@@ -20,7 +21,7 @@ export const kubectlApplySchema = {
20
21
  },
21
22
  filename: {
22
23
  type: "string",
23
- description: "Path to a YAML file to apply (optional - use either manifest or filename)",
24
+ description: "Path to a YAML file to apply (optional - use either manifest or filename). 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.",
24
25
  },
25
26
  namespace: namespaceParameter,
26
27
  dryRun: dryRunParameter,
@@ -39,6 +40,15 @@ export async function kubectlApply(k8sManager, input) {
39
40
  if (!input.manifest && !input.filename) {
40
41
  throw new McpError(ErrorCode.InvalidRequest, "Either manifest or filename must be provided");
41
42
  }
43
+ // Reject server-side filesystem reads on remote transports. Over SSE /
44
+ // Streamable HTTP the path resolves on the MCP server host, not the
45
+ // client, so `filename` (-f) would let any client that can reach the
46
+ // endpoint read arbitrary server files (kubeconfig, service-account
47
+ // token, /proc/self/environ, etc.) via kubectl's parse errors. Clients
48
+ // on these transports must pass content inline via `manifest` instead.
49
+ if (isRemoteTransport() && input.filename) {
50
+ 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.");
51
+ }
42
52
  const namespace = input.namespace || "default";
43
53
  const dryRun = input.dryRun || false;
44
54
  const force = input.force || false;
@@ -31,7 +31,7 @@ export declare const kubectlDeleteSchema: {
31
31
  };
32
32
  readonly filename: {
33
33
  readonly type: "string";
34
- readonly description: "Path to a YAML file to delete resources from (optional)";
34
+ readonly description: "Path to a YAML file to delete resources from (optional). 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.";
35
35
  };
36
36
  readonly allNamespaces: {
37
37
  readonly type: "boolean";
@@ -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, } from "../models/common-parameters.js";
8
+ import { isRemoteTransport } from "../security/transport.js";
8
9
  export const kubectlDeleteSchema = {
9
10
  name: "kubectl_delete",
10
11
  description: "Delete Kubernetes resources by resource type, name, labels, or from a manifest file",
@@ -33,7 +34,7 @@ export const kubectlDeleteSchema = {
33
34
  },
34
35
  filename: {
35
36
  type: "string",
36
- description: "Path to a YAML file to delete resources from (optional)",
37
+ description: "Path to a YAML file to delete resources from (optional). 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.",
37
38
  },
38
39
  allNamespaces: {
39
40
  type: "boolean",
@@ -64,6 +65,15 @@ export async function kubectlDelete(k8sManager, input) {
64
65
  if (input.resourceType && !input.name && !input.labelSelector) {
65
66
  throw new McpError(ErrorCode.InvalidRequest, "When using resourceType, either name or labelSelector must be provided");
66
67
  }
68
+ // Reject server-side filesystem reads on remote transports. Over SSE /
69
+ // Streamable HTTP the path resolves on the MCP server host, not the
70
+ // client, so `filename` (-f) would let any client that can reach the
71
+ // endpoint read arbitrary server files (kubeconfig, service-account
72
+ // token, /proc/self/environ, etc.) via kubectl's parse errors. Clients
73
+ // on these transports must pass content inline via `manifest` instead.
74
+ if (isRemoteTransport() && input.filename) {
75
+ 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.");
76
+ }
67
77
  const namespace = input.namespace || "default";
68
78
  const allNamespaces = input.allNamespaces || false;
69
79
  const force = input.force || false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-kubernetes",
3
- "version": "4.0.4",
3
+ "version": "4.0.6",
4
4
  "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
5
  "license": "MIT",
6
6
  "type": "module",