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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|