kubectl-mcp-server 1.14.0__py3-none-any.whl → 1.16.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 (37) hide show
  1. kubectl_mcp_server-1.16.0.dist-info/METADATA +1047 -0
  2. kubectl_mcp_server-1.16.0.dist-info/RECORD +61 -0
  3. kubectl_mcp_tool/__init__.py +1 -1
  4. kubectl_mcp_tool/crd_detector.py +247 -0
  5. kubectl_mcp_tool/k8s_config.py +304 -63
  6. kubectl_mcp_tool/mcp_server.py +27 -0
  7. kubectl_mcp_tool/tools/__init__.py +20 -0
  8. kubectl_mcp_tool/tools/backup.py +881 -0
  9. kubectl_mcp_tool/tools/capi.py +727 -0
  10. kubectl_mcp_tool/tools/certs.py +709 -0
  11. kubectl_mcp_tool/tools/cilium.py +582 -0
  12. kubectl_mcp_tool/tools/cluster.py +395 -121
  13. kubectl_mcp_tool/tools/core.py +157 -60
  14. kubectl_mcp_tool/tools/cost.py +97 -41
  15. kubectl_mcp_tool/tools/deployments.py +173 -56
  16. kubectl_mcp_tool/tools/diagnostics.py +40 -13
  17. kubectl_mcp_tool/tools/gitops.py +552 -0
  18. kubectl_mcp_tool/tools/helm.py +133 -46
  19. kubectl_mcp_tool/tools/keda.py +464 -0
  20. kubectl_mcp_tool/tools/kiali.py +652 -0
  21. kubectl_mcp_tool/tools/kubevirt.py +803 -0
  22. kubectl_mcp_tool/tools/networking.py +106 -32
  23. kubectl_mcp_tool/tools/operations.py +176 -50
  24. kubectl_mcp_tool/tools/pods.py +162 -50
  25. kubectl_mcp_tool/tools/policy.py +554 -0
  26. kubectl_mcp_tool/tools/rollouts.py +790 -0
  27. kubectl_mcp_tool/tools/security.py +89 -36
  28. kubectl_mcp_tool/tools/storage.py +35 -16
  29. tests/test_browser.py +2 -2
  30. tests/test_ecosystem.py +331 -0
  31. tests/test_tools.py +73 -10
  32. kubectl_mcp_server-1.14.0.dist-info/METADATA +0 -780
  33. kubectl_mcp_server-1.14.0.dist-info/RECORD +0 -49
  34. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/WHEEL +0 -0
  35. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/entry_points.txt +0 -0
  36. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/licenses/LICENSE +0 -0
  37. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,709 @@
