mcp-server-kubernetes 3.7.0 → 3.9.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.
- package/dist/security/kubectl-flags.d.ts +19 -0
- package/dist/security/kubectl-flags.js +55 -0
- package/dist/tools/exec_in_pod.js +2 -2
- package/dist/tools/helm-operations.js +2 -2
- package/dist/tools/kubectl-apply.js +2 -2
- package/dist/tools/kubectl-context.js +7 -7
- package/dist/tools/kubectl-create.js +2 -2
- package/dist/tools/kubectl-delete.js +2 -2
- package/dist/tools/kubectl-describe.js +2 -2
- package/dist/tools/kubectl-generic.js +2 -2
- package/dist/tools/kubectl-get.js +2 -2
- package/dist/tools/kubectl-logs.js +6 -6
- package/dist/tools/kubectl-operations.js +2 -2
- package/dist/tools/kubectl-patch.js +2 -2
- package/dist/tools/kubectl-rollout.js +3 -3
- package/dist/tools/kubectl-scale.js +2 -2
- package/dist/tools/node-management.js +2 -2
- package/dist/utils/streamable-http.js +47 -14
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ExecFileSyncOptionsWithStringEncoding } from "child_process";
|
|
1
2
|
/**
|
|
2
3
|
* Validate user-supplied kubectl flags and args. Throws an McpError if any
|
|
3
4
|
* dangerous flag is present and the unsafe-flags escape hatch is not set.
|
|
@@ -8,3 +9,21 @@
|
|
|
8
9
|
* ("--server", "x") forms, plus short aliases ("-s").
|
|
9
10
|
*/
|
|
10
11
|
export declare function assertNoDangerousFlags(flags?: Record<string, unknown>, args?: string[]): void;
|
|
12
|
+
/**
|
|
13
|
+
* Validate a fully-constructed kubectl/helm argv. Unlike assertNoDangerousFlags
|
|
14
|
+
* (which inspects only the free-form `flags`/`args` inputs of kubectl_generic),
|
|
15
|
+
* this scans every token in the final argv — including bare positional slots
|
|
16
|
+
* such as resource names, node names, and resource types that the individual
|
|
17
|
+
* tools push directly. kubectl's pflag parser treats any token beginning with
|
|
18
|
+
* "-" as a flag regardless of position, so a tool argument like
|
|
19
|
+
* name: "--server=https://attacker" would otherwise redirect the API server
|
|
20
|
+
* and leak the operator's bearer token. Throws an McpError on any dangerous
|
|
21
|
+
* flag unless ALLOW_KUBECTL_UNSAFE_FLAGS=true.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertSafeArgv(args: readonly string[]): void;
|
|
24
|
+
/**
|
|
25
|
+
* Drop-in replacement for child_process.execFileSync that scans the argv for
|
|
26
|
+
* credential/target-redirecting flags before executing. Tool files import this
|
|
27
|
+
* as `execFileSync`, so every kubectl/helm call site is guarded at one place.
|
|
28
|
+
*/
|
|
29
|
+
export declare function execFileSyncSafe(file: string, args: string[], options: ExecFileSyncOptionsWithStringEncoding): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { execFileSync, } from "child_process";
|
|
2
3
|
// Flags that would let a caller redirect kubectl to a different API server,
|
|
3
4
|
// substitute credentials, or impersonate another identity. Allowing any of
|
|
4
5
|
// these to flow in from tool inputs lets an attacker who can influence the
|
|
@@ -38,6 +39,27 @@ const DANGEROUS_FLAGS = new Set([
|
|
|
38
39
|
const SHORT_ALIASES = new Set([
|
|
39
40
|
"s", // -s is an alias for --server
|
|
40
41
|
]);
|
|
42
|
+
// helm exposes the same exfiltration surface as kubectl, but under "kube-"
|
|
43
|
+
// prefixed flag names (e.g. --kube-apiserver instead of --server). We add
|
|
44
|
+
// those here so the argv-level guard covers helm invocations too. Context
|
|
45
|
+
// selection flags (--context / --kube-context) are intentionally omitted:
|
|
46
|
+
// they can only select a cluster already present in the loaded kubeconfig,
|
|
47
|
+
// every tool legitimately emits "--context <value>", and without --server /
|
|
48
|
+
// --kubeconfig they cannot redirect kubectl/helm to an attacker host.
|
|
49
|
+
const HELM_DANGEROUS_FLAGS = new Set([
|
|
50
|
+
"kube-apiserver",
|
|
51
|
+
"kube-token",
|
|
52
|
+
"kube-ca-file",
|
|
53
|
+
"kube-as-user",
|
|
54
|
+
"kube-as-group",
|
|
55
|
+
"kube-tls-server-name",
|
|
56
|
+
"kube-insecure-skip-tls-verify",
|
|
57
|
+
]);
|
|
58
|
+
// Flag names that are dangerous when they appear anywhere in a fully
|
|
59
|
+
// constructed argv (positional slots included), regardless of which tool
|
|
60
|
+
// built it. This is DANGEROUS_FLAGS minus the context-selection flags, plus
|
|
61
|
+
// the helm equivalents. See assertSafeArgv / execFileSyncSafe below.
|
|
62
|
+
const ARGV_DANGEROUS_FLAGS = new Set([...DANGEROUS_FLAGS, ...HELM_DANGEROUS_FLAGS].filter((name) => name !== "context"));
|
|
41
63
|
function isUnsafeFlagsAllowed() {
|
|
42
64
|
return process.env.ALLOW_KUBECTL_UNSAFE_FLAGS === "true";
|
|
43
65
|
}
|
|
@@ -95,3 +117,36 @@ export function assertNoDangerousFlags(flags, args) {
|
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Validate a fully-constructed kubectl/helm argv. Unlike assertNoDangerousFlags
|
|
122
|
+
* (which inspects only the free-form `flags`/`args` inputs of kubectl_generic),
|
|
123
|
+
* this scans every token in the final argv — including bare positional slots
|
|
124
|
+
* such as resource names, node names, and resource types that the individual
|
|
125
|
+
* tools push directly. kubectl's pflag parser treats any token beginning with
|
|
126
|
+
* "-" as a flag regardless of position, so a tool argument like
|
|
127
|
+
* name: "--server=https://attacker" would otherwise redirect the API server
|
|
128
|
+
* and leak the operator's bearer token. Throws an McpError on any dangerous
|
|
129
|
+
* flag unless ALLOW_KUBECTL_UNSAFE_FLAGS=true.
|
|
130
|
+
*/
|
|
131
|
+
export function assertSafeArgv(args) {
|
|
132
|
+
if (isUnsafeFlagsAllowed())
|
|
133
|
+
return;
|
|
134
|
+
for (const tok of args) {
|
|
135
|
+
if (typeof tok !== "string")
|
|
136
|
+
continue;
|
|
137
|
+
if (!tok.startsWith("-"))
|
|
138
|
+
continue;
|
|
139
|
+
const name = normalizeFlagName(tok);
|
|
140
|
+
if (ARGV_DANGEROUS_FLAGS.has(name) || SHORT_ALIASES.has(name))
|
|
141
|
+
reject(tok);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Drop-in replacement for child_process.execFileSync that scans the argv for
|
|
146
|
+
* credential/target-redirecting flags before executing. Tool files import this
|
|
147
|
+
* as `execFileSync`, so every kubectl/helm call site is guarded at one place.
|
|
148
|
+
*/
|
|
149
|
+
export function execFileSyncSafe(file, args, options) {
|
|
150
|
+
assertSafeArgv(args);
|
|
151
|
+
return execFileSync(file, args, options);
|
|
152
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* interpretation. Shell operators (pipes, redirects, etc.) are intentionally
|
|
9
9
|
* not supported.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
12
12
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
13
13
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
14
14
|
import { contextParameter, namespaceParameter } from "../models/common-parameters.js";
|
|
@@ -84,7 +84,7 @@ export async function execInPod(k8sManager, input) {
|
|
|
84
84
|
}
|
|
85
85
|
args.push("--", ...input.command);
|
|
86
86
|
const timeoutMs = input.timeout || 60000;
|
|
87
|
-
const result =
|
|
87
|
+
const result = execFileSyncSafe("kubectl", args, {
|
|
88
88
|
encoding: "utf8",
|
|
89
89
|
maxBuffer: getSpawnMaxBuffer(),
|
|
90
90
|
timeout: timeoutMs,
|
|
@@ -4,7 +4,7 @@
|
|
|
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 {
|
|
7
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
8
8
|
import { writeFileSync, unlinkSync } from "fs";
|
|
9
9
|
import { dump } from "js-yaml";
|
|
10
10
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
@@ -142,7 +142,7 @@ export const uninstallHelmChartSchema = {
|
|
|
142
142
|
*/
|
|
143
143
|
const executeCommand = (command, args) => {
|
|
144
144
|
try {
|
|
145
|
-
return
|
|
145
|
+
return execFileSyncSafe(command, args, {
|
|
146
146
|
encoding: "utf8",
|
|
147
147
|
timeout: 300000, // 5 minutes timeout
|
|
148
148
|
maxBuffer: getSpawnMaxBuffer(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
@@ -73,7 +73,7 @@ export async function kubectlApply(k8sManager, input) {
|
|
|
73
73
|
}
|
|
74
74
|
// Execute the command
|
|
75
75
|
try {
|
|
76
|
-
const result =
|
|
76
|
+
const result = execFileSyncSafe(command, args, {
|
|
77
77
|
encoding: "utf8",
|
|
78
78
|
maxBuffer: getSpawnMaxBuffer(),
|
|
79
79
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
4
4
|
export const kubectlContextSchema = {
|
|
@@ -56,7 +56,7 @@ export async function kubectlContext(k8sManager, input) {
|
|
|
56
56
|
}
|
|
57
57
|
else if (output === "custom" || output === "json") {
|
|
58
58
|
// For custom or JSON output, we'll format it ourselves
|
|
59
|
-
const rawResult =
|
|
59
|
+
const rawResult = execFileSyncSafe(command, listArgs, {
|
|
60
60
|
encoding: "utf8",
|
|
61
61
|
maxBuffer: getSpawnMaxBuffer(),
|
|
62
62
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -110,7 +110,7 @@ export async function kubectlContext(k8sManager, input) {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
// Execute the command for non-json outputs
|
|
113
|
-
result =
|
|
113
|
+
result = execFileSyncSafe(command, listArgs, {
|
|
114
114
|
encoding: "utf8",
|
|
115
115
|
maxBuffer: getSpawnMaxBuffer(),
|
|
116
116
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -121,14 +121,14 @@ export async function kubectlContext(k8sManager, input) {
|
|
|
121
121
|
const getArgs = ["config", "current-context"];
|
|
122
122
|
// Execute the command
|
|
123
123
|
try {
|
|
124
|
-
const currentContext =
|
|
124
|
+
const currentContext = execFileSyncSafe(command, getArgs, {
|
|
125
125
|
encoding: "utf8",
|
|
126
126
|
maxBuffer: getSpawnMaxBuffer(),
|
|
127
127
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
128
128
|
}).trim();
|
|
129
129
|
if (detailed) {
|
|
130
130
|
// For detailed context info, we need to use get-contexts and filter
|
|
131
|
-
const allContextsOutput =
|
|
131
|
+
const allContextsOutput = execFileSyncSafe(command, ["config", "get-contexts"], {
|
|
132
132
|
encoding: "utf8",
|
|
133
133
|
maxBuffer: getSpawnMaxBuffer(),
|
|
134
134
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -211,7 +211,7 @@ export async function kubectlContext(k8sManager, input) {
|
|
|
211
211
|
}
|
|
212
212
|
// First check if the context exists
|
|
213
213
|
try {
|
|
214
|
-
const allContextsOutput =
|
|
214
|
+
const allContextsOutput = execFileSyncSafe(command, ["config", "get-contexts", "-o", "name"], {
|
|
215
215
|
encoding: "utf8",
|
|
216
216
|
maxBuffer: getSpawnMaxBuffer(),
|
|
217
217
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -233,7 +233,7 @@ export async function kubectlContext(k8sManager, input) {
|
|
|
233
233
|
// Build command to set context
|
|
234
234
|
const setArgs = ["config", "use-context", contextName];
|
|
235
235
|
// Execute the command
|
|
236
|
-
result =
|
|
236
|
+
result = execFileSyncSafe(command, setArgs, {
|
|
237
237
|
encoding: "utf8",
|
|
238
238
|
maxBuffer: getSpawnMaxBuffer(),
|
|
239
239
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
@@ -294,7 +294,7 @@ export async function kubectlCreate(k8sManager, input) {
|
|
|
294
294
|
}
|
|
295
295
|
// Execute the command
|
|
296
296
|
try {
|
|
297
|
-
const result =
|
|
297
|
+
const result = execFileSyncSafe(command, args, {
|
|
298
298
|
encoding: "utf8",
|
|
299
299
|
maxBuffer: getSpawnMaxBuffer(),
|
|
300
300
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
@@ -115,7 +115,7 @@ export async function kubectlDelete(k8sManager, input) {
|
|
|
115
115
|
}
|
|
116
116
|
// Execute the command
|
|
117
117
|
try {
|
|
118
|
-
const result =
|
|
118
|
+
const result = execFileSyncSafe(command, args, {
|
|
119
119
|
encoding: "utf8",
|
|
120
120
|
maxBuffer: getSpawnMaxBuffer(),
|
|
121
121
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
4
4
|
import { namespaceParameter, contextParameter, } from "../models/common-parameters.js";
|
|
@@ -52,7 +52,7 @@ export async function kubectlDescribe(k8sManager, input) {
|
|
|
52
52
|
}
|
|
53
53
|
// Execute the command
|
|
54
54
|
try {
|
|
55
|
-
const result =
|
|
55
|
+
const result = execFileSyncSafe(command, args, {
|
|
56
56
|
encoding: "utf8",
|
|
57
57
|
maxBuffer: getSpawnMaxBuffer(),
|
|
58
58
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
4
4
|
import { contextParameter, namespaceParameter, } from "../models/common-parameters.js";
|
|
@@ -101,7 +101,7 @@ export async function kubectlGeneric(k8sManager, input) {
|
|
|
101
101
|
// Execute the command
|
|
102
102
|
try {
|
|
103
103
|
console.error(`Executing: kubectl ${cmdArgs.join(" ")}`);
|
|
104
|
-
const result =
|
|
104
|
+
const result = execFileSyncSafe(command, cmdArgs, {
|
|
105
105
|
encoding: "utf8",
|
|
106
106
|
maxBuffer: getSpawnMaxBuffer(),
|
|
107
107
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
4
4
|
import * as yaml from "js-yaml";
|
|
@@ -121,7 +121,7 @@ export async function kubectlGet(k8sManager, input) {
|
|
|
121
121
|
}
|
|
122
122
|
// Execute the command
|
|
123
123
|
try {
|
|
124
|
-
const result =
|
|
124
|
+
const result = execFileSyncSafe(command, args, {
|
|
125
125
|
encoding: "utf8",
|
|
126
126
|
maxBuffer: getSpawnMaxBuffer(),
|
|
127
127
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
4
4
|
import { contextParameter, namespaceParameter, } from "../models/common-parameters.js";
|
|
@@ -84,7 +84,7 @@ export async function kubectlLogs(k8sManager, input) {
|
|
|
84
84
|
}
|
|
85
85
|
// Execute the command
|
|
86
86
|
try {
|
|
87
|
-
const result =
|
|
87
|
+
const result = execFileSyncSafe(command, args, {
|
|
88
88
|
encoding: "utf8",
|
|
89
89
|
maxBuffer: getSpawnMaxBuffer(),
|
|
90
90
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -127,7 +127,7 @@ export async function kubectlLogs(k8sManager, input) {
|
|
|
127
127
|
"jsonpath='{.items[*].metadata.name}'",
|
|
128
128
|
];
|
|
129
129
|
try {
|
|
130
|
-
const jobs =
|
|
130
|
+
const jobs = execFileSyncSafe(command, jobsArgs, {
|
|
131
131
|
encoding: "utf8",
|
|
132
132
|
maxBuffer: getSpawnMaxBuffer(),
|
|
133
133
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -177,7 +177,7 @@ export async function kubectlLogs(k8sManager, input) {
|
|
|
177
177
|
if (!selectorArgs) {
|
|
178
178
|
throw new Error("Selector command is undefined");
|
|
179
179
|
}
|
|
180
|
-
const selectorJson =
|
|
180
|
+
const selectorJson = execFileSyncSafe(command, selectorArgs, {
|
|
181
181
|
encoding: "utf8",
|
|
182
182
|
maxBuffer: getSpawnMaxBuffer(),
|
|
183
183
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -260,7 +260,7 @@ async function getLabelSelectorLogs(labelSelector, namespace, input) {
|
|
|
260
260
|
"-o",
|
|
261
261
|
"jsonpath='{.items[*].metadata.name}'",
|
|
262
262
|
];
|
|
263
|
-
const pods =
|
|
263
|
+
const pods = execFileSyncSafe(command, podsArgs, {
|
|
264
264
|
encoding: "utf8",
|
|
265
265
|
maxBuffer: getSpawnMaxBuffer(),
|
|
266
266
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -293,7 +293,7 @@ async function getLabelSelectorLogs(labelSelector, namespace, input) {
|
|
|
293
293
|
// Add other options
|
|
294
294
|
podArgs = addLogOptions(podArgs, input);
|
|
295
295
|
try {
|
|
296
|
-
const logs =
|
|
296
|
+
const logs = execFileSyncSafe(command, podArgs, {
|
|
297
297
|
encoding: "utf8",
|
|
298
298
|
maxBuffer: getSpawnMaxBuffer(),
|
|
299
299
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
3
3
|
import { contextParameter } from "../models/common-parameters.js";
|
|
4
4
|
export const explainResourceSchema = {
|
|
@@ -74,7 +74,7 @@ export const listApiResourcesSchema = {
|
|
|
74
74
|
};
|
|
75
75
|
const executeKubectlCommand = (command, args) => {
|
|
76
76
|
try {
|
|
77
|
-
return
|
|
77
|
+
return execFileSyncSafe(command, args, {
|
|
78
78
|
encoding: "utf8",
|
|
79
79
|
maxBuffer: getSpawnMaxBuffer(),
|
|
80
80
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
@@ -93,7 +93,7 @@ export async function kubectlPatch(k8sManager, input) {
|
|
|
93
93
|
}
|
|
94
94
|
// Execute the command
|
|
95
95
|
try {
|
|
96
|
-
const result =
|
|
96
|
+
const result = execFileSyncSafe(command, args, {
|
|
97
97
|
encoding: "utf8",
|
|
98
98
|
maxBuffer: getSpawnMaxBuffer(),
|
|
99
99
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
4
4
|
import { contextParameter, namespaceParameter } from "../models/common-parameters.js";
|
|
@@ -87,7 +87,7 @@ export async function kubectlRollout(k8sManager, input) {
|
|
|
87
87
|
args.push("--watch");
|
|
88
88
|
// For watch we are limited in what we can do - we'll execute it with a reasonable timeout
|
|
89
89
|
// and capture the output until that point
|
|
90
|
-
const result =
|
|
90
|
+
const result = execFileSyncSafe(command, args, {
|
|
91
91
|
encoding: "utf8",
|
|
92
92
|
maxBuffer: getSpawnMaxBuffer(),
|
|
93
93
|
timeout: 15000, // Reduced from 30 seconds to 15 seconds
|
|
@@ -104,7 +104,7 @@ export async function kubectlRollout(k8sManager, input) {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
else {
|
|
107
|
-
const result =
|
|
107
|
+
const result = execFileSyncSafe(command, args, {
|
|
108
108
|
encoding: "utf8",
|
|
109
109
|
maxBuffer: getSpawnMaxBuffer(),
|
|
110
110
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
2
2
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
4
4
|
import { contextParameter, namespaceParameter } from "../models/common-parameters.js";
|
|
@@ -49,7 +49,7 @@ export async function kubectlScale(k8sManager, input) {
|
|
|
49
49
|
}
|
|
50
50
|
// Execute the command
|
|
51
51
|
try {
|
|
52
|
-
const result =
|
|
52
|
+
const result = execFileSyncSafe(command, args, {
|
|
53
53
|
encoding: "utf8",
|
|
54
54
|
maxBuffer: getSpawnMaxBuffer(),
|
|
55
55
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and confirmation requirements for destructive operations.
|
|
6
6
|
* Note: Use kubectl_get with resourceType="nodes" to list nodes.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { execFileSyncSafe } from "../security/kubectl-flags.js";
|
|
9
9
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
10
10
|
/**
|
|
11
11
|
* Schema for node_management tool.
|
|
@@ -85,7 +85,7 @@ export const nodeManagementSchema = {
|
|
|
85
85
|
*/
|
|
86
86
|
const executeCommand = (command, args) => {
|
|
87
87
|
try {
|
|
88
|
-
return
|
|
88
|
+
return execFileSyncSafe(command, args, {
|
|
89
89
|
encoding: "utf8",
|
|
90
90
|
timeout: 300000, // 5 minutes timeout for node operations
|
|
91
91
|
maxBuffer: getSpawnMaxBuffer(),
|
|
@@ -1,22 +1,63 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
3
|
import { createAuthMiddleware, isAuthEnabled } from "./auth.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build the default allowedHosts list for DNS rebinding protection.
|
|
6
|
+
* Includes localhost variants with and without port.
|
|
7
|
+
*/
|
|
8
|
+
function buildDefaultAllowedHosts(host, port) {
|
|
9
|
+
// Always allow the bare host and host:port for common localhost addresses
|
|
10
|
+
const localhostAliases = ["127.0.0.1", "localhost", "::1"];
|
|
11
|
+
const hosts = [];
|
|
12
|
+
for (const alias of localhostAliases) {
|
|
13
|
+
hosts.push(alias);
|
|
14
|
+
// HTTP Host header uses bracket notation for IPv6: [::1]:3000
|
|
15
|
+
if (alias.includes(":")) {
|
|
16
|
+
hosts.push(`[${alias}]`);
|
|
17
|
+
hosts.push(`[${alias}]:${port}`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
hosts.push(`${alias}:${port}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Also add the configured host if it's not already covered
|
|
24
|
+
if (!localhostAliases.includes(host)) {
|
|
25
|
+
hosts.push(host);
|
|
26
|
+
if (host.includes(":") && !host.startsWith("[")) {
|
|
27
|
+
hosts.push(`[${host}]`);
|
|
28
|
+
hosts.push(`[${host}]:${port}`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
hosts.push(`${host}:${port}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return hosts;
|
|
35
|
+
}
|
|
4
36
|
export function startStreamableHTTPServer(server) {
|
|
5
37
|
const app = express();
|
|
6
38
|
app.use(express.json());
|
|
7
39
|
// Create auth middleware - when MCP_AUTH_TOKEN is set, requires X-MCP-AUTH header
|
|
8
40
|
const authMiddleware = createAuthMiddleware();
|
|
41
|
+
// DNS rebinding protection is enabled by default. Set DNS_REBINDING_PROTECTION=false to disable.
|
|
42
|
+
const enableDnsRebindingProtection = process.env.DNS_REBINDING_PROTECTION !== "false";
|
|
43
|
+
const host = process.env.HOST || "localhost";
|
|
44
|
+
const parsed = parseInt(process.env.PORT || "3000", 10);
|
|
45
|
+
const port = Number.isNaN(parsed) ? 3000 : parsed;
|
|
46
|
+
const allowedHosts = process.env.DNS_REBINDING_ALLOWED_HOST
|
|
47
|
+
? [process.env.DNS_REBINDING_ALLOWED_HOST]
|
|
48
|
+
: buildDefaultAllowedHosts(host, port);
|
|
49
|
+
// Warn when binding to all interfaces with DNS rebinding protection disabled
|
|
50
|
+
if (!enableDnsRebindingProtection && (host === "0.0.0.0" || host === "::")) {
|
|
51
|
+
console.warn("WARNING: DNS rebinding protection is disabled while HOST is set to " +
|
|
52
|
+
`'${host}'. This exposes the MCP server to DNS rebinding attacks ` +
|
|
53
|
+
"from any browser on the network. Set DNS_REBINDING_PROTECTION=true " +
|
|
54
|
+
"(the default) or restrict HOST to 'localhost' / '127.0.0.1'.");
|
|
55
|
+
}
|
|
9
56
|
app.post("/mcp", authMiddleware, async (req, res) => {
|
|
10
57
|
// In stateless mode, create a new instance of transport and server for each request
|
|
11
58
|
// to ensure complete isolation. A single instance would cause request ID collisions
|
|
12
59
|
// when multiple clients connect concurrently.
|
|
13
60
|
try {
|
|
14
|
-
// DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server
|
|
15
|
-
// locally, make sure to set DNS_REBINDING_PROTECTION=true
|
|
16
|
-
const enableDnsRebindingProtection = process.env.DNS_REBINDING_PROTECTION === "true";
|
|
17
|
-
const allowedHosts = process.env.DNS_REBINDING_ALLOWED_HOST
|
|
18
|
-
? [process.env.DNS_REBINDING_ALLOWED_HOST]
|
|
19
|
-
: ["127.0.0.1"];
|
|
20
61
|
const transport = new StreamableHTTPServerTransport({
|
|
21
62
|
sessionIdGenerator: undefined,
|
|
22
63
|
enableDnsRebindingProtection,
|
|
@@ -91,14 +132,6 @@ export function startStreamableHTTPServer(server) {
|
|
|
91
132
|
});
|
|
92
133
|
}
|
|
93
134
|
});
|
|
94
|
-
let port = 3000;
|
|
95
|
-
try {
|
|
96
|
-
port = parseInt(process.env.PORT || "3000", 10);
|
|
97
|
-
}
|
|
98
|
-
catch (e) {
|
|
99
|
-
console.error("Invalid PORT environment variable, using default port 3000.");
|
|
100
|
-
}
|
|
101
|
-
const host = process.env.HOST || "localhost";
|
|
102
135
|
const httpServer = app.listen(port, host, () => {
|
|
103
136
|
console.log(`mcp-kubernetes-server is listening on port ${port}\nUse the following url to connect to the server:\nhttp://${host}:${port}/mcp`);
|
|
104
137
|
if (isAuthEnabled()) {
|