mcp-server-kubernetes 0.1.2 → 0.1.4

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
@@ -35,19 +35,16 @@ If you have errors, open up a standard terminal and run `kubectl get pods` to se
35
35
  - [x] List all pods
36
36
  - [x] List all services
37
37
  - [x] List all deployments
38
+ - [x] List all nodes
38
39
  - [x] Create a pod
39
40
  - [x] Delete a pod
40
41
  - [x] Describe a pod
41
42
  - [x] List all namespaces
43
+ - [x] Get logs from a pod for debugging (supports pods, deployments, jobs, and label selectors)
42
44
  - [ ] Port forward to a pod
43
- - [ ] Get logs from a pod for debugging
44
45
  - [ ] Choose namespace for next commands (memory)
45
46
  - [ ] Support Helm for installing charts
46
47
 
47
- ## In Progress
48
-
49
- - [ ] [Docker support](https://github.com/Flux159/mcp-server-kubernetes/pull/9)
50
-
51
48
  ## Local Development
52
49
 
53
50
  ```bash
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: {
@@ -313,6 +308,63 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
313
308
  properties: {},
314
309
  },
315
310
  },
311
+ {
312
+ name: "list_nodes",
313
+ description: "List all nodes in the cluster",
314
+ inputSchema: {
315
+ type: "object",
316
+ properties: {},
317
+ },
318
+ },
319
+ {
320
+ name: "get_logs",
321
+ description: "Get logs from pods, deployments, jobs, or resources matching a label selector",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {
325
+ resourceType: {
326
+ type: "string",
327
+ enum: ["pod", "deployment", "job"],
328
+ description: "Type of resource to get logs from",
329
+ },
330
+ name: {
331
+ type: "string",
332
+ description: "Name of the resource",
333
+ },
334
+ namespace: {
335
+ type: "string",
336
+ description: "Namespace of the resource",
337
+ default: "default",
338
+ },
339
+ labelSelector: {
340
+ type: "string",
341
+ description: "Label selector to filter resources",
342
+ optional: true,
343
+ },
344
+ container: {
345
+ type: "string",
346
+ description: "Container name (required when pod has multiple containers)",
347
+ optional: true,
348
+ },
349
+ tail: {
350
+ type: "number",
351
+ description: "Number of lines to show from end of logs",
352
+ optional: true,
353
+ },
354
+ since: {
355
+ type: "number",
356
+ description: "Get logs since relative time in seconds",
357
+ optional: true,
358
+ },
359
+ timestamps: {
360
+ type: "boolean",
361
+ description: "Include timestamps in logs",
362
+ default: false,
363
+ },
364
+ },
365
+ required: ["resourceType"],
366
+ },
367
+ },
316
368
  ],
317
369
  };
318
370
  });
@@ -404,9 +456,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
404
456
  };
405
457
  }
406
458
  case "create_pod": {
407
- console.error("calling create_pod");
408
- console.error(input);
409
- console.error(request);
410
459
  const createPodInput = input;
411
460
  const templateConfig = containerTemplates[createPodInput.template];
412
461
  const pod = {
@@ -426,21 +475,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
426
475
  ...templateConfig,
427
476
  ...(createPodInput.command && {
428
477
  command: createPodInput.command,
478
+ args: undefined, // Clear default args when command is overridden
429
479
  }),
430
480
  },
431
481
  ],
432
482
  },
433
483
  };
434
- const { body } = await k8sManager
484
+ const response = await k8sManager
435
485
  .getCoreApi()
436
- .createNamespacedPod(createPodInput.namespace, pod);
486
+ .createNamespacedPod(createPodInput.namespace, pod)
487
+ .catch((error) => {
488
+ console.error("Pod creation error:", {
489
+ status: error.response?.statusCode,
490
+ message: error.response?.body?.message || error.message,
491
+ details: error.response?.body,
492
+ });
493
+ throw error;
494
+ });
437
495
  k8sManager.trackResource("Pod", createPodInput.name, createPodInput.namespace);
438
496
  return {
439
497
  content: [
440
498
  {
441
499
  type: "text",
442
500
  text: JSON.stringify({
443
- podName: body.metadata.name,
501
+ podName: response.body.metadata.name,
444
502
  status: "created",
445
503
  }, null, 2),
446
504
  },
@@ -496,7 +554,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
496
554
  type: "text",
497
555
  text: JSON.stringify({
498
556
  error: "Pod not found",
499
- status: "not_found"
557
+ status: "not_found",
500
558
  }, null, 2),
501
559
  },
502
560
  ],
@@ -513,7 +571,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
513
571
  labels: body.metadata?.labels,
514
572
  },
515
573
  spec: {
516
- containers: body.spec?.containers.map(container => ({
574
+ containers: body.spec?.containers.map((container) => ({
517
575
  name: container.name,
518
576
  image: container.image,
519
577
  ports: container.ports,
@@ -525,7 +583,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
525
583
  phase: body.status?.phase,
526
584
  conditions: body.status?.conditions,
527
585
  containerStatuses: body.status?.containerStatuses,
528
- }
586
+ },
529
587
  };
530
588
  return {
531
589
  content: [
@@ -544,7 +602,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
544
602
  type: "text",
545
603
  text: JSON.stringify({
546
604
  error: "Pod not found",
547
- status: "not_found"
605
+ status: "not_found",
548
606
  }, null, 2),
549
607
  },
550
608
  ],
@@ -567,6 +625,119 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
567
625
  ],
568
626
  };
569
627
  }
628
+ case "list_nodes": {
629
+ const { body } = await k8sManager.getCoreApi().listNode();
630
+ return {
631
+ content: [
632
+ {
633
+ type: "text",
634
+ text: JSON.stringify({
635
+ nodes: body.items,
636
+ }, null, 2),
637
+ },
638
+ ],
639
+ };
640
+ }
641
+ case "get_logs": {
642
+ const { resourceType, name, namespace = "default", labelSelector, container, tail = 100, sinceSeconds, timestamps, pretty = true, follow = false, } = input;
643
+ async function getPodLogs(podName, podNamespace) {
644
+ try {
645
+ const { body } = await k8sManager.getCoreApi().readNamespacedPodLog(podName, podNamespace, container, follow, undefined, // insecureSkipTLSVerifyBackend
646
+ undefined, // limitBytes
647
+ pretty ? "true" : "false", undefined, // previous
648
+ sinceSeconds, tail, timestamps);
649
+ return body;
650
+ }
651
+ catch (error) {
652
+ if (error.response?.statusCode === 404) {
653
+ throw new McpError(ErrorCode.InvalidRequest, `Pod ${podName} not found in namespace ${podNamespace}`);
654
+ }
655
+ // Log full error details
656
+ console.error("Full error:", {
657
+ statusCode: error.response?.statusCode,
658
+ message: error.response?.body?.message || error.message,
659
+ details: error.response?.body,
660
+ });
661
+ throw new McpError(ErrorCode.InternalError, `Failed to get logs for pod ${podName}: ${error.response?.body?.message || error.message}`);
662
+ }
663
+ }
664
+ const logs = {};
665
+ try {
666
+ // Get logs based on resource type
667
+ switch (resourceType.toLowerCase()) {
668
+ case "pod": {
669
+ if (!name) {
670
+ throw new McpError(ErrorCode.InvalidRequest, "Pod name is required when resourceType is 'pod'");
671
+ }
672
+ logs[name] = await getPodLogs(name, namespace);
673
+ break;
674
+ }
675
+ case "deployment": {
676
+ if (!name) {
677
+ throw new McpError(ErrorCode.InvalidRequest, "Deployment name is required when resourceType is 'deployment'");
678
+ }
679
+ const { body: deployment } = await k8sManager
680
+ .getAppsApi()
681
+ .readNamespacedDeployment(name, namespace);
682
+ if (!deployment.spec?.selector?.matchLabels) {
683
+ throw new McpError(ErrorCode.InvalidRequest, `Deployment ${name} has no selector`);
684
+ }
685
+ const selector = Object.entries(deployment.spec.selector.matchLabels)
686
+ .map(([key, value]) => `${key}=${value}`)
687
+ .join(",");
688
+ const { body: podList } = await k8sManager
689
+ .getCoreApi()
690
+ .listNamespacedPod(namespace, undefined, undefined, undefined, undefined, selector);
691
+ for (const pod of podList.items) {
692
+ if (pod.metadata?.name) {
693
+ logs[pod.metadata.name] = await getPodLogs(pod.metadata.name, namespace);
694
+ }
695
+ }
696
+ break;
697
+ }
698
+ case "job": {
699
+ if (!name) {
700
+ throw new McpError(ErrorCode.InvalidRequest, "Job name is required when resourceType is 'job'");
701
+ }
702
+ const { body: podList } = await k8sManager
703
+ .getCoreApi()
704
+ .listNamespacedPod(namespace, undefined, undefined, undefined, undefined, `job-name=${name}`);
705
+ for (const pod of podList.items) {
706
+ if (pod.metadata?.name) {
707
+ logs[pod.metadata.name] = await getPodLogs(pod.metadata.name, namespace);
708
+ }
709
+ }
710
+ break;
711
+ }
712
+ default:
713
+ throw new McpError(ErrorCode.InvalidRequest, `Unsupported resource type: ${resourceType}`);
714
+ }
715
+ // If labelSelector is provided, filter or add logs by label
716
+ if (labelSelector) {
717
+ const { body: labeledPods } = await k8sManager
718
+ .getCoreApi()
719
+ .listNamespacedPod(namespace, undefined, undefined, undefined, undefined, labelSelector);
720
+ for (const pod of labeledPods.items) {
721
+ if (pod.metadata?.name) {
722
+ logs[pod.metadata.name] = await getPodLogs(pod.metadata.name, namespace);
723
+ }
724
+ }
725
+ }
726
+ return {
727
+ content: [
728
+ {
729
+ type: "text",
730
+ text: JSON.stringify({ logs }, null, 2),
731
+ },
732
+ ],
733
+ };
734
+ }
735
+ catch (error) {
736
+ if (error instanceof McpError)
737
+ throw error;
738
+ throw new McpError(ErrorCode.InternalError, `Failed to get logs: ${error}`);
739
+ }
740
+ }
570
741
  default:
571
742
  throw new McpError(ErrorCode.InvalidRequest, `Unknown tool: ${name}`);
572
743
  }
@@ -605,6 +776,12 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
605
776
  mimeType: "application/json",
606
777
  description: "List of all namespaces",
607
778
  },
779
+ {
780
+ uri: "k8s://nodes",
781
+ name: "Kubernetes Nodes",
782
+ mimeType: "application/json",
783
+ description: "List of all nodes in the cluster",
784
+ },
608
785
  ],
609
786
  };
610
787
  });
@@ -612,8 +789,11 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
612
789
  try {
613
790
  const uri = request.params.uri;
614
791
  const parts = uri.replace("k8s://", "").split("/");
615
- if (parts[0] === "namespaces" && parts.length === 1) {
616
- const { body } = await k8sManager.getCoreApi().listNamespace();
792
+ const isNamespaces = parts[0] === "namespaces";
793
+ const isNodes = parts[0] === "nodes";
794
+ if ((isNamespaces || isNodes) && parts.length === 1) {
795
+ const fn = isNodes ? "listNode" : "listNamespace";
796
+ const { body } = await k8sManager.getCoreApi()[fn]();
617
797
  return {
618
798
  contents: [
619
799
  {
package/dist/types.d.ts CHANGED
@@ -229,6 +229,50 @@ 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
+ }>;
254
+ export declare const GetLogsResponseSchema: z.ZodObject<{
255
+ content: z.ZodArray<z.ZodObject<{
256
+ type: z.ZodLiteral<"text">;
257
+ text: z.ZodString;
258
+ }, "strip", z.ZodTypeAny, {
259
+ type: "text";
260
+ text: string;
261
+ }, {
262
+ type: "text";
263
+ text: string;
264
+ }>, "many">;
265
+ }, "strip", z.ZodTypeAny, {
266
+ content: {
267
+ type: "text";
268
+ text: string;
269
+ }[];
270
+ }, {
271
+ content: {
272
+ type: "text";
273
+ text: string;
274
+ }[];
275
+ }>;
232
276
  export declare const ListResourcesResponseSchema: z.ZodObject<{
233
277
  resources: z.ZodArray<z.ZodObject<{
234
278
  uri: z.ZodString;
package/dist/types.js CHANGED
@@ -70,6 +70,18 @@ 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
+ });
79
+ export const GetLogsResponseSchema = z.object({
80
+ content: z.array(z.object({
81
+ type: z.literal("text"),
82
+ text: z.string(),
83
+ })),
84
+ });
73
85
  export const ListResourcesResponseSchema = z.object({
74
86
  resources: z.array(ResourceSchema),
75
87
  });
package/dist/unit.test.js CHANGED
@@ -1,24 +1,79 @@
1
- import { expect, test } from "vitest";
1
+ // Import required test frameworks and SDK components
2
+ import { expect, test, describe, beforeEach, afterEach, } from "vitest";
2
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
4
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
- import { ListToolsResponseSchema, ListPodsResponseSchema, ListDeploymentsResponseSchema, ListNamespacesResponseSchema, CreatePodResponseSchema, DeletePodResponseSchema, CleanupResponseSchema, } from "./types.js";
5
+ import { ListToolsResponseSchema, ListPodsResponseSchema, ListNamespacesResponseSchema, ListNodesResponseSchema, CreatePodResponseSchema, DeletePodResponseSchema, } from "./types.js";
6
+ /**
7
+ * Utility function to create a promise that resolves after specified milliseconds
8
+ * Useful for waiting between operations or ensuring async operations complete
9
+ */
5
10
  async function sleep(ms) {
6
11
  return new Promise((resolve) => setTimeout(resolve, ms));
7
12
  }
8
- test("kubernetes server operations", async () => {
9
- const transport = new StdioClientTransport({
10
- command: "bun",
11
- args: ["src/index.ts"],
12
- stderr: "pipe",
13
+ /**
14
+ * Generates a random SHA-like string for unique resource naming
15
+ * Used to avoid naming conflicts when creating test resources
16
+ */
17
+ function generateRandomSHA() {
18
+ return Math.random().toString(36).substring(2, 15);
19
+ }
20
+ /**
21
+ * Test suite for kubernetes server operations
22
+ * Tests the core functionality of kubernetes operations including:
23
+ * - Listing available tools
24
+ * - Namespace and node operations
25
+ * - Pod lifecycle management (create, monitor, delete)
26
+ */
27
+ describe("kubernetes server operations", () => {
28
+ let transport;
29
+ let client;
30
+ /**
31
+ * Set up before each test:
32
+ * - Creates a new StdioClientTransport instance
33
+ * - Initializes and connects the MCP client
34
+ * - Waits for connection to be established
35
+ */
36
+ beforeEach(async () => {
37
+ try {
38
+ transport = new StdioClientTransport({
39
+ command: "bun",
40
+ args: ["src/index.ts"],
41
+ stderr: "pipe",
42
+ });
43
+ client = new Client({
44
+ name: "test-client",
45
+ version: "1.0.0",
46
+ }, {
47
+ capabilities: {},
48
+ });
49
+ await client.connect(transport);
50
+ // Wait for connection to be fully established
51
+ await sleep(1000);
52
+ }
53
+ catch (e) {
54
+ console.error("Error in beforeEach:", e);
55
+ throw e;
56
+ }
13
57
  });
14
- const client = new Client({
15
- name: "test-client",
16
- version: "1.0.0",
17
- }, {
18
- capabilities: {},
58
+ /**
59
+ * Clean up after each test:
60
+ * - Closes the transport connection
61
+ * - Waits to ensure clean shutdown
62
+ */
63
+ afterEach(async () => {
64
+ try {
65
+ await transport.close();
66
+ await sleep(1000);
67
+ }
68
+ catch (e) {
69
+ console.error("Error during cleanup:", e);
70
+ }
19
71
  });
20
- await client.connect(transport);
21
- try {
72
+ /**
73
+ * Test case: Verify the availability of kubernetes tools
74
+ * Ensures that the server exposes the expected kubernetes operations
75
+ */
76
+ test("list available tools", async () => {
22
77
  // List available tools stays the same
23
78
  console.log("Listing available tools...");
24
79
  const toolsList = await client.request({
@@ -26,134 +81,209 @@ test("kubernetes server operations", async () => {
26
81
  }, ListToolsResponseSchema);
27
82
  expect(toolsList.tools).toBeDefined();
28
83
  expect(toolsList.tools.length).toBeGreaterThan(0);
84
+ });
85
+ /**
86
+ * Test case: Verify namespace and node listing functionality
87
+ * Tests both namespace and node listing operations in sequence
88
+ */
89
+ test("list namespaces and nodes", async () => {
29
90
  // List namespaces
30
91
  console.log("Listing namespaces...");
31
92
  const namespacesResult = await client.request({
32
93
  method: "tools/call",
33
94
  params: {
34
95
  name: "list_namespaces",
35
- arguments: {}, // Changed from input to arguments
96
+ arguments: {},
36
97
  },
37
98
  }, ListNamespacesResponseSchema);
38
99
  expect(namespacesResult.content[0].type).toBe("text");
39
100
  const namespaces = JSON.parse(namespacesResult.content[0].text);
40
101
  expect(namespaces.namespaces).toBeDefined();
41
- // Delete test pod if it exists
42
- console.log("Deleting test pod if exists...");
43
- const deletePodResult = await client.request({
44
- method: "tools/call",
45
- params: {
46
- name: "delete_pod",
47
- arguments: {
48
- // Changed from input to arguments
49
- name: "test-pod",
50
- namespace: "default",
51
- ignoreNotFound: true,
52
- },
53
- },
54
- }, DeletePodResponseSchema);
55
- expect(deletePodResult.content[0].type).toBe("text");
56
- const deleteResult = JSON.parse(deletePodResult.content[0].text);
57
- expect(deleteResult.success).toBe(true);
58
- await sleep(2000);
59
- // Create a pod
60
- console.log("Creating test pod...");
61
- const createPodResult = await client.request({
62
- method: "tools/call",
63
- params: {
64
- name: "create_pod",
65
- arguments: {
66
- // Changed from input to arguments
67
- name: "test-pod",
68
- namespace: "default",
69
- template: "nginx",
70
- },
71
- },
72
- }, CreatePodResponseSchema);
73
- expect(createPodResult.content[0].type).toBe("text");
74
- const createResult = JSON.parse(createPodResult.content[0].text);
75
- expect(createResult.podName).toBe("test-pod");
76
- expect(createResult.status).toBe("created");
77
- // Describe the pod
78
- console.log("Describing test pod...");
79
- const describePodResult = await client.request({
102
+ // List nodes
103
+ console.log("Listing nodes...");
104
+ const listNodesResult = await client.request({
80
105
  method: "tools/call",
81
106
  params: {
82
- name: "describe_pod",
83
- arguments: {
84
- name: "test-pod",
85
- namespace: "default",
86
- },
107
+ name: "list_nodes",
108
+ arguments: {},
87
109
  },
88
- }, CreatePodResponseSchema // Reusing existing schema since response format is similar
89
- );
90
- expect(describePodResult.content[0].type).toBe("text");
91
- const podDescription = JSON.parse(describePodResult.content[0].text);
92
- expect(podDescription.metadata.name).toBe("test-pod");
93
- expect(podDescription.metadata.namespace).toBe("default");
94
- expect(podDescription.kind).toBe("Pod");
95
- // List pods to verify creation
96
- console.log("Listing pods...");
97
- const listPodsResult = await client.request({
110
+ }, ListNodesResponseSchema);
111
+ expect(listNodesResult.content[0].type).toBe("text");
112
+ const nodes = JSON.parse(listNodesResult.content[0].text);
113
+ expect(nodes.nodes).toBeDefined();
114
+ expect(Array.isArray(nodes.nodes)).toBe(true);
115
+ });
116
+ /**
117
+ * Test case: Complete pod lifecycle management
118
+ * Tests the full lifecycle of a pod including:
119
+ * 1. Cleanup of existing test pods
120
+ * 2. Creation of new test pod
121
+ * 3. Monitoring pod until running state
122
+ * 4. Verification of pod logs
123
+ * 5. Pod deletion and termination verification
124
+ *
125
+ * Note: Test timeout is set to 120 seconds to accommodate all operations via vitest.config.ts
126
+ */
127
+ test("pod lifecycle management", async () => {
128
+ const podBaseName = "unit-test";
129
+ const podName = `${podBaseName}-${generateRandomSHA()}`;
130
+ // Step 1: Check if pods with unit-test prefix exist and terminate them if found
131
+ const existingPods = await client.request({
98
132
  method: "tools/call",
99
133
  params: {
100
134
  name: "list_pods",
101
135
  arguments: {
102
- // Changed from input to arguments
103
136
  namespace: "default",
104
137
  },
105
138
  },
106
139
  }, ListPodsResponseSchema);
107
- expect(listPodsResult.content[0].type).toBe("text");
108
- const pods = JSON.parse(listPodsResult.content[0].text);
109
- expect(pods.pods).toBeDefined();
110
- expect(pods.pods.some((pod) => pod.name === "test-pod")).toBe(true);
111
- // List deployments
112
- console.log("Listing deployments...");
113
- const listDeploymentsResult = await client.request({
140
+ const podsResponse = JSON.parse(existingPods.content[0].text);
141
+ const existingTestPods = podsResponse.items?.filter((pod) => pod.metadata?.name?.startsWith(podBaseName)) || [];
142
+ // Terminate existing test pods if found
143
+ for (const pod of existingTestPods) {
144
+ await client.request({
145
+ method: "tools/call",
146
+ params: {
147
+ name: "delete_pod",
148
+ arguments: {
149
+ name: pod.metadata.name,
150
+ namespace: "default",
151
+ ignoreNotFound: true,
152
+ },
153
+ },
154
+ }, DeletePodResponseSchema);
155
+ // Wait for pod to be fully terminated
156
+ let podDeleted = false;
157
+ const terminationStartTime = Date.now();
158
+ while (!podDeleted && Date.now() - terminationStartTime < 10000) {
159
+ try {
160
+ await client.request({
161
+ method: "tools/call",
162
+ params: {
163
+ name: "describe_pod",
164
+ arguments: {
165
+ name: pod.metadata.name,
166
+ namespace: "default",
167
+ },
168
+ },
169
+ }, ListPodsResponseSchema);
170
+ await sleep(500);
171
+ }
172
+ catch (error) {
173
+ // If we get an error, it might be because the pod is gone (404)
174
+ podDeleted = true;
175
+ }
176
+ }
177
+ }
178
+ // Create new pod with random SHA name
179
+ const createPodResult = await client.request({
114
180
  method: "tools/call",
115
181
  params: {
116
- name: "list_deployments",
182
+ name: "create_pod",
117
183
  arguments: {
118
- // Changed from input to arguments
184
+ name: podName,
119
185
  namespace: "default",
186
+ template: "busybox",
187
+ command: ["/bin/sh", "-c", "echo Pod is running && sleep infinity"],
120
188
  },
121
189
  },
122
- }, ListDeploymentsResponseSchema);
123
- expect(listDeploymentsResult.content[0].type).toBe("text");
124
- const deployments = JSON.parse(listDeploymentsResult.content[0].text);
125
- expect(deployments.deployments).toBeDefined();
126
- // Cleanup
127
- console.log("Cleaning up...");
128
- const cleanupResult = await client.request({
129
- method: "tools/call",
130
- params: {
131
- name: "cleanup",
132
- arguments: {}, // Changed from input to arguments
133
- },
134
- }, CleanupResponseSchema);
135
- expect(cleanupResult.content[0].type).toBe("text");
136
- const cleanupData = JSON.parse(cleanupResult.content[0].text);
137
- expect(cleanupData.success).toBe(true);
138
- // Verify cleanup by listing pods again
139
- console.log("Verifying cleanup...");
140
- const finalPodsResult = await client.request({
190
+ }, CreatePodResponseSchema);
191
+ expect(createPodResult.content[0].type).toBe("text");
192
+ const podResult = JSON.parse(createPodResult.content[0].text);
193
+ expect(podResult.podName).toBe(podName);
194
+ // Step 2: Wait for Running state (up to 60 seconds)
195
+ let podRunning = false;
196
+ const startTime = Date.now();
197
+ while (!podRunning && Date.now() - startTime < 60000) {
198
+ const podStatus = await client.request({
199
+ method: "tools/call",
200
+ params: {
201
+ name: "describe_pod",
202
+ arguments: {
203
+ name: podName,
204
+ namespace: "default",
205
+ },
206
+ },
207
+ }, ListPodsResponseSchema);
208
+ const status = JSON.parse(podStatus.content[0].text);
209
+ if (status.status?.phase === "Running") {
210
+ podRunning = true;
211
+ console.log(`Pod ${podName} is running. Checking logs...`);
212
+ // Check pod logs once running
213
+ const logsResult = await client.request({
214
+ method: "tools/call",
215
+ params: {
216
+ name: "get_logs",
217
+ arguments: {
218
+ resourceType: "pod",
219
+ name: podName,
220
+ namespace: "default",
221
+ },
222
+ },
223
+ }, ListPodsResponseSchema);
224
+ expect(logsResult.content[0].type).toBe("text");
225
+ const logs = JSON.parse(logsResult.content[0].text);
226
+ expect(logs.logs[podName]).toContain("Pod is running");
227
+ break;
228
+ }
229
+ await sleep(1000);
230
+ }
231
+ expect(podRunning).toBe(true);
232
+ // Step 3: Terminate pod and verify termination (wait up to 10 seconds)
233
+ const deletePodResult = await client.request({
141
234
  method: "tools/call",
142
235
  params: {
143
- name: "list_pods",
236
+ name: "delete_pod",
144
237
  arguments: {
145
- // Changed from input to arguments
238
+ name: podName,
146
239
  namespace: "default",
147
240
  },
148
241
  },
149
- }, ListPodsResponseSchema);
150
- const finalPods = JSON.parse(finalPodsResult.content[0].text);
151
- console.log(finalPods);
152
- // expect(finalPods.pods.some((pod: any) => pod.name === "test-pod")).toBe(
153
- // false
154
- // );
155
- }
156
- finally {
157
- // await client.disconnect(); // Re-enabled client disconnect
158
- }
242
+ }, DeletePodResponseSchema);
243
+ expect(deletePodResult.content[0].type).toBe("text");
244
+ const deleteResult = JSON.parse(deletePodResult.content[0].text);
245
+ expect(deleteResult.status).toBe("deleted");
246
+ // Try to verify pod termination, but don't fail the test if we can't confirm it
247
+ try {
248
+ let podTerminated = false;
249
+ const terminationStartTime = Date.now();
250
+ while (!podTerminated && Date.now() - terminationStartTime < 10000) {
251
+ try {
252
+ const podStatus = await client.request({
253
+ method: "tools/call",
254
+ params: {
255
+ name: "describe_pod",
256
+ arguments: {
257
+ name: podName,
258
+ namespace: "default",
259
+ },
260
+ },
261
+ }, ListPodsResponseSchema);
262
+ // Pod still exists, check if it's in Terminating state
263
+ const status = JSON.parse(podStatus.content[0].text);
264
+ if (status.status?.phase === "Terminating") {
265
+ podTerminated = true;
266
+ break;
267
+ }
268
+ await sleep(500);
269
+ }
270
+ catch (error) {
271
+ // If we get an error (404), the pod is gone which also means it's terminated
272
+ podTerminated = true;
273
+ break;
274
+ }
275
+ }
276
+ // Log termination status but don't fail the test
277
+ if (podTerminated) {
278
+ console.log(`Pod ${podName} termination confirmed`);
279
+ }
280
+ else {
281
+ console.log(`Pod ${podName} termination could not be confirmed within timeout, but deletion was initiated`);
282
+ }
283
+ }
284
+ catch (error) {
285
+ // Ignore any errors during termination check
286
+ console.log(`Error checking pod termination status: ${error}`);
287
+ }
288
+ });
159
289
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-kubernetes",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,8 +19,9 @@
19
19
  "build": "tsc && shx chmod +x dist/*.js",
20
20
  "dev": "tsc --watch",
21
21
  "start": "node dist/index.js",
22
- "test": "vitest --testTimeout=20000",
23
- "prepublishOnly": "npm run build"
22
+ "test": "vitest run",
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",