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.
- package/dist/tools/kubectl-get.js +86 -5
- package/dist/utils/auth.d.ts +19 -0
- package/dist/utils/auth.js +53 -0
- package/dist/utils/sse.js +10 -4
- package/dist/utils/streamable-http.js +9 -3
- package/package.json +2 -2
|
@@ -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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
//
|
|
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:\
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|