kubectl-mcp-server 1.12.0__py3-none-any.whl
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.
- kubectl_mcp_server-1.12.0.dist-info/METADATA +711 -0
- kubectl_mcp_server-1.12.0.dist-info/RECORD +45 -0
- kubectl_mcp_server-1.12.0.dist-info/WHEEL +5 -0
- kubectl_mcp_server-1.12.0.dist-info/entry_points.txt +3 -0
- kubectl_mcp_server-1.12.0.dist-info/licenses/LICENSE +21 -0
- kubectl_mcp_server-1.12.0.dist-info/top_level.txt +2 -0
- kubectl_mcp_tool/__init__.py +21 -0
- kubectl_mcp_tool/__main__.py +46 -0
- kubectl_mcp_tool/auth/__init__.py +13 -0
- kubectl_mcp_tool/auth/config.py +71 -0
- kubectl_mcp_tool/auth/scopes.py +148 -0
- kubectl_mcp_tool/auth/verifier.py +82 -0
- kubectl_mcp_tool/cli/__init__.py +9 -0
- kubectl_mcp_tool/cli/__main__.py +10 -0
- kubectl_mcp_tool/cli/cli.py +111 -0
- kubectl_mcp_tool/diagnostics.py +355 -0
- kubectl_mcp_tool/k8s_config.py +289 -0
- kubectl_mcp_tool/mcp_server.py +530 -0
- kubectl_mcp_tool/prompts/__init__.py +5 -0
- kubectl_mcp_tool/prompts/prompts.py +823 -0
- kubectl_mcp_tool/resources/__init__.py +5 -0
- kubectl_mcp_tool/resources/resources.py +305 -0
- kubectl_mcp_tool/tools/__init__.py +28 -0
- kubectl_mcp_tool/tools/browser.py +371 -0
- kubectl_mcp_tool/tools/cluster.py +315 -0
- kubectl_mcp_tool/tools/core.py +421 -0
- kubectl_mcp_tool/tools/cost.py +680 -0
- kubectl_mcp_tool/tools/deployments.py +381 -0
- kubectl_mcp_tool/tools/diagnostics.py +174 -0
- kubectl_mcp_tool/tools/helm.py +1561 -0
- kubectl_mcp_tool/tools/networking.py +296 -0
- kubectl_mcp_tool/tools/operations.py +501 -0
- kubectl_mcp_tool/tools/pods.py +582 -0
- kubectl_mcp_tool/tools/security.py +333 -0
- kubectl_mcp_tool/tools/storage.py +133 -0
- kubectl_mcp_tool/utils/__init__.py +17 -0
- kubectl_mcp_tool/utils/helpers.py +80 -0
- tests/__init__.py +9 -0
- tests/conftest.py +379 -0
- tests/test_auth.py +256 -0
- tests/test_browser.py +349 -0
- tests/test_prompts.py +536 -0
- tests/test_resources.py +343 -0
- tests/test_server.py +384 -0
- tests/test_tools.py +659 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from mcp.types import ToolAnnotations
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("mcp-server")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_core_tools(server, non_destructive: bool):
|
|
10
|
+
"""Register core Kubernetes resource tools."""
|
|
11
|
+
|
|
12
|
+
@server.tool(
|
|
13
|
+
annotations=ToolAnnotations(
|
|
14
|
+
title="Get Namespaces",
|
|
15
|
+
readOnlyHint=True,
|
|
16
|
+
),
|
|
17
|
+
)
|
|
18
|
+
def get_namespaces() -> Dict[str, Any]:
|
|
19
|
+
"""Get all Kubernetes namespaces."""
|
|
20
|
+
try:
|
|
21
|
+
from kubernetes import client, config
|
|
22
|
+
config.load_kube_config()
|
|
23
|
+
v1 = client.CoreV1Api()
|
|
24
|
+
namespaces = v1.list_namespace()
|
|
25
|
+
return {
|
|
26
|
+
"success": True,
|
|
27
|
+
"namespaces": [ns.metadata.name for ns in namespaces.items]
|
|
28
|
+
}
|
|
29
|
+
except Exception as e:
|
|
30
|
+
logger.error(f"Error getting namespaces: {e}")
|
|
31
|
+
return {"success": False, "error": str(e)}
|
|
32
|
+
|
|
33
|
+
@server.tool(
|
|
34
|
+
annotations=ToolAnnotations(
|
|
35
|
+
title="Get Services",
|
|
36
|
+
readOnlyHint=True,
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
def get_services(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
40
|
+
"""Get all services in the specified namespace."""
|
|
41
|
+
try:
|
|
42
|
+
from kubernetes import client, config
|
|
43
|
+
config.load_kube_config()
|
|
44
|
+
v1 = client.CoreV1Api()
|
|
45
|
+
if namespace:
|
|
46
|
+
services = v1.list_namespaced_service(namespace)
|
|
47
|
+
else:
|
|
48
|
+
services = v1.list_service_for_all_namespaces()
|
|
49
|
+
return {
|
|
50
|
+
"success": True,
|
|
51
|
+
"services": [
|
|
52
|
+
{
|
|
53
|
+
"name": svc.metadata.name,
|
|
54
|
+
"namespace": svc.metadata.namespace,
|
|
55
|
+
"type": svc.spec.type,
|
|
56
|
+
"cluster_ip": svc.spec.cluster_ip
|
|
57
|
+
} for svc in services.items
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"Error getting services: {e}")
|
|
62
|
+
return {"success": False, "error": str(e)}
|
|
63
|
+
|
|
64
|
+
@server.tool(
|
|
65
|
+
annotations=ToolAnnotations(
|
|
66
|
+
title="Get Nodes",
|
|
67
|
+
readOnlyHint=True,
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
def get_nodes() -> Dict[str, Any]:
|
|
71
|
+
"""Get all nodes in the cluster."""
|
|
72
|
+
try:
|
|
73
|
+
from kubernetes import client, config
|
|
74
|
+
config.load_kube_config()
|
|
75
|
+
v1 = client.CoreV1Api()
|
|
76
|
+
nodes = v1.list_node()
|
|
77
|
+
return {
|
|
78
|
+
"success": True,
|
|
79
|
+
"nodes": [
|
|
80
|
+
{
|
|
81
|
+
"name": node.metadata.name,
|
|
82
|
+
"status": (
|
|
83
|
+
"Ready"
|
|
84
|
+
if any(
|
|
85
|
+
cond.type == "Ready" and cond.status == "True"
|
|
86
|
+
for cond in node.status.conditions
|
|
87
|
+
)
|
|
88
|
+
else "NotReady"
|
|
89
|
+
),
|
|
90
|
+
"addresses": [
|
|
91
|
+
addr.address for addr in node.status.addresses
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
for node in nodes.items
|
|
95
|
+
],
|
|
96
|
+
}
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f"Error getting nodes: {e}")
|
|
99
|
+
return {"success": False, "error": str(e)}
|
|
100
|
+
|
|
101
|
+
@server.tool(
|
|
102
|
+
annotations=ToolAnnotations(
|
|
103
|
+
title="Get ConfigMaps",
|
|
104
|
+
readOnlyHint=True,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
def get_configmaps(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
108
|
+
"""Get all ConfigMaps in the specified namespace."""
|
|
109
|
+
try:
|
|
110
|
+
from kubernetes import client, config
|
|
111
|
+
config.load_kube_config()
|
|
112
|
+
v1 = client.CoreV1Api()
|
|
113
|
+
if namespace:
|
|
114
|
+
cms = v1.list_namespaced_config_map(namespace)
|
|
115
|
+
else:
|
|
116
|
+
cms = v1.list_config_map_for_all_namespaces()
|
|
117
|
+
return {
|
|
118
|
+
"success": True,
|
|
119
|
+
"configmaps": [
|
|
120
|
+
{
|
|
121
|
+
"name": cm.metadata.name,
|
|
122
|
+
"namespace": cm.metadata.namespace,
|
|
123
|
+
"data": cm.data
|
|
124
|
+
} for cm in cms.items
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Error getting ConfigMaps: {e}")
|
|
129
|
+
return {"success": False, "error": str(e)}
|
|
130
|
+
|
|
131
|
+
@server.tool(
|
|
132
|
+
annotations=ToolAnnotations(
|
|
133
|
+
title="Get Secrets",
|
|
134
|
+
readOnlyHint=True,
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
def get_secrets(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
138
|
+
"""Get all Secrets in the specified namespace."""
|
|
139
|
+
try:
|
|
140
|
+
from kubernetes import client, config
|
|
141
|
+
config.load_kube_config()
|
|
142
|
+
v1 = client.CoreV1Api()
|
|
143
|
+
if namespace:
|
|
144
|
+
secrets = v1.list_namespaced_secret(namespace)
|
|
145
|
+
else:
|
|
146
|
+
secrets = v1.list_secret_for_all_namespaces()
|
|
147
|
+
return {
|
|
148
|
+
"success": True,
|
|
149
|
+
"secrets": [
|
|
150
|
+
{
|
|
151
|
+
"name": secret.metadata.name,
|
|
152
|
+
"namespace": secret.metadata.namespace,
|
|
153
|
+
"type": secret.type
|
|
154
|
+
} for secret in secrets.items
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Error getting Secrets: {e}")
|
|
159
|
+
return {"success": False, "error": str(e)}
|
|
160
|
+
|
|
161
|
+
@server.tool(
|
|
162
|
+
annotations=ToolAnnotations(
|
|
163
|
+
title="Get Events",
|
|
164
|
+
readOnlyHint=True,
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
def get_events(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
168
|
+
"""Get Kubernetes events."""
|
|
169
|
+
try:
|
|
170
|
+
from kubernetes import client, config
|
|
171
|
+
config.load_kube_config()
|
|
172
|
+
v1 = client.CoreV1Api()
|
|
173
|
+
if namespace:
|
|
174
|
+
events = v1.list_namespaced_event(namespace)
|
|
175
|
+
else:
|
|
176
|
+
events = v1.list_event_for_all_namespaces()
|
|
177
|
+
return {
|
|
178
|
+
"success": True,
|
|
179
|
+
"events": [
|
|
180
|
+
{
|
|
181
|
+
"name": event.metadata.name,
|
|
182
|
+
"namespace": event.metadata.namespace,
|
|
183
|
+
"type": event.type,
|
|
184
|
+
"reason": event.reason,
|
|
185
|
+
"message": event.message,
|
|
186
|
+
"timestamp": event.last_timestamp.isoformat() if event.last_timestamp else None
|
|
187
|
+
} for event in events.items
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Error getting events: {e}")
|
|
192
|
+
return {"success": False, "error": str(e)}
|
|
193
|
+
|
|
194
|
+
@server.tool(
|
|
195
|
+
annotations=ToolAnnotations(
|
|
196
|
+
title="Get Resource Usage",
|
|
197
|
+
readOnlyHint=True,
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
def get_resource_usage(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
201
|
+
"""Get resource usage metrics for pods."""
|
|
202
|
+
try:
|
|
203
|
+
import subprocess
|
|
204
|
+
cmd = ["kubectl", "top", "pods"]
|
|
205
|
+
if namespace:
|
|
206
|
+
cmd.extend(["-n", namespace])
|
|
207
|
+
else:
|
|
208
|
+
cmd.append("--all-namespaces")
|
|
209
|
+
cmd.append("--no-headers")
|
|
210
|
+
|
|
211
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
212
|
+
if result.returncode != 0:
|
|
213
|
+
return {"success": False, "error": result.stderr or "Failed to get metrics"}
|
|
214
|
+
|
|
215
|
+
pods = []
|
|
216
|
+
for line in result.stdout.strip().split("\n"):
|
|
217
|
+
if not line.strip():
|
|
218
|
+
continue
|
|
219
|
+
parts = line.split()
|
|
220
|
+
if namespace:
|
|
221
|
+
if len(parts) >= 3:
|
|
222
|
+
pods.append({
|
|
223
|
+
"name": parts[0],
|
|
224
|
+
"cpu": parts[1],
|
|
225
|
+
"memory": parts[2]
|
|
226
|
+
})
|
|
227
|
+
else:
|
|
228
|
+
if len(parts) >= 4:
|
|
229
|
+
pods.append({
|
|
230
|
+
"namespace": parts[0],
|
|
231
|
+
"name": parts[1],
|
|
232
|
+
"cpu": parts[2],
|
|
233
|
+
"memory": parts[3]
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
return {"success": True, "pods": pods}
|
|
237
|
+
except subprocess.TimeoutExpired:
|
|
238
|
+
return {"success": False, "error": "Metrics retrieval timed out"}
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Error getting resource usage: {e}")
|
|
241
|
+
return {"success": False, "error": str(e)}
|
|
242
|
+
|
|
243
|
+
@server.tool(
|
|
244
|
+
annotations=ToolAnnotations(
|
|
245
|
+
title="Get Service Accounts",
|
|
246
|
+
readOnlyHint=True,
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
def get_service_accounts(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
250
|
+
"""Get service accounts in a namespace or cluster-wide."""
|
|
251
|
+
try:
|
|
252
|
+
from kubernetes import client, config
|
|
253
|
+
config.load_kube_config()
|
|
254
|
+
v1 = client.CoreV1Api()
|
|
255
|
+
|
|
256
|
+
if namespace:
|
|
257
|
+
sas = v1.list_namespaced_service_account(namespace)
|
|
258
|
+
else:
|
|
259
|
+
sas = v1.list_service_account_for_all_namespaces()
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"success": True,
|
|
263
|
+
"serviceAccounts": [
|
|
264
|
+
{
|
|
265
|
+
"name": sa.metadata.name,
|
|
266
|
+
"namespace": sa.metadata.namespace,
|
|
267
|
+
"secrets": [s.name for s in (sa.secrets or [])],
|
|
268
|
+
"imagePullSecrets": [s.name for s in (sa.image_pull_secrets or [])],
|
|
269
|
+
"automountToken": sa.automount_service_account_token
|
|
270
|
+
}
|
|
271
|
+
for sa in sas.items
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"Error getting service accounts: {e}")
|
|
276
|
+
return {"success": False, "error": str(e)}
|
|
277
|
+
|
|
278
|
+
@server.tool(
|
|
279
|
+
annotations=ToolAnnotations(
|
|
280
|
+
title="Get Custom Resource Definitions",
|
|
281
|
+
readOnlyHint=True,
|
|
282
|
+
),
|
|
283
|
+
)
|
|
284
|
+
def get_crds(group: Optional[str] = None) -> Dict[str, Any]:
|
|
285
|
+
"""Get Custom Resource Definitions in the cluster."""
|
|
286
|
+
try:
|
|
287
|
+
from kubernetes import client, config
|
|
288
|
+
config.load_kube_config()
|
|
289
|
+
api = client.ApiextensionsV1Api()
|
|
290
|
+
|
|
291
|
+
crds = api.list_custom_resource_definition()
|
|
292
|
+
|
|
293
|
+
result = []
|
|
294
|
+
for crd in crds.items:
|
|
295
|
+
if group and crd.spec.group != group:
|
|
296
|
+
continue
|
|
297
|
+
result.append({
|
|
298
|
+
"name": crd.metadata.name,
|
|
299
|
+
"group": crd.spec.group,
|
|
300
|
+
"version": crd.spec.versions[0].name if crd.spec.versions else None,
|
|
301
|
+
"scope": crd.spec.scope,
|
|
302
|
+
"kind": crd.spec.names.kind,
|
|
303
|
+
"plural": crd.spec.names.plural,
|
|
304
|
+
"established": any(
|
|
305
|
+
c.type == "Established" and c.status == "True"
|
|
306
|
+
for c in (crd.status.conditions or [])
|
|
307
|
+
)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
return {"success": True, "crds": result}
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Error getting CRDs: {e}")
|
|
313
|
+
return {"success": False, "error": str(e)}
|
|
314
|
+
|
|
315
|
+
@server.tool(
|
|
316
|
+
annotations=ToolAnnotations(
|
|
317
|
+
title="Get Resource Quotas",
|
|
318
|
+
readOnlyHint=True,
|
|
319
|
+
),
|
|
320
|
+
)
|
|
321
|
+
def get_resource_quotas(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
322
|
+
"""Get resource quotas for namespaces."""
|
|
323
|
+
try:
|
|
324
|
+
from kubernetes import client, config
|
|
325
|
+
config.load_kube_config()
|
|
326
|
+
v1 = client.CoreV1Api()
|
|
327
|
+
|
|
328
|
+
if namespace:
|
|
329
|
+
quotas = v1.list_namespaced_resource_quota(namespace)
|
|
330
|
+
else:
|
|
331
|
+
quotas = v1.list_resource_quota_for_all_namespaces()
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
"success": True,
|
|
335
|
+
"quotas": [
|
|
336
|
+
{
|
|
337
|
+
"name": q.metadata.name,
|
|
338
|
+
"namespace": q.metadata.namespace,
|
|
339
|
+
"hard": q.status.hard,
|
|
340
|
+
"used": q.status.used
|
|
341
|
+
}
|
|
342
|
+
for q in quotas.items
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.error(f"Error getting resource quotas: {e}")
|
|
347
|
+
return {"success": False, "error": str(e)}
|
|
348
|
+
|
|
349
|
+
@server.tool(
|
|
350
|
+
annotations=ToolAnnotations(
|
|
351
|
+
title="Get Limit Ranges",
|
|
352
|
+
readOnlyHint=True,
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
def get_limit_ranges(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
356
|
+
"""Get limit ranges for namespaces."""
|
|
357
|
+
try:
|
|
358
|
+
from kubernetes import client, config
|
|
359
|
+
config.load_kube_config()
|
|
360
|
+
v1 = client.CoreV1Api()
|
|
361
|
+
|
|
362
|
+
if namespace:
|
|
363
|
+
limits = v1.list_namespaced_limit_range(namespace)
|
|
364
|
+
else:
|
|
365
|
+
limits = v1.list_limit_range_for_all_namespaces()
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
"success": True,
|
|
369
|
+
"limitRanges": [
|
|
370
|
+
{
|
|
371
|
+
"name": lr.metadata.name,
|
|
372
|
+
"namespace": lr.metadata.namespace,
|
|
373
|
+
"limits": [
|
|
374
|
+
{
|
|
375
|
+
"type": item.type,
|
|
376
|
+
"default": item.default,
|
|
377
|
+
"defaultRequest": item.default_request,
|
|
378
|
+
"max": item.max,
|
|
379
|
+
"min": item.min
|
|
380
|
+
}
|
|
381
|
+
for item in (lr.spec.limits or [])
|
|
382
|
+
]
|
|
383
|
+
}
|
|
384
|
+
for lr in limits.items
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
except Exception as e:
|
|
388
|
+
logger.error(f"Error getting limit ranges: {e}")
|
|
389
|
+
return {"success": False, "error": str(e)}
|
|
390
|
+
|
|
391
|
+
@server.tool(
|
|
392
|
+
annotations=ToolAnnotations(
|
|
393
|
+
title="Get Priority Classes",
|
|
394
|
+
readOnlyHint=True,
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
def get_priority_classes() -> Dict[str, Any]:
|
|
398
|
+
"""Get priority classes in the cluster."""
|
|
399
|
+
try:
|
|
400
|
+
from kubernetes import client, config
|
|
401
|
+
config.load_kube_config()
|
|
402
|
+
api = client.SchedulingV1Api()
|
|
403
|
+
|
|
404
|
+
pcs = api.list_priority_class()
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
"success": True,
|
|
408
|
+
"priorityClasses": [
|
|
409
|
+
{
|
|
410
|
+
"name": pc.metadata.name,
|
|
411
|
+
"value": pc.value,
|
|
412
|
+
"globalDefault": pc.global_default,
|
|
413
|
+
"description": pc.description,
|
|
414
|
+
"preemptionPolicy": pc.preemption_policy
|
|
415
|
+
}
|
|
416
|
+
for pc in pcs.items
|
|
417
|
+
]
|
|
418
|
+
}
|
|
419
|
+
except Exception as e:
|
|
420
|
+
logger.error(f"Error getting priority classes: {e}")
|
|
421
|
+
return {"success": False, "error": str(e)}
|