mcp-server-kubernetes 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Suyog Sonwalkar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # mcp-server-kubernetes
2
+
3
+ MCP Server that can connect to a Kubernetes cluster and manage it.
4
+
5
+ ## How to run tests locally
6
+
7
+ ```bash
8
+ git clone https://github.com/Flux159/mcp-server-kubernetes.git
9
+ cd mcp-server-kubernetes
10
+ bun install
11
+ bun run test
12
+ ```
13
+
14
+ ## Usage with Claude Desktop
15
+
16
+ Clone the repo, install the dependencies, and build the dist folder:
17
+
18
+ ```
19
+ bun run build
20
+ ```
21
+
22
+ To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
23
+
24
+ Note that you can use `node` or `bun` to run the server. Tests will currently only run properly with bun at the moment though.
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "kubernetes": {
30
+ "command": "bun",
31
+ "args": ["/your/path/to/mcp-server-kubernetes/dist/index.js"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ The server will automatically connect to your current kubectl context. Make sure you have:
38
+
39
+ 1. kubectl installed and in your PATH
40
+ 2. A valid kubeconfig file with contexts configured
41
+ 3. Access to a Kubernetes cluster configured for kubectl (e.g. minikube, Rancher Desktop, GKE, etc.)
42
+
43
+ You can verify your connection by asking Claude to list your pods or create a test deployment.
44
+
45
+ ## Features
46
+
47
+ - [x] Connect to a Kubernetes cluster
48
+ - [x] List all pods
49
+ - [x] List all services
50
+ - [x] List all deployments
51
+ - [x] Create a pod
52
+ - [x] Delete a pod
53
+ - [x] List all namespaces
54
+ - [] Port forward to a pod
55
+ - [] Get logs from a pod for debugging
56
+ - [] Choose namespace for next commands (memory)
57
+ - [] Support Helm for installing charts
58
+
59
+ ## Not planned
60
+
61
+ Authentication / adding clusters to kubectx.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,599 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
+ import * as k8s from "@kubernetes/client-node";
6
+ class KubernetesManager {
7
+ resources = [];
8
+ portForwards = [];
9
+ watches = [];
10
+ kc;
11
+ k8sApi;
12
+ k8sAppsApi;
13
+ constructor() {
14
+ this.kc = new k8s.KubeConfig();
15
+ this.kc.loadFromDefault();
16
+ this.k8sApi = this.kc.makeApiClient(k8s.CoreV1Api);
17
+ this.k8sAppsApi = this.kc.makeApiClient(k8s.AppsV1Api);
18
+ // process.on("SIGINT", () => this.cleanup());
19
+ // process.on("SIGTERM", () => this.cleanup());
20
+ }
21
+ async cleanup() {
22
+ console.log("Cleaning up resources...");
23
+ // Stop port forwards
24
+ // for (const pf of this.portForwards) {
25
+ // try {
26
+ // await pf.server.stop();
27
+ // } catch (error) {
28
+ // console.error(`Failed to close port-forward ${pf.id}:`, error);
29
+ // }
30
+ // }
31
+ // Stop watches
32
+ for (const watch of this.watches) {
33
+ watch.abort.abort();
34
+ }
35
+ // Delete tracked resources in reverse order
36
+ for (const resource of [...this.resources].reverse()) {
37
+ try {
38
+ await this.deleteResource(resource.kind, resource.name, resource.namespace);
39
+ }
40
+ catch (error) {
41
+ console.error(`Failed to delete ${resource.kind} ${resource.name}:`, error);
42
+ }
43
+ }
44
+ }
45
+ trackResource(kind, name, namespace) {
46
+ this.resources.push({ kind, name, namespace, createdAt: new Date() });
47
+ }
48
+ async deleteResource(kind, name, namespace) {
49
+ switch (kind.toLowerCase()) {
50
+ case "pod":
51
+ await this.k8sApi.deleteNamespacedPod(name, namespace);
52
+ break;
53
+ case "deployment":
54
+ await this.k8sAppsApi.deleteNamespacedDeployment(name, namespace);
55
+ break;
56
+ case "service":
57
+ await this.k8sApi.deleteNamespacedService(name, namespace);
58
+ break;
59
+ }
60
+ this.resources = this.resources.filter((r) => !(r.kind === kind && r.name === name && r.namespace === namespace));
61
+ }
62
+ trackPortForward(pf) {
63
+ this.portForwards.push(pf);
64
+ }
65
+ getPortForward(id) {
66
+ return this.portForwards.find((p) => p.id === id);
67
+ }
68
+ removePortForward(id) {
69
+ this.portForwards = this.portForwards.filter((p) => p.id !== id);
70
+ }
71
+ trackWatch(watch) {
72
+ this.watches.push(watch);
73
+ }
74
+ getKubeConfig() {
75
+ return this.kc;
76
+ }
77
+ getCoreApi() {
78
+ return this.k8sApi;
79
+ }
80
+ getAppsApi() {
81
+ return this.k8sAppsApi;
82
+ }
83
+ }
84
+ const k8sManager = new KubernetesManager();
85
+ // Template configurations with health checks and resource limits
86
+ const containerTemplates = {
87
+ ubuntu: {
88
+ name: "main",
89
+ image: "ubuntu:latest",
90
+ command: ["/bin/bash"],
91
+ args: ["-c", "sleep infinity"],
92
+ resources: {
93
+ limits: {
94
+ cpu: "200m",
95
+ memory: "256Mi",
96
+ },
97
+ requests: {
98
+ cpu: "100m",
99
+ memory: "128Mi",
100
+ },
101
+ },
102
+ livenessProbe: {
103
+ exec: {
104
+ command: ["cat", "/proc/1/status"],
105
+ },
106
+ initialDelaySeconds: 5,
107
+ periodSeconds: 10,
108
+ },
109
+ },
110
+ nginx: {
111
+ name: "main",
112
+ image: "nginx:latest",
113
+ ports: [{ containerPort: 80 }],
114
+ resources: {
115
+ limits: {
116
+ cpu: "200m",
117
+ memory: "256Mi",
118
+ },
119
+ requests: {
120
+ cpu: "100m",
121
+ memory: "128Mi",
122
+ },
123
+ },
124
+ livenessProbe: {
125
+ httpGet: {
126
+ path: "/",
127
+ port: 80,
128
+ },
129
+ initialDelaySeconds: 5,
130
+ periodSeconds: 10,
131
+ },
132
+ readinessProbe: {
133
+ httpGet: {
134
+ path: "/",
135
+ port: 80,
136
+ },
137
+ initialDelaySeconds: 2,
138
+ periodSeconds: 5,
139
+ },
140
+ },
141
+ busybox: {
142
+ name: "main",
143
+ image: "busybox:latest",
144
+ command: ["sh"],
145
+ args: ["-c", "sleep infinity"],
146
+ resources: {
147
+ limits: {
148
+ cpu: "100m",
149
+ memory: "64Mi",
150
+ },
151
+ requests: {
152
+ cpu: "50m",
153
+ memory: "32Mi",
154
+ },
155
+ },
156
+ livenessProbe: {
157
+ exec: {
158
+ command: ["true"],
159
+ },
160
+ periodSeconds: 10,
161
+ },
162
+ },
163
+ alpine: {
164
+ name: "main",
165
+ image: "alpine:latest",
166
+ command: ["sh"],
167
+ args: ["-c", "sleep infinity"],
168
+ resources: {
169
+ limits: {
170
+ cpu: "100m",
171
+ memory: "64Mi",
172
+ },
173
+ requests: {
174
+ cpu: "50m",
175
+ memory: "32Mi",
176
+ },
177
+ },
178
+ livenessProbe: {
179
+ exec: {
180
+ command: ["true"],
181
+ },
182
+ periodSeconds: 10,
183
+ },
184
+ },
185
+ };
186
+ const server = new Server({
187
+ name: "kubernetes",
188
+ version: "0.1.0",
189
+ }, {
190
+ capabilities: {
191
+ resources: {},
192
+ tools: {},
193
+ },
194
+ });
195
+ // Tools handlers
196
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
197
+ return {
198
+ tools: [
199
+ {
200
+ name: "list_pods",
201
+ description: "List pods in a namespace",
202
+ inputSchema: {
203
+ type: "object",
204
+ properties: {
205
+ namespace: { type: "string", default: "default" },
206
+ },
207
+ required: ["namespace"],
208
+ },
209
+ },
210
+ {
211
+ name: "list_deployments",
212
+ description: "List deployments in a namespace",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: {
216
+ namespace: { type: "string", default: "default" },
217
+ },
218
+ required: ["namespace"],
219
+ },
220
+ },
221
+ {
222
+ name: "list_services",
223
+ description: "List services in a namespace",
224
+ inputSchema: {
225
+ type: "object",
226
+ properties: {
227
+ namespace: { type: "string", default: "default" },
228
+ },
229
+ required: ["namespace"],
230
+ },
231
+ },
232
+ {
233
+ name: "list_namespaces",
234
+ description: "List all namespaces",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {},
238
+ },
239
+ },
240
+ {
241
+ name: "create_pod",
242
+ description: "Create a new Kubernetes pod",
243
+ inputSchema: {
244
+ type: "object",
245
+ properties: {
246
+ name: { type: "string" },
247
+ namespace: { type: "string" },
248
+ template: {
249
+ type: "string",
250
+ enum: ["ubuntu", "nginx", "busybox", "alpine"],
251
+ },
252
+ command: {
253
+ type: "array",
254
+ items: { type: "string" },
255
+ optional: true,
256
+ },
257
+ },
258
+ required: ["name", "namespace", "template"],
259
+ },
260
+ },
261
+ {
262
+ name: "create_deployment",
263
+ description: "Create a new Kubernetes deployment",
264
+ inputSchema: {
265
+ type: "object",
266
+ properties: {
267
+ name: { type: "string" },
268
+ namespace: { type: "string" },
269
+ template: {
270
+ type: "string",
271
+ enum: ["ubuntu", "nginx", "busybox", "alpine"],
272
+ },
273
+ replicas: { type: "number", default: 1 },
274
+ ports: {
275
+ type: "array",
276
+ items: { type: "number" },
277
+ optional: true,
278
+ },
279
+ },
280
+ required: ["name", "namespace", "template"],
281
+ },
282
+ },
283
+ {
284
+ name: "delete_pod",
285
+ description: "Delete a Kubernetes pod",
286
+ inputSchema: {
287
+ type: "object",
288
+ properties: {
289
+ name: { type: "string" },
290
+ namespace: { type: "string" },
291
+ ignoreNotFound: { type: "boolean", default: false },
292
+ },
293
+ required: ["name", "namespace"],
294
+ },
295
+ },
296
+ {
297
+ name: "cleanup",
298
+ description: "Cleanup all managed resources",
299
+ inputSchema: {
300
+ type: "object",
301
+ properties: {},
302
+ },
303
+ },
304
+ ],
305
+ };
306
+ });
307
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
308
+ try {
309
+ const { name } = request.params;
310
+ const input = request.params.arguments;
311
+ switch (name) {
312
+ case "list_pods": {
313
+ const listPodsInput = input;
314
+ const namespace = listPodsInput.namespace || "default";
315
+ const { body } = await k8sManager
316
+ .getCoreApi()
317
+ .listNamespacedPod(namespace);
318
+ const pods = body.items.map((pod) => ({
319
+ name: pod.metadata?.name || "",
320
+ namespace: pod.metadata?.namespace || "",
321
+ status: pod.status?.phase,
322
+ createdAt: pod.metadata?.creationTimestamp,
323
+ }));
324
+ return {
325
+ content: [
326
+ {
327
+ type: "text",
328
+ text: JSON.stringify({ pods }, null, 2),
329
+ },
330
+ ],
331
+ };
332
+ }
333
+ case "list_deployments": {
334
+ const listDeploymentsInput = input;
335
+ const namespace = listDeploymentsInput.namespace || "default";
336
+ const { body } = await k8sManager
337
+ .getAppsApi()
338
+ .listNamespacedDeployment(namespace);
339
+ const deployments = body.items.map((deployment) => ({
340
+ name: deployment.metadata?.name || "",
341
+ namespace: deployment.metadata?.namespace || "",
342
+ replicas: deployment.spec?.replicas || 0,
343
+ availableReplicas: deployment.status?.availableReplicas || 0,
344
+ createdAt: deployment.metadata?.creationTimestamp,
345
+ }));
346
+ return {
347
+ content: [
348
+ {
349
+ type: "text",
350
+ text: JSON.stringify({ deployments }, null, 2),
351
+ },
352
+ ],
353
+ };
354
+ }
355
+ case "list_services": {
356
+ const listServicesInput = input;
357
+ const namespace = listServicesInput.namespace || "default";
358
+ const { body } = await k8sManager
359
+ .getCoreApi()
360
+ .listNamespacedService(namespace);
361
+ const services = body.items.map((service) => ({
362
+ name: service.metadata?.name || "",
363
+ namespace: service.metadata?.namespace || "",
364
+ type: service.spec?.type,
365
+ clusterIP: service.spec?.clusterIP,
366
+ ports: service.spec?.ports || [],
367
+ createdAt: service.metadata?.creationTimestamp,
368
+ }));
369
+ return {
370
+ content: [
371
+ {
372
+ type: "text",
373
+ text: JSON.stringify({ services }, null, 2),
374
+ },
375
+ ],
376
+ };
377
+ }
378
+ case "list_namespaces": {
379
+ const { body } = await k8sManager.getCoreApi().listNamespace();
380
+ const namespaces = body.items.map((ns) => ({
381
+ name: ns.metadata?.name || "",
382
+ status: ns.status?.phase || "",
383
+ createdAt: ns.metadata?.creationTimestamp,
384
+ }));
385
+ return {
386
+ content: [
387
+ {
388
+ type: "text",
389
+ text: JSON.stringify({ namespaces }, null, 2),
390
+ },
391
+ ],
392
+ };
393
+ }
394
+ case "create_pod": {
395
+ console.error("calling create_pod");
396
+ console.error(input);
397
+ console.error(request);
398
+ const createPodInput = input;
399
+ const templateConfig = containerTemplates[createPodInput.template];
400
+ const pod = {
401
+ apiVersion: "v1",
402
+ kind: "Pod",
403
+ metadata: {
404
+ name: createPodInput.name,
405
+ namespace: createPodInput.namespace,
406
+ labels: {
407
+ "mcp-managed": "true",
408
+ app: createPodInput.name,
409
+ },
410
+ },
411
+ spec: {
412
+ containers: [
413
+ {
414
+ ...templateConfig,
415
+ ...(createPodInput.command && {
416
+ command: createPodInput.command,
417
+ }),
418
+ },
419
+ ],
420
+ },
421
+ };
422
+ const { body } = await k8sManager
423
+ .getCoreApi()
424
+ .createNamespacedPod(createPodInput.namespace, pod);
425
+ k8sManager.trackResource("Pod", createPodInput.name, createPodInput.namespace);
426
+ return {
427
+ content: [
428
+ {
429
+ type: "text",
430
+ text: JSON.stringify({
431
+ podName: body.metadata.name,
432
+ status: "created",
433
+ }, null, 2),
434
+ },
435
+ ],
436
+ };
437
+ }
438
+ case "delete_pod": {
439
+ const deletePodInput = input;
440
+ try {
441
+ await k8sManager
442
+ .getCoreApi()
443
+ .deleteNamespacedPod(deletePodInput.name, deletePodInput.namespace);
444
+ return {
445
+ content: [
446
+ {
447
+ type: "text",
448
+ text: JSON.stringify({
449
+ success: true,
450
+ status: "deleted",
451
+ }, null, 2),
452
+ },
453
+ ],
454
+ };
455
+ }
456
+ catch (error) {
457
+ if (deletePodInput.ignoreNotFound &&
458
+ error.response?.statusCode === 404) {
459
+ return {
460
+ content: [
461
+ {
462
+ type: "text",
463
+ text: JSON.stringify({
464
+ success: true,
465
+ status: "not_found",
466
+ }, null, 2),
467
+ },
468
+ ],
469
+ };
470
+ }
471
+ throw error;
472
+ }
473
+ }
474
+ case "cleanup": {
475
+ await k8sManager.cleanup();
476
+ return {
477
+ content: [
478
+ {
479
+ type: "text",
480
+ text: JSON.stringify({
481
+ success: true,
482
+ }, null, 2),
483
+ },
484
+ ],
485
+ };
486
+ }
487
+ default:
488
+ throw new McpError(ErrorCode.InvalidRequest, `Unknown tool: ${name}`);
489
+ }
490
+ }
491
+ catch (error) {
492
+ if (error instanceof McpError)
493
+ throw error;
494
+ throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error}`);
495
+ }
496
+ });
497
+ // Resources handlers
498
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
499
+ return {
500
+ resources: [
501
+ {
502
+ uri: "k8s://default/pods",
503
+ name: "Kubernetes Pods",
504
+ mimeType: "application/json",
505
+ description: "List of pods in the default namespace",
506
+ },
507
+ {
508
+ uri: "k8s://default/deployments",
509
+ name: "Kubernetes Deployments",
510
+ mimeType: "application/json",
511
+ description: "List of deployments in the default namespace",
512
+ },
513
+ {
514
+ uri: "k8s://default/services",
515
+ name: "Kubernetes Services",
516
+ mimeType: "application/json",
517
+ description: "List of services in the default namespace",
518
+ },
519
+ {
520
+ uri: "k8s://namespaces",
521
+ name: "Kubernetes Namespaces",
522
+ mimeType: "application/json",
523
+ description: "List of all namespaces",
524
+ },
525
+ ],
526
+ };
527
+ });
528
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
529
+ try {
530
+ const uri = request.params.uri;
531
+ const parts = uri.replace("k8s://", "").split("/");
532
+ if (parts[0] === "namespaces" && parts.length === 1) {
533
+ const { body } = await k8sManager.getCoreApi().listNamespace();
534
+ return {
535
+ contents: [
536
+ {
537
+ uri: request.params.uri,
538
+ mimeType: "application/json",
539
+ text: JSON.stringify(body.items, null, 2),
540
+ },
541
+ ],
542
+ };
543
+ }
544
+ const [namespace, resourceType] = parts;
545
+ switch (resourceType) {
546
+ case "pods": {
547
+ const { body } = await k8sManager
548
+ .getCoreApi()
549
+ .listNamespacedPod(namespace);
550
+ return {
551
+ contents: [
552
+ {
553
+ uri: request.params.uri,
554
+ mimeType: "application/json",
555
+ text: JSON.stringify(body.items, null, 2),
556
+ },
557
+ ],
558
+ };
559
+ }
560
+ case "deployments": {
561
+ const { body } = await k8sManager
562
+ .getAppsApi()
563
+ .listNamespacedDeployment(namespace);
564
+ return {
565
+ contents: [
566
+ {
567
+ uri: request.params.uri,
568
+ mimeType: "application/json",
569
+ text: JSON.stringify(body.items, null, 2),
570
+ },
571
+ ],
572
+ };
573
+ }
574
+ case "services": {
575
+ const { body } = await k8sManager
576
+ .getCoreApi()
577
+ .listNamespacedService(namespace);
578
+ return {
579
+ contents: [
580
+ {
581
+ uri: request.params.uri,
582
+ mimeType: "application/json",
583
+ text: JSON.stringify(body.items, null, 2),
584
+ },
585
+ ],
586
+ };
587
+ }
588
+ default:
589
+ throw new McpError(ErrorCode.InvalidRequest, `Unsupported resource type: ${resourceType}`);
590
+ }
591
+ }
592
+ catch (error) {
593
+ if (error instanceof McpError)
594
+ throw error;
595
+ throw new McpError(ErrorCode.InternalError, `Failed to read resource: ${error}`);
596
+ }
597
+ });
598
+ const transport = new StdioServerTransport();
599
+ await server.connect(transport);
@@ -0,0 +1,312 @@
1
+ import { z } from "zod";
2
+ export declare const ContainerTemplate: z.ZodEnum<["ubuntu", "nginx", "busybox", "alpine"]>;
3
+ export declare const ResourceSchema: z.ZodObject<{
4
+ uri: z.ZodString;
5
+ name: z.ZodString;
6
+ description: z.ZodString;
7
+ }, "strip", z.ZodTypeAny, {
8
+ uri: string;
9
+ name: string;
10
+ description: string;
11
+ }, {
12
+ uri: string;
13
+ name: string;
14
+ description: string;
15
+ }>;
16
+ export declare const ToolSchema: z.ZodObject<{
17
+ name: z.ZodString;
18
+ description: z.ZodString;
19
+ inputSchema: z.ZodRecord<z.ZodString, z.ZodAny>;
20
+ }, "strip", z.ZodTypeAny, {
21
+ name: string;
22
+ description: string;
23
+ inputSchema: Record<string, any>;
24
+ }, {
25
+ name: string;
26
+ description: string;
27
+ inputSchema: Record<string, any>;
28
+ }>;
29
+ export declare const ListToolsResponseSchema: z.ZodObject<{
30
+ tools: z.ZodArray<z.ZodObject<{
31
+ name: z.ZodString;
32
+ description: z.ZodString;
33
+ inputSchema: z.ZodRecord<z.ZodString, z.ZodAny>;
34
+ }, "strip", z.ZodTypeAny, {
35
+ name: string;
36
+ description: string;
37
+ inputSchema: Record<string, any>;
38
+ }, {
39
+ name: string;
40
+ description: string;
41
+ inputSchema: Record<string, any>;
42
+ }>, "many">;
43
+ }, "strip", z.ZodTypeAny, {
44
+ tools: {
45
+ name: string;
46
+ description: string;
47
+ inputSchema: Record<string, any>;
48
+ }[];
49
+ }, {
50
+ tools: {
51
+ name: string;
52
+ description: string;
53
+ inputSchema: Record<string, any>;
54
+ }[];
55
+ }>;
56
+ export declare const CreatePodResponseSchema: z.ZodObject<{
57
+ content: z.ZodArray<z.ZodObject<{
58
+ type: z.ZodLiteral<"text">;
59
+ text: z.ZodString;
60
+ }, "strip", z.ZodTypeAny, {
61
+ type: "text";
62
+ text: string;
63
+ }, {
64
+ type: "text";
65
+ text: string;
66
+ }>, "many">;
67
+ }, "strip", z.ZodTypeAny, {
68
+ content: {
69
+ type: "text";
70
+ text: string;
71
+ }[];
72
+ }, {
73
+ content: {
74
+ type: "text";
75
+ text: string;
76
+ }[];
77
+ }>;
78
+ export declare const CreateDeploymentResponseSchema: z.ZodObject<{
79
+ content: z.ZodArray<z.ZodObject<{
80
+ type: z.ZodLiteral<"text">;
81
+ text: z.ZodString;
82
+ }, "strip", z.ZodTypeAny, {
83
+ type: "text";
84
+ text: string;
85
+ }, {
86
+ type: "text";
87
+ text: string;
88
+ }>, "many">;
89
+ }, "strip", z.ZodTypeAny, {
90
+ content: {
91
+ type: "text";
92
+ text: string;
93
+ }[];
94
+ }, {
95
+ content: {
96
+ type: "text";
97
+ text: string;
98
+ }[];
99
+ }>;
100
+ export declare const DeletePodResponseSchema: z.ZodObject<{
101
+ content: z.ZodArray<z.ZodObject<{
102
+ type: z.ZodLiteral<"text">;
103
+ text: z.ZodString;
104
+ }, "strip", z.ZodTypeAny, {
105
+ type: "text";
106
+ text: string;
107
+ }, {
108
+ type: "text";
109
+ text: string;
110
+ }>, "many">;
111
+ }, "strip", z.ZodTypeAny, {
112
+ content: {
113
+ type: "text";
114
+ text: string;
115
+ }[];
116
+ }, {
117
+ content: {
118
+ type: "text";
119
+ text: string;
120
+ }[];
121
+ }>;
122
+ export declare const CleanupResponseSchema: z.ZodObject<{
123
+ content: z.ZodArray<z.ZodObject<{
124
+ type: z.ZodLiteral<"text">;
125
+ text: z.ZodString;
126
+ }, "strip", z.ZodTypeAny, {
127
+ type: "text";
128
+ text: string;
129
+ }, {
130
+ type: "text";
131
+ text: string;
132
+ }>, "many">;
133
+ }, "strip", z.ZodTypeAny, {
134
+ content: {
135
+ type: "text";
136
+ text: string;
137
+ }[];
138
+ }, {
139
+ content: {
140
+ type: "text";
141
+ text: string;
142
+ }[];
143
+ }>;
144
+ export declare const ListPodsResponseSchema: z.ZodObject<{
145
+ content: z.ZodArray<z.ZodObject<{
146
+ type: z.ZodLiteral<"text">;
147
+ text: z.ZodString;
148
+ }, "strip", z.ZodTypeAny, {
149
+ type: "text";
150
+ text: string;
151
+ }, {
152
+ type: "text";
153
+ text: string;
154
+ }>, "many">;
155
+ }, "strip", z.ZodTypeAny, {
156
+ content: {
157
+ type: "text";
158
+ text: string;
159
+ }[];
160
+ }, {
161
+ content: {
162
+ type: "text";
163
+ text: string;
164
+ }[];
165
+ }>;
166
+ export declare const ListDeploymentsResponseSchema: z.ZodObject<{
167
+ content: z.ZodArray<z.ZodObject<{
168
+ type: z.ZodLiteral<"text">;
169
+ text: z.ZodString;
170
+ }, "strip", z.ZodTypeAny, {
171
+ type: "text";
172
+ text: string;
173
+ }, {
174
+ type: "text";
175
+ text: string;
176
+ }>, "many">;
177
+ }, "strip", z.ZodTypeAny, {
178
+ content: {
179
+ type: "text";
180
+ text: string;
181
+ }[];
182
+ }, {
183
+ content: {
184
+ type: "text";
185
+ text: string;
186
+ }[];
187
+ }>;
188
+ export declare const ListServicesResponseSchema: z.ZodObject<{
189
+ content: z.ZodArray<z.ZodObject<{
190
+ type: z.ZodLiteral<"text">;
191
+ text: z.ZodString;
192
+ }, "strip", z.ZodTypeAny, {
193
+ type: "text";
194
+ text: string;
195
+ }, {
196
+ type: "text";
197
+ text: string;
198
+ }>, "many">;
199
+ }, "strip", z.ZodTypeAny, {
200
+ content: {
201
+ type: "text";
202
+ text: string;
203
+ }[];
204
+ }, {
205
+ content: {
206
+ type: "text";
207
+ text: string;
208
+ }[];
209
+ }>;
210
+ export declare const ListNamespacesResponseSchema: z.ZodObject<{
211
+ content: z.ZodArray<z.ZodObject<{
212
+ type: z.ZodLiteral<"text">;
213
+ text: z.ZodString;
214
+ }, "strip", z.ZodTypeAny, {
215
+ type: "text";
216
+ text: string;
217
+ }, {
218
+ type: "text";
219
+ text: string;
220
+ }>, "many">;
221
+ }, "strip", z.ZodTypeAny, {
222
+ content: {
223
+ type: "text";
224
+ text: string;
225
+ }[];
226
+ }, {
227
+ content: {
228
+ type: "text";
229
+ text: string;
230
+ }[];
231
+ }>;
232
+ export declare const ListResourcesResponseSchema: z.ZodObject<{
233
+ resources: z.ZodArray<z.ZodObject<{
234
+ uri: z.ZodString;
235
+ name: z.ZodString;
236
+ description: z.ZodString;
237
+ }, "strip", z.ZodTypeAny, {
238
+ uri: string;
239
+ name: string;
240
+ description: string;
241
+ }, {
242
+ uri: string;
243
+ name: string;
244
+ description: string;
245
+ }>, "many">;
246
+ }, "strip", z.ZodTypeAny, {
247
+ resources: {
248
+ uri: string;
249
+ name: string;
250
+ description: string;
251
+ }[];
252
+ }, {
253
+ resources: {
254
+ uri: string;
255
+ name: string;
256
+ description: string;
257
+ }[];
258
+ }>;
259
+ export declare const ReadResourceResponseSchema: z.ZodObject<{
260
+ contents: z.ZodArray<z.ZodObject<{
261
+ uri: z.ZodString;
262
+ mimeType: z.ZodString;
263
+ text: z.ZodString;
264
+ }, "strip", z.ZodTypeAny, {
265
+ uri: string;
266
+ text: string;
267
+ mimeType: string;
268
+ }, {
269
+ uri: string;
270
+ text: string;
271
+ mimeType: string;
272
+ }>, "many">;
273
+ }, "strip", z.ZodTypeAny, {
274
+ contents: {
275
+ uri: string;
276
+ text: string;
277
+ mimeType: string;
278
+ }[];
279
+ }, {
280
+ contents: {
281
+ uri: string;
282
+ text: string;
283
+ mimeType: string;
284
+ }[];
285
+ }>;
286
+ export type K8sResource = z.infer<typeof ResourceSchema>;
287
+ export type K8sTool = z.infer<typeof ToolSchema>;
288
+ export interface ResourceTracker {
289
+ kind: string;
290
+ name: string;
291
+ namespace: string;
292
+ createdAt: Date;
293
+ }
294
+ export interface PortForwardTracker {
295
+ id: string;
296
+ server: {
297
+ stop: () => Promise<void>;
298
+ };
299
+ resourceType: string;
300
+ name: string;
301
+ namespace: string;
302
+ ports: {
303
+ local: number;
304
+ remote: number;
305
+ }[];
306
+ }
307
+ export interface WatchTracker {
308
+ id: string;
309
+ abort: AbortController;
310
+ resourceType: string;
311
+ namespace: string;
312
+ }
package/dist/types.js ADDED
@@ -0,0 +1,82 @@
1
+ import { z } from "zod";
2
+ // Kubernetes-specific types
3
+ export const ContainerTemplate = z.enum([
4
+ "ubuntu",
5
+ "nginx",
6
+ "busybox",
7
+ "alpine",
8
+ ]);
9
+ // Resource response schemas
10
+ export const ResourceSchema = z.object({
11
+ uri: z.string(),
12
+ name: z.string(),
13
+ description: z.string(),
14
+ });
15
+ // Tool response schemas
16
+ export const ToolSchema = z.object({
17
+ name: z.string(),
18
+ description: z.string(),
19
+ inputSchema: z.record(z.any()),
20
+ });
21
+ export const ListToolsResponseSchema = z.object({
22
+ tools: z.array(ToolSchema),
23
+ });
24
+ // Tool-specific response schemas
25
+ export const CreatePodResponseSchema = z.object({
26
+ content: z.array(z.object({
27
+ type: z.literal("text"),
28
+ text: z.string(),
29
+ })),
30
+ });
31
+ export const CreateDeploymentResponseSchema = z.object({
32
+ content: z.array(z.object({
33
+ type: z.literal("text"),
34
+ text: z.string(),
35
+ })),
36
+ });
37
+ export const DeletePodResponseSchema = z.object({
38
+ content: z.array(z.object({
39
+ type: z.literal("text"),
40
+ text: z.string(),
41
+ })),
42
+ });
43
+ export const CleanupResponseSchema = z.object({
44
+ content: z.array(z.object({
45
+ type: z.literal("text"),
46
+ text: z.string(),
47
+ })),
48
+ });
49
+ export const ListPodsResponseSchema = z.object({
50
+ content: z.array(z.object({
51
+ type: z.literal("text"),
52
+ text: z.string(),
53
+ })),
54
+ });
55
+ export const ListDeploymentsResponseSchema = z.object({
56
+ content: z.array(z.object({
57
+ type: z.literal("text"),
58
+ text: z.string(),
59
+ })),
60
+ });
61
+ export const ListServicesResponseSchema = z.object({
62
+ content: z.array(z.object({
63
+ type: z.literal("text"),
64
+ text: z.string(),
65
+ })),
66
+ });
67
+ export const ListNamespacesResponseSchema = z.object({
68
+ content: z.array(z.object({
69
+ type: z.literal("text"),
70
+ text: z.string(),
71
+ })),
72
+ });
73
+ export const ListResourcesResponseSchema = z.object({
74
+ resources: z.array(ResourceSchema),
75
+ });
76
+ export const ReadResourceResponseSchema = z.object({
77
+ contents: z.array(z.object({
78
+ uri: z.string(),
79
+ mimeType: z.string(),
80
+ text: z.string(),
81
+ })),
82
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,141 @@
1
+ import { expect, test } from "vitest";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { ListToolsResponseSchema, ListPodsResponseSchema, ListDeploymentsResponseSchema, ListNamespacesResponseSchema, CreatePodResponseSchema, DeletePodResponseSchema, CleanupResponseSchema, } from "./types.js";
5
+ async function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ test("kubernetes server operations", async () => {
9
+ const transport = new StdioClientTransport({
10
+ command: "bun",
11
+ args: ["src/index.ts"],
12
+ stderr: "pipe",
13
+ });
14
+ const client = new Client({
15
+ name: "test-client",
16
+ version: "1.0.0",
17
+ }, {
18
+ capabilities: {},
19
+ });
20
+ await client.connect(transport);
21
+ try {
22
+ // List available tools stays the same
23
+ console.log("Listing available tools...");
24
+ const toolsList = await client.request({
25
+ method: "tools/list",
26
+ }, ListToolsResponseSchema);
27
+ expect(toolsList.tools).toBeDefined();
28
+ expect(toolsList.tools.length).toBeGreaterThan(0);
29
+ // List namespaces
30
+ console.log("Listing namespaces...");
31
+ const namespacesResult = await client.request({
32
+ method: "tools/call",
33
+ params: {
34
+ name: "list_namespaces",
35
+ arguments: {}, // Changed from input to arguments
36
+ },
37
+ }, ListNamespacesResponseSchema);
38
+ expect(namespacesResult.content[0].type).toBe("text");
39
+ const namespaces = JSON.parse(namespacesResult.content[0].text);
40
+ expect(namespaces.namespaces).toBeDefined();
41
+ // Delete test pod if it exists
42
+ console.log("Deleting test pod if exists...");
43
+ const deletePodResult = await client.request({
44
+ method: "tools/call",
45
+ params: {
46
+ name: "delete_pod",
47
+ arguments: {
48
+ // Changed from input to arguments
49
+ name: "test-pod",
50
+ namespace: "default",
51
+ ignoreNotFound: true,
52
+ },
53
+ },
54
+ }, DeletePodResponseSchema);
55
+ expect(deletePodResult.content[0].type).toBe("text");
56
+ const deleteResult = JSON.parse(deletePodResult.content[0].text);
57
+ expect(deleteResult.success).toBe(true);
58
+ await sleep(2000);
59
+ // Create a pod
60
+ console.log("Creating test pod...");
61
+ const createPodResult = await client.request({
62
+ method: "tools/call",
63
+ params: {
64
+ name: "create_pod",
65
+ arguments: {
66
+ // Changed from input to arguments
67
+ name: "test-pod",
68
+ namespace: "default",
69
+ template: "nginx",
70
+ },
71
+ },
72
+ }, CreatePodResponseSchema);
73
+ expect(createPodResult.content[0].type).toBe("text");
74
+ const createResult = JSON.parse(createPodResult.content[0].text);
75
+ expect(createResult.podName).toBe("test-pod");
76
+ expect(createResult.status).toBe("created");
77
+ // List pods to verify creation
78
+ console.log("Listing pods...");
79
+ const listPodsResult = await client.request({
80
+ method: "tools/call",
81
+ params: {
82
+ name: "list_pods",
83
+ arguments: {
84
+ // Changed from input to arguments
85
+ namespace: "default",
86
+ },
87
+ },
88
+ }, ListPodsResponseSchema);
89
+ expect(listPodsResult.content[0].type).toBe("text");
90
+ const pods = JSON.parse(listPodsResult.content[0].text);
91
+ expect(pods.pods).toBeDefined();
92
+ expect(pods.pods.some((pod) => pod.name === "test-pod")).toBe(true);
93
+ // List deployments
94
+ console.log("Listing deployments...");
95
+ const listDeploymentsResult = await client.request({
96
+ method: "tools/call",
97
+ params: {
98
+ name: "list_deployments",
99
+ arguments: {
100
+ // Changed from input to arguments
101
+ namespace: "default",
102
+ },
103
+ },
104
+ }, ListDeploymentsResponseSchema);
105
+ expect(listDeploymentsResult.content[0].type).toBe("text");
106
+ const deployments = JSON.parse(listDeploymentsResult.content[0].text);
107
+ expect(deployments.deployments).toBeDefined();
108
+ // Cleanup
109
+ console.log("Cleaning up...");
110
+ const cleanupResult = await client.request({
111
+ method: "tools/call",
112
+ params: {
113
+ name: "cleanup",
114
+ arguments: {}, // Changed from input to arguments
115
+ },
116
+ }, CleanupResponseSchema);
117
+ expect(cleanupResult.content[0].type).toBe("text");
118
+ const cleanupData = JSON.parse(cleanupResult.content[0].text);
119
+ expect(cleanupData.success).toBe(true);
120
+ // Verify cleanup by listing pods again
121
+ console.log("Verifying cleanup...");
122
+ const finalPodsResult = await client.request({
123
+ method: "tools/call",
124
+ params: {
125
+ name: "list_pods",
126
+ arguments: {
127
+ // Changed from input to arguments
128
+ namespace: "default",
129
+ },
130
+ },
131
+ }, ListPodsResponseSchema);
132
+ const finalPods = JSON.parse(finalPodsResult.content[0].text);
133
+ console.log(finalPods);
134
+ // expect(finalPods.pods.some((pod: any) => pod.name === "test-pod")).toBe(
135
+ // false
136
+ // );
137
+ }
138
+ finally {
139
+ // await client.disconnect(); // Re-enabled client disconnect
140
+ }
141
+ });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "mcp-server-kubernetes",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "author": "Flux159",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/Flux159/mcp-server-kubernetes"
11
+ },
12
+ "bin": {
13
+ "mcp-server-kubernetes": "dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc && shx chmod +x dist/*.js",
20
+ "dev": "tsc --watch",
21
+ "start": "node dist/index.js",
22
+ "test": "vitest --testTimeout=20000",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "kubernetes",
28
+ "claude",
29
+ "anthropic",
30
+ "kubectl"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "dependencies": {
36
+ "@kubernetes/client-node": "^0.20.0",
37
+ "@modelcontextprotocol/sdk": "1.0.1",
38
+ "zod": "^3.22.4"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.9.3",
42
+ "shx": "^0.3.4",
43
+ "typescript": "^5.6.2",
44
+ "vitest": "2.1.8"
45
+ }
46
+ }