Coverage for mcp_server/server.py: 67%
362 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-16 16:37 +0300
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-16 16:37 +0300
1#!/usr/bin/env python
2"""
3MCP Server for OpenShift Python Wrapper (ocp_resources)
5This MCP server provides tools to interact with OpenShift/Kubernetes resources
6using the ocp_resources library through the Model Context Protocol.
7"""
9import importlib
10import inspect
11import io
12import os
13import tempfile
14import traceback
15from pathlib import Path
16from typing import Any, Type
18import yaml
19from fastmcp import FastMCP
20from simple_logger.logger import get_logger, logging
22import ocp_resources
23from ocp_resources.event import Event
24from ocp_resources.exceptions import ExecOnPodError
25from ocp_resources.pod import Pod
27# OpenShift specific resources
28# Import ocp_resources modules
29from ocp_resources.resource import NamespacedResource, Resource, get_client
31# Configure logging to show debug messages
32log_file = os.path.join(tempfile.gettempdir(), "mcp_server_debug.log")
33LOGGER = get_logger(name=__name__, filename=log_file, level=logging.DEBUG)
35# Initialize the MCP server
36mcp = FastMCP(name="openshift-python-wrapper")
38# Global client variable
39_client = None
42def get_dynamic_client(config_file: str | None = None, context: str | None = None) -> Any:
43 """Get or create a dynamic client for Kubernetes/OpenShift"""
44 global _client
45 if _client is None:
46 LOGGER.debug("Creating new dynamic client")
47 _client = get_client(config_file=config_file, context=context)
48 return _client
51def _scan_resource_classes():
52 """Scan ocp_resources modules and return a dict mapping resource types to their classes."""
53 resource_map = {}
54 LOGGER.debug("Starting to scan ocp_resources module for resource types")
56 # Get the ocp_resources directory path
57 ocp_resources_path = Path(ocp_resources.__file__).parent
59 # Scan all .py files in the ocp_resources directory
60 for file_path in ocp_resources_path.glob("*.py"):
61 if file_path.name.startswith("_") or file_path.name == "utils.py":
62 continue
64 module_name = file_path.stem
65 try:
66 # Import the module dynamically
67 module = importlib.import_module(f"ocp_resources.{module_name}")
69 # Look for classes in the module
70 for name in dir(module):
71 if name.startswith("_"):
72 continue
74 obj = getattr(module, name)
75 if inspect.isclass(obj) and hasattr(obj, "kind"):
76 try:
77 if obj.kind: # Some base classes might not have a kind
78 resource_type = obj.kind.lower()
79 # Store the class in the map (later occurrences override earlier ones)
80 resource_map[resource_type] = obj
81 LOGGER.debug(f"Added resource type from {module_name}: {resource_type}")
82 except Exception as e:
83 LOGGER.debug(f"Error processing {name} in {module_name}: {e}")
84 continue
85 except Exception as e:
86 LOGGER.debug(f"Error importing module {module_name}: {e}")
87 continue
89 LOGGER.debug(f"Found {len(resource_map)} resource types total")
90 return resource_map
93def _get_available_resource_types():
94 """Get all available resource types from the resource map."""
95 return sorted(RESOURCE_CLASS_MAP.keys())
98# Use it in the server initialization
99# Initialize resource class map and available resource types
100RESOURCE_CLASS_MAP = _scan_resource_classes()
101RESOURCE_TYPES = _get_available_resource_types()
102LOGGER.info(f"Available resource types: {RESOURCE_TYPES}")
103LOGGER.info(f"Total resource types found: {len(RESOURCE_TYPES)}")
106def get_resource_class(resource_type: str) -> Type[Resource] | None:
107 """Get the resource class for a given resource type"""
108 # Convert resource_type to lowercase for comparison
109 resource_type_lower = resource_type.lower()
111 # Look up the resource class in the pre-scanned map
112 resource_class = RESOURCE_CLASS_MAP.get(resource_type_lower)
114 if resource_class:
115 LOGGER.debug(f"Found resource class for type {resource_type}")
116 return resource_class
118 LOGGER.warning(f"Resource type '{resource_type}' not found in RESOURCE_CLASS_MAP")
119 return None
122def _validate_resource_type(resource_type: str) -> tuple[Type[Resource] | None, dict[str, Any] | None]:
123 """Validate resource type and return (resource_class, error_dict) tuple."""
124 resource_class = get_resource_class(resource_type=resource_type)
125 if not resource_class:
126 return None, {"error": f"Unknown resource type: {resource_type}", "available_types": RESOURCE_TYPES}
127 return resource_class, None
130def _create_resource_instance(
131 resource_class: Type[Resource],
132 name: str,
133 namespace: str | None = None,
134 client: Any | None = None,
135) -> tuple[Any, dict[str, Any] | None]:
136 """Create a resource instance with proper namespace handling.
137 Returns (resource, error_dict) tuple."""
138 if client is None:
139 client = get_dynamic_client()
141 # Check if resource is namespaced
142 is_namespaced = issubclass(resource_class, NamespacedResource)
143 if is_namespaced and not namespace:
144 return None, {"error": f"Namespace is required for {resource_class.kind} resources"}
146 # Create resource instance
147 kwargs = {"name": name, "client": client}
148 if is_namespaced:
149 kwargs["namespace"] = namespace
151 resource = resource_class(**kwargs)
152 return resource, None
155def _format_not_found_error(resource_type: str, name: str, namespace: str | None = None) -> dict[str, Any]:
156 """Format a consistent not found error message."""
157 return {"error": f"{resource_type} '{name}' not found" + (f" in namespace '{namespace}'" if namespace else "")}
160def _format_exception_error(action: str, resource_info: str, exception: Exception) -> dict[str, Any]:
161 """Format a consistent exception error message.
163 Args:
164 action: The action that failed (e.g., "Failed to create", "Failed to get")
165 resource_info: Resource description (e.g., "Pod 'my-pod'", "configmap")
166 exception: The exception that occurred
167 """
168 return {"error": f"{action} {resource_info}: {str(exception)}", "type": type(exception).__name__}
171def format_resource_info(resource: Any) -> dict[str, Any]:
172 """Format resource information for output"""
173 try:
174 metadata = resource.instance.metadata
175 status = getattr(resource.instance, "status", None)
177 info = {
178 "name": metadata.name,
179 "namespace": getattr(metadata, "namespace", None),
180 "uid": metadata.uid,
181 "resourceVersion": metadata.resourceVersion,
182 "creationTimestamp": metadata.creationTimestamp,
183 "labels": dict(metadata.labels) if metadata.labels else {},
184 "annotations": dict(metadata.annotations) if metadata.annotations else {},
185 }
187 if status:
188 if hasattr(status, "phase"):
189 info["phase"] = status.phase
190 if hasattr(status, "conditions"):
191 info["conditions"] = [dict(c) for c in status.conditions] if status.conditions else []
193 return info
194 except Exception as e:
195 return {"name": getattr(resource, "name", "unknown"), "error": str(e)}
198# Tools for resource management
201@mcp.tool
202def list_resources(
203 resource_type: str,
204 namespace: str | None = None,
205 label_selector: str | None = None,
206 field_selector: str | None = None,
207 limit: int | None = None,
208) -> dict[str, Any]:
209 """
210 List Kubernetes/OpenShift resources of a specific type.
212 Returns a list of resources with their basic information.
213 """
214 try:
215 LOGGER.info(f"Listing resources: type={resource_type}, namespace={namespace}")
216 # Validate resource type
217 resource_class, error_response = _validate_resource_type(resource_type=resource_type)
218 if error_response:
219 return error_response
221 client = get_dynamic_client()
222 assert resource_class is not None # Type checker hint - we already checked this above
223 # Build kwargs for the get method
224 kwargs: dict[str, Any] = {}
225 if namespace:
226 kwargs["namespace"] = namespace
227 if label_selector:
228 kwargs["label_selector"] = label_selector
229 if field_selector:
230 kwargs["field_selector"] = field_selector
231 if limit:
232 kwargs["limit"] = limit
234 resources = []
235 for resource in resource_class.get(dyn_client=client, **kwargs):
236 resources.append(format_resource_info(resource))
238 return {
239 "resource_type": resource_type,
240 "namespace": namespace or "all",
241 "count": len(resources),
242 "resources": resources,
243 }
244 except Exception as e:
245 return _format_exception_error("Failed to list", f"{resource_type} resources", e)
248@mcp.tool
249def get_resource(
250 resource_type: str,
251 name: str,
252 namespace: str | None = None,
253 output_format: str = "info",
254) -> dict[str, Any]:
255 """
256 Get a specific Kubernetes/OpenShift resource by name.
258 Returns detailed information about the resource.
259 """
260 try:
261 # Validate resource type
262 resource_class, error_response = _validate_resource_type(resource_type=resource_type)
263 if error_response:
264 return error_response
266 client = get_dynamic_client()
267 assert resource_class is not None # Type checker hint - we already validated this
268 resource_instance, error = _create_resource_instance(
269 resource_class=resource_class, name=name, namespace=namespace, client=client
270 )
271 if error:
272 return error
274 if not resource_instance.exists:
275 return _format_not_found_error(resource_type=resource_type, name=name, namespace=namespace)
277 if output_format == "yaml":
278 return {
279 "resource_type": resource_type,
280 "name": name,
281 "namespace": namespace,
282 "yaml": yaml.dump(resource_instance.instance.to_dict(), default_flow_style=False),
283 }
284 elif output_format == "json":
285 return {
286 "resource_type": resource_type,
287 "name": name,
288 "namespace": namespace,
289 "json": resource_instance.instance.to_dict(),
290 }
291 else: # info format
292 info = format_resource_info(resource_instance)
293 info["resource_type"] = resource_type
295 # Add resource-specific information
296 if resource_type.lower() == "pod":
297 info["node"] = getattr(resource_instance.instance.spec, "nodeName", None)
298 info["containers"] = (
299 [c.name for c in resource_instance.instance.spec.containers]
300 if hasattr(resource_instance.instance.spec, "containers")
301 else []
302 )
304 # Get container statuses
305 if hasattr(resource_instance.instance.status, "containerStatuses"):
306 info["container_statuses"] = []
307 for cs in resource_instance.instance.status.containerStatuses:
308 status_info = {
309 "name": cs.name,
310 "ready": cs.ready,
311 "restartCount": cs.restartCount,
312 }
313 if cs.state.running:
314 status_info["state"] = "running"
315 elif cs.state.waiting:
316 status_info["state"] = "waiting"
317 status_info["reason"] = cs.state.waiting.reason
318 elif cs.state.terminated:
319 status_info["state"] = "terminated"
320 status_info["reason"] = cs.state.terminated.reason
321 info["container_statuses"].append(status_info)
323 elif resource_type.lower() == "deployment":
324 if hasattr(resource_instance.instance.status, "replicas"):
325 info["replicas"] = resource_instance.instance.status.replicas
326 info["readyReplicas"] = getattr(resource_instance.instance.status, "readyReplicas", 0)
327 info["availableReplicas"] = getattr(resource_instance.instance.status, "availableReplicas", 0)
329 elif resource_type.lower() == "service":
330 if hasattr(resource_instance.instance.spec, "type"):
331 info["type"] = resource_instance.instance.spec.type
332 info["clusterIP"] = resource_instance.instance.spec.clusterIP
333 info["ports"] = (
334 [
335 {"port": p.port, "targetPort": str(p.targetPort), "protocol": p.protocol}
336 for p in resource_instance.instance.spec.ports
337 ]
338 if hasattr(resource_instance.instance.spec, "ports")
339 else []
340 )
342 return info
343 except Exception as e:
344 return _format_exception_error("Failed to get", f"{resource_type} '{name}'", e)
347@mcp.tool
348def create_resource(
349 resource_type: str,
350 name: str,
351 namespace: str | None = None,
352 yaml_content: str | None = None,
353 spec: dict[str, Any] | None = None,
354 labels: dict[str, str] | None = None,
355 annotations: dict[str, str] | None = None,
356 wait: bool = False,
357) -> dict[str, Any]:
358 """
359 Create a new Kubernetes/OpenShift resource.
361 You can provide either yaml_content or spec to define the resource.
362 """
363 try:
364 if yaml_content and spec:
365 return {"error": "Provide either yaml_content or spec, not both"}
367 if not yaml_content and not spec:
368 return {"error": "Either yaml_content or spec must be provided"}
370 client = get_dynamic_client()
372 if yaml_content:
373 # Parse YAML content
374 yaml_data = yaml.safe_load(io.StringIO(yaml_content))
376 # Extract resource info from YAML
377 resource_type = yaml_data.get("kind", resource_type).lower()
378 name = yaml_data.get("metadata", {}).get("name", name)
379 namespace = yaml_data.get("metadata", {}).get("namespace", namespace)
381 resource_class, error_response = _validate_resource_type(resource_type=resource_type)
382 if error_response:
383 return error_response
385 # Create resource from parsed YAML using kind_dict
386 assert resource_class is not None # Type checker hint
387 kwargs = {"client": client, "kind_dict": yaml_data}
388 resource_instance = resource_class(**kwargs)
389 else:
390 # Create resource from spec
391 resource_class, error_response = _validate_resource_type(resource_type=resource_type)
392 if error_response:
393 return error_response
395 # Check if resource is namespaced
396 assert resource_class is not None # Type checker hint
397 is_namespaced = issubclass(resource_class, NamespacedResource)
398 if is_namespaced and not namespace:
399 return {"error": f"Namespace is required for {resource_type} resources"}
401 # Build kwargs
402 kwargs = {
403 "name": name,
404 "client": client,
405 "label": labels,
406 "annotations": annotations,
407 }
408 if is_namespaced:
409 kwargs["namespace"] = namespace
411 # Add spec-specific parameters based on resource type
412 if spec:
413 kwargs.update(spec)
415 resource_instance = resource_class(**kwargs)
417 # Deploy the resource
418 resource_instance.deploy(wait=wait)
420 return {
421 "success": True,
422 "resource_type": resource_type,
423 "name": resource_instance.name,
424 "namespace": getattr(resource_instance, "namespace", None),
425 "message": f"{resource_type} '{resource_instance.name}' created successfully",
426 }
427 except Exception as e:
428 return _format_exception_error("Failed to create", resource_type, e)
431@mcp.tool
432def update_resource(
433 resource_type: str,
434 name: str,
435 patch: dict[str, Any],
436 namespace: str | None = None,
437 patch_type: str = "merge",
438) -> dict[str, Any]:
439 """
440 Update an existing Kubernetes/OpenShift resource using a patch.
442 The patch should be a dictionary containing the fields to update.
443 """
444 try:
445 # Validate resource type
446 resource_class, error_response = _validate_resource_type(resource_type=resource_type)
447 if error_response:
448 return error_response
450 client = get_dynamic_client()
451 assert resource_class is not None # Type checker hint - we already validated this
452 resource_instance, error = _create_resource_instance(
453 resource_class=resource_class, name=name, namespace=namespace, client=client
454 )
455 if error:
456 return error
458 if not resource_instance.exists:
459 return _format_not_found_error(resource_type=resource_type, name=name, namespace=namespace)
461 # Apply the patch
462 content_type = (
463 "application/merge-patch+json" if patch_type == "merge" else "application/strategic-merge-patch+json"
464 )
465 resource_instance.api.patch(body=patch, namespace=namespace, content_type=content_type)
467 return {
468 "success": True,
469 "resource_type": resource_type,
470 "name": name,
471 "namespace": namespace,
472 "message": f"{resource_type} '{name}' updated successfully",
473 }
474 except Exception as e:
475 return _format_exception_error("Failed to update", f"{resource_type} '{name}'", e)
478@mcp.tool
479def delete_resource(
480 resource_type: str,
481 name: str,
482 namespace: str | None = None,
483 wait: bool = True,
484 timeout: int = 60,
485) -> dict[str, Any]:
486 """
487 Delete a Kubernetes/OpenShift resource.
488 """
489 try:
490 # Validate resource type
491 resource_class, error_response = _validate_resource_type(resource_type=resource_type)
492 if error_response:
493 return error_response
495 client = get_dynamic_client()
496 assert resource_class is not None # Type checker hint - we already validated this
497 resource_instance, error = _create_resource_instance(
498 resource_class=resource_class, name=name, namespace=namespace, client=client
499 )
500 if error:
501 return error
503 if not resource_instance.exists:
504 return {
505 "warning": f"{resource_type} '{name}' not found"
506 + (f" in namespace '{namespace}'" if namespace else ""),
507 "success": True,
508 }
510 # Delete the resource
511 success = resource_instance.delete(wait=wait, timeout=timeout)
513 return {
514 "success": success,
515 "resource_type": resource_type,
516 "name": name,
517 "namespace": namespace,
518 "message": f"{resource_type} '{name}' deleted successfully"
519 if success
520 else f"Failed to delete {resource_type} '{name}'",
521 }
522 except Exception as e:
523 return _format_exception_error("Failed to delete", f"{resource_type} '{name}'", e)
526@mcp.tool
527def get_pod_logs(
528 name: str,
529 namespace: str,
530 container: str | None = None,
531 previous: bool = False,
532 since_seconds: int | None = None,
533 tail_lines: int | None = None,
534) -> dict[str, Any]:
535 """
536 Get logs from a pod container.
537 """
538 try:
539 client = get_dynamic_client()
540 pod = Pod(client=client, name=name, namespace=namespace)
542 if not pod.exists:
543 return _format_not_found_error(resource_type="Pod", name=name, namespace=namespace)
545 # Build kwargs for log method
546 kwargs: dict[str, Any] = {}
547 if container:
548 kwargs["container"] = container
549 if previous:
550 kwargs["previous"] = previous
551 if tail_lines:
552 kwargs["tail_lines"] = tail_lines
553 if since_seconds:
554 kwargs["since_seconds"] = since_seconds
556 logs = pod.log(**kwargs)
558 return {"pod": name, "namespace": namespace, "container": container, "logs": logs}
559 except Exception as e:
560 return _format_exception_error("Failed to get logs for", f"pod '{name}'", e)
563@mcp.tool
564def exec_in_pod(
565 name: str,
566 namespace: str,
567 command: list[str],
568 container: str | None = None,
569) -> dict[str, Any]:
570 """
571 Execute a command in a pod container.
572 """
573 try:
574 client = get_dynamic_client()
575 pod = Pod(client=client, name=name, namespace=namespace)
577 if not pod.exists:
578 return _format_not_found_error(resource_type="Pod", name=name, namespace=namespace)
580 # Execute command
581 try:
582 if container:
583 stdout = pod.execute(command=command, container=container)
584 else:
585 stdout = pod.execute(command=command)
587 return {
588 "pod": name,
589 "namespace": namespace,
590 "container": container,
591 "command": command,
592 "stdout": stdout,
593 "stderr": "",
594 "returncode": 0,
595 }
596 except ExecOnPodError as e:
597 return {
598 "pod": name,
599 "namespace": namespace,
600 "container": container,
601 "command": command,
602 "stdout": e.out,
603 "stderr": str(e.err),
604 "returncode": e.rc,
605 }
606 except Exception as e:
607 return _format_exception_error("Failed to execute command in", f"pod '{name}'", e)
610@mcp.tool
611def get_resource_events(
612 resource_type: str,
613 name: str,
614 namespace: str | None = None,
615 limit: int = 10,
616) -> dict[str, Any]:
617 """
618 Get events related to a specific resource.
620 Args:
621 resource_type: Type of the resource (e.g., 'pod', 'deployment')
622 name: Name of the resource
623 namespace: Namespace of the resource
624 limit: Maximum number of events to return (default: 10)
626 Returns:
627 Dictionary containing event information
628 """
629 try:
630 LOGGER.info(f"Getting events for {resource_type}/{name} in namespace {namespace}")
631 client = get_dynamic_client()
633 # Validate resource type and get the resource class
634 resource_class, error_response = _validate_resource_type(resource_type=resource_type)
635 if error_response:
636 return error_response
638 # Build field selector for events with correct format
639 field_selectors = []
640 if name:
641 field_selectors.append(f"involvedObject.name=={name}")
642 if namespace:
643 field_selectors.append(f"involvedObject.namespace=={namespace}")
644 if resource_type:
645 # Get the correct Kind value from the resource class
646 kind = resource_class.kind if resource_class else resource_type
647 field_selectors.append(f"involvedObject.kind=={kind}")
649 field_selector = ",".join(field_selectors) if field_selectors else None
650 LOGGER.debug(f"Using field selector: {field_selector}")
652 events = []
653 # Event.get() returns a generator of watch events
654 for watch_event in Event.get(
655 client, # Pass as positional argument, not keyword
656 namespace=namespace,
657 field_selector=field_selector,
658 timeout=5, # Add timeout to avoid hanging
659 ):
660 # Debug logging to understand the structure
661 LOGGER.debug(f"watch_event type: {type(watch_event)}")
663 # Extract the event object from the watch event
664 # The watch event is a dict with keys: ['type', 'object', 'raw_object']
665 if isinstance(watch_event, dict) and "object" in watch_event:
666 event_obj = watch_event["object"]
667 event_type = watch_event.get("type", "UNKNOWN") # ADDED, MODIFIED, DELETED
668 LOGGER.debug(f"Watch event type: {event_type}, object type: {type(event_obj)}")
669 else:
670 # Fallback for unexpected structure
671 event_obj = watch_event
672 event_type = "UNKNOWN"
674 # The event_obj is a kubernetes.dynamic.resource.ResourceInstance
675 # We can access its attributes directly
676 try:
677 event_info = {
678 "type": event_obj.type,
679 "reason": event_obj.reason,
680 "message": event_obj.message,
681 "count": getattr(event_obj, "count", 1),
682 "firstTimestamp": str(getattr(event_obj, "firstTimestamp", "")),
683 "lastTimestamp": str(getattr(event_obj, "lastTimestamp", "")),
684 "source": {
685 "component": event_obj.source.get("component") if hasattr(event_obj, "source") else None,
686 "host": event_obj.source.get("host") if hasattr(event_obj, "source") else None,
687 },
688 "involvedObject": {
689 "kind": event_obj.involvedObject.get("kind") if hasattr(event_obj, "involvedObject") else None,
690 "name": event_obj.involvedObject.get("name") if hasattr(event_obj, "involvedObject") else None,
691 "namespace": event_obj.involvedObject.get("namespace")
692 if hasattr(event_obj, "involvedObject")
693 else None,
694 },
695 }
696 events.append(event_info)
697 LOGGER.debug(f"Successfully extracted event: {event_info['reason']} - {event_info['message'][:50]}...")
698 except Exception as e:
699 LOGGER.error(f"Failed to extract event data: {e}")
700 # Try to get whatever we can
701 event_info = {
702 "type": "Unknown",
703 "reason": "ExtractionError",
704 "message": str(e),
705 "count": 0,
706 "firstTimestamp": "",
707 "lastTimestamp": "",
708 "source": {"component": None, "host": None},
709 "involvedObject": {"kind": None, "name": None, "namespace": None},
710 }
711 events.append(event_info)
713 if len(events) >= limit:
714 break
716 return {
717 "resource_type": resource_type,
718 "name": name,
719 "namespace": namespace,
720 "event_count": len(events),
721 "events": events,
722 }
723 except Exception as e:
724 error_dict = _format_exception_error("Failed to get", "events", e)
725 error_dict["traceback"] = traceback.format_exc()
726 return error_dict
729@mcp.tool
730def apply_yaml(
731 yaml_content: str,
732 namespace: str | None = None,
733) -> dict[str, Any]:
734 """
735 Apply YAML content containing one or more Kubernetes/OpenShift resources.
736 """
737 try:
738 client = get_dynamic_client()
739 results = []
740 successful = 0
741 failed = 0
743 # Parse YAML content (could contain multiple documents)
744 documents = yaml.safe_load_all(yaml_content)
746 for doc in documents:
747 if not doc:
748 continue
750 kind = doc.get("kind", "").lower()
751 if not kind:
752 results.append({"error": "Missing 'kind' field in YAML document", "success": False})
753 failed += 1
754 continue
756 # Validate resource type
757 resource_class, error_response = _validate_resource_type(resource_type=kind)
758 if error_response:
759 results.append({
760 "kind": kind,
761 "name": doc.get("metadata", {}).get("name", "unknown"),
762 "error": error_response.get("error", f"Unknown resource type: {kind}"),
763 "success": False,
764 })
765 failed += 1
766 continue
768 try:
769 # Create resource using kind_dict which is more efficient than YAML string
770 assert resource_class is not None # Type checker hint
771 kwargs = {"client": client, "kind_dict": doc}
772 resource_instance = resource_class(**kwargs)
773 resource_instance.deploy()
775 results.append({
776 "kind": kind,
777 "name": resource_instance.name,
778 "namespace": getattr(resource_instance, "namespace", None),
779 "success": True,
780 "message": f"Created {kind} '{resource_instance.name}'",
781 })
782 successful += 1
783 except Exception as e:
784 results.append({"kind": kind, "name": doc.get("metadata", {}).get("name", "unknown"), "error": str(e)})
785 failed += 1
787 # Summary
788 return {"total_resources": len(results), "successful": successful, "failed": failed, "results": results}
789 except Exception as e:
790 return _format_exception_error("Failed to apply", "YAML", e)
793@mcp.tool
794def get_resource_types(random_string: str) -> dict[str, Any]:
795 """
796 Get a list of all available resource types that can be managed.
797 """
798 return {
799 "resource_types": sorted(RESOURCE_TYPES), # RESOURCE_TYPES is already a list
800 "total_count": len(RESOURCE_TYPES),
801 "categories": {
802 "workloads": ["pod", "deployment", "replicaset", "daemonset", "job", "cronjob"],
803 "services": ["service", "route", "networkpolicy"],
804 "config": ["configmap", "secret"],
805 "storage": ["persistentvolume", "persistentvolumeclaim", "storageclass"],
806 "rbac": ["serviceaccount", "role", "rolebinding", "clusterrole", "clusterrolebinding"],
807 "cluster": ["namespace", "node", "event", "limitrange", "resourcequota"],
808 "custom": ["customresourcedefinition"],
809 "openshift": ["project", "route", "imagestream", "template"],
810 },
811 }
814def main() -> None:
815 mcp.run()
818if __name__ == "__main__":
819 main()