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,333 @@
|
|
|
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_security_tools(server, non_destructive: bool):
|
|
10
|
+
"""Register RBAC and security-related tools."""
|
|
11
|
+
|
|
12
|
+
@server.tool(
|
|
13
|
+
annotations=ToolAnnotations(
|
|
14
|
+
title="Get RBAC Roles",
|
|
15
|
+
readOnlyHint=True,
|
|
16
|
+
),
|
|
17
|
+
)
|
|
18
|
+
def get_rbac_roles(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
19
|
+
"""Get RBAC Roles in a namespace or cluster-wide."""
|
|
20
|
+
try:
|
|
21
|
+
from kubernetes import client, config
|
|
22
|
+
config.load_kube_config()
|
|
23
|
+
rbac = client.RbacAuthorizationV1Api()
|
|
24
|
+
|
|
25
|
+
if namespace:
|
|
26
|
+
roles = rbac.list_namespaced_role(namespace)
|
|
27
|
+
else:
|
|
28
|
+
roles = rbac.list_role_for_all_namespaces()
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
"success": True,
|
|
32
|
+
"roles": [
|
|
33
|
+
{
|
|
34
|
+
"name": role.metadata.name,
|
|
35
|
+
"namespace": role.metadata.namespace,
|
|
36
|
+
"rules": [
|
|
37
|
+
{
|
|
38
|
+
"apiGroups": rule.api_groups,
|
|
39
|
+
"resources": rule.resources,
|
|
40
|
+
"verbs": rule.verbs
|
|
41
|
+
}
|
|
42
|
+
for rule in (role.rules or [])
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
for role in roles.items
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f"Error getting RBAC roles: {e}")
|
|
50
|
+
return {"success": False, "error": str(e)}
|
|
51
|
+
|
|
52
|
+
@server.tool(
|
|
53
|
+
annotations=ToolAnnotations(
|
|
54
|
+
title="Get Cluster Roles",
|
|
55
|
+
readOnlyHint=True,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
def get_cluster_roles() -> Dict[str, Any]:
|
|
59
|
+
"""Get ClusterRoles in the cluster."""
|
|
60
|
+
try:
|
|
61
|
+
from kubernetes import client, config
|
|
62
|
+
config.load_kube_config()
|
|
63
|
+
rbac = client.RbacAuthorizationV1Api()
|
|
64
|
+
roles = rbac.list_cluster_role()
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"success": True,
|
|
68
|
+
"clusterRoles": [
|
|
69
|
+
{
|
|
70
|
+
"name": role.metadata.name,
|
|
71
|
+
"rules": [
|
|
72
|
+
{
|
|
73
|
+
"apiGroups": rule.api_groups,
|
|
74
|
+
"resources": rule.resources,
|
|
75
|
+
"verbs": rule.verbs
|
|
76
|
+
}
|
|
77
|
+
for rule in (role.rules or [])
|
|
78
|
+
][:5]
|
|
79
|
+
}
|
|
80
|
+
for role in roles.items[:20]
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Error getting cluster roles: {e}")
|
|
85
|
+
return {"success": False, "error": str(e)}
|
|
86
|
+
|
|
87
|
+
@server.tool(
|
|
88
|
+
annotations=ToolAnnotations(
|
|
89
|
+
title="Analyze Pod Security",
|
|
90
|
+
readOnlyHint=True,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
def analyze_pod_security(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
94
|
+
"""Analyze pod security configurations."""
|
|
95
|
+
try:
|
|
96
|
+
from kubernetes import client, config
|
|
97
|
+
config.load_kube_config()
|
|
98
|
+
v1 = client.CoreV1Api()
|
|
99
|
+
|
|
100
|
+
if namespace:
|
|
101
|
+
pods = v1.list_namespaced_pod(namespace)
|
|
102
|
+
else:
|
|
103
|
+
pods = v1.list_pod_for_all_namespaces()
|
|
104
|
+
|
|
105
|
+
issues = []
|
|
106
|
+
for pod in pods.items:
|
|
107
|
+
pod_issues = []
|
|
108
|
+
spec = pod.spec
|
|
109
|
+
|
|
110
|
+
if spec.host_network:
|
|
111
|
+
pod_issues.append("hostNetwork enabled")
|
|
112
|
+
if spec.host_pid:
|
|
113
|
+
pod_issues.append("hostPID enabled")
|
|
114
|
+
if spec.host_ipc:
|
|
115
|
+
pod_issues.append("hostIPC enabled")
|
|
116
|
+
|
|
117
|
+
for container in (spec.containers or []):
|
|
118
|
+
sc = container.security_context
|
|
119
|
+
if sc:
|
|
120
|
+
if sc.privileged:
|
|
121
|
+
pod_issues.append(f"Container {container.name}: privileged mode")
|
|
122
|
+
if sc.run_as_root:
|
|
123
|
+
pod_issues.append(f"Container {container.name}: runs as root")
|
|
124
|
+
if sc.allow_privilege_escalation:
|
|
125
|
+
pod_issues.append(f"Container {container.name}: privilege escalation allowed")
|
|
126
|
+
|
|
127
|
+
if pod_issues:
|
|
128
|
+
issues.append({
|
|
129
|
+
"pod": pod.metadata.name,
|
|
130
|
+
"namespace": pod.metadata.namespace,
|
|
131
|
+
"issues": pod_issues
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"success": True,
|
|
136
|
+
"totalPods": len(pods.items),
|
|
137
|
+
"podsWithIssues": len(issues),
|
|
138
|
+
"issues": issues[:50]
|
|
139
|
+
}
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Error analyzing pod security: {e}")
|
|
142
|
+
return {"success": False, "error": str(e)}
|
|
143
|
+
|
|
144
|
+
@server.tool(
|
|
145
|
+
annotations=ToolAnnotations(
|
|
146
|
+
title="Analyze Network Policies",
|
|
147
|
+
readOnlyHint=True,
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
def analyze_network_policies(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
151
|
+
"""Analyze network policies in the cluster."""
|
|
152
|
+
try:
|
|
153
|
+
from kubernetes import client, config
|
|
154
|
+
config.load_kube_config()
|
|
155
|
+
networking = client.NetworkingV1Api()
|
|
156
|
+
v1 = client.CoreV1Api()
|
|
157
|
+
|
|
158
|
+
if namespace:
|
|
159
|
+
policies = networking.list_namespaced_network_policy(namespace)
|
|
160
|
+
namespaces = [v1.read_namespace(namespace)]
|
|
161
|
+
else:
|
|
162
|
+
policies = networking.list_network_policy_for_all_namespaces()
|
|
163
|
+
namespaces = v1.list_namespace().items
|
|
164
|
+
|
|
165
|
+
protected_namespaces = set()
|
|
166
|
+
for policy in policies.items:
|
|
167
|
+
protected_namespaces.add(policy.metadata.namespace)
|
|
168
|
+
|
|
169
|
+
unprotected = [
|
|
170
|
+
ns.metadata.name for ns in namespaces
|
|
171
|
+
if ns.metadata.name not in protected_namespaces
|
|
172
|
+
and ns.metadata.name not in ["kube-system", "kube-public", "kube-node-lease"]
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
"success": True,
|
|
177
|
+
"totalPolicies": len(policies.items),
|
|
178
|
+
"protectedNamespaces": list(protected_namespaces),
|
|
179
|
+
"unprotectedNamespaces": unprotected,
|
|
180
|
+
"policies": [
|
|
181
|
+
{
|
|
182
|
+
"name": p.metadata.name,
|
|
183
|
+
"namespace": p.metadata.namespace,
|
|
184
|
+
"podSelector": p.spec.pod_selector.match_labels if p.spec.pod_selector else {},
|
|
185
|
+
"policyTypes": p.spec.policy_types
|
|
186
|
+
}
|
|
187
|
+
for p in policies.items
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Error analyzing network policies: {e}")
|
|
192
|
+
return {"success": False, "error": str(e)}
|
|
193
|
+
|
|
194
|
+
@server.tool(
|
|
195
|
+
annotations=ToolAnnotations(
|
|
196
|
+
title="Audit RBAC Permissions",
|
|
197
|
+
readOnlyHint=True,
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
def audit_rbac_permissions(namespace: Optional[str] = None, subject: Optional[str] = None) -> Dict[str, Any]:
|
|
201
|
+
"""Audit RBAC permissions for subjects."""
|
|
202
|
+
try:
|
|
203
|
+
from kubernetes import client, config
|
|
204
|
+
config.load_kube_config()
|
|
205
|
+
rbac = client.RbacAuthorizationV1Api()
|
|
206
|
+
|
|
207
|
+
cluster_bindings = rbac.list_cluster_role_binding()
|
|
208
|
+
if namespace:
|
|
209
|
+
role_bindings = rbac.list_namespaced_role_binding(namespace)
|
|
210
|
+
else:
|
|
211
|
+
role_bindings = rbac.list_role_binding_for_all_namespaces()
|
|
212
|
+
|
|
213
|
+
permissions = []
|
|
214
|
+
|
|
215
|
+
for binding in cluster_bindings.items:
|
|
216
|
+
for subj in (binding.subjects or []):
|
|
217
|
+
if subject and subj.name != subject:
|
|
218
|
+
continue
|
|
219
|
+
permissions.append({
|
|
220
|
+
"subject": subj.name,
|
|
221
|
+
"subjectKind": subj.kind,
|
|
222
|
+
"roleRef": binding.role_ref.name,
|
|
223
|
+
"scope": "cluster",
|
|
224
|
+
"bindingName": binding.metadata.name
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
for binding in role_bindings.items:
|
|
228
|
+
for subj in (binding.subjects or []):
|
|
229
|
+
if subject and subj.name != subject:
|
|
230
|
+
continue
|
|
231
|
+
permissions.append({
|
|
232
|
+
"subject": subj.name,
|
|
233
|
+
"subjectKind": subj.kind,
|
|
234
|
+
"roleRef": binding.role_ref.name,
|
|
235
|
+
"scope": binding.metadata.namespace,
|
|
236
|
+
"bindingName": binding.metadata.name
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"success": True,
|
|
241
|
+
"permissions": permissions[:100]
|
|
242
|
+
}
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"Error auditing RBAC: {e}")
|
|
245
|
+
return {"success": False, "error": str(e)}
|
|
246
|
+
|
|
247
|
+
@server.tool(
|
|
248
|
+
annotations=ToolAnnotations(
|
|
249
|
+
title="Check Secrets Security",
|
|
250
|
+
readOnlyHint=True,
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
def check_secrets_security(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
254
|
+
"""Check security posture of secrets."""
|
|
255
|
+
try:
|
|
256
|
+
from kubernetes import client, config
|
|
257
|
+
config.load_kube_config()
|
|
258
|
+
v1 = client.CoreV1Api()
|
|
259
|
+
|
|
260
|
+
if namespace:
|
|
261
|
+
secrets = v1.list_namespaced_secret(namespace)
|
|
262
|
+
else:
|
|
263
|
+
secrets = v1.list_secret_for_all_namespaces()
|
|
264
|
+
|
|
265
|
+
findings = []
|
|
266
|
+
for secret in secrets.items:
|
|
267
|
+
issues = []
|
|
268
|
+
|
|
269
|
+
if secret.type == "Opaque":
|
|
270
|
+
issues.append("Generic secret type - consider using specific types")
|
|
271
|
+
|
|
272
|
+
if not secret.metadata.annotations:
|
|
273
|
+
issues.append("No annotations - consider adding metadata")
|
|
274
|
+
|
|
275
|
+
if issues:
|
|
276
|
+
findings.append({
|
|
277
|
+
"name": secret.metadata.name,
|
|
278
|
+
"namespace": secret.metadata.namespace,
|
|
279
|
+
"type": secret.type,
|
|
280
|
+
"issues": issues
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
"success": True,
|
|
285
|
+
"totalSecrets": len(secrets.items),
|
|
286
|
+
"secretsWithIssues": len(findings),
|
|
287
|
+
"findings": findings[:50]
|
|
288
|
+
}
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error(f"Error checking secrets security: {e}")
|
|
291
|
+
return {"success": False, "error": str(e)}
|
|
292
|
+
|
|
293
|
+
@server.tool(
|
|
294
|
+
annotations=ToolAnnotations(
|
|
295
|
+
title="Get Pod Security Policies (Deprecated) / Pod Security Standards",
|
|
296
|
+
readOnlyHint=True,
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
def get_pod_security_info(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
300
|
+
"""Get Pod Security Standards information for namespaces."""
|
|
301
|
+
try:
|
|
302
|
+
from kubernetes import client, config
|
|
303
|
+
config.load_kube_config()
|
|
304
|
+
v1 = client.CoreV1Api()
|
|
305
|
+
|
|
306
|
+
if namespace:
|
|
307
|
+
namespaces = [v1.read_namespace(namespace)]
|
|
308
|
+
else:
|
|
309
|
+
namespaces = v1.list_namespace().items
|
|
310
|
+
|
|
311
|
+
result = []
|
|
312
|
+
for ns in namespaces:
|
|
313
|
+
labels = ns.metadata.labels or {}
|
|
314
|
+
pss_info = {
|
|
315
|
+
"namespace": ns.metadata.name,
|
|
316
|
+
"enforce": labels.get("pod-security.kubernetes.io/enforce"),
|
|
317
|
+
"enforceVersion": labels.get("pod-security.kubernetes.io/enforce-version"),
|
|
318
|
+
"audit": labels.get("pod-security.kubernetes.io/audit"),
|
|
319
|
+
"auditVersion": labels.get("pod-security.kubernetes.io/audit-version"),
|
|
320
|
+
"warn": labels.get("pod-security.kubernetes.io/warn"),
|
|
321
|
+
"warnVersion": labels.get("pod-security.kubernetes.io/warn-version")
|
|
322
|
+
}
|
|
323
|
+
if any(v for k, v in pss_info.items() if k != "namespace"):
|
|
324
|
+
result.append(pss_info)
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
"success": True,
|
|
328
|
+
"note": "Pod Security Policies are deprecated. Using Pod Security Standards (PSS) labels.",
|
|
329
|
+
"namespacesWithPSS": result
|
|
330
|
+
}
|
|
331
|
+
except Exception as e:
|
|
332
|
+
logger.error(f"Error getting pod security info: {e}")
|
|
333
|
+
return {"success": False, "error": str(e)}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from mcp.types import ToolAnnotations
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("mcp-server")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_storage_tools(server, non_destructive: bool):
|
|
10
|
+
"""Register storage-related tools."""
|
|
11
|
+
|
|
12
|
+
@server.tool(
|
|
13
|
+
annotations=ToolAnnotations(
|
|
14
|
+
title="Get Persistent Volumes",
|
|
15
|
+
readOnlyHint=True,
|
|
16
|
+
),
|
|
17
|
+
)
|
|
18
|
+
def get_persistent_volumes(name: Optional[str] = None) -> Dict[str, Any]:
|
|
19
|
+
"""Get Persistent Volumes in the cluster."""
|
|
20
|
+
try:
|
|
21
|
+
from kubernetes import client, config
|
|
22
|
+
config.load_kube_config()
|
|
23
|
+
v1 = client.CoreV1Api()
|
|
24
|
+
|
|
25
|
+
if name:
|
|
26
|
+
pv = v1.read_persistent_volume(name)
|
|
27
|
+
pvs = [pv]
|
|
28
|
+
else:
|
|
29
|
+
pvs = v1.list_persistent_volume().items
|
|
30
|
+
|
|
31
|
+
def get_pv_source(spec):
|
|
32
|
+
sources = ['nfs', 'hostPath', 'gcePersistentDisk', 'awsElasticBlockStore',
|
|
33
|
+
'azureDisk', 'azureFile', 'csi', 'local', 'fc', 'iscsi']
|
|
34
|
+
for source in sources:
|
|
35
|
+
source_attr = getattr(spec, source.replace('P', '_p').replace('D', '_d'), None)
|
|
36
|
+
if source_attr:
|
|
37
|
+
return {"type": source, "details": str(source_attr)[:100]}
|
|
38
|
+
return {"type": "unknown"}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
"success": True,
|
|
42
|
+
"persistentVolumes": [
|
|
43
|
+
{
|
|
44
|
+
"name": pv.metadata.name,
|
|
45
|
+
"capacity": pv.spec.capacity.get("storage") if pv.spec.capacity else None,
|
|
46
|
+
"accessModes": pv.spec.access_modes,
|
|
47
|
+
"reclaimPolicy": pv.spec.persistent_volume_reclaim_policy,
|
|
48
|
+
"status": pv.status.phase,
|
|
49
|
+
"storageClass": pv.spec.storage_class_name,
|
|
50
|
+
"claimRef": {
|
|
51
|
+
"name": pv.spec.claim_ref.name,
|
|
52
|
+
"namespace": pv.spec.claim_ref.namespace
|
|
53
|
+
} if pv.spec.claim_ref else None,
|
|
54
|
+
"source": get_pv_source(pv.spec)
|
|
55
|
+
}
|
|
56
|
+
for pv in pvs
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Error getting PVs: {e}")
|
|
61
|
+
return {"success": False, "error": str(e)}
|
|
62
|
+
|
|
63
|
+
@server.tool(
|
|
64
|
+
annotations=ToolAnnotations(
|
|
65
|
+
title="Get Persistent Volume Claims",
|
|
66
|
+
readOnlyHint=True,
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
def get_pvcs(namespace: Optional[str] = None) -> Dict[str, Any]:
|
|
70
|
+
"""Get Persistent Volume Claims in a namespace or cluster-wide."""
|
|
71
|
+
try:
|
|
72
|
+
from kubernetes import client, config
|
|
73
|
+
config.load_kube_config()
|
|
74
|
+
v1 = client.CoreV1Api()
|
|
75
|
+
|
|
76
|
+
if namespace:
|
|
77
|
+
pvcs = v1.list_namespaced_persistent_volume_claim(namespace)
|
|
78
|
+
else:
|
|
79
|
+
pvcs = v1.list_persistent_volume_claim_for_all_namespaces()
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"success": True,
|
|
83
|
+
"pvcs": [
|
|
84
|
+
{
|
|
85
|
+
"name": pvc.metadata.name,
|
|
86
|
+
"namespace": pvc.metadata.namespace,
|
|
87
|
+
"status": pvc.status.phase,
|
|
88
|
+
"capacity": pvc.status.capacity.get("storage") if pvc.status.capacity else None,
|
|
89
|
+
"accessModes": pvc.spec.access_modes,
|
|
90
|
+
"storageClass": pvc.spec.storage_class_name,
|
|
91
|
+
"volumeName": pvc.spec.volume_name
|
|
92
|
+
}
|
|
93
|
+
for pvc in pvcs.items
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Error getting PVCs: {e}")
|
|
98
|
+
return {"success": False, "error": str(e)}
|
|
99
|
+
|
|
100
|
+
@server.tool(
|
|
101
|
+
annotations=ToolAnnotations(
|
|
102
|
+
title="Get Storage Classes",
|
|
103
|
+
readOnlyHint=True,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
def get_storage_classes() -> Dict[str, Any]:
|
|
107
|
+
"""Get Storage Classes in the cluster."""
|
|
108
|
+
try:
|
|
109
|
+
from kubernetes import client, config
|
|
110
|
+
config.load_kube_config()
|
|
111
|
+
storage = client.StorageV1Api()
|
|
112
|
+
|
|
113
|
+
scs = storage.list_storage_class()
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
"success": True,
|
|
117
|
+
"storageClasses": [
|
|
118
|
+
{
|
|
119
|
+
"name": sc.metadata.name,
|
|
120
|
+
"provisioner": sc.provisioner,
|
|
121
|
+
"reclaimPolicy": sc.reclaim_policy,
|
|
122
|
+
"volumeBindingMode": sc.volume_binding_mode,
|
|
123
|
+
"allowVolumeExpansion": sc.allow_volume_expansion,
|
|
124
|
+
"default": sc.metadata.annotations.get(
|
|
125
|
+
"storageclass.kubernetes.io/is-default-class"
|
|
126
|
+
) == "true" if sc.metadata.annotations else False
|
|
127
|
+
}
|
|
128
|
+
for sc in scs.items
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Error getting Storage Classes: {e}")
|
|
133
|
+
return {"success": False, "error": str(e)}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .helpers import (
|
|
2
|
+
mask_secrets,
|
|
3
|
+
check_dependencies,
|
|
4
|
+
check_tool_availability,
|
|
5
|
+
check_kubectl_availability,
|
|
6
|
+
check_helm_availability,
|
|
7
|
+
get_logger,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"mask_secrets",
|
|
12
|
+
"check_dependencies",
|
|
13
|
+
"check_tool_availability",
|
|
14
|
+
"check_kubectl_availability",
|
|
15
|
+
"check_helm_availability",
|
|
16
|
+
"get_logger",
|
|
17
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
_log_file = os.environ.get("MCP_LOG_FILE")
|
|
10
|
+
_log_level = logging.DEBUG if os.environ.get("MCP_DEBUG", "").lower() in ("1", "true") else logging.INFO
|
|
11
|
+
|
|
12
|
+
_handlers: List[logging.Handler] = []
|
|
13
|
+
if _log_file:
|
|
14
|
+
try:
|
|
15
|
+
os.makedirs(os.path.dirname(_log_file), exist_ok=True)
|
|
16
|
+
_handlers.append(logging.FileHandler(_log_file))
|
|
17
|
+
except (OSError, ValueError):
|
|
18
|
+
_handlers.append(logging.StreamHandler(sys.stderr))
|
|
19
|
+
else:
|
|
20
|
+
_handlers.append(logging.StreamHandler(sys.stderr))
|
|
21
|
+
|
|
22
|
+
logging.basicConfig(
|
|
23
|
+
level=_log_level,
|
|
24
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
25
|
+
handlers=_handlers
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
for handler in logging.root.handlers[:]:
|
|
29
|
+
if isinstance(handler, logging.StreamHandler) and handler.stream == sys.stdout:
|
|
30
|
+
logging.root.removeHandler(handler)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("mcp-server")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_logger(name: str = "mcp-server") -> logging.Logger:
|
|
36
|
+
return logging.getLogger(name)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def mask_secrets(text: str) -> str:
|
|
40
|
+
masked = re.sub(r'(data:\s*\n)(\s+\w+:\s*)([A-Za-z0-9+/=]{20,})', r'\1\2[MASKED]', text)
|
|
41
|
+
masked = re.sub(r'(password|secret|token|key|credential)(["\s:=]+)([^\s"\n]+)', r'\1\2[MASKED]', masked, flags=re.IGNORECASE)
|
|
42
|
+
return masked
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_tool_availability(tool: str) -> bool:
|
|
46
|
+
try:
|
|
47
|
+
if shutil.which(tool) is None:
|
|
48
|
+
return False
|
|
49
|
+
if tool == "kubectl":
|
|
50
|
+
subprocess.check_output(
|
|
51
|
+
[tool, "version", "--client", "--output=json"],
|
|
52
|
+
stderr=subprocess.PIPE,
|
|
53
|
+
timeout=2
|
|
54
|
+
)
|
|
55
|
+
elif tool == "helm":
|
|
56
|
+
subprocess.check_output(
|
|
57
|
+
[tool, "version", "--short"],
|
|
58
|
+
stderr=subprocess.PIPE,
|
|
59
|
+
timeout=2
|
|
60
|
+
)
|
|
61
|
+
return True
|
|
62
|
+
except (subprocess.SubprocessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def check_kubectl_availability() -> bool:
|
|
67
|
+
return check_tool_availability("kubectl")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check_helm_availability() -> bool:
|
|
71
|
+
return check_tool_availability("helm")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_dependencies() -> bool:
|
|
75
|
+
all_available = True
|
|
76
|
+
for tool in ["kubectl", "helm"]:
|
|
77
|
+
if not check_tool_availability(tool):
|
|
78
|
+
logger.warning(f"{tool} not found in PATH. Operations requiring {tool} will not work.")
|
|
79
|
+
all_available = False
|
|
80
|
+
return all_available
|
tests/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test suite for kubectl-mcp-server.
|
|
3
|
+
|
|
4
|
+
This package contains comprehensive tests for:
|
|
5
|
+
- MCP Tools (80+ Kubernetes operations)
|
|
6
|
+
- MCP Resources (kubeconfig, namespace, cluster, manifests)
|
|
7
|
+
- MCP Prompts (troubleshoot, deploy, security, cost, DR)
|
|
8
|
+
- Server initialization and configuration
|
|
9
|
+
"""
|