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.
Files changed (45) hide show
  1. kubectl_mcp_server-1.12.0.dist-info/METADATA +711 -0
  2. kubectl_mcp_server-1.12.0.dist-info/RECORD +45 -0
  3. kubectl_mcp_server-1.12.0.dist-info/WHEEL +5 -0
  4. kubectl_mcp_server-1.12.0.dist-info/entry_points.txt +3 -0
  5. kubectl_mcp_server-1.12.0.dist-info/licenses/LICENSE +21 -0
  6. kubectl_mcp_server-1.12.0.dist-info/top_level.txt +2 -0
  7. kubectl_mcp_tool/__init__.py +21 -0
  8. kubectl_mcp_tool/__main__.py +46 -0
  9. kubectl_mcp_tool/auth/__init__.py +13 -0
  10. kubectl_mcp_tool/auth/config.py +71 -0
  11. kubectl_mcp_tool/auth/scopes.py +148 -0
  12. kubectl_mcp_tool/auth/verifier.py +82 -0
  13. kubectl_mcp_tool/cli/__init__.py +9 -0
  14. kubectl_mcp_tool/cli/__main__.py +10 -0
  15. kubectl_mcp_tool/cli/cli.py +111 -0
  16. kubectl_mcp_tool/diagnostics.py +355 -0
  17. kubectl_mcp_tool/k8s_config.py +289 -0
  18. kubectl_mcp_tool/mcp_server.py +530 -0
  19. kubectl_mcp_tool/prompts/__init__.py +5 -0
  20. kubectl_mcp_tool/prompts/prompts.py +823 -0
  21. kubectl_mcp_tool/resources/__init__.py +5 -0
  22. kubectl_mcp_tool/resources/resources.py +305 -0
  23. kubectl_mcp_tool/tools/__init__.py +28 -0
  24. kubectl_mcp_tool/tools/browser.py +371 -0
  25. kubectl_mcp_tool/tools/cluster.py +315 -0
  26. kubectl_mcp_tool/tools/core.py +421 -0
  27. kubectl_mcp_tool/tools/cost.py +680 -0
  28. kubectl_mcp_tool/tools/deployments.py +381 -0
  29. kubectl_mcp_tool/tools/diagnostics.py +174 -0
  30. kubectl_mcp_tool/tools/helm.py +1561 -0
  31. kubectl_mcp_tool/tools/networking.py +296 -0
  32. kubectl_mcp_tool/tools/operations.py +501 -0
  33. kubectl_mcp_tool/tools/pods.py +582 -0
  34. kubectl_mcp_tool/tools/security.py +333 -0
  35. kubectl_mcp_tool/tools/storage.py +133 -0
  36. kubectl_mcp_tool/utils/__init__.py +17 -0
  37. kubectl_mcp_tool/utils/helpers.py +80 -0
  38. tests/__init__.py +9 -0
  39. tests/conftest.py +379 -0
  40. tests/test_auth.py +256 -0
  41. tests/test_browser.py +349 -0
  42. tests/test_prompts.py +536 -0
  43. tests/test_resources.py +343 -0
  44. tests/test_server.py +384 -0
  45. 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
+ """