mcp-server-kubernetes 0.1.5 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +29 -9
  2. package/dist/config/cleanup-config.d.ts +8 -0
  3. package/dist/config/cleanup-config.js +8 -0
  4. package/dist/config/container-templates.d.ts +5 -0
  5. package/dist/config/container-templates.js +109 -0
  6. package/dist/config/deployment-config.d.ts +31 -0
  7. package/dist/config/deployment-config.js +23 -0
  8. package/dist/config/namespace-config.d.ts +8 -0
  9. package/dist/config/namespace-config.js +8 -0
  10. package/dist/config/server-config.d.ts +8 -0
  11. package/dist/config/server-config.js +8 -0
  12. package/dist/index.js +49 -928
  13. package/dist/models/helm-models.d.ts +39 -0
  14. package/dist/models/helm-models.js +8 -0
  15. package/dist/models/resource-models.d.ts +94 -0
  16. package/dist/models/resource-models.js +17 -0
  17. package/dist/models/response-schemas.d.ts +221 -0
  18. package/dist/models/response-schemas.js +36 -0
  19. package/dist/models/tool-models.d.ts +42 -0
  20. package/dist/models/tool-models.js +10 -0
  21. package/dist/resources/handlers.d.ts +22 -0
  22. package/dist/resources/handlers.js +112 -0
  23. package/dist/tools/create_pod.d.ts +39 -0
  24. package/dist/tools/create_pod.js +71 -0
  25. package/dist/tools/delete_pod.d.ts +31 -0
  26. package/dist/tools/delete_pod.js +45 -0
  27. package/dist/tools/describe_pod.d.ts +33 -0
  28. package/dist/tools/describe_pod.js +81 -0
  29. package/dist/tools/get_logs.d.ts +67 -0
  30. package/dist/tools/get_logs.js +150 -0
  31. package/dist/tools/helm-operations.d.ts +99 -0
  32. package/dist/tools/helm-operations.js +198 -0
  33. package/dist/tools/list_deployments.d.ts +23 -0
  34. package/dist/tools/list_deployments.js +30 -0
  35. package/dist/tools/list_nodes.d.ts +15 -0
  36. package/dist/tools/list_nodes.js +21 -0
  37. package/dist/tools/list_pods.d.ts +23 -0
  38. package/dist/tools/list_pods.js +29 -0
  39. package/dist/tools/list_services.d.ts +23 -0
  40. package/dist/tools/list_services.js +31 -0
  41. package/dist/types.d.ts +4 -433
  42. package/dist/types.js +6 -120
  43. package/dist/utils/kubernetes-manager.d.ts +21 -0
  44. package/dist/utils/kubernetes-manager.js +68 -0
  45. package/package.json +3 -2
  46. package/dist/helm.test.d.ts +0 -1
  47. package/dist/helm.test.js +0 -208
  48. package/dist/unit.test.d.ts +0 -1
  49. package/dist/unit.test.js +0 -293
