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 +6 -1
- package/dist/config/server-config.d.ts +1 -1
- package/dist/config/server-config.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/security/kubectl-flags.js +29 -3
- package/dist/tools/helm-operations.js +48 -43
- package/dist/tools/kubectl-apply.d.ts +1 -1
- package/dist/tools/kubectl-apply.js +11 -1
- package/dist/tools/kubectl-delete.d.ts +1 -1
- package/dist/tools/kubectl-delete.js +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -425,7 +425,12 @@ Adding clusters to kubectx.
|
|
|
425
425
|
|
|
426
426
|
## Star History
|
|
427
427
|
|
|
428
|
-
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
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:
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
219
|
+
}
|
|
220
|
+
let yamlOutput;
|
|
221
|
+
try {
|
|
222
|
+
yamlOutput = executeCommand("helm", templateArgs);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
209
225
|
// Clean up temp file
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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;
|