mcp-server-kubernetes 3.1.1 → 3.2.1

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.
@@ -168,7 +168,7 @@ export async function kubectlGet(k8sManager, input) {
168
168
  name: item.metadata?.name || "",
169
169
  namespace: item.metadata?.namespace || "",
170
170
  kind: item.kind || resourceType,
171
- status: getResourceStatus(item),
171
+ status: getResourceStatus(item, resourceType),
172
172
  createdAt: item.metadata?.creationTimestamp,
173
173
  }));
174
174
  return {
@@ -218,13 +218,94 @@ export async function kubectlGet(k8sManager, input) {
218
218
  throw new McpError(ErrorCode.InternalError, `Failed to execute kubectl get command: ${error.message}`);
219
219
  }
220
220
  }
221
+ // Compute pod status the same way kubectl does, by inspecting container statuses
222
+ // Based on kubernetes/pkg/printers/internalversion/printers.go printPod()
223
+ function getPodStatus(pod) {
224
+ let reason = pod.status?.phase || "Unknown";
225
+ if (pod.status?.reason) {
226
+ reason = pod.status.reason;
227
+ }
228
+ // Check init container statuses
229
+ const initContainerStatuses = pod.status?.initContainerStatuses || [];
230
+ for (let i = 0; i < initContainerStatuses.length; i++) {
231
+ const container = initContainerStatuses[i];
232
+ const terminated = container.state?.terminated;
233
+ const waiting = container.state?.waiting;
234
+ if (terminated && terminated.exitCode === 0) {
235
+ // Init container completed successfully, continue
236
+ continue;
237
+ }
238
+ if (terminated) {
239
+ if (terminated.reason) {
240
+ reason = `Init:${terminated.reason}`;
241
+ }
242
+ else if (terminated.signal) {
243
+ reason = `Init:Signal:${terminated.signal}`;
244
+ }
245
+ else {
246
+ reason = `Init:ExitCode:${terminated.exitCode}`;
247
+ }
248
+ }
249
+ else if (waiting && waiting.reason && waiting.reason !== "PodInitializing") {
250
+ reason = `Init:${waiting.reason}`;
251
+ }
252
+ else {
253
+ const totalInit = initContainerStatuses.length;
254
+ reason = `Init:${i}/${totalInit}`;
255
+ }
256
+ break;
257
+ }
258
+ // If all init containers are done, check regular container statuses
259
+ if (initContainerStatuses.length === 0 ||
260
+ !reason.startsWith("Init:")) {
261
+ const containerStatuses = pod.status?.containerStatuses || [];
262
+ let hasRunning = false;
263
+ for (let i = containerStatuses.length - 1; i >= 0; i--) {
264
+ const container = containerStatuses[i];
265
+ const waiting = container.state?.waiting;
266
+ const terminated = container.state?.terminated;
267
+ if (waiting && waiting.reason) {
268
+ reason = waiting.reason;
269
+ }
270
+ else if (terminated) {
271
+ if (terminated.reason) {
272
+ reason = terminated.reason;
273
+ }
274
+ else if (terminated.signal) {
275
+ reason = `Signal:${terminated.signal}`;
276
+ }
277
+ else {
278
+ reason = `ExitCode:${terminated.exitCode}`;
279
+ }
280
+ }
281
+ else if (container.ready && container.state?.running) {
282
+ hasRunning = true;
283
+ }
284
+ }
285
+ // If all containers are ready and running, use the phase
286
+ if (hasRunning && reason === (pod.status?.phase || "Unknown")) {
287
+ reason = pod.status?.phase || "Running";
288
+ }
289
+ }
290
+ // Handle pod deletion
291
+ if (pod.metadata?.deletionTimestamp) {
292
+ reason = "Terminating";
293
+ }
294
+ return reason;
295
+ }
221
296
  // Extract status from various resource types