package/dist/index.js CHANGED
@@ -1,523 +1,60 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { listPods, listPodsSchema } from "./tools/list_pods.js";
5
+ import { listNodes, listNodesSchema } from "./tools/list_nodes.js";
6
+ import { listServices, listServicesSchema } from "./tools/list_services.js";
7
+ import { listDeployments, listDeploymentsSchema } from "./tools/list_deployments.js";
8
+ import { installHelmChart, installHelmChartSchema, upgradeHelmChart, upgradeHelmChartSchema, uninstallHelmChart, uninstallHelmChartSchema, } from "./tools/helm-operations.js";
9
+ import { createPod, createPodSchema } from "./tools/create_pod.js";
10
+ import { deletePod, deletePodSchema } from "./tools/delete_pod.js";
11
+ import { describePod, describePodSchema } from "./tools/describe_pod.js";
12
+ import { getLogs, getLogsSchema } from "./tools/get_logs.js";
13
+ import { getResourceHandlers } from "./resources/handlers.js";
4
14
  import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
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";
9
- class KubernetesManager {
10
- resources = [];
11
- portForwards = [];
12
- watches = [];
13
- kc;
14
- k8sApi;
15
- k8sAppsApi;
16
- constructor() {
17
- this.kc = new k8s.KubeConfig();
18
- this.kc.loadFromDefault();
19
- this.k8sApi = this.kc.makeApiClient(k8s.CoreV1Api);
20
- this.k8sAppsApi = this.kc.makeApiClient(k8s.AppsV1Api);
21
- // process.on("SIGINT", () => this.cleanup());
22
- // process.on("SIGTERM", () => this.cleanup());
23
- }
24
- async cleanup() {
25
- // Stop watches
26
- for (const watch of this.watches) {
27
- watch.abort.abort();
28
- }
29
- // Delete tracked resources in reverse order
30
- for (const resource of [...this.resources].reverse()) {
31
- try {
32
- await this.deleteResource(resource.kind, resource.name, resource.namespace);
33
- }
34
- catch (error) {
35
- console.error(`Failed to delete ${resource.kind} ${resource.name}:`, error);
36
- }
37
- }
38
- // TODO: Cleanup port forwards when implemented
39
- }
40
- trackResource(kind, name, namespace) {
41
- this.resources.push({ kind, name, namespace, createdAt: new Date() });
42
- }
43
- async deleteResource(kind, name, namespace) {
44
- switch (kind.toLowerCase()) {
45
- case "pod":
46
- await this.k8sApi.deleteNamespacedPod(name, namespace);
47
- break;
48
- case "deployment":
49
- await this.k8sAppsApi.deleteNamespacedDeployment(name, namespace);
50
- break;
51
- case "service":
52
- await this.k8sApi.deleteNamespacedService(name, namespace);
53
- break;
54
- }
55
- this.resources = this.resources.filter((r) => !(r.kind === kind && r.name === name && r.namespace === namespace));
56
- }
57
- trackPortForward(pf) {
58
- this.portForwards.push(pf);
59
- }
60
- getPortForward(id) {
61
- return this.portForwards.find((p) => p.id === id);
62
- }
63
- removePortForward(id) {
64
- this.portForwards = this.portForwards.filter((p) => p.id !== id);
65
- }
66
- trackWatch(watch) {
67
- this.watches.push(watch);
68
- }
69
- getKubeConfig() {
70
- return this.kc;
71
- }
72
- getCoreApi() {
73
- return this.k8sApi;
74
- }
75
- getAppsApi() {
76
- return this.k8sAppsApi;
77
- }
78
- }
15
+ import { KubernetesManager } from "./types.js";
16
+ import { serverConfig } from "./config/server-config.js";
17
+ import { createDeploymentSchema } from "./config/deployment-config.js";
18
+ import { listNamespacesSchema } from "./config/namespace-config.js";
19
+ import { cleanupSchema } from "./config/cleanup-config.js";
79
20
  const k8sManager = new KubernetesManager();
80
- // Template configurations with health checks and resource limits
81
- // TODO: Update create_pod to accept custom images and custom template files
82
- const containerTemplates = {
83
- ubuntu: {
84
- name: "main",
85
- image: "ubuntu:latest",
86
- command: ["/bin/bash"],
87
- args: ["-c", "sleep infinity"],
88
- resources: {
89
- limits: {
90
- cpu: "200m",
91
- memory: "256Mi",
92
- },
93
- requests: {
94
- cpu: "100m",
95
- memory: "128Mi",
96
- },
97
- },
98
- livenessProbe: {
99
- exec: {
100
- command: ["cat", "/proc/1/status"],
101
- },
102
- initialDelaySeconds: 5,
103
- periodSeconds: 10,
104
- },
105
- },
106
- nginx: {
107
- name: "main",
108
- image: "nginx:latest",
109
- ports: [{ containerPort: 80 }],
110
- resources: {
111
- limits: {
112
- cpu: "200m",
113
- memory: "256Mi",
114
- },
115
- requests: {
116
- cpu: "100m",
117
- memory: "128Mi",
118
- },
119
- },
120
- livenessProbe: {
121
- httpGet: {
122
- path: "/",
123
- port: 80,
124
- },
125
- initialDelaySeconds: 5,
126
- periodSeconds: 10,
127
- },
128
- readinessProbe: {
129
- httpGet: {
130
- path: "/",
131
- port: 80,
132
- },
133
- initialDelaySeconds: 2,
134
- periodSeconds: 5,
135
- },
136
- },
137
- busybox: {
138
- name: "main",
139
- image: "busybox:latest",
140
- command: ["sh"],
141
- args: ["-c", "sleep infinity"],
142
- resources: {
143
- limits: {
144
- cpu: "100m",
145
- memory: "64Mi",
146
- },
147
- requests: {
148
- cpu: "50m",
149
- memory: "32Mi",
150
- },
151
- },
152
- livenessProbe: {
153
- exec: {
154
- command: ["true"],
155
- },
156
- periodSeconds: 10,
157
- },
158
- },
159
- alpine: {
160
- name: "main",
161
- image: "alpine:latest",
162
- command: ["sh"],
163
- args: ["-c", "sleep infinity"],
164
- resources: {
165
- limits: {
166
- cpu: "100m",
167
- memory: "64Mi",
168
- },
169
- requests: {
170
- cpu: "50m",
171
- memory: "32Mi",
172
- },
173
- },
174
- livenessProbe: {
175
- exec: {
176
- command: ["true"],
177
- },
178
- periodSeconds: 10,
179
- },
180
- },
181
- };
182
21
  const server = new Server({
183
- name: "kubernetes",
184
- version: "0.1.0",
185
- }, {
186
- capabilities: {
187
- resources: {},
188
- tools: {},
189
- },
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
- }
22
+ name: serverConfig.name,
23
+ version: serverConfig.version,
24
+ }, serverConfig);
203
25
  // Tools handlers
