mcp-server-kubernetes 4.0.4 → 4.0.5
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/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";
|
|
@@ -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;
|