mcp-server-kubernetes 0.1.1 → 0.1.3

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 CHANGED
@@ -4,6 +4,8 @@ MCP Server that can connect to a Kubernetes cluster and manage it.
4
4
 
5
5
  https://github.com/user-attachments/assets/f25f8f4e-4d04-479b-9ae0-5dac452dd2ed
6
6
 
7
+ <a href="https://glama.ai/mcp/servers/w71ieamqrt"><img width="380" height="200" src="https://glama.ai/mcp/servers/w71ieamqrt/badge" /></a>
8
+
7
9
  ## Usage with Claude Desktop
8
10
 
9
11
  ```json
@@ -33,23 +35,115 @@ If you have errors, open up a standard terminal and run `kubectl get pods` to se
33
35
  - [x] List all pods
34
36
  - [x] List all services
35
37
  - [x] List all deployments
38
+ - [x] List all nodes
36
39
  - [x] Create a pod
37
40
  - [x] Delete a pod
41
+ - [x] Describe a pod
38
42
  - [x] List all namespaces
39
- - [] Port forward to a pod
40
- - [] Get logs from a pod for debugging
41
- - [] Choose namespace for next commands (memory)
42
- - [] Support Helm for installing charts
43
+ - [ ] Port forward to a pod
44
+ - [ ] Get logs from a pod for debugging
45
+ - [ ] Choose namespace for next commands (memory)
46
+ - [ ] Support Helm for installing charts
47
+
48
+ ## In Progress
49
+
50
+ - [ ] [Docker support](https://github.com/Flux159/mcp-server-kubernetes/pull/9)
43
51
 
44
- ## Development & Testing
52
+ ## Local Development
45
53
 
46
54
  ```bash
47
55
  git clone https://github.com/Flux159/mcp-server-kubernetes.git
48
56
  cd mcp-server-kubernetes
49
57
  bun install
58
+ ```
59
+
60
+ ### Development Workflow
61
+
62
+ 1. Start the server in development mode (watches for file changes):
63
+
64
+ ```bash
65
+ bun run dev
66
+ ```
67
+
68
+ 2. Run unit tests:
69
+
70
+ ```bash
50
71
  bun run test