204
26
  server.setRequestHandler(ListToolsRequestSchema, async () => {
205
27
  return {
206
28
  tools: [
207
- {
208
- name: "list_pods",
209
- description: "List pods in a namespace",
210
- inputSchema: {
211
- type: "object",
212
- properties: {
213
- namespace: { type: "string", default: "default" },
214
- },
215
- required: ["namespace"],
216
- },
217
- },
218
- {
219
- name: "list_deployments",
220
- description: "List deployments in a namespace",
221
- inputSchema: {
222
- type: "object",
223
- properties: {
224
- namespace: { type: "string", default: "default" },
225
- },
226
- required: ["namespace"],
227
- },
228
- },
229
- {
230
- name: "list_services",
231
- description: "List services in a namespace",
232
- inputSchema: {
233
- type: "object",
234
- properties: {
235
- namespace: { type: "string", default: "default" },
236
- },
237
- required: ["namespace"],
238
- },
239
- },
240
- {
241
- name: "list_namespaces",
242
- description: "List all namespaces",
243
- inputSchema: {
244
- type: "object",
245
- properties: {},
246
- },
247
- },
248
- {
249
- // TODO: Add support for custom images and templates (see above in containerTemplates definition)
250
- name: "create_pod",
251
- description: "Create a new Kubernetes pod",
252
- inputSchema: {
253
- type: "object",
254
- properties: {
255
- name: { type: "string" },
256
- namespace: { type: "string" },
257
- template: {
258
- type: "string",
259
- enum: ["ubuntu", "nginx", "busybox", "alpine"],
260
- },
261
- command: {
262
- type: "array",
263
- items: { type: "string" },
264
- optional: true,
265
- },
266
- },
267
- required: ["name", "namespace", "template"],
268
- },
269
- },
270
- {
271
- // TODO: Support for custom deployments (see above)
272
- name: "create_deployment",
273
- description: "Create a new Kubernetes deployment",
274
- inputSchema: {
275
- type: "object",
276
- properties: {
277
- name: { type: "string" },
278
- namespace: { type: "string" },
279
- template: {
280
- type: "string",
281
- enum: ["ubuntu", "nginx", "busybox", "alpine"],
282
- },
283
- replicas: { type: "number", default: 1 },
284
- ports: {
285
- type: "array",
286
- items: { type: "number" },
287
- optional: true,
288
- },
289
- },
290
- required: ["name", "namespace", "template"],
291
- },
292
- },
293
- {
294
- name: "delete_pod",
295
- description: "Delete a Kubernetes pod",
296
- inputSchema: {
297
- type: "object",
298
- properties: {
299
- name: { type: "string" },
300
- namespace: { type: "string" },
301
- ignoreNotFound: { type: "boolean", default: false },
302
- },
303
- required: ["name", "namespace"],
304
- },
305
- },
306
- {
307
- name: "describe_pod",
308
- description: "Describe a Kubernetes pod (read details like status, containers, etc.)",
309
- inputSchema: {
310
- type: "object",
311
- properties: {
312
- name: { type: "string" },
313
- namespace: { type: "string" },
314
- },
315
- required: ["name", "namespace"],
316
- },
317
- },
318
- {
319
- name: "cleanup",
320
- description: "Cleanup all managed resources",
321
- inputSchema: {
322
- type: "object",
323
- properties: {},
324
- },
325
- },
326
- {
327
- name: "list_nodes",
328
- description: "List all nodes in the cluster",
329
- inputSchema: {
330
- type: "object",
331
- properties: {},
332
- },
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
- },
29
+ listPodsSchema,
30
+ listDeploymentsSchema,
31
+ listServicesSchema,
32
+ listNamespacesSchema,
33
+ createPodSchema,
34
+ createDeploymentSchema,
35
+ deletePodSchema,
36
+ describePodSchema,
37
+ cleanupSchema,
38
+ listNodesSchema,
39
+ getLogsSchema,
40
+ installHelmChartSchema,
41
+ upgradeHelmChartSchema,
42
+ uninstallHelmChartSchema,
448
43
  ],
449
44
  };
450
45
  });
