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,296 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from mcp.types import ToolAnnotations
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("mcp-server")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register_networking_tools(server, non_destructive: bool):
|
|
11
|
+
"""Register networking-related tools."""
|
|
12
|
+
|
|
13
|
+
@server.tool(
|
|
14
|
+
annotations=ToolAnnotations(
|
|
15
|
+
title="Get Ingress Resources",
|
|
16
|
+
readOnlyHint=True,
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
def get_ingress(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
20
|
+
"""Get Ingress resources in a namespace or cluster-wide."""
|
|
21
|
+
try:
|
|
22
|
+
from kubernetes import client, config
|
|
23
|
+
config.load_kube_config()
|
|
24
|
+
networking = client.NetworkingV1Api()
|
|
25
|
+
|
|
26
|
+
if namespace:
|
|
27
|
+
ingresses = networking.list_namespaced_ingress(namespace)
|
|
28
|
+
else:
|
|
29
|
+
ingresses = networking.list_ingress_for_all_namespaces()
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
"success": True,
|
|
33
|
+
"ingresses": [
|
|
34
|
+
{
|
|
35
|
+
"name": ing.metadata.name,
|
|
36
|
+
"namespace": ing.metadata.namespace,
|
|
37
|
+
"ingressClassName": ing.spec.ingress_class_name,
|
|
38
|
+
"rules": [
|
|
39
|
+
{
|
|
40
|
+
"host": rule.host,
|
|
41
|
+
"paths": [
|
|
42
|
+
{
|
|
43
|
+
"path": path.path,
|
|
44
|
+
"pathType": path.path_type,
|
|
45
|
+
"backend": {
|
|
46
|
+
"serviceName": path.backend.service.name if path.backend.service else None,
|
|
47
|
+
"servicePort": path.backend.service.port.number if path.backend.service and path.backend.service.port else None
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for path in (rule.http.paths if rule.http else [])
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
for rule in (ing.spec.rules or [])
|
|
54
|
+
],
|
|
55
|
+
"tls": [
|
|
56
|
+
{"hosts": tls.hosts, "secretName": tls.secret_name}
|
|
57
|
+
for tls in (ing.spec.tls or [])
|
|
58
|
+
] if ing.spec.tls else None
|
|
59
|
+
}
|
|
60
|
+
for ing in ingresses.items
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Error getting Ingresses: {e}")
|
|
65
|
+
return {"success": False, "error": str(e)}
|
|
66
|
+
|
|
67
|
+
@server.tool(
|
|
68
|
+
annotations=ToolAnnotations(
|
|
69
|
+
title="Get Endpoints",
|
|
70
|
+
readOnlyHint=True,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
def get_endpoints(namespace: Optional[str] = None, service_name: Optional[str] = None) -> Dict[str, Any]:
|
|
74
|
+
"""Get Endpoints for services."""
|
|
75
|
+
try:
|
|
76
|
+
from kubernetes import client, config
|
|
77
|
+
config.load_kube_config()
|
|
78
|
+
v1 = client.CoreV1Api()
|
|
79
|
+
|
|
80
|
+
if namespace:
|
|
81
|
+
endpoints = v1.list_namespaced_endpoints(namespace)
|
|
82
|
+
else:
|
|
83
|
+
endpoints = v1.list_endpoints_for_all_namespaces()
|
|
84
|
+
|
|
85
|
+
result = []
|
|
86
|
+
for ep in endpoints.items:
|
|
87
|
+
if service_name and ep.metadata.name != service_name:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
addresses = []
|
|
91
|
+
for subset in (ep.subsets or []):
|
|
92
|
+
for addr in (subset.addresses or []):
|
|
93
|
+
for port in (subset.ports or []):
|
|
94
|
+
addresses.append({
|
|
95
|
+
"ip": addr.ip,
|
|
96
|
+
"port": port.port,
|
|
97
|
+
"protocol": port.protocol,
|
|
98
|
+
"targetRef": {
|
|
99
|
+
"kind": addr.target_ref.kind,
|
|
100
|
+
"name": addr.target_ref.name
|
|
101
|
+
} if addr.target_ref else None
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
result.append({
|
|
105
|
+
"name": ep.metadata.name,
|
|
106
|
+
"namespace": ep.metadata.namespace,
|
|
107
|
+
"addresses": addresses
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return {"success": True, "endpoints": result}
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(f"Error getting Endpoints: {e}")
|
|
113
|
+
return {"success": False, "error": str(e)}
|
|
114
|
+
|
|
115
|
+
@server.tool(
|
|
116
|
+
annotations=ToolAnnotations(
|
|
117
|
+
title="Diagnose Network Connectivity",
|
|
118
|
+
readOnlyHint=True,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
def diagnose_network_connectivity(
|
|
122
|
+
source_pod: str,
|
|
123
|
+
target: str,
|
|
124
|
+
source_namespace: str = "default",
|
|
125
|
+
port: Optional[int] = None
|
|
126
|
+
) -> Dict[str, Any]:
|
|
127
|
+
"""Diagnose network connectivity between pods or to external endpoints."""
|
|
128
|
+
try:
|
|
129
|
+
results = {"success": True, "tests": []}
|
|
130
|
+
|
|
131
|
+
# Test DNS resolution
|
|
132
|
+
dns_cmd = ["kubectl", "exec", source_pod, "-n", source_namespace, "--", "nslookup", target]
|
|
133
|
+
dns_result = subprocess.run(dns_cmd, capture_output=True, text=True, timeout=30)
|
|
134
|
+
results["tests"].append({
|
|
135
|
+
"test": "DNS Resolution",
|
|
136
|
+
"passed": dns_result.returncode == 0,
|
|
137
|
+
"output": dns_result.stdout if dns_result.returncode == 0 else dns_result.stderr
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
# Test connectivity
|
|
141
|
+
if port:
|
|
142
|
+
conn_cmd = ["kubectl", "exec", source_pod, "-n", source_namespace, "--",
|
|
143
|
+
"nc", "-zv", "-w", "5", target, str(port)]
|
|
144
|
+
else:
|
|
145
|
+
conn_cmd = ["kubectl", "exec", source_pod, "-n", source_namespace, "--",
|
|
146
|
+
"ping", "-c", "3", target]
|
|
147
|
+
|
|
148
|
+
conn_result = subprocess.run(conn_cmd, capture_output=True, text=True, timeout=30)
|
|
149
|
+
results["tests"].append({
|
|
150
|
+
"test": f"Connectivity to {target}" + (f":{port}" if port else ""),
|
|
151
|
+
"passed": conn_result.returncode == 0,
|
|
152
|
+
"output": conn_result.stdout if conn_result.returncode == 0 else conn_result.stderr
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
results["allPassed"] = all(t["passed"] for t in results["tests"])
|
|
156
|
+
return results
|
|
157
|
+
except subprocess.TimeoutExpired:
|
|
158
|
+
return {"success": False, "error": "Network test timed out"}
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Error diagnosing network: {e}")
|
|
161
|
+
return {"success": False, "error": str(e)}
|
|
162
|
+
|
|
163
|
+
@server.tool(
|
|
164
|
+
annotations=ToolAnnotations(
|
|
165
|
+
title="Check DNS Resolution",
|
|
166
|
+
readOnlyHint=True,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
def check_dns_resolution(hostname: str, namespace: str = "default", pod_name: Optional[str] = None) -> Dict[str, Any]:
|
|
170
|
+
"""Check DNS resolution from within the cluster."""
|
|
171
|
+
try:
|
|
172
|
+
if not pod_name:
|
|
173
|
+
# Find a running pod in the namespace
|
|
174
|
+
from kubernetes import client, config
|
|
175
|
+
config.load_kube_config()
|
|
176
|
+
v1 = client.CoreV1Api()
|
|
177
|
+
pods = v1.list_namespaced_pod(namespace, field_selector="status.phase=Running")
|
|
178
|
+
if not pods.items:
|
|
179
|
+
return {"success": False, "error": f"No running pods in namespace {namespace}"}
|
|
180
|
+
pod_name = pods.items[0].metadata.name
|
|
181
|
+
|
|
182
|
+
cmd = ["kubectl", "exec", pod_name, "-n", namespace, "--", "nslookup", hostname]
|
|
183
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
"success": result.returncode == 0,
|
|
187
|
+
"hostname": hostname,
|
|
188
|
+
"pod": pod_name,
|
|
189
|
+
"namespace": namespace,
|
|
190
|
+
"output": result.stdout if result.returncode == 0 else result.stderr
|
|
191
|
+
}
|
|
192
|
+
except subprocess.TimeoutExpired:
|
|
193
|
+
return {"success": False, "error": "DNS resolution timed out"}
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.error(f"Error checking DNS: {e}")
|
|
196
|
+
return {"success": False, "error": str(e)}
|
|
197
|
+
|
|
198
|
+
@server.tool(
|
|
199
|
+
annotations=ToolAnnotations(
|
|
200
|
+
title="Trace Service to Pods",
|
|
201
|
+
readOnlyHint=True,
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
def trace_service_chain(service_name: str, namespace: str = "default") -> Dict[str, Any]:
|
|
205
|
+
"""Trace the connection chain from service to pods."""
|
|
206
|
+
try:
|
|
207
|
+
from kubernetes import client, config
|
|
208
|
+
config.load_kube_config()
|
|
209
|
+
v1 = client.CoreV1Api()
|
|
210
|
+
|
|
211
|
+
# Get service
|
|
212
|
+
service = v1.read_namespaced_service(service_name, namespace)
|
|
213
|
+
|
|
214
|
+
# Get endpoints
|
|
215
|
+
try:
|
|
216
|
+
endpoints = v1.read_namespaced_endpoints(service_name, namespace)
|
|
217
|
+
except:
|
|
218
|
+
endpoints = None
|
|
219
|
+
|
|
220
|
+
# Get pods matching selector
|
|
221
|
+
selector = service.spec.selector
|
|
222
|
+
if selector:
|
|
223
|
+
label_selector = ",".join([f"{k}={v}" for k, v in selector.items()])
|
|
224
|
+
pods = v1.list_namespaced_pod(namespace, label_selector=label_selector)
|
|
225
|
+
else:
|
|
226
|
+
pods = None
|
|
227
|
+
|
|
228
|
+
result = {
|
|
229
|
+
"success": True,
|
|
230
|
+
"service": {
|
|
231
|
+
"name": service.metadata.name,
|
|
232
|
+
"type": service.spec.type,
|
|
233
|
+
"clusterIP": service.spec.cluster_ip,
|
|
234
|
+
"ports": [
|
|
235
|
+
{
|
|
236
|
+
"name": p.name,
|
|
237
|
+
"port": p.port,
|
|
238
|
+
"targetPort": str(p.target_port),
|
|
239
|
+
"protocol": p.protocol
|
|
240
|
+
}
|
|
241
|
+
for p in (service.spec.ports or [])
|
|
242
|
+
],
|
|
243
|
+
"selector": selector
|
|
244
|
+
},
|
|
245
|
+
"endpoints": [],
|
|
246
|
+
"pods": []
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if endpoints:
|
|
250
|
+
for subset in (endpoints.subsets or []):
|
|
251
|
+
for addr in (subset.addresses or []):
|
|
252
|
+
for port in (subset.ports or []):
|
|
253
|
+
result["endpoints"].append({
|
|
254
|
+
"ip": addr.ip,
|
|
255
|
+
"port": port.port,
|
|
256
|
+
"podName": addr.target_ref.name if addr.target_ref else None
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if pods:
|
|
260
|
+
result["pods"] = [
|
|
261
|
+
{
|
|
262
|
+
"name": p.metadata.name,
|
|
263
|
+
"status": p.status.phase,
|
|
264
|
+
"podIP": p.status.pod_ip,
|
|
265
|
+
"ready": all(
|
|
266
|
+
c.ready for c in (p.status.container_statuses or [])
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
for p in pods.items
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
return result
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.error(f"Error tracing service chain: {e}")
|
|
275
|
+
return {"success": False, "error": str(e)}
|
|
276
|
+
|
|
277
|
+
@server.tool(
|
|
278
|
+
annotations=ToolAnnotations(
|
|
279
|
+
title="Port Forward",
|
|
280
|
+
destructiveHint=True,
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
def port_forward(pod_name: str, local_port: int, pod_port: int, namespace: Optional[str] = "default") -> Dict[str, Any]:
|
|
284
|
+
"""Start port forwarding to a pod (note: this starts a background process)."""
|
|
285
|
+
try:
|
|
286
|
+
import os
|
|
287
|
+
cmd = f"kubectl port-forward {pod_name} {local_port}:{pod_port} -n {namespace} &"
|
|
288
|
+
os.system(cmd)
|
|
289
|
+
return {
|
|
290
|
+
"success": True,
|
|
291
|
+
"message": f"Port forwarding started: localhost:{local_port} -> {pod_name}:{pod_port}",
|
|
292
|
+
"note": "Port forwarding is running in background. Use 'pkill -f port-forward' to stop."
|
|
293
|
+
}
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.error(f"Error setting up port forward: {e}")
|
|
296
|
+
return {"success": False, "error": str(e)}
|