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

1#!/usr/bin/env python 

2""" 

3MCP Server for OpenShift Python Wrapper (ocp_resources) 

4 

5This MCP server provides tools to interact with OpenShift/Kubernetes resources 

6using the ocp_resources library through the Model Context Protocol. 

7""" 

8 

9import importlib 

10import inspect 

11import io 

12import os 

13import tempfile 

14import traceback 

15from pathlib import Path 

16from typing import Any, Type 

17 

18import yaml 

19from fastmcp import FastMCP 

20from simple_logger.logger import get_logger, logging 

21 

22import ocp_resources 

23from ocp_resources.event import Event 

24from ocp_resources.exceptions import ExecOnPodError 

25from ocp_resources.pod import Pod 

26 

27# OpenShift specific resources 

28# Import ocp_resources modules 

29from ocp_resources.resource import NamespacedResource, Resource, get_client 

30 

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) 

34 

35# Initialize the MCP server 

36mcp = FastMCP(name="openshift-python-wrapper") 

37 

38# Global client variable 

39_client = None 

40 

41 

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 

49 

50 

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") 

55 

56 # Get the ocp_resources directory path 

57 ocp_resources_path = Path(ocp_resources.__file__).parent 

58 

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 

63 

64 module_name = file_path.stem 

65 try: 

66 # Import the module dynamically 

67 module = importlib.import_module(f"ocp_resources.{module_name}") 

68 

69 # Look for classes in the module 

70 for name in dir(module): 

71 if name.startswith("_"): 

72 continue 

73 

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 

88 

89 LOGGER.debug(f"Found {len(resource_map)} resource types total") 

90 return resource_map 

91 

92 

93def _get_available_resource_types(): 

94 """Get all available resource types from the resource map.""" 

95 return sorted(RESOURCE_CLASS_MAP.keys()) 

96 

97 

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)}") 

104 

105 

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() 

110 

111 # Look up the resource class in the pre-scanned map 

112 resource_class = RESOURCE_CLASS_MAP.get(resource_type_lower) 

113 

114 if resource_class: 

115 LOGGER.debug(f"Found resource class for type {resource_type}") 

116 return resource_class 

117 

118 LOGGER.warning(f"Resource type '{resource_type}' not found in RESOURCE_CLASS_MAP") 

119 return None 

120 

121 

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 

128 

129 

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() 

140 

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"} 

145 

146 # Create resource instance 

147 kwargs = {"name": name, "client": client} 

148 if is_namespaced: 

149 kwargs["namespace"] = namespace 

150 

151 resource = resource_class(**kwargs) 

152 return resource, None 

153 

154 

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 "")} 

158 

159 

160def _format_exception_error(action: str, resource_info: str, exception: Exception) -> dict[str, Any]: 

161 """Format a consistent exception error message. 

162 

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__} 

169 

170 

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) 

176 

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 } 

186 

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 [] 

192 

193 return info 

194 except Exception as e: 

195 return {"name": getattr(resource, "name", "unknown"), "error": str(e)} 

196 

197 

198# Tools for resource management 

199 

200 

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. 

211 

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 

220 

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 

233 

234 resources = [] 

235 for resource in resource_class.get(dyn_client=client, **kwargs): 

236 resources.append(format_resource_info(resource)) 

237 

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) 

246 

247 

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. 

257 

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 

265 

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 

273 

274 if not resource_instance.exists: 

275 return _format_not_found_error(resource_type=resource_type, name=name, namespace=namespace) 

276 

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 

294 

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 ) 

303 

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) 

322 

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) 

328 

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 ) 

341 

342 return info 

343 except Exception as e: 

344 return _format_exception_error("Failed to get", f"{resource_type} '{name}'", e) 

345 

346 

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. 

360 

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"} 

366 

367 if not yaml_content and not spec: 

368 return {"error": "Either yaml_content or spec must be provided"} 

369 

370 client = get_dynamic_client() 

371 

372 if yaml_content: 

373 # Parse YAML content 

374 yaml_data = yaml.safe_load(io.StringIO(yaml_content)) 

375 

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) 

380 

381 resource_class, error_response = _validate_resource_type(resource_type=resource_type) 

382 if error_response: 

383 return error_response 

384 

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 

394 

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"} 

400 

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 

410 

411 # Add spec-specific parameters based on resource type 

412 if spec: 

413 kwargs.update(spec) 

414 

415 resource_instance = resource_class(**kwargs) 

416 

417 # Deploy the resource 

418 resource_instance.deploy(wait=wait) 

419 

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) 

429 

430 

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. 

441 

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 

449 

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 

457 

458 if not resource_instance.exists: 

459 return _format_not_found_error(resource_type=resource_type, name=name, namespace=namespace) 

460 

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) 

466 

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) 

476 

477 

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 

494 

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 

502 

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 } 

509 

510 # Delete the resource 

511 success = resource_instance.delete(wait=wait, timeout=timeout) 

512 

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) 

524 

525 

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) 

541 

542 if not pod.exists: 

543 return _format_not_found_error(resource_type="Pod", name=name, namespace=namespace) 

544 

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 

555 

556 logs = pod.log(**kwargs) 

557 

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) 

561 

562 

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) 

576 

577 if not pod.exists: 

578 return _format_not_found_error(resource_type="Pod", name=name, namespace=namespace) 

579 

580 # Execute command 

581 try: 

582 if container: 

583 stdout = pod.execute(command=command, container=container) 

584 else: 

585 stdout = pod.execute(command=command) 

586 

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) 

608 

609 

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. 

619 

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) 

625 

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() 

632 

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 

637 

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}") 

648 

649 field_selector = ",".join(field_selectors) if field_selectors else None 

650 LOGGER.debug(f"Using field selector: {field_selector}") 

651 

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)}") 

662 

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" 

673 

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) 

712 

713 if len(events) >= limit: 

714 break 

715 

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 

727 

728 

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 

742 

743 # Parse YAML content (could contain multiple documents) 

744 documents = yaml.safe_load_all(yaml_content) 

745 

746 for doc in documents: 

747 if not doc: 

748 continue 

749 

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 

755 

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 

767 

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() 

774 

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 

786 

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) 

791 

792 

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 } 

812 

813 

814def main() -> None: 

815 mcp.run() 

816 

817 

818if __name__ == "__main__": 

819 main()