451
46
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
452
47
  try {
453
- const { name } = request.params;
454
- const input = request.params.arguments;
48
+ const { name, arguments: input = {} } = request.params;
455
49
  switch (name) {
456
50
  case "list_pods": {
457
- const listPodsInput = input;
458
- const namespace = listPodsInput.namespace || "default";
459
- const { body } = await k8sManager
460
- .getCoreApi()
461
- .listNamespacedPod(namespace);
462
- const pods = body.items.map((pod) => ({
463
- name: pod.metadata?.name || "",
464
- namespace: pod.metadata?.namespace || "",
465
- status: pod.status?.phase,
466
- createdAt: pod.metadata?.creationTimestamp,
467
- }));
468
- return {
469
- content: [
470
- {
471
- type: "text",
472
- text: JSON.stringify({ pods }, null, 2),
473
- },
474
- ],
475
- };
51
+ return await listPods(k8sManager, input);
476
52
  }
477
53
  case "list_deployments": {
478
- const listDeploymentsInput = input;
479
- const namespace = listDeploymentsInput.namespace || "default";
480
- const { body } = await k8sManager
481
- .getAppsApi()
482
- .listNamespacedDeployment(namespace);
483
- const deployments = body.items.map((deployment) => ({
484
- name: deployment.metadata?.name || "",
485
- namespace: deployment.metadata?.namespace || "",
486
- replicas: deployment.spec?.replicas || 0,
487
- availableReplicas: deployment.status?.availableReplicas || 0,
488
- createdAt: deployment.metadata?.creationTimestamp,
489
- }));
490
- return {
491
- content: [
492
- {
493
- type: "text",
494
- text: JSON.stringify({ deployments }, null, 2),
495
- },
496
- ],
497
- };
54
+ return await listDeployments(k8sManager, input);
498
55
  }
499
56
  case "list_services": {
500
- const listServicesInput = input;
501
- const namespace = listServicesInput.namespace || "default";
502
- const { body } = await k8sManager
503
- .getCoreApi()
504
- .listNamespacedService(namespace);
505
- const services = body.items.map((service) => ({
506
- name: service.metadata?.name || "",
507
- namespace: service.metadata?.namespace || "",
508
- type: service.spec?.type,
509
- clusterIP: service.spec?.clusterIP,
510
- ports: service.spec?.ports || [],
511
- createdAt: service.metadata?.creationTimestamp,
512
- }));
513
- return {
514
- content: [
515
- {
516
- type: "text",
517
- text: JSON.stringify({ services }, null, 2),
518
- },
519
- ],
520
- };
57
+ return await listServices(k8sManager, input);
521
58
  }
522
59
  case "list_namespaces": {
523
60
  const { body } = await k8sManager.getCoreApi().listNamespace();
@@ -536,161 +73,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
536
73
  };
537
74
  }
538
75
  case "create_pod": {
539
- const createPodInput = input;
540
- const templateConfig = containerTemplates[createPodInput.template];
541
- const pod = {
542
- apiVersion: "v1",
543
- kind: "Pod",
544
- metadata: {
545
- name: createPodInput.name,
546
- namespace: createPodInput.namespace,
547
- labels: {
548
- "mcp-managed": "true",
549
- app: createPodInput.name,
550
- },
551
- },
552
- spec: {
553
- containers: [
554
- {
555
- ...templateConfig,
556
- ...(createPodInput.command && {
557
- command: createPodInput.command,
558
- args: undefined, // Clear default args when command is overridden
559
- }),
560
- },
561
- ],
562
- },
563
- };
564
- const response = await k8sManager
565
- .getCoreApi()
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
- });
575
- k8sManager.trackResource("Pod", createPodInput.name, createPodInput.namespace);
576
- return {
577
- content: [
578
- {
579
- type: "text",
580
- text: JSON.stringify({
581
- podName: response.body.metadata.name,
582
- status: "created",
583
- }, null, 2),
584
- },
585
- ],
586
- };
76
+ return await createPod(k8sManager, input);
587
77
  }
