mcp-server-kubernetes 0.1.4 → 0.1.5

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
@@ -24,6 +24,7 @@ The server will automatically connect to your current kubectl context. Make sure
24
24
  1. kubectl installed and in your PATH
25
25
  2. A valid kubeconfig file with contexts configured
26
26
  3. Access to a Kubernetes cluster configured for kubectl (e.g. minikube, Rancher Desktop, GKE, etc.)
27
+ 4. Helm v3 installed and in your PATH (no Tiller required)
27
28
 
28
29
  You can verify your connection by asking Claude to list your pods or create a test deployment.
29
30
 
@@ -41,9 +42,15 @@ If you have errors, open up a standard terminal and run `kubectl get pods` to se
41
42
  - [x] Describe a pod
42
43
  - [x] List all namespaces
43
44
  - [x] Get logs from a pod for debugging (supports pods, deployments, jobs, and label selectors)
45
+ - [x] Support Helm v3 for installing charts
46
+ - Install charts with custom values
47
+ - Uninstall releases
48
+ - Upgrade existing releases
49
+ - Support for namespaces
50
+ - Support for version specification
51
+ - Support for custom repositories
44
52
  - [ ] Port forward to a pod
45
53
  - [ ] Choose namespace for next commands (memory)
46
- - [ ] Support Helm for installing charts
47
54
 
48
55
  ## Local Development
49
56
 
@@ -86,6 +93,7 @@ npx @modelcontextprotocol/inspector node build/index.js
86
93
  src/
87
94
  ├── index.ts # Main server implementation
88
95
  ├── types.ts # TypeScript type definitions
96
+ ├── helm.test.ts # Helm chart installation tests
89
97
  └── unit.test.ts # Unit tests