222
- function getResourceStatus(resource) {
297
+ function getResourceStatus(resource, resourceType) {
223
298
  if (!resource)
224
299
  return "Unknown";
225
- // Pod status
226
- if (resource.status?.phase) {
227
- return resource.status.phase;
300
+ const isPod = resource.kind === "Pod" ||
301
+ resourceType === "pods" ||
302
+ resourceType === "pod" ||
303
+ resourceType === "po" ||
304
+ (resource.status?.phase !== undefined &&
305
+ resource.status?.containerStatuses !== undefined);
306
+ // Pod status - use kubectl-equivalent logic
307
+ if (isPod) {
308
+ return getPodStatus(resource);
228
309
  }
229
310
  // Deployment, ReplicaSet, StatefulSet status
230
311
  if (resource.status?.readyReplicas !== undefined) {
@@ -0,0 +1,19 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ /**
3
+ * Authentication middleware for MCP HTTP transports.
4
+ *
5
+ * When the MCP_AUTH_TOKEN environment variable is set, this middleware
6
+ * requires all requests to include a matching X-MCP-AUTH header.
7
+ *
8
+ * This provides a simple authentication mechanism for securing MCP endpoints
9
+ * in cluster environments where full OAuth may be overkill.
10
+ *
11
+ * Example usage:
12
+ * Server: MCP_AUTH_TOKEN=my-secret-token
13
+ * Client: X-MCP-AUTH: my-secret-token
14
+ */
15
+ export declare function createAuthMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
16
+ /**
17
+ * Returns whether authentication is enabled (MCP_AUTH_TOKEN is set)
18
+ */
19
+ export declare function isAuthEnabled(): boolean;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Authentication middleware for MCP HTTP transports.
3
+ *
4
+ * When the MCP_AUTH_TOKEN environment variable is set, this middleware
5
+ * requires all requests to include a matching X-MCP-AUTH header.
6
+ *
7
+ * This provides a simple authentication mechanism for securing MCP endpoints
8
+ * in cluster environments where full OAuth may be overkill.
9
+ *
10
+ * Example usage:
11
+ * Server: MCP_AUTH_TOKEN=my-secret-token
12
+ * Client: X-MCP-AUTH: my-secret-token
13
+ */
14
+ export function createAuthMiddleware() {
15
+ const authToken = process.env.MCP_AUTH_TOKEN;
16
+ return (req, res, next) => {
17
+ // If no auth token is configured, allow all requests
18
+ if (!authToken) {
19
+ next();
20
+ return;
21
+ }
22
+ const providedToken = req.headers["x-mcp-auth"];
23
+ if (!providedToken) {
24
+ res.status(401).json({
25
+ jsonrpc: "2.0",
26
+ error: {
27
+ code: -32001,
28
+ message: "Unauthorized: X-MCP-AUTH header is required",
29
+ },
30
+ id: null,
31
+ });
32
+ return;
33
+ }
34
+ if (providedToken !== authToken) {
35
+ res.status(403).json({
36
+ jsonrpc: "2.0",
37
+ error: {
38
+ code: -32002,
39
+ message: "Forbidden: Invalid authentication token",
40
+ },
41
+ id: null,
42
+ });
43
+ return;
44
+ }
45
+ next();
46
+ };
47
+ }
48
+ /**
49
+ * Returns whether authentication is enabled (MCP_AUTH_TOKEN is set)
50
+ */
51
+ export function isAuthEnabled() {
52
+ return !!process.env.MCP_AUTH_TOKEN;
53
+ }
package/dist/utils/sse.js CHANGED
@@ -1,16 +1,19 @@
1
1
  import express from "express";
2
2
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
3
+ import { createAuthMiddleware, isAuthEnabled } from "./auth.js";
3
4
  export function startSSEServer(server) {
4
5
  const app = express();
6
+ // Create auth middleware - when MCP_AUTH_TOKEN is set, requires X-MCP-AUTH header
7
+ const authMiddleware = createAuthMiddleware();
5
8
  // Currently just copying from docs & allowing for multiple transport connections: https://modelcontextprotocol.io/docs/concepts/transports#server-sent-events-sse
6
- // TODO: If exposed to web, then this will enable any client to connect to the server via http - so marked as UNSAFE until mcp has a proper auth solution.
9
+ // Note: When MCP_AUTH_TOKEN is set, requests require X-MCP-AUTH header for authentication
7
10
  let transports = [];
8
- app.get("/sse", async (req, res) => {
11
+ app.get("/sse", authMiddleware, async (req, res) => {
9
12
  const transport = new SSEServerTransport("/messages", res);
10
13
  transports.push(transport);
11
14
  await server.connect(transport);
12
15
  });
13
- app.post("/messages", (req, res) => {
16
+ app.post("/messages", authMiddleware, (req, res) => {
14
17
  const transport = transports.find((t) => t.sessionId === req.query.sessionId);
15
18
  if (transport) {
16
19
  transport.handlePostMessage(req, res);
@@ -51,6 +54,9 @@ export function startSSEServer(server) {
51
54
  }
52
55
  const host = process.env.HOST || "localhost";
53
56
  app.listen(port, host, () => {
54
- console.log(`mcp-kubernetes-server is listening on port ${port}\nUse the following url to connect to the server:\n\http://${host}:${port}/sse`);
57
+ console.log(`mcp-kubernetes-server is listening on port ${port}\nUse the following url to connect to the server:\nhttp://${host}:${port}/sse`);
58
+ if (isAuthEnabled()) {
59
+ console.log("Authentication enabled: X-MCP-AUTH header required for all MCP requests");
60
+ }
55
61
  });
56
62
  }
@@ -1,9 +1,12 @@
1
1
  import express from "express";
2
2
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import { createAuthMiddleware, isAuthEnabled } from "./auth.js";
3
4
  export function startStreamableHTTPServer(server) {
4
5
  const app = express();
5
6
  app.use(express.json());
6
- app.post("/mcp", async (req, res) => {
7
+ // Create auth middleware - when MCP_AUTH_TOKEN is set, requires X-MCP-AUTH header
8
+ const authMiddleware = createAuthMiddleware();
9
+ app.post("/mcp", authMiddleware, async (req, res) => {
7
10
  // In stateless mode, create a new instance of transport and server for each request
8
11
  // to ensure complete isolation. A single instance would cause request ID collisions
9
12
  // when multiple clients connect concurrently.
@@ -44,7 +47,7 @@ export function startStreamableHTTPServer(server) {
44
47
  }
45
48
  });
46
49
  // SSE notifications not supported in stateless mode
47
- app.get("/mcp", async (req, res) => {
50
+ app.get("/mcp", authMiddleware, async (req, res) => {
48
51
  console.log("Received GET MCP request");
49
52
  res.writeHead(405).end(JSON.stringify({
50
53
  jsonrpc: "2.0",
@@ -56,7 +59,7 @@ export function startStreamableHTTPServer(server) {
56
59
  }));
57
60
  });
58
61
  // Session termination not needed in stateless mode
59
- app.delete("/mcp", async (req, res) => {
62
+ app.delete("/mcp", authMiddleware, async (req, res) => {
60
63
  console.log("Received DELETE MCP request");
61
64
  res.writeHead(405).end(JSON.stringify({
62
65
  jsonrpc: "2.0",
@@ -98,6 +101,9 @@ export function startStreamableHTTPServer(server) {
98
101
  const host = process.env.HOST || "localhost";
99
102
  const httpServer = app.listen(port, host, () => {
100
103
  console.log(`mcp-kubernetes-server is listening on port ${port}\nUse the following url to connect to the server:\nhttp://${host}:${port}/mcp`);
104
+ if (isAuthEnabled()) {
105
+ console.log("Authentication enabled: X-MCP-AUTH header required for all MCP requests");
106
+ }
101
107
  });
102
108
  return httpServer;
103
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-kubernetes",
3
- "version": "3.1.1",
3
+ "version": "3.2.1",
4
4
  "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@kubernetes/client-node": "1.3.0",
41
- "@modelcontextprotocol/sdk": "1.25.2",
41
+ "@modelcontextprotocol/sdk": "1.26.0",
42
42
  "express": "4.21.2",
43
43
  "js-yaml": "4.1.1",
44
44
  "yaml": "2.7.0",