588
78
  case "delete_pod": {
589
- const deletePodInput = input;
590
- try {
591
- await k8sManager
592
- .getCoreApi()
593
- .deleteNamespacedPod(deletePodInput.name, deletePodInput.namespace);
594
- return {
595
- content: [
596
- {
597
- type: "text",
598
- text: JSON.stringify({
599
- success: true,
600
- status: "deleted",
601
- }, null, 2),
602
- },
603
- ],
604
- };
605
- }
606
- catch (error) {
607
- if (deletePodInput.ignoreNotFound &&
608
- error.response?.statusCode === 404) {
609
- return {
610
- content: [
611
- {
612
- type: "text",
613
- text: JSON.stringify({
614
- success: true,
615
- status: "not_found",
616
- }, null, 2),
617
- },
618
- ],
619
- };
620
- }
621
- throw error;
622
- }
79
+ return await deletePod(k8sManager, input);
623
80
  }
624
81
  case "describe_pod": {
625
- const describePodInput = input;
626
- try {
627
- const { body } = await k8sManager
628
- .getCoreApi()
629
- .readNamespacedPod(describePodInput.name, describePodInput.namespace);
630
- if (!body) {
631
- return {
632
- content: [
633
- {
634
- type: "text",
635
- text: JSON.stringify({
636
- error: "Pod not found",
637
- status: "not_found",
638
- }, null, 2),
639
- },
640
- ],
641
- isError: true,
642
- };
643
- }
644
- // Format the pod details for better readability
645
- const podDetails = {
646
- kind: body.kind,
647
- metadata: {
648
- name: body.metadata?.name,
649
- namespace: body.metadata?.namespace,
650
- creationTimestamp: body.metadata?.creationTimestamp,
651
- labels: body.metadata?.labels,
652
- },
653
- spec: {
654
- containers: body.spec?.containers.map((container) => ({
655
- name: container.name,
656
- image: container.image,
657
- ports: container.ports,
658
- resources: container.resources,
659
- })),
660
- nodeName: body.spec?.nodeName,
661
- },
662
- status: {
663
- phase: body.status?.phase,
664
- conditions: body.status?.conditions,
665
- containerStatuses: body.status?.containerStatuses,
666
- },
667
- };
668
- return {
669
- content: [
670
- {
671
- type: "text",
672
- text: JSON.stringify(podDetails, null, 2),
673
- },
674
- ],
675
- };
676
- }
677
- catch (error) {
678
- if (error.response?.statusCode === 404) {
679
- return {
680
- content: [
681
- {
682
- type: "text",
683
- text: JSON.stringify({
684
- error: "Pod not found",
685
- status: "not_found",
686
- }, null, 2),
687
- },
688
- ],
689
- isError: true,
690
- };
691
- }
692
- throw new McpError(ErrorCode.InternalError, `Failed to describe pod: ${error.response?.body?.message || error.message}`);
693
- }
82
+ return await describePod(k8sManager, input);
694
83
  }
695
84
  case "cleanup": {
696
85
  await k8sManager.cleanup();
@@ -706,181 +95,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
706
95
  };
707
96
  }
708
97
  case "list_nodes": {
709
- const { body } = await k8sManager.getCoreApi().listNode();
710
- return {
711
- content: [
712
- {
713
- type: "text",
714
- text: JSON.stringify({
715
- nodes: body.items,
716
- }, null, 2),
717
- },
718
- ],
719
- };
98
+ return await listNodes(k8sManager);
720
99
  }
721
100
  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
