omnimancer-cli 0.1.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 (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,649 @@
1
+ """
2
+ MCP Manager for managing multiple MCP server connections.
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import Dict, List, Optional
8
+
9
+ from ..core.models import (
10
+ MCPConfig,
11
+ MCPError,
12
+ MCPServerConfig,
13
+ ToolDefinition,
14
+ ToolResult,
15
+ )
16
+ from ..utils.error_handler import handle_mcp_error
17
+ from ..utils.errors import (
18
+ MCPConfigurationError,
19
+ MCPConnectionError,
20
+ MCPTimeoutError,
21
+ )
22
+ from .client import MCPClient
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class MCPManager:
28
+ """
29
+ Manager for multiple MCP server connections.
30
+
31
+ This class handles the lifecycle of multiple MCP servers,
32
+ aggregates tools from all servers, and routes tool calls
33
+ to the appropriate server.
34
+ """
35
+
36
+ def __init__(self, mcp_config: MCPConfig):
37
+ """
38
+ Initialize MCP manager.
39
+
40
+ Args:
41
+ mcp_config: MCP configuration
42
+ """
43
+ self.mcp_config = mcp_config
44
+ self.clients: Dict[str, MCPClient] = {}
45
+ self.initialized = False
46
+
47
+ async def initialize_servers(
48
+ self, new_server_config: Optional[Dict[str, MCPServerConfig]] = None
49
+ ) -> None:
50
+ """
51
+ Initialize all configured MCP servers with comprehensive error handling.
52
+
53
+ This method attempts to connect to all enabled servers and provides
54
+ graceful degradation when some servers fail to connect.
55
+ """
56
+ if not self.mcp_config.enabled:
57
+ logger.info("MCP integration is disabled")
58
+ self.initialized = True # Successfully initialized with no servers
59
+ return
60
+
61
+ logger.info("Initializing MCP servers...")
62
+
63
+ # Log disabled servers for transparency
64
+ disabled_servers = {
65
+ name: config
66
+ for name, config in self.mcp_config.servers.items()
67
+ if not config.enabled
68
+ }
69
+ if disabled_servers:
70
+ logger.debug(f"Disabled MCP servers: {', '.join(disabled_servers.keys())}")
71
+
72
+ enabled_servers = self.mcp_config.get_enabled_servers()
73
+ if not enabled_servers:
74
+ logger.info("No enabled MCP servers found - MCP initialization complete")
75
+ self.initialized = True # Successfully initialized with no servers
76
+ return
77
+
78
+ # Track initialization results
79
+ successful_connections = 0
80
+ failed_connections = []
81
+
82
+ # Limit concurrent server connections
83
+ max_concurrent = min(
84
+ len(enabled_servers),
85
+ getattr(self.mcp_config, "max_concurrent_servers", 3),
86
+ )
87
+
88
+ # Create semaphore to limit concurrent connections
89
+ semaphore = asyncio.Semaphore(max_concurrent)
90
+
91
+ async def connect_server(
92
+ server_name: str, server_config: MCPServerConfig
93
+ ) -> None:
94
+ nonlocal successful_connections
95
+
96
+ async with semaphore:
97
+ try:
98
+ logger.debug(f"Attempting to connect to MCP server: {server_name}")
99
+ client = MCPClient(server_config)
100
+ await client.connect()
101
+ self.clients[server_name] = client
102
+ successful_connections += 1
103
+ logger.info(f"Successfully initialized MCP server: {server_name}")
104
+
105
+ except MCPConfigurationError as e:
106
+ error_msg, suggestions, _ = handle_mcp_error(e, server_name)
107
+ logger.error(
108
+ f"Configuration error for MCP server '{server_name}': {error_msg}"
109
+ )
110
+ for suggestion in suggestions:
111
+ logger.info(f" Suggestion: {suggestion}")
112
+ failed_connections.append(
113
+ (server_name, "configuration_error", str(e))
114
+ )
115
+
116
+ except MCPConnectionError as e:
117
+ error_msg, suggestions, should_retry = handle_mcp_error(
118
+ e, server_name
119
+ )
120
+ logger.warning(
121
+ f"Connection failed for MCP server '{server_name}': {error_msg}"
122
+ )
123
+
124
+ # Attempt one retry for connection errors
125
+ if should_retry:
126
+ try:
127
+ logger.info(
128
+ f"Retrying connection to MCP server: {server_name}"
129
+ )
130
+ await asyncio.sleep(2.0) # Brief delay before retry
131
+ client = MCPClient(server_config)
132
+ await client.connect()
133
+ self.clients[server_name] = client
134
+ successful_connections += 1
135
+ logger.info(
136
+ f"Successfully connected to MCP server on retry: {server_name}"
137
+ )
138
+ except Exception as retry_error:
139
+ logger.error(
140
+ f"Retry failed for MCP server '{server_name}': {retry_error}"
141
+ )
142
+ failed_connections.append(
143
+ (server_name, "connection_failed", str(e))
144
+ )
145
+ else:
146
+ failed_connections.append(
147
+ (server_name, "connection_failed", str(e))
148
+ )
149
+
150
+ except MCPTimeoutError as e:
151
+ error_msg, suggestions, _ = handle_mcp_error(e, server_name)
152
+ logger.warning(
153
+ f"Timeout connecting to MCP server '{server_name}': {error_msg}"
154
+ )
155
+ failed_connections.append((server_name, "timeout", str(e)))
156
+
157
+ except Exception as e:
158
+ error_msg, suggestions, _ = handle_mcp_error(e, server_name)
159
+ logger.error(
160
+ f"Unexpected error initializing MCP server '{server_name}': {error_msg}"
161
+ )
162
+ failed_connections.append((server_name, "unknown_error", str(e)))
163
+
164
+ # Connect to all servers concurrently
165
+ tasks = [
166
+ connect_server(name, config) for name, config in enabled_servers.items()
167
+ ]
168
+
169
+ await asyncio.gather(*tasks, return_exceptions=True)
170
+
171
+ # Only mark as initialized if at least one server connected successfully
172
+ self.initialized = successful_connections > 0
173
+
174
+ # Log initialization summary
175
+ total_servers = len(enabled_servers)
176
+ logger.info(
177
+ f"MCP initialization complete. Connected to {successful_connections}/{total_servers} servers."
178
+ )
179
+
180
+ if failed_connections:
181
+ logger.warning(
182
+ f"Failed to connect to {len(failed_connections)} MCP servers:"
183
+ )
184
+ for server_name, error_type, error_msg in failed_connections:
185
+ logger.warning(f" - {server_name}: {error_type}")
186
+
187
+ # Provide graceful degradation information
188
+ if successful_connections == 0 and total_servers > 0:
189
+ logger.warning(
190
+ "No MCP servers connected. Tool functionality will be unavailable."
191
+ )
192
+ elif successful_connections < total_servers:
193
+ logger.info(
194
+ f"Partial MCP connectivity: {successful_connections} servers available for tool execution."
195
+ )
196
+
197
+ async def shutdown(self) -> None:
198
+ """Shutdown all MCP server connections."""
199
+ logger.info("Shutting down MCP servers...")
200
+
201
+ # Disconnect all clients
202
+ disconnect_tasks = [client.disconnect() for client in self.clients.values()]
203
+
204
+ await asyncio.gather(*disconnect_tasks, return_exceptions=True)
205
+
206
+ self.clients.clear()
207
+ self.initialized = False
208
+ logger.info("MCP shutdown complete")
209
+
210
+ async def get_available_tools(self) -> List[ToolDefinition]:
211
+ """
212
+ Get all available tools from connected MCP servers.
213
+
214
+ Returns:
215
+ List of available tool definitions
216
+ """
217
+ if not self.initialized:
218
+ return []
219
+
220
+ all_tools = []
221
+
222
+ for client in self.clients.values():
223
+ if client.is_connected:
224
+ try:
225
+ tools = await client.list_tools()
226
+ all_tools.extend(tools)
227
+ except Exception as e:
228
+ logger.warning(
229
+ f"Failed to get tools from server '{client.server_name}': {e}"
230
+ )
231
+
232
+ return all_tools
233
+
234
+ async def execute_tool(self, name: str, arguments: Dict) -> ToolResult:
235
+ """
236
+ Execute a tool by name with given arguments, with comprehensive error handling.
237
+
238
+ Args:
239
+ name: Name of the tool to execute
240
+ arguments: Arguments to pass to the tool
241
+
242
+ Returns:
243
+ Result of the tool execution
244
+
245
+ Raises:
246
+ MCPError: If tool execution fails
247
+ """
248
+ if not self.initialized:
249
+ raise MCPError("MCP manager not initialized")
250
+
251
+ # Find the server that has this tool
252
+ target_client = None
253
+ for client in self.clients.values():
254
+ if client.is_connected and client.get_tool_definition(name):
255
+ target_client = client
256
+ break
257
+
258
+ if not target_client:
259
+ # Provide helpful error message with available tools
260
+ available_tools = []
261
+ for client in self.clients.values():
262
+ if client.is_connected:
263
+ available_tools.extend(
264
+ [tool.name for tool in client.tools.values()]
265
+ )
266
+
267
+ if available_tools:
268
+ raise MCPError(
269
+ f"Tool '{name}' not found. Available tools: {', '.join(available_tools[:5])}"
270
+ )
271
+ else:
272
+ raise MCPError(
273
+ f"Tool '{name}' not found. No MCP servers are connected."
274
+ )
275
+
276
+ try:
277
+ # Use the enhanced call_tool_with_retry method
278
+ return await target_client.call_tool_with_retry(name, arguments)
279
+
280
+ except Exception as e:
281
+ error_msg, suggestions, should_retry = handle_mcp_error(
282
+ e, target_client.server_name
283
+ )
284
+
285
+ # Log the error with suggestions
286
+ logger.error(f"Tool execution failed: {error_msg}")
287
+ for suggestion in suggestions:
288
+ logger.info(f" Suggestion: {suggestion}")
289
+
290
+ # If the client is no longer connected, try to find an alternative
291
+ if not target_client.is_connected:
292
+ logger.info(
293
+ f"Server {target_client.server_name} disconnected, looking for alternatives..."
294
+ )
295
+
296
+ # Try to find another server with the same tool
297
+ alternative_client = None
298
+ for client in self.clients.values():
299
+ if (
300
+ client != target_client
301
+ and client.is_connected
302
+ and client.get_tool_definition(name)
303
+ ):
304
+ alternative_client = client
305
+ break
306
+
307
+ if alternative_client:
308
+ logger.info(
309
+ f"Found alternative server: {alternative_client.server_name}"
310
+ )
311
+ try:
312
+ return await alternative_client.call_tool_with_retry(
313
+ name, arguments
314
+ )
315
+ except Exception as alt_error:
316
+ logger.error(f"Alternative server also failed: {alt_error}")
317
+
318
+ raise MCPError(f"Failed to execute tool '{name}': {e}")
319
+
320
+ async def reload_servers(self, server_name: Optional[str] = None) -> bool:
321
+ """
322
+ Reload server configurations and reconnect.
323
+
324
+ This method shuts down existing connections and reinitializes
325
+ servers based on the current configuration.
326
+ """
327
+ logger.info("Reloading MCP servers...")
328
+
329
+ # Shutdown existing connections
330
+ await self.shutdown()
331
+
332
+ # Reinitialize with current configuration
333
+ await self.initialize_servers()
334
+
335
+ logger.info("MCP server reload complete")
336
+
337
+ def get_server_status(
338
+ self, server_name: Optional[str] = None
339
+ ) -> Dict[str, Dict[str, any]]:
340
+ """
341
+ Get status information for all configured servers.
342
+
343
+ Returns:
344
+ Dictionary with server status information
345
+ """
346
+ status = {}
347
+
348
+ for server_name, server_config in self.mcp_config.servers.items():
349
+ client = self.clients.get(server_name)
350
+
351
+ status[server_name] = {
352
+ "enabled": server_config.enabled,
353
+ "connected": client.is_connected if client else False,
354
+ "tool_count": len(client.tools) if client else 0,
355
+ "command": server_config.command,
356
+ "args": server_config.args,
357
+ }
358
+
359
+ return status
360
+
361
+ async def health_check(self) -> Dict[str, any]:
362
+ """
363
+ Perform health checks on all connected servers.
364
+
365
+ Returns:
366
+ Dictionary with server health status and overall health
367
+ """
368
+ health_status = {}
369
+
370
+ health_tasks = []
371
+ server_names = []
372
+
373
+ for server_name, client in self.clients.items():
374
+ if client.is_connected:
375
+ health_tasks.append(client.health_check())
376
+ server_names.append(server_name)
377
+
378
+ if health_tasks:
379
+ results = await asyncio.gather(*health_tasks, return_exceptions=True)
380
+
381
+ for server_name, result in zip(server_names, results):
382
+ if isinstance(result, Exception):
383
+ health_status[server_name] = False
384
+ else:
385
+ health_status[server_name] = result
386
+
387
+ # Add disconnected servers as unhealthy
388
+ for server_name in self.mcp_config.servers:
389
+ if server_name not in health_status:
390
+ health_status[server_name] = False
391
+
392
+ # Calculate overall health
393
+ all_healthy = all(health_status.values()) if health_status else False
394
+ health_status["overall_healthy"] = all_healthy
395
+
396
+ return health_status
397
+
398
+ def get_tool_by_name(self, tool_name: str) -> Optional[ToolDefinition]:
399
+ """
400
+ Get a tool definition by name.
401
+
402
+ Args:
403
+ tool_name: Name of the tool
404
+
405
+ Returns:
406
+ ToolDefinition or None if not found
407
+ """
408
+ for client in self.clients.values():
409
+ if client.is_connected:
410
+ tool_def = client.get_tool_definition(tool_name)
411
+ if tool_def:
412
+ return tool_def
413
+
414
+ return None
415
+
416
+ def get_tools_by_server(self, server_name: str) -> List[ToolDefinition]:
417
+ """
418
+ Get all tools from a specific server.
419
+
420
+ Args:
421
+ server_name: Name of the server
422
+
423
+ Returns:
424
+ List of tool definitions from the server
425
+ """
426
+ client = self.clients.get(server_name)
427
+ if client and client.is_connected:
428
+ return list(client.tools.values())
429
+
430
+ return []
431
+
432
+ @property
433
+ def is_enabled(self) -> bool:
434
+ """Check if MCP integration is enabled."""
435
+ return self.mcp_config.enabled
436
+
437
+ @property
438
+ def connected_server_count(self) -> int:
439
+ """Get the number of connected servers."""
440
+ return sum(1 for client in self.clients.values() if client.is_connected)
441
+
442
+ @property
443
+ def total_tool_count(self) -> int:
444
+ """Get the total number of available tools across all servers."""
445
+ return sum(
446
+ len(client.tools) for client in self.clients.values() if client.is_connected
447
+ )
448
+
449
+ async def recover_failed_servers(self) -> Dict[str, bool]:
450
+ """
451
+ Attempt to recover failed or disconnected servers.
452
+
453
+ Returns:
454
+ Dictionary mapping server names to recovery success status
455
+ """
456
+ recovery_results = {}
457
+
458
+ for server_name, server_config in self.mcp_config.servers.items():
459
+ if not server_config.enabled:
460
+ continue
461
+
462
+ client = self.clients.get(server_name)
463
+
464
+ # Check if server needs recovery
465
+ needs_recovery = (
466
+ client is None
467
+ or not client.is_connected
468
+ or not await client.health_check()
469
+ )
470
+
471
+ if needs_recovery:
472
+ logger.info(f"Attempting to recover MCP server: {server_name}")
473
+
474
+ try:
475
+ # Remove old client if exists
476
+ if client:
477
+ await client.disconnect()
478
+
479
+ # Create new client and connect
480
+ new_client = MCPClient(server_config)
481
+ await new_client.connect()
482
+ self.clients[server_name] = new_client
483
+
484
+ recovery_results[server_name] = True
485
+ logger.info(f"Successfully recovered MCP server: {server_name}")
486
+
487
+ except Exception as e:
488
+ recovery_results[server_name] = False
489
+ logger.error(f"Failed to recover MCP server '{server_name}': {e}")
490
+ else:
491
+ recovery_results[server_name] = True # Already healthy
492
+
493
+ return recovery_results
494
+
495
+ async def monitor_and_recover(self, check_interval: float = 30.0) -> None:
496
+ """
497
+ Continuously monitor server health and attempt recovery.
498
+
499
+ Args:
500
+ check_interval: Seconds between health checks
501
+ """
502
+ logger.info(
503
+ f"Starting MCP server monitoring (check interval: {check_interval}s)"
504
+ )
505
+
506
+ while self.initialized:
507
+ try:
508
+ # Perform health checks
509
+ health_status = await self.health_check()
510
+
511
+ # Identify unhealthy servers
512
+ unhealthy_servers = [
513
+ name for name, healthy in health_status.items() if not healthy
514
+ ]
515
+
516
+ if unhealthy_servers:
517
+ logger.warning(
518
+ f"Unhealthy MCP servers detected: {unhealthy_servers}"
519
+ )
520
+
521
+ # Attempt recovery
522
+ recovery_results = await self.recover_failed_servers()
523
+
524
+ # Log recovery results
525
+ for server_name, recovered in recovery_results.items():
526
+ if server_name in unhealthy_servers:
527
+ if recovered:
528
+ logger.info(
529
+ f"Successfully recovered server: {server_name}"
530
+ )
531
+ else:
532
+ logger.error(f"Failed to recover server: {server_name}")
533
+
534
+ # Wait before next check
535
+ await asyncio.sleep(check_interval)
536
+
537
+ except Exception as e:
538
+ logger.error(f"Error during MCP monitoring: {e}")
539
+ await asyncio.sleep(check_interval)
540
+
541
+ def get_degradation_status(self) -> Dict[str, any]:
542
+ """
543
+ Get information about current degradation status.
544
+
545
+ Returns:
546
+ Dictionary with degradation information
547
+ """
548
+ total_configured = len(self.mcp_config.servers)
549
+ enabled_servers = len(self.mcp_config.get_enabled_servers())
550
+ connected_servers = self.connected_server_count
551
+
552
+ degradation_level = "none"
553
+ if connected_servers == 0 and enabled_servers > 0:
554
+ degradation_level = "complete"
555
+ elif connected_servers < enabled_servers:
556
+ degradation_level = "partial"
557
+
558
+ return {
559
+ "degradation_level": degradation_level,
560
+ "total_configured": total_configured,
561
+ "enabled_servers": enabled_servers,
562
+ "connected_servers": connected_servers,
563
+ "available_tools": self.total_tool_count,
564
+ "functionality_impact": self._get_functionality_impact(degradation_level),
565
+ }
566
+
567
+ def _get_functionality_impact(self, degradation_level: str) -> List[str]:
568
+ """
569
+ Get list of functionality impacts based on degradation level.
570
+
571
+ Args:
572
+ degradation_level: Level of degradation
573
+
574
+ Returns:
575
+ List of impact descriptions
576
+ """
577
+ if degradation_level == "complete":
578
+ return [
579
+ "All MCP tool functionality unavailable",
580
+ "AI conversations limited to basic text generation",
581
+ "No external tool integration available",
582
+ ]
583
+ elif degradation_level == "partial":
584
+ return [
585
+ "Some MCP tools may be unavailable",
586
+ "Reduced tool functionality",
587
+ "Some integrations may not work",
588
+ ]
589
+ else:
590
+ return ["All MCP functionality available"]
591
+
592
+ async def execute_tool_with_fallback(
593
+ self, name: str, arguments: Dict, fallback_tools: List[str] = None
594
+ ) -> ToolResult:
595
+ """
596
+ Execute a tool with fallback to alternative tools if the primary fails.
597
+
598
+ Args:
599
+ name: Primary tool name to execute
600
+ arguments: Arguments to pass to the tool
601
+ fallback_tools: List of alternative tool names to try
602
+
603
+ Returns:
604
+ Result of the tool execution
605
+
606
+ Raises:
607
+ MCPError: If all tools fail
608
+ """
609
+ fallback_tools = fallback_tools or []
610
+
611
+ # Try primary tool first
612
+ try:
613
+ return await self.execute_tool(name, arguments)
614
+ except MCPError as primary_error:
615
+ logger.warning(f"Primary tool '{name}' failed: {primary_error}")
616
+
617
+ # Try fallback tools
618
+ for fallback_tool in fallback_tools:
619
+ try:
620
+ logger.info(f"Trying fallback tool: {fallback_tool}")
621
+ return await self.execute_tool(fallback_tool, arguments)
622
+ except MCPError as fallback_error:
623
+ logger.warning(
624
+ f"Fallback tool '{fallback_tool}' failed: {fallback_error}"
625
+ )
626
+ continue
627
+
628
+ # If all tools failed, raise the original error
629
+ raise primary_error
630
+
631
+ def get_available_tools_by_server(self, server_name: str) -> List[ToolDefinition]:
632
+ """Get tools available from a specific server."""
633
+ return self.get_tools_by_server(server_name)
634
+
635
+ def _find_tool_server(self, tool_name: str) -> Optional[str]:
636
+ """Find which server has a specific tool."""
637
+ for server_name, client in self.clients.items():
638
+ if client.is_connected and client.get_tool_definition(tool_name):
639
+ return server_name
640
+ return None
641
+
642
+ async def shutdown_servers(self, server_name: str) -> bool:
643
+ """Shutdown a specific server."""
644
+ if server_name in self.clients:
645
+ client = self.clients[server_name]
646
+ await client.disconnect()
647
+ del self.clients[server_name]
648
+ return True
649
+ return False
@@ -0,0 +1,24 @@
1
+ """
2
+ Providers module for Omnimancer.
3
+
4
+ This module contains AI provider implementations and the base provider interface.
5
+ Supports multiple AI services including Claude, OpenAI, Gemini, Cohere, and Ollama.
6
+ """
7
+
8
+ from .base import BaseProvider
9
+ from .claude import ClaudeProvider
10
+ from .cohere import CohereProvider
11
+ from .factory import ProviderFactory
12
+ from .gemini import GeminiProvider
13
+ from .ollama import OllamaProvider
14
+ from .openai import OpenAIProvider
15
+
16
+ __all__ = [
17
+ "BaseProvider",
18
+ "ClaudeProvider",
19
+ "OpenAIProvider",
20
+ "GeminiProvider",
21
+ "CohereProvider",
22
+ "OllamaProvider",
23
+ "ProviderFactory",
24
+ ]