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 +21 -0
- package/README.md +61 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +599 -0
- package/dist/types.d.ts +312 -0
- package/dist/types.js +82 -0
- package/dist/unit.test.d.ts +1 -0
- package/dist/unit.test.js +141 -0
- package/package.json +46 -0
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.
|
package/dist/index.d.ts
ADDED
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);
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|