- }
101
+ return await getLogs(k8sManager, input);
820
102
  }
821
103
  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
- };
104
+ return await installHelmChart(input);
863
105
  }
864
106
  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
- };
107
+ return await upgradeHelmChart(input);
108
+ }
109
+ case "uninstall_helm_chart": {
110
+ return await uninstallHelmChart(input);
884
111
  }
885
112
  default:
886
113
  throw new McpError(ErrorCode.InvalidRequest, `Unknown tool: ${name}`);
@@ -893,115 +120,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
893
120
  }
894
121
  });
895
122
  // Resources handlers
896
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
897
- return {
898
- resources: [
899
- {
900
- uri: "k8s://default/pods",
901
- name: "Kubernetes Pods",
902
- mimeType: "application/json",
903
- description: "List of pods in the default namespace",
904
- },
905
- {
906
- uri: "k8s://default/deployments",
907
- name: "Kubernetes Deployments",
908
- mimeType: "application/json",
909
- description: "List of deployments in the default namespace",
910
- },
911
- {
912
- uri: "k8s://default/services",
913
- name: "Kubernetes Services",
914
- mimeType: "application/json",
915
- description: "List of services in the default namespace",
916
- },
917
- {
918
- uri: "k8s://namespaces",
919
- name: "Kubernetes Namespaces",
920
- mimeType: "application/json",
921
- description: "List of all namespaces",
922
- },
923
- {
924
- uri: "k8s://nodes",
925
- name: "Kubernetes Nodes",
926
- mimeType: "application/json",
927
- description: "List of all nodes in the cluster",
928
- },
929
- ],
930
- };
931
- });
932
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
933
- try {
934
- const uri = request.params.uri;
935
- const parts = uri.replace("k8s://", "").split("/");
936
- const isNamespaces = parts[0] === "namespaces";
937
- const isNodes = parts[0] === "nodes";
938
- if ((isNamespaces || isNodes) && parts.length === 1) {
939
- const fn = isNodes ? "listNode" : "listNamespace";
940
- const { body } = await k8sManager.getCoreApi()[fn]();
941
- return {
942
- contents: [
943
- {
944
- uri: request.params.uri,
945
- mimeType: "application/json",
946
- text: JSON.stringify(body.items, null, 2),
947
- },
948
- ],
949
- };
950
- }
951
- const [namespace, resourceType] = parts;
952
- switch (resourceType) {
953
- case "pods": {
954
- const { body } = await k8sManager
955
- .getCoreApi()
956
- .listNamespacedPod(namespace);
957
- return {
958
- contents: [
959
- {
960
- uri: request.params.uri,
961
- mimeType: "application/json",
962
- text: JSON.stringify(body.items, null, 2),
963
- },
964
- ],
965
- };
966
- }
967
- case "deployments": {
968
- const { body } = await k8sManager
969
- .getAppsApi()
970
- .listNamespacedDeployment(namespace);
971
- return {
972
- contents: [
973
- {
974
- uri: request.params.uri,
975
- mimeType: "application/json",
976
- text: JSON.stringify(body.items, null, 2),
977
- },
978
- ],
979
- };
980
- }
981
- case "services": {
982
- const { body } = await k8sManager
983
- .getCoreApi()
984
- .listNamespacedService(namespace);
985
- return {
986
- contents: [
987
- {
988
- uri: request.params.uri,
989
- mimeType: "application/json",
990
- text: JSON.stringify(body.items, null, 2),
991
- },
992
- ],
993
- };
994
- }
995
- default:
996
- throw new McpError(ErrorCode.InvalidRequest, `Unsupported resource type: ${resourceType}`);
997
- }
998
- }
999
- catch (error) {
1000
- if (error instanceof McpError)
1001
- throw error;
1002
- throw new McpError(ErrorCode.InternalError, `Failed to read resource: ${error}`);
1003
- }
1004
- });
123
+ const resourceHandlers = getResourceHandlers(k8sManager);
124
+ server.setRequestHandler(ListResourcesRequestSchema, resourceHandlers.listResources);
125
+ server.setRequestHandler(ReadResourceRequestSchema, resourceHandlers.readResource);
1005
126
  const transport = new StdioServerTransport();
1006
127
  await server.connect(transport);
1007
128
  ["SIGINT", "SIGTERM"].forEach((signal) => {