90
98
  ```
91
99
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,208 @@
1
+ import { expect, test, describe, beforeEach, afterEach } from "vitest";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { HelmResponseSchema } from "./types.js";
5
+ import * as fs from "fs";
6
+ async function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ describe("helm operations", () => {
10
+ let transport;
11
+ let client;
12
+ const testReleaseName = "test-nginx";
13
+ const testNamespace = "default";
14
+ beforeEach(async () => {
15
+ try {
16
+ transport = new StdioClientTransport({
17
+ command: "bun",
18
+ args: ["src/index.ts"],
19
+ stderr: "pipe",
20
+ });
21
+ client = new Client({
22
+ name: "test-client",
23
+ version: "1.0.0",
24
+ }, {
25
+ capabilities: {},
26
+ });
27
+ await client.connect(transport);
28
+ await sleep(1000);
29
+ }
30
+ catch (e) {
31
+ console.error("Error in beforeEach:", e);
32
+ throw e;
33
+ }
34
+ });
35
+ afterEach(async () => {
36
+ try {
37
+ // Cleanup: Uninstall the test release if it exists
38
+ await client
39
+ .request({
40
+ method: "tools/call",
41
+ params: {
42
+ name: "uninstall_helm_chart",
43
+ arguments: {
44
+ name: testReleaseName,
45
+ namespace: testNamespace,
46
+ },
47
+ },
48
+ }, HelmResponseSchema)
49
+ .catch(() => { }); // Ignore errors if release doesn't exist
50
+ await transport.close();
51
+ await sleep(1000);
52
+ // Cleanup generated values files
53
+ if (fs.existsSync("test-nginx-values.yaml")) {
54
+ fs.unlinkSync("test-nginx-values.yaml");
55
+ }
56
+ }
57
+ catch (e) {
58
+ console.error("Error during cleanup:", e);
59
+ }
60
+ });
61
+ test("install helm chart", async () => {
62
+ const installResult = await client.request({
63
+ method: "tools/call",
64
+ params: {
65
+ name: "install_helm_chart",
66
+ arguments: {
67
+ name: testReleaseName,
68
+ chart: "nginx",
69
+ repo: "https://charts.bitnami.com/bitnami",
70
+ namespace: testNamespace,
71
+ values: {
72
+ service: {
73
+ type: "ClusterIP",
74
+ },
75
+ resources: {
76
+ limits: {
77
+ cpu: "100m",
78
+ memory: "128Mi",
79
+ },
80
+ requests: {
81
+ cpu: "50m",
82
+ memory: "64Mi",
83
+ },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ }, HelmResponseSchema);
89
+ expect(installResult.content[0].type).toBe("text");
90
+ const result = JSON.parse(installResult.content[0].text);
91
+ expect(result.status).toBe("installed");
92
+ // Wait for the deployment to be ready
93
+ await sleep(5000);
94
+ // Verify the deployment exists
95
+ const deploymentResult = await client.request({
96
+ method: "tools/call",
97
+ params: {
98
+ name: "list_deployments",
99
+ arguments: {
100
+ namespace: testNamespace,
101
+ },
102
+ },
103
+ }, HelmResponseSchema);
104
+ const deployments = JSON.parse(deploymentResult.content[0].text);
105
+ expect(deployments.deployments.some((d) => d.name.startsWith(testReleaseName))).toBe(true);
106
+ }, 30000); // Increase timeout to 30s for chart installation
107
+ test("upgrade helm chart values", async () => {
108
+ // First install the chart
109
+ await client.request({
110
+ method: "tools/call",
111
+ params: {
112
+ name: "install_helm_chart",
113
+ arguments: {
114
+ name: testReleaseName,
115
+ chart: "nginx",
116
+ repo: "https://charts.bitnami.com/bitnami",
117
+ namespace: testNamespace,
118
+ },
119
+ },
120
+ }, HelmResponseSchema);
121
+ await sleep(5000);
122
+ // Then upgrade it with new values
123
+ const upgradeResult = await client.request({
124
+ method: "tools/call",
125
+ params: {
126
+ name: "upgrade_helm_chart",
127
+ arguments: {
128
+ name: testReleaseName,
129
+ chart: "nginx",
130
+ repo: "https://charts.bitnami.com/bitnami",
131
+ namespace: testNamespace,
132
+ values: {
133
+ replicaCount: 2,
134
+ resources: {
135
+ limits: {
136
+ cpu: "200m",
137
+ memory: "256Mi",
138
+ },
139
+ },
140
+ },
141
+ },
142
+ },
143
+ }, HelmResponseSchema);
144
+ expect(upgradeResult.content[0].type).toBe("text");
145
+ const result = JSON.parse(upgradeResult.content[0].text);
146
+ expect(result.status).toBe("upgraded");
147
+ // Wait for the upgrade to take effect
148
+ await sleep(5000);
149
+ // Verify the deployment was updated
150
+ const deploymentResult = await client.request({
151
+ method: "tools/call",
152
+ params: {
153
+ name: "list_deployments",
154
+ arguments: {
155
+ namespace: testNamespace,
156
+ },
157
+ },
158
+ }, HelmResponseSchema);
159
+ const deployments = JSON.parse(deploymentResult.content[0].text);
160
+ const nginxDeployment = deployments.deployments.find((d) => d.name.startsWith(testReleaseName));
161
+ expect(nginxDeployment).toBeDefined();
162
+ expect(nginxDeployment.replicas).toBe(2);
163
+ }, 60000); // Increase timeout to 60s for install + upgrade
164
+ test("uninstall helm chart", async () => {
165
+ // First install the chart
166
+ await client.request({
167
+ method: "tools/call",
168
+ params: {
169
+ name: "install_helm_chart",
170
+ arguments: {
171
+ name: testReleaseName,
172
+ chart: "nginx",
173
+ repo: "https://charts.bitnami.com/bitnami",
174
+ namespace: testNamespace,
175
+ },
176
+ },
177
+ }, HelmResponseSchema);
178
+ await sleep(5000);
179
+ // Then uninstall it
180
+ const uninstallResult = await client.request({
181
+ method: "tools/call",
182
+ params: {
183
+ name: "uninstall_helm_chart",
184
+ arguments: {
185
+ name: testReleaseName,
186
+ namespace: testNamespace,
187
+ },
188
+ },
189
+ }, HelmResponseSchema);
190
+ expect(uninstallResult.content[0].type).toBe("text");
191
+ const result = JSON.parse(uninstallResult.content[0].text);
192
+ expect(result.status).toBe("uninstalled");
193
+ // Wait for resources to be cleaned up
194
+ await sleep(5000);
195
+ // Verify the deployment is gone
196
+ const deploymentResult = await client.request({
197
+ method: "tools/call",
198
+ params: {
199
+ name: "list_deployments",
200
+ arguments: {
201
+ namespace: testNamespace,
202
+ },
203
+ },
204
+ }, HelmResponseSchema);
205
+ const deployments = JSON.parse(deploymentResult.content[0].text);
206
+ expect(deployments.deployments.every((d) => !d.name.startsWith(testReleaseName))).toBe(true);
207
+ }, 60000); // Increase timeout to 60s for install + uninstall
208
+ });
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import * as k8s from "@kubernetes/client-node";
6
+ import * as fs from "fs/promises";
7
+ import * as yaml from "js-yaml";
8
+ import { exec } from "child_process";
6
9
  class KubernetesManager {
7
10
  resources = [];
8
11
  portForwards = [];
@@ -185,6 +188,18 @@ const server = new Server({
185
188
  tools: {},
186
189
  },
187
190
  });
191
+ // Helper function to execute shell commands
192
+ function execCommand(command) {
193
+ return new Promise((resolve, reject) => {
194
+ exec(command, (error, stdout, stderr) => {
195
+ if (error) {
196
+ reject(new Error(`Command failed: ${error.message}\n${stderr}`));
197
+ return;
198
+ }
199
+ resolve(stdout.trim());
200
+ });
201
+ });
202
+ }
188
203
  // Tools handlers
189
204
  server.setRequestHandler(ListToolsRequestSchema, async () => {
190
205
  return {
@@ -365,6 +380,71 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
365
380
  required: ["resourceType"],
366
381
  },
367
382
  },
383
+ {
384
+ name: "install_helm_chart",
385
+ description: "Install a Helm chart",
386
+ inputSchema: {
387
+ type: "object",
388
+ properties: {
389
+ name: { type: "string", description: "Release name" },
390
+ chart: { type: "string", description: "Chart name or URL" },
391
+ namespace: {
392
+ type: "string",
393
+ description: "Target namespace",
394
+ optional: true,
395
+ },
396
+ values: {
397
+ type: "object",
398
+ description: "Values to override",
399
+ optional: true,
400
+ },
401
+ version: {
402
+ type: "string",
403
+ description: "Chart version",
404
+ optional: true,
405
+ },
406
+ repo: {
407
+ type: "string",
408
+ description: "Chart repository URL",
409
+ optional: true,
410
+ },
411
+ },
412
+ required: ["name", "chart"],
413
+ },
414
+ },
415
+ {
416
+ name: "uninstall_helm_chart",
417
+ description: "Uninstall a Helm release",
418
+ inputSchema: {
419
+ type: "object",
420
+ properties: {
421
+ name: { type: "string", description: "Release name" },
422
+ namespace: {
423
+ type: "string",
424
+ description: "Release namespace",
425
+ optional: true,
426
+ },
427
+ },
428
+ required: ["name"],
429
+ },
430
+ },
431
+ {
432
+ name: "upgrade_helm_chart",
433
+ description: "Upgrade a Helm release with new values",
434
+ inputSchema: {
435
+ type: "object",
436
+ properties: {
437
+ name: { type: "string", description: "Release name" },
438
+ values: { type: "object", description: "New values to apply" },
439
+ namespace: {
440
+ type: "string",
441
+ description: "Release namespace",
442
+ optional: true,
443
+ },
444
+ },
445
+ required: ["name", "values"],
446
+ },
447
+ },
368
448
  ],
369
449
  };
370
450
  });
@@ -738,6 +818,70 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
738
818
  throw new McpError(ErrorCode.InternalError, `Failed to get logs: ${error}`);
739
819
  }
740
820
  }
821
+ case "install_helm_chart": {
822
+ const installInput = input;
823
+ let command = `helm install ${installInput.name} ${installInput.chart}`;
824
+ if (installInput.namespace) {
825
+ command += ` -n ${installInput.namespace}`;
826
+ }
827
+ if (installInput.values) {
828
+ const valuesFile = `${installInput.name}-values.yaml`;
829
+ await fs.writeFile(valuesFile, yaml.dump(installInput.values));
830
+ command += ` -f ${valuesFile}`;
831
+ }
832
+ if (installInput.version) {
833
+ command += ` --version ${installInput.version}`;
834
+ }
835
+ if (installInput.repo) {
836
+ command += ` --repo ${installInput.repo}`;
837
+ }
838
+ const result = await execCommand(command);
839
+ return {
840
+ content: [
841
+ {
842
+ type: "text",
843
+ text: JSON.stringify({ status: "installed", output: result }, null, 2),
844
+ },
845
+ ],
846
+ };
847
+ }
848
+ case "uninstall_helm_chart": {
849
+ const uninstallInput = input;
850
+ let command = `helm uninstall ${uninstallInput.name}`;
851
+ if (uninstallInput.namespace) {
852
+ command += ` -n ${uninstallInput.namespace}`;
853
+ }
854
+ const result = await execCommand(command);
855
+ return {
856
+ content: [
857
+ {
858
+ type: "text",
859
+ text: JSON.stringify({ status: "uninstalled", output: result }, null, 2),
860
+ },
861
+ ],
862
+ };
863
+ }
864
+ case "upgrade_helm_chart": {
865
+ const upgradeInput = input;
866
+ const valuesFile = `${upgradeInput.name}-values.yaml`;
867
+ await fs.writeFile(valuesFile, yaml.dump(upgradeInput.values));
868
+ let command = `helm upgrade ${upgradeInput.name} ${upgradeInput.chart} -f ${valuesFile}`;
869
+ if (upgradeInput.namespace) {
870
+ command += ` -n ${upgradeInput.namespace}`;
871
+ }
872
+ if (upgradeInput.repo) {
873
+ command += ` --repo ${upgradeInput.repo}`;
874
+ }
875
+ const result = await execCommand(command);
876
+ return {
877
+ content: [
878
+ {
879
+ type: "text",
880
+ text: JSON.stringify({ status: "upgraded", output: result }, null, 2),
881
+ },
882
+ ],
883
+ };
884
+ }
741
885
  default:
742
886
  throw new McpError(ErrorCode.InvalidRequest, `Unknown tool: ${name}`);
743
887
  }
package/dist/types.d.ts CHANGED
@@ -354,3 +354,80 @@ export interface WatchTracker {
354
354
  resourceType: string;
355
355
  namespace: string;
356
356
  }
357
+ export declare const HelmInstallRequestSchema: z.ZodObject<{
358
+ name: z.ZodString;
359
+ chart: z.ZodString;
360
+ namespace: z.ZodOptional<z.ZodString>;
361
+ values: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
362
+ version: z.ZodOptional<z.ZodString>;
363
+ repo: z.ZodOptional<z.ZodString>;
364
+ }, "strip", z.ZodTypeAny, {
365
+ name: string;
366
+ chart: string;
367
+ values?: Record<string, any> | undefined;
368
+ namespace?: string | undefined;
369
+ version?: string | undefined;
370
+ repo?: string | undefined;
371
+ }, {
372
+ name: string;
373
+ chart: string;
374
+ values?: Record<string, any> | undefined;
375
+ namespace?: string | undefined;
376
+ version?: string | undefined;
377
+ repo?: string | undefined;
378
+ }>;
379
+ export declare const HelmUninstallRequestSchema: z.ZodObject<{
380
+ name: z.ZodString;
381
+ namespace: z.ZodOptional<z.ZodString>;
382
+ }, "strip", z.ZodTypeAny, {
383
+ name: string;
384
+ namespace?: string | undefined;
385
+ }, {
386
+ name: string;
387
+ namespace?: string | undefined;
388
+ }>;
389
+ export declare const HelmUpgradeRequestSchema: z.ZodObject<{
390
+ name: z.ZodString;
391
+ chart: z.ZodString;
392
+ repo: z.ZodOptional<z.ZodString>;
393
+ values: z.ZodRecord<z.ZodString, z.ZodAny>;
394
+ namespace: z.ZodOptional<z.ZodString>;
395
+ }, "strip", z.ZodTypeAny, {
396
+ name: string;
397
+ values: Record<string, any>;
398
+ chart: string;
399
+ namespace?: string | undefined;
400
+ repo?: string | undefined;
401
+ }, {
402
+ name: string;
403
+ values: Record<string, any>;
404
+ chart: string;
405
+ namespace?: string | undefined;
406
+ repo?: string | undefined;
407
+ }>;
408
+ export declare const HelmResponseSchema: z.ZodObject<{
409
+ content: z.ZodArray<z.ZodObject<{
410
+ type: z.ZodLiteral<"text">;
411
+ text: z.ZodString;
412
+ }, "strip", z.ZodTypeAny, {
413
+ type: "text";
414
+ text: string;
415
+ }, {
416
+ type: "text";
417
+ text: string;
418
+ }>, "many">;
419
+ }, "strip", z.ZodTypeAny, {
420
+ content: {
421
+ type: "text";
422
+ text: string;
423
+ }[];
424
+ }, {
425
+ content: {
426
+ type: "text";
427
+ text: string;
428
+ }[];
429
+ }>;
430
+ export type HelmInstallRequest = z.infer<typeof HelmInstallRequestSchema>;
431
+ export type HelmUninstallRequest = z.infer<typeof HelmUninstallRequestSchema>;
432
+ export type HelmUpgradeRequest = z.infer<typeof HelmUpgradeRequestSchema>;
433
+ export type HelmResponse = z.infer<typeof HelmResponseSchema>;
package/dist/types.js CHANGED
@@ -92,3 +92,29 @@ export const ReadResourceResponseSchema = z.object({
92
92
  text: z.string(),
93
93
  })),
94
94
  });
95
+ // Helm-related types
96
+ export const HelmInstallRequestSchema = z.object({
97
+ name: z.string(),
98
+ chart: z.string(),
99
+ namespace: z.string().optional(),
100
+ values: z.record(z.any()).optional(),
101
+ version: z.string().optional(),
102
+ repo: z.string().optional(),
103
+ });
104
+ export const HelmUninstallRequestSchema = z.object({
105
+ name: z.string(),
106
+ namespace: z.string().optional(),
107
+ });
108
+ export const HelmUpgradeRequestSchema = z.object({
109
+ name: z.string(),
110
+ chart: z.string(),
111
+ repo: z.string().optional(),
112
+ values: z.record(z.any()),
113
+ namespace: z.string().optional(),
114
+ });
115
+ export const HelmResponseSchema = z.object({
116
+ content: z.array(z.object({
117
+ type: z.literal("text"),
118
+ text: z.string(),
119
+ })),
120
+ });
package/dist/unit.test.js CHANGED
@@ -184,7 +184,11 @@ describe("kubernetes server operations", () => {
184
184
  name: podName,
185
185
  namespace: "default",
186
186
  template: "busybox",
187
- command: ["/bin/sh", "-c", "echo Pod is running && sleep infinity"],
187
+ command: [
188
+ "/bin/sh",
189
+ "-c",
190
+ "echo Pod is running && sleep infinity",
191
+ ],
188
192
  },
189
193
  },
190
194
  }, CreatePodResponseSchema);
@@ -285,5 +289,5 @@ describe("kubernetes server operations", () => {
285
289
  // Ignore any errors during termination check
286
290
  console.log(`Error checking pod termination status: ${error}`);
287
291
  }
288
- });
292
+ }, { timeout: 120000 });
289
293
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-kubernetes",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,9 +36,11 @@
36
36
  "dependencies": {
37
37
  "@kubernetes/client-node": "^0.20.0",
38
38
  "@modelcontextprotocol/sdk": "1.0.1",
39
+ "js-yaml": "^4.1.0",
39
40
  "zod": "^3.22.4"
40
41
  },
41
42
  "devDependencies": {
43
+ "@types/js-yaml": "^4.0.9",
42
44
  "@types/node": "^22.9.3",
43
45
  "shx": "^0.3.4",
44
46
  "typescript": "^5.6.2",