51
72
  ```
52
73
 
74
+ 3. Build the project:
75
+
76
+ ```bash
77
+ bun run build
78
+ ```
79
+
80
+ 4. Local Testing with [Inspector](https://github.com/modelcontextprotocol/inspector)
81
+
82
+ ```bash
83
+ npx @modelcontextprotocol/inspector node build/index.js
84
+ # Follow further instructions on terminal for Inspector link
85
+ ```
86
+
87
+ ### Project Structure
88
+
89
+ ```
90
+ src/
91
+ ├── index.ts # Main server implementation
92
+ ├── types.ts # TypeScript type definitions
93
+ └── unit.test.ts # Unit tests
94
+ ```
95
+
96
+ ### Contributing
97
+
98
+ 1. Fork the repository
99
+ 2. Create a feature branch
100
+ 3. Make your changes
101
+ 4. Add tests for new functionality
102
+ 5. Ensure all tests pass
103
+ 6. Submit a pull request
104
+
105
+ For bigger changes, please open an issue first to discuss the proposed changes.
106
+
107
+ ## Architecture
108
+
109
+ This section describes the high-level architecture of the MCP Kubernetes server.
110
+
111
+ ### Request Flow
112
+
113
+ The sequence diagram below illustrates how requests flow through the system:
114
+
115
+ ```mermaid
116
+ sequenceDiagram
117
+ participant Client
118
+ participant Transport as StdioTransport
119
+ participant Server as MCP Server
120
+ participant Handler as Request Handler
121
+ participant K8sManager as KubernetesManager
122
+ participant K8s as Kubernetes API
123
+
124
+ Client->>Transport: Send Request via STDIO
125
+ Transport->>Server: Forward Request
126
+
127
+ alt Tools Request
128
+ Server->>Handler: Route to tools handler
129
+ Handler->>K8sManager: Execute tool operation
130
+ K8sManager->>K8s: Make API call
131
+ K8s-->>K8sManager: Return result
132
+ K8sManager-->>Handler: Process response
133
+ Handler-->>Server: Return tool result
134
+ else Resource Request
135
+ Server->>Handler: Route to resource handler
136
+ Handler->>K8sManager: Get resource data
137
+ K8sManager->>K8s: Query API
138
+ K8s-->>K8sManager: Return data
139
+ K8sManager-->>Handler: Format response
140
+ Handler-->>Server: Return resource data
141
+ end
142
+
143
+ Server-->>Transport: Send Response
144
+ Transport-->>Client: Return Final Response
145
+ ```
146
+
53
147
  ## Not planned
54
148
 
55
149
  Authentication / adding clusters to kubectx.
package/dist/index.js CHANGED
@@ -19,15 +19,6 @@ class KubernetesManager {
19
19
  // process.on("SIGTERM", () => this.cleanup());
20
20
  }
21
21
  async cleanup() {
22
- console.log("Cleaning up resources...");
23
- // Stop port forwards
24
- // for (const pf of this.portForwards) {
25
- // try {
26
- // await pf.server.stop();
27
- // } catch (error) {
28
- // console.error(`Failed to close port-forward ${pf.id}:`, error);
29
- // }
30
- // }
31
22
  // Stop watches
32
23
  for (const watch of this.watches) {
33
24
  watch.abort.abort();
@@ -41,6 +32,7 @@ class KubernetesManager {
41
32
  console.error(`Failed to delete ${resource.kind} ${resource.name}:`, error);
42
33
  }
43
34
  }
35
+ // TODO: Cleanup port forwards when implemented
44
36
  }
45
37
  trackResource(kind, name, namespace) {
46
38
  this.resources.push({ kind, name, namespace, createdAt: new Date() });
@@ -83,6 +75,7 @@ class KubernetesManager {
83
75
  }
84
76
  const k8sManager = new KubernetesManager();
85
77
  // Template configurations with health checks and resource limits
78
+ // TODO: Update create_pod to accept custom images and custom template files
86
79
  const containerTemplates = {
87
80
  ubuntu: {
88
81
  name: "main",
@@ -238,6 +231,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
238
231
  },
239
232
  },
240
233
  {
234
+ // TODO: Add support for custom images and templates (see above in containerTemplates definition)
241
235
  name: "create_pod",
242
236
  description: "Create a new Kubernetes pod",
243
237
  inputSchema: {
@@ -259,6 +253,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
259
253
  },
260
254
  },
261
255
  {
256
+ // TODO: Support for custom deployments (see above)
262
257
  name: "create_deployment",
263
258
  description: "Create a new Kubernetes deployment",
264
259
  inputSchema: {
@@ -293,6 +288,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
293
288
  required: ["name", "namespace"],
294
289
  },
295
290
  },
291
+ {
292
+ name: "describe_pod",
293
+ description: "Describe a Kubernetes pod (read details like status, containers, etc.)",
294
+ inputSchema: {
295
+ type: "object",
296
+ properties: {
297
+ name: { type: "string" },
298
+ namespace: { type: "string" },
299
+ },
300
+ required: ["name", "namespace"],
301
+ },
302
+ },
296
303
  {
297
304
  name: "cleanup",
298
305
  description: "Cleanup all managed resources",
@@ -301,6 +308,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
301
308
  properties: {},
302
309
  },
303
310
  },
311
+ {
312
+ name: "list_nodes",
313
+ description: "List all nodes in the cluster",
314
+ inputSchema: {
315
+ type: "object",
316
+ properties: {},
317
+ },
318
+ },
304
319
  ],
305
320
  };
306
321
  });
@@ -392,9 +407,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
392
407
  };
393
408
  }
394
409
  case "create_pod": {
395
- console.error("calling create_pod");
396
- console.error(input);
397
- console.error(request);
398
410
  const createPodInput = input;
399
411
  const templateConfig = containerTemplates[createPodInput.template];
400
412
  const pod = {
@@ -471,6 +483,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
471
483
  throw error;
472
484
  }
473
485
  }
486
+ case "describe_pod": {
487
+ const describePodInput = input;
488
+ try {
489
+ const { body } = await k8sManager
490
+ .getCoreApi()
491
+ .readNamespacedPod(describePodInput.name, describePodInput.namespace);
492
+ if (!body) {
493
+ return {
494
+ content: [
495
+ {
496
+ type: "text",
497
+ text: JSON.stringify({
498
+ error: "Pod not found",
499
+ status: "not_found",
500
+ }, null, 2),
501
+ },
502
+ ],
503
+ isError: true,
504
+ };
505
+ }
506
+ // Format the pod details for better readability
507
+ const podDetails = {
508
+ kind: body.kind,
509
+ metadata: {
510
+ name: body.metadata?.name,
511
+ namespace: body.metadata?.namespace,
512
+ creationTimestamp: body.metadata?.creationTimestamp,
513
+ labels: body.metadata?.labels,
514
+ },
515
+ spec: {
516
+ containers: body.spec?.containers.map((container) => ({
517
+ name: container.name,
518
+ image: container.image,
519
+ ports: container.ports,
520
+ resources: container.resources,
521
+ })),
522
+ nodeName: body.spec?.nodeName,
523
+ },
524
+ status: {
525
+ phase: body.status?.phase,
526
+ conditions: body.status?.conditions,
527
+ containerStatuses: body.status?.containerStatuses,
528
+ },
529
+ };
530
+ return {
531
+ content: [
532
+ {
533
+ type: "text",
534
+ text: JSON.stringify(podDetails, null, 2),
535
+ },
536
+ ],
537
+ };
538
+ }
539
+ catch (error) {
540
+ if (error.response?.statusCode === 404) {
541
+ return {
542
+ content: [
543
+ {
544
+ type: "text",
545
+ text: JSON.stringify({
546
+ error: "Pod not found",
547
+ status: "not_found",
548
+ }, null, 2),
549
+ },
550
+ ],
551
+ isError: true,
552
+ };
553
+ }
554
+ throw new McpError(ErrorCode.InternalError, `Failed to describe pod: ${error.response?.body?.message || error.message}`);
555
+ }
556
+ }
474
557
  case "cleanup": {
475
558
  await k8sManager.cleanup();
476
559
  return {
@@ -484,6 +567,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
484
567
  ],
485
568
  };
486
569
  }
570
+ case "list_nodes": {
571
+ const { body } = await k8sManager.getCoreApi().listNode();
572
+ return {
573
+ content: [
574
+ {
575
+ type: "text",
576
+ text: JSON.stringify({
577
+ nodes: body.items,
578
+ }, null, 2),
579
+ },
580
+ ],
581
+ };
582
+ }
487
583
  default:
488
584
  throw new McpError(ErrorCode.InvalidRequest, `Unknown tool: ${name}`);
489
585
  }
@@ -522,6 +618,12 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
522
618
  mimeType: "application/json",
523
619
  description: "List of all namespaces",
524
620
  },
621
+ {
622
+ uri: "k8s://nodes",
623
+ name: "Kubernetes Nodes",
624
+ mimeType: "application/json",
625
+ description: "List of all nodes in the cluster",
626
+ },
525
627
  ],
526
628
  };
527
629
  });
@@ -529,8 +631,11 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
529
631
  try {
530
632
  const uri = request.params.uri;
531
633
  const parts = uri.replace("k8s://", "").split("/");
532
- if (parts[0] === "namespaces" && parts.length === 1) {
533
- const { body } = await k8sManager.getCoreApi().listNamespace();
634
+ const isNamespaces = parts[0] === "namespaces";
635
+ const isNodes = parts[0] === "nodes";
636
+ if ((isNamespaces || isNodes) && parts.length === 1) {
637
+ const fn = isNodes ? "listNode" : "listNamespace";
638
+ const { body } = await k8sManager.getCoreApi()[fn]();
534
639
  return {
535
640
  contents: [
536
641
  {
@@ -597,3 +702,10 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
597
702
  });
598
703
  const transport = new StdioServerTransport();
599
704
  await server.connect(transport);
705
+ ["SIGINT", "SIGTERM"].forEach((signal) => {
706
+ process.on(signal, async () => {
707
+ console.log(`Received ${signal}, shutting down...`);
708
+ await server.close();
709
+ process.exit(0);
710
+ });
711
+ });
package/dist/types.d.ts CHANGED
@@ -229,6 +229,28 @@ export declare const ListNamespacesResponseSchema: z.ZodObject<{
229
229
  text: string;
230
230
  }[];
231
231
  }>;
232
+ export declare const ListNodesResponseSchema: z.ZodObject<{
233
+ content: z.ZodArray<z.ZodObject<{
234
+ type: z.ZodLiteral<"text">;
235
+ text: z.ZodString;
236
+ }, "strip", z.ZodTypeAny, {
237
+ type: "text";
238
+ text: string;
239
+ }, {
240
+ type: "text";
241
+ text: string;
242
+ }>, "many">;
243
+ }, "strip", z.ZodTypeAny, {
244
+ content: {
245
+ type: "text";
246
+ text: string;
247
+ }[];
248
+ }, {
249
+ content: {
250
+ type: "text";
251
+ text: string;
252
+ }[];
253
+ }>;
232
254
  export declare const ListResourcesResponseSchema: z.ZodObject<{
233
255
  resources: z.ZodArray<z.ZodObject<{
234
256
  uri: z.ZodString;
package/dist/types.js CHANGED
@@ -70,6 +70,12 @@ export const ListNamespacesResponseSchema = z.object({
70
70
  text: z.string(),
71
71
  })),
72
72
  });
73
+ export const ListNodesResponseSchema = z.object({
74
+ content: z.array(z.object({
75
+ type: z.literal("text"),
76
+ text: z.string(),
77
+ })),
78
+ });
73
79
  export const ListResourcesResponseSchema = z.object({
74
80
  resources: z.array(ResourceSchema),
75
81
  });
package/dist/unit.test.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { expect, test } from "vitest";
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
- import { ListToolsResponseSchema, ListPodsResponseSchema, ListDeploymentsResponseSchema, ListNamespacesResponseSchema, CreatePodResponseSchema, DeletePodResponseSchema, CleanupResponseSchema, } from "./types.js";
4
+ import { ListToolsResponseSchema, ListPodsResponseSchema, ListDeploymentsResponseSchema, ListNamespacesResponseSchema, ListNodesResponseSchema, CreatePodResponseSchema, DeletePodResponseSchema, CleanupResponseSchema, } from "./types.js";
5
5
  async function sleep(ms) {
6
6
  return new Promise((resolve) => setTimeout(resolve, ms));
7
7
  }
@@ -38,6 +38,19 @@ test("kubernetes server operations", async () => {
38
38
  expect(namespacesResult.content[0].type).toBe("text");
39
39
  const namespaces = JSON.parse(namespacesResult.content[0].text);
40
40
  expect(namespaces.namespaces).toBeDefined();
41
+ // List nodes
42
+ console.log("Listing nodes...");
43
+ const listNodesResult = await client.request({
44
+ method: "tools/call",
45
+ params: {
46
+ name: "list_nodes",
47
+ arguments: {},
48
+ },
49
+ }, ListNodesResponseSchema);
50
+ expect(listNodesResult.content[0].type).toBe("text");
51
+ const nodes = JSON.parse(listNodesResult.content[0].text);
52
+ expect(nodes.nodes).toBeDefined();
53
+ expect(Array.isArray(nodes.nodes)).toBe(true);
41
54
  // Delete test pod if it exists
42
55
  console.log("Deleting test pod if exists...");
43
56
  const deletePodResult = await client.request({
@@ -74,6 +87,24 @@ test("kubernetes server operations", async () => {
74
87
  const createResult = JSON.parse(createPodResult.content[0].text);
75
88
  expect(createResult.podName).toBe("test-pod");
76
89
  expect(createResult.status).toBe("created");
90
+ // Describe the pod
91
+ console.log("Describing test pod...");
92
+ const describePodResult = await client.request({
93
+ method: "tools/call",
94
+ params: {
95
+ name: "describe_pod",
96
+ arguments: {
97
+ name: "test-pod",
98
+ namespace: "default",
99
+ },
100
+ },
101
+ }, CreatePodResponseSchema // Reusing existing schema since response format is similar
102
+ );
103
+ expect(describePodResult.content[0].type).toBe("text");
104
+ const podDescription = JSON.parse(describePodResult.content[0].text);
105
+ expect(podDescription.metadata.name).toBe("test-pod");
106
+ expect(podDescription.metadata.namespace).toBe("default");
107
+ expect(podDescription.kind).toBe("Pod");
77
108
  // List pods to verify creation
78
109
  console.log("Listing pods...");
79
110
  const listPodsResult = await client.request({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-kubernetes",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,8 @@
20
20
  "dev": "tsc --watch",
21
21
  "start": "node dist/index.js",
22
22
  "test": "vitest --testTimeout=20000",
23
- "prepublishOnly": "npm run build"
23
+ "prepublishOnly": "npm run build",
24
+ "dockerbuild": "docker buildx build -t flux159/mcp-server-kubernetes --platform linux/amd64,linux/arm64 --push ."
24
25
  },
25
26
  "keywords": [
26
27
  "mcp",
@@ -41,6 +42,6 @@
41
42
  "@types/node": "^22.9.3",
42
43
  "shx": "^0.3.4",
43
44
  "typescript": "^5.6.2",
44
- "vitest": "2.1.8"
45
+ "vitest": "2.1.9"
45
46
  }
46
47
  }