1
+ """Cert-Manager toolset for kubectl-mcp-server.
2
+
3
+ Provides tools for managing certificates via cert-manager.
4
+ """
5
+
6
+ import subprocess
7
+ import json
8
+ from typing import Dict, Any, List, Optional
9
+ from datetime import datetime, timezone
10
+
11
+ try:
12
+ from fastmcp import FastMCP
13
+ from fastmcp.tools import ToolAnnotations
14
+ except ImportError:
15
+ from mcp.server.fastmcp import FastMCP
16
+ from mcp.types import ToolAnnotations
17
+
18
+ from ..k8s_config import _get_kubectl_context_args
19
+ from ..crd_detector import crd_exists
20
+
21
+
22
+ CERTIFICATE_CRD = "certificates.cert-manager.io"
23
+ ISSUER_CRD = "issuers.cert-manager.io"
24
+ CLUSTER_ISSUER_CRD = "clusterissuers.cert-manager.io"
25
+ CERTIFICATE_REQUEST_CRD = "certificaterequests.cert-manager.io"
26
+ ORDER_CRD = "orders.acme.cert-manager.io"
27
+ CHALLENGE_CRD = "challenges.acme.cert-manager.io"
28
+
29
+
30
+ def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
31
+ """Run kubectl command and return result."""
32
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
33
+ try:
34
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
35
+ if result.returncode == 0:
36
+ return {"success": True, "output": result.stdout}
37
+ return {"success": False, "error": result.stderr}
38
+ except subprocess.TimeoutExpired:
39
+ return {"success": False, "error": "Command timed out"}
40
+ except Exception as e:
41
+ return {"success": False, "error": str(e)}
42
+
43
+
44
+ class CertsResourceError(Exception):
45
+ """Exception raised when fetching cert-manager resources fails."""
46
+ pass
47
+
48
+
49
+ def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
50
+ """Get Kubernetes resources of a specific kind.
51
+
52
+ Raises:
53
+ CertsResourceError: If kubectl command fails or output cannot be parsed
54
+ """
55
+ args = ["get", kind, "-o", "json"]
56
+ if namespace:
57
+ args.extend(["-n", namespace])
58
+ else:
59
+ args.append("-A")
60
+ if label_selector:
61
+ args.extend(["-l", label_selector])
62
+
63
+ result = _run_kubectl(args, context)
64
+ if not result["success"]:
65
+ error_msg = result.get("error", "Unknown error")
66
+ raise CertsResourceError(
67
+ f"Failed to get {kind} (namespace={namespace or 'all'}, context={context or 'current'}, "
68
+ f"label_selector={label_selector or 'none'}): {error_msg}"
69
+ )
70
+
71
+ try:
72
+ data = json.loads(result["output"])
73
+ return data.get("items", [])
74
+ except json.JSONDecodeError as e:
75
+ raise CertsResourceError(
76
+ f"Failed to parse JSON response for {kind} (namespace={namespace or 'all'}, "
77
+ f"context={context or 'current'}): {e}"
78
+ )
79
+
80
+
81
+ def _get_condition(conditions: List[Dict], condition_type: str) -> Optional[Dict]:
82
+ """Get a specific condition from conditions list."""
83
+ return next((c for c in conditions if c.get("type") == condition_type), None)
84
+
85
+
86
+ def _parse_timestamp(ts: str) -> Optional[datetime]:
87
+ """Parse Kubernetes timestamp string."""
88
+ if not ts:
89
+ return None
90
+ try:
91
+ return datetime.fromisoformat(ts.replace("Z", "+00:00"))
92
+ except Exception:
93
+ return None
94
+
95
+
96
+ def certs_list(
97
+ namespace: str = "",
98
+ context: str = "",
99
+ label_selector: str = ""
100
+ ) -> Dict[str, Any]:
101
+ """List cert-manager certificates with status.
102
+
103
+ Args:
104
+ namespace: Filter by namespace (empty for all namespaces)
105
+ context: Kubernetes context to use (optional)
106
+ label_selector: Label selector to filter certificates
107
+
108
+ Returns:
109
+ List of certificates with their status and expiry information
110
+ """
111
+ if not crd_exists(CERTIFICATE_CRD, context):
112
+ return {
113
+ "success": False,
114
+ "error": "cert-manager is not installed (certificates.cert-manager.io CRD not found)"
115
+ }
116
+
117
+ try:
118
+ resources = _get_resources("certificates.cert-manager.io", namespace, context, label_selector)
119
+ except CertsResourceError as e:
120
+ return {"success": False, "error": str(e)}
121
+
122
+ certs = []
123
+ for item in resources:
124
+ status = item.get("status", {})
125
+ conditions = status.get("conditions", [])
126
+ ready_cond = _get_condition(conditions, "Ready")
127
+ spec = item.get("spec", {})
128
+
129
+ not_after = _parse_timestamp(status.get("notAfter", ""))
130
+
131
+ days_until_expiry = None
132
+ if not_after:
133
+ days_until_expiry = (not_after - datetime.now(timezone.utc)).days
134
+
135
+ certs.append({
136
+ "name": item["metadata"]["name"],
137
+ "namespace": item["metadata"]["namespace"],
138
+ "ready": ready_cond.get("status") == "True" if ready_cond else False,
139
+ "status": ready_cond.get("reason", "Unknown") if ready_cond else "Unknown",
140
+ "message": ready_cond.get("message", "") if ready_cond else "",
141
+ "issuer": spec.get("issuerRef", {}).get("name", ""),
142
+ "issuer_kind": spec.get("issuerRef", {}).get("kind", "Issuer"),
143
+ "secret_name": spec.get("secretName", ""),
144
+ "dns_names": spec.get("dnsNames", []),
145
+ "common_name": spec.get("commonName", ""),
146
+ "not_before": status.get("notBefore", ""),
147
+ "not_after": status.get("notAfter", ""),
148
+ "renewal_time": status.get("renewalTime", ""),
149
+ "days_until_expiry": days_until_expiry,
150
+ "revision": status.get("revision"),
151
+ })
152
+
153
+ expiring_soon = [c for c in certs if c.get("days_until_expiry") is not None and c["days_until_expiry"] < 30]
154
+
155
+ return {
156
+ "context": context or "current",
157
+ "total": len(certs),
158
+ "expiring_soon": len(expiring_soon),
159
+ "certificates": certs,
160
+ }
161
+
162
+
163
+ def certs_get(
164
+ name: str,
165
+ namespace: str,
166
+ context: str = ""
167
+ ) -> Dict[str, Any]:
168
+ """Get detailed information about a certificate.
169
+
170
+ Args:
171
+ name: Name of the certificate
172
+ namespace: Namespace of the certificate
173
+ context: Kubernetes context to use (optional)
174
+
175
+ Returns:
176
+ Detailed certificate information
177
+ """
178
+ if not crd_exists(CERTIFICATE_CRD, context):
179
+ return {"success": False, "error": "cert-manager is not installed"}
180
+
181
+ args = ["get", "certificates.cert-manager.io", name, "-n", namespace, "-o", "json"]
182
+ result = _run_kubectl(args, context)
183
+
184
+ if result["success"]:
185
+ try:
186
+ data = json.loads(result["output"])
187
+ return {
188
+ "success": True,
189
+ "context": context or "current",
190
+ "certificate": data,
191
+ }
192
+ except json.JSONDecodeError:
193
+ return {"success": False, "error": "Failed to parse response"}
194
+
195
+ return {"success": False, "error": result.get("error", "Unknown error")}
196
+
197
+
198
+ def certs_issuers_list(
199
+ namespace: str = "",
200
+ context: str = "",
201
+ include_cluster_issuers: bool = True
202
+ ) -> Dict[str, Any]:
203
+ """List cert-manager Issuers and ClusterIssuers.
204
+
205
+ Args:
206
+ namespace: Filter by namespace for Issuers (empty for all)
207
+ context: Kubernetes context to use (optional)
208
+ include_cluster_issuers: Include ClusterIssuers in the list
209
+
210
+ Returns:
211
+ List of issuers with their status
212
+ """
213
+ issuers = []
214
+
215
+ if crd_exists(ISSUER_CRD, context):
216
+ try:
217
+ issuer_resources = _get_resources("issuers.cert-manager.io", namespace, context)
218
+ except CertsResourceError as e:
219
+ return {"success": False, "error": str(e)}
220
+
221
+ for item in issuer_resources:
222
+ status = item.get("status", {})
223
+ conditions = status.get("conditions", [])
224
+ ready_cond = _get_condition(conditions, "Ready")
225
+ spec = item.get("spec", {})
226
+
227
+ issuer_type = "Unknown"
228
+ if "acme" in spec:
229
+ issuer_type = "ACME"
230
+ elif "ca" in spec:
231
+ issuer_type = "CA"
232
+ elif "selfSigned" in spec:
233
+ issuer_type = "SelfSigned"
234
+ elif "vault" in spec:
235
+ issuer_type = "Vault"
236
+ elif "venafi" in spec:
237
+ issuer_type = "Venafi"
238
+
239
+ issuers.append({
240
+ "name": item["metadata"]["name"],
241
+ "namespace": item["metadata"]["namespace"],
242
+ "kind": "Issuer",
243
+ "type": issuer_type,
244
+ "ready": ready_cond.get("status") == "True" if ready_cond else False,
245
+ "status": ready_cond.get("reason", "Unknown") if ready_cond else "Unknown",
246
+ "message": ready_cond.get("message", "") if ready_cond else "",
247
+ })
248
+
249
+ if include_cluster_issuers and crd_exists(CLUSTER_ISSUER_CRD, context):
250
+ try:
251
+ cluster_issuer_resources = _get_resources("clusterissuers.cert-manager.io", "", context)
252
+ except CertsResourceError as e:
253
+ return {"success": False, "error": str(e)}
254
+
255
+ for item in cluster_issuer_resources:
256
+ status = item.get("status", {})
257
+ conditions = status.get("conditions", [])
258
+ ready_cond = _get_condition(conditions, "Ready")
259
+ spec = item.get("spec", {})
260
+
261
+ issuer_type = "Unknown"
262
+ if "acme" in spec:
263
+ issuer_type = "ACME"
264
+ elif "ca" in spec:
265
+ issuer_type = "CA"
266
+ elif "selfSigned" in spec:
267
+ issuer_type = "SelfSigned"
268
+ elif "vault" in spec:
269
+ issuer_type = "Vault"
270
+ elif "venafi" in spec:
271
+ issuer_type = "Venafi"
272
+
273
+ issuers.append({
274
+ "name": item["metadata"]["name"],
275
+ "namespace": "",
276
+ "kind": "ClusterIssuer",
277
+ "type": issuer_type,
278
+ "ready": ready_cond.get("status") == "True" if ready_cond else False,
279
+ "status": ready_cond.get("reason", "Unknown") if ready_cond else "Unknown",
280
+ "message": ready_cond.get("message", "") if ready_cond else "",
281
+ })
282
+
283
+ return {
284
+ "context": context or "current",
285
+ "total": len(issuers),
286
+ "issuers": issuers,
287
+ }
288
+
289
+
290
+ def certs_issuer_get(
291
+ name: str,
292
+ namespace: str = "",
293
+ kind: str = "Issuer",
294
+ context: str = ""
295
+ ) -> Dict[str, Any]:
296
+ """Get detailed information about an Issuer or ClusterIssuer.
297
+
298
+ Args:
299
+ name: Name of the issuer
300
+ namespace: Namespace (only for Issuer, not ClusterIssuer)
301
+ kind: Issuer or ClusterIssuer
302
+ context: Kubernetes context to use (optional)
303
+
304
+ Returns:
305
+ Detailed issuer information
306
+ """
307
+ if kind.lower() == "clusterissuer":
308
+ crd = "clusterissuers.cert-manager.io"
309
+ args = ["get", crd, name, "-o", "json"]
310
+ else:
311
+ # Issuers are namespaced resources, namespace is required
312
+ if not namespace or not namespace.strip():
313
+ return {"success": False, "error": "namespace is required for Issuer lookups"}
314
+ crd = "issuers.cert-manager.io"
315
+ args = ["get", crd, name, "-n", namespace, "-o", "json"]
316
+
317
+ if not crd_exists(crd, context):
318
+ return {"success": False, "error": f"{crd} not found"}
319
+
320
+ result = _run_kubectl(args, context)
321
+
322
+ if result["success"]:
323
+ try:
324
+ data = json.loads(result["output"])
325
+ return {
326
+ "success": True,
327
+ "context": context or "current",
328
+ "issuer": data,
329
+ }
330
+ except json.JSONDecodeError:
331
+ return {"success": False, "error": "Failed to parse response"}
332
+
333
+ return {"success": False, "error": result.get("error", "Unknown error")}
334
+
335
+
336
+ def certs_renew(
337
+ name: str,
338
+ namespace: str,
339
+ context: str = ""
340
+ ) -> Dict[str, Any]:
341
+ """Trigger certificate renewal by adding renew annotation.
342
+
343
+ Args:
344
+ name: Name of the certificate
345
+ namespace: Namespace of the certificate
346
+ context: Kubernetes context to use (optional)
347
+
348
+ Returns:
349
+ Renewal trigger result
350
+ """
351
+ if not crd_exists(CERTIFICATE_CRD, context):
352
+ return {"success": False, "error": "cert-manager is not installed"}
353
+
354
+ cmctl_available = False
355
+ try:
356
+ check = subprocess.run(["cmctl", "version"], capture_output=True, timeout=5)
357
+ cmctl_available = check.returncode == 0
358
+ except Exception:
359
+ pass
360
+
361
+ if cmctl_available:
362
+ cmd = ["cmctl", "renew", name, "-n", namespace]
363
+ if context:
364
+ cmd.extend(["--context", context])
365
+ try:
366
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
367
+ if result.returncode == 0:
368
+ return {
369
+ "success": True,
370
+ "context": context or "current",
371
+ "message": f"Triggered renewal for certificate {name} using cmctl",
372
+ "output": result.stdout,
373
+ }
374
+ except Exception:
375
+ pass
376
+
377
+ # Fallback: Delete the certificate's secret to trigger cert-manager to reissue
378
+ # This is the recommended way to trigger renewal without cmctl
379
+ # First, get the secret name from the certificate spec
380
+ get_args = [
381
+ "get", "certificates.cert-manager.io", name,
382
+ "-n", namespace,
383
+ "-o", "jsonpath={.spec.secretName}"
384
+ ]
385
+ secret_result = _run_kubectl(get_args, context)
386
+
387
+ if not secret_result["success"]:
388
+ return {"success": False, "error": f"Failed to get certificate: {secret_result.get('error', 'Unknown error')}"}
389
+
390
+ secret_name = secret_result.get("output", "").strip()
391
+ if not secret_name:
392
+ return {"success": False, "error": "Certificate does not have a secretName configured"}
393
+
394
+ # Delete the secret to trigger renewal
395
+ delete_args = ["delete", "secret", secret_name, "-n", namespace, "--ignore-not-found"]
396
+ result = _run_kubectl(delete_args, context)
397
+
398
+ if result["success"]:
399
+ return {
400
+ "success": True,
401
+ "context": context or "current",
402
+ "message": f"Deleted secret '{secret_name}' to trigger renewal for certificate {name}",
403
+ "note": "cert-manager will automatically reissue the certificate",
404
+ }
405
+
406
+ return {"success": False, "error": result.get("error", "Failed to trigger renewal")}
407
+
408
+
409
+ def certs_status_explain(
410
+ name: str,
411
+ namespace: str,
412
+ context: str = ""
413
+ ) -> Dict[str, Any]:
414
+ """Explain certificate status with diagnosis and recommendations.
415
+
416
+ Args:
417
+ name: Name of the certificate
418
+ namespace: Namespace of the certificate
419
+ context: Kubernetes context to use (optional)
420
+
421
+ Returns:
422
+ Status explanation with diagnosis and recommendations
423
+ """
424
+ cert_result = certs_get(name, namespace, context)
425
+ if not cert_result.get("success"):
426
+ return cert_result
427
+
428
+ cert = cert_result["certificate"]
429
+ status = cert.get("status", {})
430
+ spec = cert.get("spec", {})
431
+ conditions = status.get("conditions", [])
432
+
433
+ diagnosis = []
434
+ recommendations = []
435
+
436
+ ready_cond = _get_condition(conditions, "Ready")
437
+ issuing_cond = _get_condition(conditions, "Issuing")
438
+
439
+ if ready_cond:
440
+ if ready_cond.get("status") != "True":
441
+ diagnosis.append(f"Certificate not ready: {ready_cond.get('message', 'Unknown reason')}")
442
+
443
+ reason = ready_cond.get("reason", "")
444
+ if "Pending" in reason:
445
+ recommendations.append("Check if the Issuer is ready and properly configured")
446
+ recommendations.append("Verify DNS records are correctly configured for ACME challenges")
447
+ elif "Failed" in reason:
448
+ recommendations.append("Check certificate request logs for detailed error")
449
+ recommendations.append("Verify Issuer credentials and permissions")
450
+
451
+ if issuing_cond and issuing_cond.get("status") == "True":
452
+ diagnosis.append("Certificate is currently being issued")
453
+ recommendations.append("Wait for issuance to complete")
454
+
455
+ issuer_ref = spec.get("issuerRef", {})
456
+ issuer_name = issuer_ref.get("name", "")
457
+ issuer_kind = issuer_ref.get("kind", "Issuer")
458
+
459
+ if issuer_name:
460
+ if issuer_kind == "ClusterIssuer":
461
+ issuer_result = certs_issuer_get(issuer_name, "", "ClusterIssuer", context)
462
+ else:
463
+ issuer_result = certs_issuer_get(issuer_name, namespace, "Issuer", context)
464
+
465
+ if not issuer_result.get("success"):
466
+ diagnosis.append(f"Referenced {issuer_kind} '{issuer_name}' not found")
467
+ recommendations.append(f"Create the {issuer_kind} or update the certificate to reference an existing one")
468
+ else:
469
+ issuer = issuer_result["issuer"]
470
+ issuer_conditions = issuer.get("status", {}).get("conditions", [])
471
+ issuer_ready = _get_condition(issuer_conditions, "Ready")
472
+ if issuer_ready and issuer_ready.get("status") != "True":
473
+ diagnosis.append(f"{issuer_kind} '{issuer_name}' is not ready: {issuer_ready.get('message', '')}")
474
+ recommendations.append(f"Fix the {issuer_kind} configuration before the certificate can be issued")
475
+
476
+ not_after = _parse_timestamp(status.get("notAfter", ""))
477
+ if not_after:
478
+ days_left = (not_after - datetime.now(timezone.utc)).days
479
+ if days_left < 0:
480
+ diagnosis.append("Certificate has EXPIRED!")
481
+ recommendations.append("Trigger immediate renewal or check why auto-renewal failed")
482
+ elif days_left < 7:
483
+ diagnosis.append(f"Certificate expires in {days_left} days - CRITICAL")
484
+ recommendations.append("Check if auto-renewal is configured and working")
485
+ elif days_left < 30:
486
+ diagnosis.append(f"Certificate expires in {days_left} days")
487
+
488
+ return {
489
+ "success": True,
490
+ "context": context or "current",
491
+ "name": name,
492
+ "namespace": namespace,
493
+ "ready": ready_cond.get("status") == "True" if ready_cond else False,
494
+ "status": ready_cond.get("reason", "Unknown") if ready_cond else "Unknown",
495
+ "message": ready_cond.get("message", "") if ready_cond else "",
496
+ "diagnosis": diagnosis,
497
+ "recommendations": recommendations,
498
+ "issuer": {
499
+ "name": issuer_name,
500
+ "kind": issuer_kind,
501
+ },
502
+ "expiry": {
503
+ "not_after": status.get("notAfter", ""),
504
+ "days_remaining": (not_after - datetime.now(timezone.utc)).days if not_after else None,
505
+ },
506
+ }
507
+
508
+
509
+ def certs_challenges_list(
510
+ namespace: str = "",
511
+ context: str = "",
512
+ label_selector: str = ""
513
+ ) -> Dict[str, Any]:
514
+ """List ACME challenges (for debugging certificate issuance).
515
+
516
+ Args:
517
+ namespace: Filter by namespace (empty for all namespaces)
518
+ context: Kubernetes context to use (optional)
519
+ label_selector: Label selector to filter challenges
520
+
521
+ Returns:
522
+ List of ACME challenges with their status
523
+ """
524
+ if not crd_exists(CHALLENGE_CRD, context):
525
+ return {
526
+ "success": False,
527
+ "error": "ACME challenges CRD not found (challenges.acme.cert-manager.io)"
528
+ }
529
+
530
+ challenges = []
531
+ for item in _get_resources("challenges.acme.cert-manager.io", namespace, context, label_selector):
532
+ status = item.get("status", {})
533
+ spec = item.get("spec", {})
534
+
535
+ challenges.append({
536
+ "name": item["metadata"]["name"],
537
+ "namespace": item["metadata"]["namespace"],
538
+ "state": status.get("state", "Unknown"),
539
+ "reason": status.get("reason", ""),
540
+ "type": spec.get("type", ""),
541
+ "token": spec.get("token", "")[:20] + "..." if spec.get("token") else "",
542
+ "dns_name": spec.get("dnsName", ""),
543
+ "presented": status.get("presented", False),
544
+ "processing": status.get("processing", False),
545
+ })
546
+
547
+ pending = [c for c in challenges if c["state"] not in ("valid", "ready")]
548
+
549
+ return {
550
+ "context": context or "current",
551
+ "total": len(challenges),
552
+ "pending": len(pending),
553
+ "challenges": challenges,
554
+ }
555
+
556
+
557
+ def certs_requests_list(
558
+ namespace: str = "",
559
+ context: str = "",
560
+ label_selector: str = ""
561
+ ) -> Dict[str, Any]:
562
+ """List CertificateRequests.
563
+
564
+ Args:
565
+ namespace: Filter by namespace (empty for all namespaces)
566
+ context: Kubernetes context to use (optional)
567
+ label_selector: Label selector to filter requests
568
+
569
+ Returns:
570
+ List of certificate requests with their status
571
+ """
572
+ if not crd_exists(CERTIFICATE_REQUEST_CRD, context):
573
+ return {
574
+ "success": False,
575
+ "error": "CertificateRequest CRD not found"
576
+ }
577
+
578
+ requests = []
579
+ for item in _get_resources("certificaterequests.cert-manager.io", namespace, context, label_selector):
580
+ status = item.get("status", {})
581
+ conditions = status.get("conditions", [])
582
+ ready_cond = _get_condition(conditions, "Ready")
583
+ approved_cond = _get_condition(conditions, "Approved")
584
+ spec = item.get("spec", {})
585
+
586
+ requests.append({
587
+ "name": item["metadata"]["name"],
588
+ "namespace": item["metadata"]["namespace"],
589
+ "ready": ready_cond.get("status") == "True" if ready_cond else False,
590
+ "approved": approved_cond.get("status") == "True" if approved_cond else False,
591
+ "status": ready_cond.get("reason", "Unknown") if ready_cond else "Unknown",
592
+ "message": ready_cond.get("message", "") if ready_cond else "",
593
+ "issuer": spec.get("issuerRef", {}).get("name", ""),
594
+ "issuer_kind": spec.get("issuerRef", {}).get("kind", ""),
595
+ "created": item["metadata"].get("creationTimestamp", ""),
596
+ })
597
+
598
+ return {
599
+ "context": context or "current",
600
+ "total": len(requests),
601
+ "requests": requests,
602
+ }
603
+
604
+
605
+ def certs_detect(context: str = "") -> Dict[str, Any]:
606
+ """Detect if cert-manager is installed and its components.
607
+
608
+ Args:
609
+ context: Kubernetes context to use (optional)
610
+
611
+ Returns:
612
+ Detection results for cert-manager
613
+ """
614
+ return {
615
+ "context": context or "current",
616
+ "installed": crd_exists(CERTIFICATE_CRD, context),
617
+ "crds": {
618
+ "certificates": crd_exists(CERTIFICATE_CRD, context),
619
+ "issuers": crd_exists(ISSUER_CRD, context),
620
+ "clusterissuers": crd_exists(CLUSTER_ISSUER_CRD, context),
621
+ "certificaterequests": crd_exists(CERTIFICATE_REQUEST_CRD, context),
622
+ "orders": crd_exists(ORDER_CRD, context),
623
+ "challenges": crd_exists(CHALLENGE_CRD, context),
624
+ },
625
+ }
626
+
627
+
628
+ def register_certs_tools(mcp: FastMCP, non_destructive: bool = False):
629
+ """Register cert-manager tools with the MCP server."""
630
+
631
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
632
+ def certs_list_tool(
633
+ namespace: str = "",
634
+ context: str = "",
635
+ label_selector: str = ""
636
+ ) -> str:
637
+ """List cert-manager certificates with status."""
638
+ return json.dumps(certs_list(namespace, context, label_selector), indent=2)
639
+
640
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
641
+ def certs_get_tool(
642
+ name: str,
643
+ namespace: str,
644
+ context: str = ""
645
+ ) -> str:
646
+ """Get detailed information about a certificate."""
647
+ return json.dumps(certs_get(name, namespace, context), indent=2)
648
+
649
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
650
+ def certs_issuers_list_tool(
651
+ namespace: str = "",
652
+ context: str = "",
653
+ include_cluster_issuers: bool = True
654
+ ) -> str:
655
+ """List cert-manager Issuers and ClusterIssuers."""
656
+ return json.dumps(certs_issuers_list(namespace, context, include_cluster_issuers), indent=2)
657
+
658
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
659
+ def certs_issuer_get_tool(
660
+ name: str,
661
+ namespace: str = "",
662
+ kind: str = "Issuer",
663
+ context: str = ""
664
+ ) -> str:
665
+ """Get detailed information about an Issuer or ClusterIssuer."""
666
+ return json.dumps(certs_issuer_get(name, namespace, kind, context), indent=2)
667
+
668
+ @mcp.tool()
669
+ def certs_renew_tool(
670
+ name: str,
671
+ namespace: str,
672
+ context: str = ""
673
+ ) -> str:
674
+ """Trigger certificate renewal."""
675
+ if non_destructive:
676
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
677
+ return json.dumps(certs_renew(name, namespace, context), indent=2)
678
+
679
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
680
+ def certs_status_explain_tool(
681
+ name: str,
682
+ namespace: str,
683
+ context: str = ""
684
+ ) -> str:
685
+ """Explain certificate status with diagnosis and recommendations."""
686
+ return json.dumps(certs_status_explain(name, namespace, context), indent=2)
687
+
688
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
689
+ def certs_challenges_list_tool(
690
+ namespace: str = "",
691
+ context: str = "",
692
+ label_selector: str = ""
693
+ ) -> str:
694
+ """List ACME challenges for debugging certificate issuance."""
695
+ return json.dumps(certs_challenges_list(namespace, context, label_selector), indent=2)
696
+
697
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
698
+ def certs_requests_list_tool(
699
+ namespace: str = "",
700
+ context: str = "",
701
+ label_selector: str = ""
702
+ ) -> str:
703
+ """List CertificateRequests."""
704
+ return json.dumps(certs_requests_list(namespace, context, label_selector), indent=2)
705
+
706
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
707
+ def certs_detect_tool(context: str = "") -> str:
708
+ """Detect if cert-manager is installed and its components."""
709
+ return json.dumps(certs_detect(context), indent=2)