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,305 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("mcp-server")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_resources(server):
|
|
10
|
+
"""Register all MCP resources for Kubernetes data exposure.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
server: FastMCP server instance
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@server.resource("kubeconfig://contexts")
|
|
17
|
+
def get_kubeconfig_contexts() -> str:
|
|
18
|
+
"""List all available kubectl contexts."""
|
|
19
|
+
try:
|
|
20
|
+
from kubernetes import config
|
|
21
|
+
contexts, active_context = config.list_kube_config_contexts()
|
|
22
|
+
result = {
|
|
23
|
+
"active_context": active_context.get("name") if active_context else None,
|
|
24
|
+
"contexts": [
|
|
25
|
+
{
|
|
26
|
+
"name": ctx.get("name"),
|
|
27
|
+
"cluster": ctx.get("context", {}).get("cluster"),
|
|
28
|
+
"user": ctx.get("context", {}).get("user"),
|
|
29
|
+
"namespace": ctx.get("context", {}).get("namespace", "default")
|
|
30
|
+
}
|
|
31
|
+
for ctx in contexts
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
return json.dumps(result, indent=2)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
return json.dumps({"error": str(e)})
|
|
37
|
+
|
|
38
|
+
@server.resource("kubeconfig://current-context")
|
|
39
|
+
def get_current_context() -> str:
|
|
40
|
+
"""Get the current active kubectl context."""
|
|
41
|
+
try:
|
|
42
|
+
from kubernetes import config
|
|
43
|
+
_, active_context = config.list_kube_config_contexts()
|
|
44
|
+
if active_context:
|
|
45
|
+
result = {
|
|
46
|
+
"name": active_context.get("name"),
|
|
47
|
+
"cluster": active_context.get("context", {}).get("cluster"),
|
|
48
|
+
"user": active_context.get("context", {}).get("user"),
|
|
49
|
+
"namespace": active_context.get("context", {}).get("namespace", "default")
|
|
50
|
+
}
|
|
51
|
+
else:
|
|
52
|
+
result = {"error": "No active context found"}
|
|
53
|
+
return json.dumps(result, indent=2)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
return json.dumps({"error": str(e)})
|
|
56
|
+
|
|
57
|
+
@server.resource("namespace://current")
|
|
58
|
+
def get_current_namespace() -> str:
|
|
59
|
+
"""Get the current namespace from kubectl context."""
|
|
60
|
+
try:
|
|
61
|
+
from kubernetes import config
|
|
62
|
+
_, active_context = config.list_kube_config_contexts()
|
|
63
|
+
namespace = "default"
|
|
64
|
+
if active_context:
|
|
65
|
+
namespace = active_context.get("context", {}).get("namespace", "default")
|
|
66
|
+
return json.dumps({"namespace": namespace}, indent=2)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return json.dumps({"error": str(e)})
|
|
69
|
+
|
|
70
|
+
@server.resource("namespace://list")
|
|
71
|
+
def list_all_namespaces() -> str:
|
|
72
|
+
"""List all namespaces in the cluster."""
|
|
73
|
+
try:
|
|
74
|
+
from kubernetes import client, config
|
|
75
|
+
config.load_kube_config()
|
|
76
|
+
v1 = client.CoreV1Api()
|
|
77
|
+
namespaces = v1.list_namespace()
|
|
78
|
+
result = {
|
|
79
|
+
"namespaces": [
|
|
80
|
+
{
|
|
81
|
+
"name": ns.metadata.name,
|
|
82
|
+
"status": ns.status.phase,
|
|
83
|
+
"created": ns.metadata.creation_timestamp.isoformat() if ns.metadata.creation_timestamp else None,
|
|
84
|
+
"labels": ns.metadata.labels or {}
|
|
85
|
+
}
|
|
86
|
+
for ns in namespaces.items
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
return json.dumps(result, indent=2, default=str)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return json.dumps({"error": str(e)})
|
|
92
|
+
|
|
93
|
+
@server.resource("cluster://info")
|
|
94
|
+
def get_cluster_info() -> str:
|
|
95
|
+
"""Get cluster information including version and nodes."""
|
|
96
|
+
try:
|
|
97
|
+
from kubernetes import client, config
|
|
98
|
+
config.load_kube_config()
|
|
99
|
+
v1 = client.CoreV1Api()
|
|
100
|
+
version_api = client.VersionApi()
|
|
101
|
+
|
|
102
|
+
version_info = version_api.get_code()
|
|
103
|
+
nodes = v1.list_node()
|
|
104
|
+
|
|
105
|
+
result = {
|
|
106
|
+
"version": {
|
|
107
|
+
"git_version": version_info.git_version,
|
|
108
|
+
"platform": version_info.platform,
|
|
109
|
+
"go_version": version_info.go_version
|
|
110
|
+
},
|
|
111
|
+
"nodes": {
|
|
112
|
+
"count": len(nodes.items),
|
|
113
|
+
"ready": sum(1 for n in nodes.items if any(
|
|
114
|
+
c.type == "Ready" and c.status == "True"
|
|
115
|
+
for c in n.status.conditions
|
|
116
|
+
))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return json.dumps(result, indent=2)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
return json.dumps({"error": str(e)})
|
|
122
|
+
|
|
123
|
+
@server.resource("cluster://nodes")
|
|
124
|
+
def get_cluster_nodes() -> str:
|
|
125
|
+
"""Get detailed information about all cluster nodes."""
|
|
126
|
+
try:
|
|
127
|
+
from kubernetes import client, config
|
|
128
|
+
config.load_kube_config()
|
|
129
|
+
v1 = client.CoreV1Api()
|
|
130
|
+
nodes = v1.list_node()
|
|
131
|
+
|
|
132
|
+
result = {
|
|
133
|
+
"nodes": [
|
|
134
|
+
{
|
|
135
|
+
"name": node.metadata.name,
|
|
136
|
+
"status": next(
|
|
137
|
+
(c.status for c in node.status.conditions if c.type == "Ready"),
|
|
138
|
+
"Unknown"
|
|
139
|
+
),
|
|
140
|
+
"roles": [
|
|
141
|
+
k.replace("node-role.kubernetes.io/", "")
|
|
142
|
+
for k in (node.metadata.labels or {}).keys()
|
|
143
|
+
if k.startswith("node-role.kubernetes.io/")
|
|
144
|
+
] or ["worker"],
|
|
145
|
+
"kubernetes_version": node.status.node_info.kubelet_version,
|
|
146
|
+
"os": node.status.node_info.os_image,
|
|
147
|
+
"architecture": node.status.node_info.architecture,
|
|
148
|
+
"capacity": {
|
|
149
|
+
"cpu": node.status.capacity.get("cpu"),
|
|
150
|
+
"memory": node.status.capacity.get("memory"),
|
|
151
|
+
"pods": node.status.capacity.get("pods")
|
|
152
|
+
},
|
|
153
|
+
"allocatable": {
|
|
154
|
+
"cpu": node.status.allocatable.get("cpu"),
|
|
155
|
+
"memory": node.status.allocatable.get("memory"),
|
|
156
|
+
"pods": node.status.allocatable.get("pods")
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for node in nodes.items
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
return json.dumps(result, indent=2)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
return json.dumps({"error": str(e)})
|
|
165
|
+
|
|
166
|
+
@server.resource("cluster://version")
|
|
167
|
+
def get_cluster_version() -> str:
|
|
168
|
+
"""Get Kubernetes cluster version."""
|
|
169
|
+
try:
|
|
170
|
+
from kubernetes import client, config
|
|
171
|
+
config.load_kube_config()
|
|
172
|
+
version_api = client.VersionApi()
|
|
173
|
+
version_info = version_api.get_code()
|
|
174
|
+
|
|
175
|
+
result = {
|
|
176
|
+
"git_version": version_info.git_version,
|
|
177
|
+
"major": version_info.major,
|
|
178
|
+
"minor": version_info.minor,
|
|
179
|
+
"platform": version_info.platform,
|
|
180
|
+
"build_date": version_info.build_date,
|
|
181
|
+
"go_version": version_info.go_version,
|
|
182
|
+
"compiler": version_info.compiler
|
|
183
|
+
}
|
|
184
|
+
return json.dumps(result, indent=2)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
return json.dumps({"error": str(e)})
|
|
187
|
+
|
|
188
|
+
@server.resource("cluster://api-resources")
|
|
189
|
+
def get_api_resources() -> str:
|
|
190
|
+
"""Get available API resources in the cluster."""
|
|
191
|
+
try:
|
|
192
|
+
result = subprocess.run(
|
|
193
|
+
["kubectl", "api-resources", "--output=wide"],
|
|
194
|
+
capture_output=True, text=True, timeout=30
|
|
195
|
+
)
|
|
196
|
+
if result.returncode == 0:
|
|
197
|
+
lines = result.stdout.strip().split("\n")
|
|
198
|
+
if len(lines) > 1:
|
|
199
|
+
resources = []
|
|
200
|
+
for line in lines[1:]:
|
|
201
|
+
parts = line.split()
|
|
202
|
+
if len(parts) >= 4:
|
|
203
|
+
resources.append({
|
|
204
|
+
"name": parts[0],
|
|
205
|
+
"shortnames": parts[1] if len(parts) > 4 else "",
|
|
206
|
+
"apigroup": parts[-4] if len(parts) > 4 else "",
|
|
207
|
+
"namespaced": parts[-3] if len(parts) > 4 else parts[-2],
|
|
208
|
+
"kind": parts[-2] if len(parts) > 4 else parts[-1]
|
|
209
|
+
})
|
|
210
|
+
return json.dumps({"resources": resources}, indent=2)
|
|
211
|
+
return json.dumps({"error": result.stderr or "Failed to get API resources"})
|
|
212
|
+
except Exception as e:
|
|
213
|
+
return json.dumps({"error": str(e)})
|
|
214
|
+
|
|
215
|
+
@server.resource("manifest://deployments/{namespace}/{name}")
|
|
216
|
+
def get_deployment_manifest(namespace: str, name: str) -> str:
|
|
217
|
+
"""Get YAML manifest for a specific deployment."""
|
|
218
|
+
try:
|
|
219
|
+
from kubernetes import client, config
|
|
220
|
+
import yaml
|
|
221
|
+
config.load_kube_config()
|
|
222
|
+
apps_v1 = client.AppsV1Api()
|
|
223
|
+
|
|
224
|
+
deployment = apps_v1.read_namespaced_deployment(name, namespace)
|
|
225
|
+
manifest = client.ApiClient().sanitize_for_serialization(deployment)
|
|
226
|
+
return yaml.dump(manifest, default_flow_style=False)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
return f"# Error: {str(e)}"
|
|
229
|
+
|
|
230
|
+
@server.resource("manifest://services/{namespace}/{name}")
|
|
231
|
+
def get_service_manifest(namespace: str, name: str) -> str:
|
|
232
|
+
"""Get YAML manifest for a specific service."""
|
|
233
|
+
try:
|
|
234
|
+
from kubernetes import client, config
|
|
235
|
+
import yaml
|
|
236
|
+
config.load_kube_config()
|
|
237
|
+
v1 = client.CoreV1Api()
|
|
238
|
+
|
|
239
|
+
service = v1.read_namespaced_service(name, namespace)
|
|
240
|
+
manifest = client.ApiClient().sanitize_for_serialization(service)
|
|
241
|
+
return yaml.dump(manifest, default_flow_style=False)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
return f"# Error: {str(e)}"
|
|
244
|
+
|
|
245
|
+
@server.resource("manifest://configmaps/{namespace}/{name}")
|
|
246
|
+
def get_configmap_manifest(namespace: str, name: str) -> str:
|
|
247
|
+
"""Get YAML manifest for a specific ConfigMap."""
|
|
248
|
+
try:
|
|
249
|
+
from kubernetes import client, config
|
|
250
|
+
import yaml
|
|
251
|
+
config.load_kube_config()
|
|
252
|
+
v1 = client.CoreV1Api()
|
|
253
|
+
|
|
254
|
+
configmap = v1.read_namespaced_config_map(name, namespace)
|
|
255
|
+
manifest = client.ApiClient().sanitize_for_serialization(configmap)
|
|
256
|
+
return yaml.dump(manifest, default_flow_style=False)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
return f"# Error: {str(e)}"
|
|
259
|
+
|
|
260
|
+
@server.resource("manifest://pods/{namespace}/{name}")
|
|
261
|
+
def get_pod_manifest(namespace: str, name: str) -> str:
|
|
262
|
+
"""Get YAML manifest for a specific pod."""
|
|
263
|
+
try:
|
|
264
|
+
from kubernetes import client, config
|
|
265
|
+
import yaml
|
|
266
|
+
config.load_kube_config()
|
|
267
|
+
v1 = client.CoreV1Api()
|
|
268
|
+
|
|
269
|
+
pod = v1.read_namespaced_pod(name, namespace)
|
|
270
|
+
manifest = client.ApiClient().sanitize_for_serialization(pod)
|
|
271
|
+
return yaml.dump(manifest, default_flow_style=False)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
return f"# Error: {str(e)}"
|
|
274
|
+
|
|
275
|
+
@server.resource("manifest://secrets/{namespace}/{name}")
|
|
276
|
+
def get_secret_manifest(namespace: str, name: str) -> str:
|
|
277
|
+
"""Get YAML manifest for a specific secret (data masked)."""
|
|
278
|
+
try:
|
|
279
|
+
from kubernetes import client, config
|
|
280
|
+
import yaml
|
|
281
|
+
config.load_kube_config()
|
|
282
|
+
v1 = client.CoreV1Api()
|
|
283
|
+
|
|
284
|
+
secret = v1.read_namespaced_secret(name, namespace)
|
|
285
|
+
manifest = client.ApiClient().sanitize_for_serialization(secret)
|
|
286
|
+
if "data" in manifest and manifest["data"]:
|
|
287
|
+
manifest["data"] = {k: "[MASKED]" for k in manifest["data"].keys()}
|
|
288
|
+
return yaml.dump(manifest, default_flow_style=False)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
return f"# Error: {str(e)}"
|
|
291
|
+
|
|
292
|
+
@server.resource("manifest://ingresses/{namespace}/{name}")
|
|
293
|
+
def get_ingress_manifest(namespace: str, name: str) -> str:
|
|
294
|
+
"""Get YAML manifest for a specific ingress."""
|
|
295
|
+
try:
|
|
296
|
+
from kubernetes import client, config
|
|
297
|
+
import yaml
|
|
298
|
+
config.load_kube_config()
|
|
299
|
+
networking_v1 = client.NetworkingV1Api()
|
|
300
|
+
|
|
301
|
+
ingress = networking_v1.read_namespaced_ingress(name, namespace)
|
|
302
|
+
manifest = client.ApiClient().sanitize_for_serialization(ingress)
|
|
303
|
+
return yaml.dump(manifest, default_flow_style=False)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
return f"# Error: {str(e)}"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .helm import register_helm_tools
|
|
2
|
+
from .pods import register_pod_tools
|
|
3
|
+
from .core import register_core_tools
|
|
4
|
+
from .cluster import register_cluster_tools
|
|
5
|
+
from .deployments import register_deployment_tools
|
|
6
|
+
from .security import register_security_tools
|
|
7
|
+
from .networking import register_networking_tools
|
|
8
|
+
from .storage import register_storage_tools
|
|
9
|
+
from .operations import register_operations_tools
|
|
10
|
+
from .diagnostics import register_diagnostics_tools
|
|
11
|
+
from .cost import register_cost_tools
|
|
12
|
+
from .browser import register_browser_tools, is_browser_available
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"register_helm_tools",
|
|
16
|
+
"register_pod_tools",
|
|
17
|
+
"register_core_tools",
|
|
18
|
+
"register_cluster_tools",
|
|
19
|
+
"register_deployment_tools",
|
|
20
|
+
"register_security_tools",
|
|
21
|
+
"register_networking_tools",
|
|
22
|
+
"register_storage_tools",
|
|
23
|
+
"register_operations_tools",
|
|
24
|
+
"register_diagnostics_tools",
|
|
25
|
+
"register_cost_tools",
|
|
26
|
+
"register_browser_tools",
|
|
27
|
+
"is_browser_available",
|
|
28
|
+
]
|