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.
@@ -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 { execFileSync } from "child_process";
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 = execFileSync("kubectl", args, {
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 { execFileSync } from "child_process";
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 execFileSync(command, args, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, args, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, listArgs, {
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 = execFileSync(command, listArgs, {
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 = execFileSync(command, getArgs, {
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 = execFileSync(command, ["config", "get-contexts"], {
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 = execFileSync(command, ["config", "get-contexts", "-o", "name"], {
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 = execFileSync(command, setArgs, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, args, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, args, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, args, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, cmdArgs, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, args, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, args, {
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 = execFileSync(command, jobsArgs, {
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 = execFileSync(command, selectorArgs, {
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 = execFileSync(command, podsArgs, {
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 = execFileSync(command, podArgs, {
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 { execFileSync } from "child_process";
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 execFileSync(command, args, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, args, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, args, {
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 = execFileSync(command, args, {
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 { execFileSync } from "child_process";
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 = execFileSync(command, args, {
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 { execFileSync } from "child_process";
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 execFileSync(command, args, {
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()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-kubernetes",
3
- "version": "3.7.0",
3
+ "version": "3.9.0",
4
4
  "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
5
  "license": "MIT",
6
6
  "type": "module",