mcp-server-kubernetes 0.1.3 → 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 +10 -6
- package/dist/helm.test.d.ts +1 -0
- package/dist/helm.test.js +208 -0
- package/dist/index.js +305 -3
- package/dist/types.d.ts +99 -0
- package/dist/types.js +32 -0
- package/dist/unit.test.js +231 -110
- package/package.json +4 -2
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
|
|
|
@@ -40,14 +41,16 @@ If you have errors, open up a standard terminal and run `kubectl get pods` to se
|
|
|
40
41
|
- [x] Delete a pod
|
|
41
42
|
- [x] Describe a pod
|
|
42
43
|
- [x] List all namespaces
|
|
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
|
|
43
52
|
- [ ] Port forward to a pod
|
|
44
|
-
- [ ] Get logs from a pod for debugging
|
|
45
53
|
- [ ] 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)
|
|
51
54
|
|
|
52
55
|
## Local Development
|
|
53
56
|
|
|
@@ -90,6 +93,7 @@ npx @modelcontextprotocol/inspector node build/index.js
|
|
|
90
93
|
src/
|
|
91
94
|
├── index.ts # Main server implementation
|
|
92
95
|
├── types.ts # TypeScript type definitions
|
|
96
|
+
├── helm.test.ts # Helm chart installation tests
|
|
93
97
|
└── unit.test.ts # Unit tests
|
|
94
98
|
```
|
|
95
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 {
|
|
@@ -316,6 +331,120 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
316
331
|
properties: {},
|
|
317
332
|
},
|
|
318
333
|
},
|
|
334
|
+
{
|
|
335
|
+
name: "get_logs",
|
|
336
|
+
description: "Get logs from pods, deployments, jobs, or resources matching a label selector",
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: "object",
|
|
339
|
+
properties: {
|
|
340
|
+
resourceType: {
|
|
341
|
+
type: "string",
|
|
342
|
+
enum: ["pod", "deployment", "job"],
|
|
343
|
+
description: "Type of resource to get logs from",
|
|
344
|
+
},
|
|
345
|
+
name: {
|
|
346
|
+
type: "string",
|
|
347
|
+
description: "Name of the resource",
|
|
348
|
+
},
|
|
349
|
+
namespace: {
|
|
350
|
+
type: "string",
|
|
351
|
+
description: "Namespace of the resource",
|
|
352
|
+
default: "default",
|
|
353
|
+
},
|
|
354
|
+
labelSelector: {
|
|
355
|
+
type: "string",
|
|
356
|
+
description: "Label selector to filter resources",
|
|
357
|
+
optional: true,
|
|
358
|
+
},
|
|
359
|
+
container: {
|
|
360
|
+
type: "string",
|
|
361
|
+
description: "Container name (required when pod has multiple containers)",
|
|
362
|
+
optional: true,
|
|
363
|
+
},
|
|
364
|
+
tail: {
|
|
365
|
+
type: "number",
|
|
366
|
+
description: "Number of lines to show from end of logs",
|
|
367
|
+
optional: true,
|
|
368
|
+
},
|
|
369
|
+
since: {
|
|
370
|
+
type: "number",
|
|
371
|
+
description: "Get logs since relative time in seconds",
|
|
372
|
+
optional: true,
|
|
373
|
+
},
|
|
374
|
+
timestamps: {
|
|
375
|
+
type: "boolean",
|
|
376
|
+
description: "Include timestamps in logs",
|
|
377
|
+
default: false,
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
required: ["resourceType"],
|
|
381
|
+
},
|
|
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
|
+
},
|
|
319
448
|
],
|
|
320
449
|
};
|
|
321
450
|
});
|
|
@@ -426,21 +555,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
426
555
|
...templateConfig,
|
|
427
556
|
...(createPodInput.command && {
|
|
428
557
|
command: createPodInput.command,
|
|
558
|
+
args: undefined, // Clear default args when command is overridden
|
|
429
559
|
}),
|
|
430
560
|
},
|
|
431
561
|
],
|
|
432
562
|
},
|
|
433
563
|
};
|
|
434
|
-
const
|
|
564
|
+
const response = await k8sManager
|
|
435
565
|
.getCoreApi()
|
|
436
|
-
.createNamespacedPod(createPodInput.namespace, pod)
|
|
566
|
+
.createNamespacedPod(createPodInput.namespace, pod)
|
|
567
|
+
.catch((error) => {
|
|
568
|
+
console.error("Pod creation error:", {
|
|
569
|
+
status: error.response?.statusCode,
|
|
570
|
+
message: error.response?.body?.message || error.message,
|
|
571
|
+
details: error.response?.body,
|
|
572
|
+
});
|
|
573
|
+
throw error;
|
|
574
|
+
});
|
|
437
575
|
k8sManager.trackResource("Pod", createPodInput.name, createPodInput.namespace);
|
|
438
576
|
return {
|
|
439
577
|
content: [
|
|
440
578
|
{
|
|
441
579
|
type: "text",
|
|
442
580
|
text: JSON.stringify({
|
|
443
|
-
podName: body.metadata.name,
|
|
581
|
+
podName: response.body.metadata.name,
|
|
444
582
|
status: "created",
|
|
445
583
|
}, null, 2),
|
|
446
584
|
},
|
|
@@ -580,6 +718,170 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
580
718
|
],
|
|
581
719
|
};
|
|
582
720
|
}
|
|
721
|
+
case "get_logs": {
|
|
722
|
+
const { resourceType, name, namespace = "default", labelSelector, container, tail = 100, sinceSeconds, timestamps, pretty = true, follow = false, } = input;
|
|
723
|
+
async function getPodLogs(podName, podNamespace) {
|
|
724
|
+
try {
|
|
725
|
+
const { body } = await k8sManager.getCoreApi().readNamespacedPodLog(podName, podNamespace, container, follow, undefined, // insecureSkipTLSVerifyBackend
|
|
726
|
+
undefined, // limitBytes
|
|
727
|
+
pretty ? "true" : "false", undefined, // previous
|
|
728
|
+
sinceSeconds, tail, timestamps);
|
|
729
|
+
return body;
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
if (error.response?.statusCode === 404) {
|
|
733
|
+
throw new McpError(ErrorCode.InvalidRequest, `Pod ${podName} not found in namespace ${podNamespace}`);
|
|
734
|
+
}
|
|
735
|
+
// Log full error details
|
|
736
|
+
console.error("Full error:", {
|
|
737
|
+
statusCode: error.response?.statusCode,
|
|
738
|
+
message: error.response?.body?.message || error.message,
|
|
739
|
+
details: error.response?.body,
|
|
740
|
+
});
|
|
741
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get logs for pod ${podName}: ${error.response?.body?.message || error.message}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const logs = {};
|
|
745
|
+
try {
|
|
746
|
+
// Get logs based on resource type
|
|
747
|
+
switch (resourceType.toLowerCase()) {
|
|
748
|
+
case "pod": {
|
|
749
|
+
if (!name) {
|
|
750
|
+
throw new McpError(ErrorCode.InvalidRequest, "Pod name is required when resourceType is 'pod'");
|
|
751
|
+
}
|
|
752
|
+
logs[name] = await getPodLogs(name, namespace);
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
case "deployment": {
|
|
756
|
+
if (!name) {
|
|
757
|
+
throw new McpError(ErrorCode.InvalidRequest, "Deployment name is required when resourceType is 'deployment'");
|
|
758
|
+
}
|
|
759
|
+
const { body: deployment } = await k8sManager
|
|
760
|
+
.getAppsApi()
|
|
761
|
+
.readNamespacedDeployment(name, namespace);
|
|
762
|
+
if (!deployment.spec?.selector?.matchLabels) {
|
|
763
|
+
throw new McpError(ErrorCode.InvalidRequest, `Deployment ${name} has no selector`);
|
|
764
|
+
}
|
|
765
|
+
const selector = Object.entries(deployment.spec.selector.matchLabels)
|
|
766
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
767
|
+
.join(",");
|
|
768
|
+
const { body: podList } = await k8sManager
|
|
769
|
+
.getCoreApi()
|
|
770
|
+
.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, selector);
|
|
771
|
+
for (const pod of podList.items) {
|
|
772
|
+
if (pod.metadata?.name) {
|
|
773
|
+
logs[pod.metadata.name] = await getPodLogs(pod.metadata.name, namespace);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
case "job": {
|
|
779
|
+
if (!name) {
|
|
780
|
+
throw new McpError(ErrorCode.InvalidRequest, "Job name is required when resourceType is 'job'");
|
|
781
|
+
}
|
|
782
|
+
const { body: podList } = await k8sManager
|
|
783
|
+
.getCoreApi()
|
|
784
|
+
.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, `job-name=${name}`);
|
|
785
|
+
for (const pod of podList.items) {
|
|
786
|
+
if (pod.metadata?.name) {
|
|
787
|
+
logs[pod.metadata.name] = await getPodLogs(pod.metadata.name, namespace);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
default:
|
|
793
|
+
throw new McpError(ErrorCode.InvalidRequest, `Unsupported resource type: ${resourceType}`);
|
|
794
|
+
}
|
|
795
|
+
// If labelSelector is provided, filter or add logs by label
|
|
796
|
+
if (labelSelector) {
|
|
797
|
+
const { body: labeledPods } = await k8sManager
|
|
798
|
+
.getCoreApi()
|
|
799
|
+
.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, labelSelector);
|
|
800
|
+
for (const pod of labeledPods.items) {
|
|
801
|
+
if (pod.metadata?.name) {
|
|
802
|
+
logs[pod.metadata.name] = await getPodLogs(pod.metadata.name, namespace);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
content: [
|
|
808
|
+
{
|
|
809
|
+
type: "text",
|
|
810
|
+
text: JSON.stringify({ logs }, null, 2),
|
|
811
|
+
},
|
|
812
|
+
],
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
if (error instanceof McpError)
|
|
817
|
+
throw error;
|
|
818
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get logs: ${error}`);
|
|
819
|
+
}
|
|
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
|
+
}
|
|
583
885
|
default:
|
|
584
886
|
throw new McpError(ErrorCode.InvalidRequest, `Unknown tool: ${name}`);
|
|
585
887
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -251,6 +251,28 @@ export declare const ListNodesResponseSchema: z.ZodObject<{
|
|
|
251
251
|
text: string;
|
|
252
252
|
}[];
|
|
253
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
|
+
}>;
|
|
254
276
|
export declare const ListResourcesResponseSchema: z.ZodObject<{
|
|
255
277
|
resources: z.ZodArray<z.ZodObject<{
|
|
256
278
|
uri: z.ZodString;
|
|
@@ -332,3 +354,80 @@ export interface WatchTracker {
|
|
|
332
354
|
resourceType: string;
|
|
333
355
|
namespace: string;
|
|
334
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
|
@@ -76,6 +76,12 @@ export const ListNodesResponseSchema = z.object({
|
|
|
76
76
|
text: z.string(),
|
|
77
77
|
})),
|
|
78
78
|
});
|
|
79
|
+
export const GetLogsResponseSchema = z.object({
|
|
80
|
+
content: z.array(z.object({
|
|
81
|
+
type: z.literal("text"),
|
|
82
|
+
text: z.string(),
|
|
83
|
+
})),
|
|
84
|
+
});
|
|
79
85
|
export const ListResourcesResponseSchema = z.object({
|
|
80
86
|
resources: z.array(ResourceSchema),
|
|
81
87
|
});
|
|
@@ -86,3 +92,29 @@ export const ReadResourceResponseSchema = z.object({
|
|
|
86
92
|
text: z.string(),
|
|
87
93
|
})),
|
|
88
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
|
@@ -1,24 +1,79 @@
|
|
|
1
|
-
|
|
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,
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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,13 +81,19 @@ 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: {},
|
|
96
|
+
arguments: {},
|
|
36
97
|
},
|
|
37
98
|
}, ListNamespacesResponseSchema);
|
|
38
99
|
expect(namespacesResult.content[0].type).toBe("text");
|
|
@@ -51,122 +112,182 @@ test("kubernetes server operations", async () => {
|
|
|
51
112
|
const nodes = JSON.parse(listNodesResult.content[0].text);
|
|
52
113
|
expect(nodes.nodes).toBeDefined();
|
|
53
114
|
expect(Array.isArray(nodes.nodes)).toBe(true);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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({
|
|
57
132
|
method: "tools/call",
|
|
58
133
|
params: {
|
|
59
|
-
name: "
|
|
134
|
+
name: "list_pods",
|
|
60
135
|
arguments: {
|
|
61
|
-
// Changed from input to arguments
|
|
62
|
-
name: "test-pod",
|
|
63
136
|
namespace: "default",
|
|
64
|
-
ignoreNotFound: true,
|
|
65
137
|
},
|
|
66
138
|
},
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
139
|
+
}, ListPodsResponseSchema);
|
|
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
|
|
74
179
|
const createPodResult = await client.request({
|
|
75
180
|
method: "tools/call",
|
|
76
181
|
params: {
|
|
77
182
|
name: "create_pod",
|
|
78
183
|
arguments: {
|
|
79
|
-
|
|
80
|
-
name: "test-pod",
|
|
184
|
+
name: podName,
|
|
81
185
|
namespace: "default",
|
|
82
|
-
template: "
|
|
186
|
+
template: "busybox",
|
|
187
|
+
command: [
|
|
188
|
+
"/bin/sh",
|
|
189
|
+
"-c",
|
|
190
|
+
"echo Pod is running && sleep infinity",
|
|
191
|
+
],
|
|
83
192
|
},
|
|
84
193
|
},
|
|
85
194
|
}, CreatePodResponseSchema);
|
|
86
195
|
expect(createPodResult.content[0].type).toBe("text");
|
|
87
|
-
const
|
|
88
|
-
expect(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
196
|
+
const podResult = JSON.parse(createPodResult.content[0].text);
|
|
197
|
+
expect(podResult.podName).toBe(podName);
|
|
198
|
+
// Step 2: Wait for Running state (up to 60 seconds)
|
|
199
|
+
let podRunning = false;
|
|
200
|
+
const startTime = Date.now();
|
|
201
|
+
while (!podRunning && Date.now() - startTime < 60000) {
|
|
202
|
+
const podStatus = await client.request({
|
|
203
|
+
method: "tools/call",
|
|
204
|
+
params: {
|
|
205
|
+
name: "describe_pod",
|
|
206
|
+
arguments: {
|
|
207
|
+
name: podName,
|
|
208
|
+
namespace: "default",
|
|
209
|
+
},
|
|
99
210
|
},
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
method: "tools/call",
|
|
128
|
-
params: {
|
|
129
|
-
name: "list_deployments",
|
|
130
|
-
arguments: {
|
|
131
|
-
// Changed from input to arguments
|
|
132
|
-
namespace: "default",
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
}, ListDeploymentsResponseSchema);
|
|
136
|
-
expect(listDeploymentsResult.content[0].type).toBe("text");
|
|
137
|
-
const deployments = JSON.parse(listDeploymentsResult.content[0].text);
|
|
138
|
-
expect(deployments.deployments).toBeDefined();
|
|
139
|
-
// Cleanup
|
|
140
|
-
console.log("Cleaning up...");
|
|
141
|
-
const cleanupResult = await client.request({
|
|
142
|
-
method: "tools/call",
|
|
143
|
-
params: {
|
|
144
|
-
name: "cleanup",
|
|
145
|
-
arguments: {}, // Changed from input to arguments
|
|
146
|
-
},
|
|
147
|
-
}, CleanupResponseSchema);
|
|
148
|
-
expect(cleanupResult.content[0].type).toBe("text");
|
|
149
|
-
const cleanupData = JSON.parse(cleanupResult.content[0].text);
|
|
150
|
-
expect(cleanupData.success).toBe(true);
|
|
151
|
-
// Verify cleanup by listing pods again
|
|
152
|
-
console.log("Verifying cleanup...");
|
|
153
|
-
const finalPodsResult = await client.request({
|
|
211
|
+
}, ListPodsResponseSchema);
|
|
212
|
+
const status = JSON.parse(podStatus.content[0].text);
|
|
213
|
+
if (status.status?.phase === "Running") {
|
|
214
|
+
podRunning = true;
|
|
215
|
+
console.log(`Pod ${podName} is running. Checking logs...`);
|
|
216
|
+
// Check pod logs once running
|
|
217
|
+
const logsResult = await client.request({
|
|
218
|
+
method: "tools/call",
|
|
219
|
+
params: {
|
|
220
|
+
name: "get_logs",
|
|
221
|
+
arguments: {
|
|
222
|
+
resourceType: "pod",
|
|
223
|
+
name: podName,
|
|
224
|
+
namespace: "default",
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
}, ListPodsResponseSchema);
|
|
228
|
+
expect(logsResult.content[0].type).toBe("text");
|
|
229
|
+
const logs = JSON.parse(logsResult.content[0].text);
|
|
230
|
+
expect(logs.logs[podName]).toContain("Pod is running");
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
await sleep(1000);
|
|
234
|
+
}
|
|
235
|
+
expect(podRunning).toBe(true);
|
|
236
|
+
// Step 3: Terminate pod and verify termination (wait up to 10 seconds)
|
|
237
|
+
const deletePodResult = await client.request({
|
|
154
238
|
method: "tools/call",
|
|
155
239
|
params: {
|
|
156
|
-
name: "
|
|
240
|
+
name: "delete_pod",
|
|
157
241
|
arguments: {
|
|
158
|
-
|
|
242
|
+
name: podName,
|
|
159
243
|
namespace: "default",
|
|
160
244
|
},
|
|
161
245
|
},
|
|
162
|
-
},
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
246
|
+
}, DeletePodResponseSchema);
|
|
247
|
+
expect(deletePodResult.content[0].type).toBe("text");
|
|
248
|
+
const deleteResult = JSON.parse(deletePodResult.content[0].text);
|
|
249
|
+
expect(deleteResult.status).toBe("deleted");
|
|
250
|
+
// Try to verify pod termination, but don't fail the test if we can't confirm it
|
|
251
|
+
try {
|
|
252
|
+
let podTerminated = false;
|
|
253
|
+
const terminationStartTime = Date.now();
|
|
254
|
+
while (!podTerminated && Date.now() - terminationStartTime < 10000) {
|
|
255
|
+
try {
|
|
256
|
+
const podStatus = await client.request({
|
|
257
|
+
method: "tools/call",
|
|
258
|
+
params: {
|
|
259
|
+
name: "describe_pod",
|
|
260
|
+
arguments: {
|
|
261
|
+
name: podName,
|
|
262
|
+
namespace: "default",
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
}, ListPodsResponseSchema);
|
|
266
|
+
// Pod still exists, check if it's in Terminating state
|
|
267
|
+
const status = JSON.parse(podStatus.content[0].text);
|
|
268
|
+
if (status.status?.phase === "Terminating") {
|
|
269
|
+
podTerminated = true;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
await sleep(500);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
// If we get an error (404), the pod is gone which also means it's terminated
|
|
276
|
+
podTerminated = true;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Log termination status but don't fail the test
|
|
281
|
+
if (podTerminated) {
|
|
282
|
+
console.log(`Pod ${podName} termination confirmed`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
console.log(`Pod ${podName} termination could not be confirmed within timeout, but deletion was initiated`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
// Ignore any errors during termination check
|
|
290
|
+
console.log(`Error checking pod termination status: ${error}`);
|
|
291
|
+
}
|
|
292
|
+
}, { timeout: 120000 });
|
|
172
293
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-server-kubernetes",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
20
20
|
"dev": "tsc --watch",
|
|
21
21
|
"start": "node dist/index.js",
|
|
22
|
-
"test": "vitest
|
|
22
|
+
"test": "vitest run",
|
|
23
23
|
"prepublishOnly": "npm run build",
|
|
24
24
|
"dockerbuild": "docker buildx build -t flux159/mcp-server-kubernetes --platform linux/amd64,linux/arm64 --push ."
|
|
25
25
|
},
|
|
@@ -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",
|