claude-mpm 4.0.16__py3-none-any.whl → 4.0.19__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/qa.json +23 -11
- claude_mpm/cli/__init__.py +62 -0
- claude_mpm/cli/commands/mcp_install_commands.py +62 -5
- claude_mpm/cli/commands/mcp_server_commands.py +60 -79
- claude_mpm/scripts/mcp_server.py +68 -0
- claude_mpm/scripts/mcp_wrapper.py +39 -0
- claude_mpm/services/agent_capabilities_service.py +1 -0
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +8 -0
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +312 -0
- claude_mpm/services/mcp_gateway/core/startup_verification.py +315 -0
- claude_mpm/services/mcp_gateway/main.py +7 -0
- claude_mpm/services/mcp_gateway/server/stdio_server.py +145 -29
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +453 -0
- {claude_mpm-4.0.16.dist-info → claude_mpm-4.0.19.dist-info}/METADATA +1 -1
- {claude_mpm-4.0.16.dist-info → claude_mpm-4.0.19.dist-info}/RECORD +20 -15
- {claude_mpm-4.0.16.dist-info → claude_mpm-4.0.19.dist-info}/entry_points.txt +1 -0
- {claude_mpm-4.0.16.dist-info → claude_mpm-4.0.19.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.16.dist-info → claude_mpm-4.0.19.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.16.dist-info → claude_mpm-4.0.19.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Gateway Singleton Manager
|
|
3
|
+
============================
|
|
4
|
+
|
|
5
|
+
Provides singleton management for MCP Gateway instances to ensure only one
|
|
6
|
+
gateway is running per claude-mpm installation.
|
|
7
|
+
|
|
8
|
+
WHY: MCP gateways are stdio-based protocol handlers that should have only one
|
|
9
|
+
instance per installation to avoid conflicts and resource contention.
|
|
10
|
+
|
|
11
|
+
DESIGN DECISIONS:
|
|
12
|
+
- File-based locking for cross-process coordination
|
|
13
|
+
- PID tracking for instance validation
|
|
14
|
+
- Automatic cleanup on process termination
|
|
15
|
+
- Thread-safe singleton pattern
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import fcntl
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import signal
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Dict, Optional
|
|
27
|
+
|
|
28
|
+
from claude_mpm.config.paths import paths
|
|
29
|
+
from claude_mpm.core.logger import get_logger
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MCPGatewayManager:
|
|
33
|
+
"""
|
|
34
|
+
Singleton manager for MCP Gateway instances.
|
|
35
|
+
|
|
36
|
+
Ensures only one gateway instance is running per installation using
|
|
37
|
+
file-based locking and PID tracking.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_instance: Optional['MCPGatewayManager'] = None
|
|
41
|
+
_lock = threading.Lock()
|
|
42
|
+
|
|
43
|
+
def __new__(cls):
|
|
44
|
+
"""Singleton pattern implementation."""
|
|
45
|
+
with cls._lock:
|
|
46
|
+
if cls._instance is None:
|
|
47
|
+
cls._instance = super().__new__(cls)
|
|
48
|
+
cls._instance._initialized = False
|
|
49
|
+
return cls._instance
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
"""Initialize the gateway manager."""
|
|
53
|
+
if self._initialized:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
self.logger = get_logger("MCPGatewayManager")
|
|
57
|
+
self._initialized = True
|
|
58
|
+
|
|
59
|
+
# Paths for coordination
|
|
60
|
+
self.mcp_dir = paths.claude_mpm_dir_hidden / "mcp"
|
|
61
|
+
self.lock_file = self.mcp_dir / "gateway.lock"
|
|
62
|
+
self.instance_file = self.mcp_dir / "gateway.json"
|
|
63
|
+
|
|
64
|
+
# Ensure directory exists
|
|
65
|
+
self.mcp_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
# Lock file handle
|
|
68
|
+
self._lock_fd: Optional[int] = None
|
|
69
|
+
self._current_instance: Optional[Dict[str, Any]] = None
|
|
70
|
+
|
|
71
|
+
# Setup cleanup handlers
|
|
72
|
+
self._setup_cleanup_handlers()
|
|
73
|
+
|
|
74
|
+
def _setup_cleanup_handlers(self):
|
|
75
|
+
"""Setup signal handlers for cleanup on termination."""
|
|
76
|
+
def cleanup_handler(signum, frame):
|
|
77
|
+
self.logger.info(f"Received signal {signum}, cleaning up gateway")
|
|
78
|
+
self.cleanup()
|
|
79
|
+
|
|
80
|
+
signal.signal(signal.SIGTERM, cleanup_handler)
|
|
81
|
+
signal.signal(signal.SIGINT, cleanup_handler)
|
|
82
|
+
|
|
83
|
+
def acquire_lock(self) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Acquire exclusive lock for gateway instance.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
True if lock acquired successfully, False otherwise
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
# Open lock file
|
|
92
|
+
self._lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_WRONLY | os.O_TRUNC)
|
|
93
|
+
|
|
94
|
+
# Try to acquire exclusive lock (non-blocking)
|
|
95
|
+
fcntl.flock(self._lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
96
|
+
|
|
97
|
+
# Write current PID to lock file
|
|
98
|
+
os.write(self._lock_fd, str(os.getpid()).encode())
|
|
99
|
+
os.fsync(self._lock_fd)
|
|
100
|
+
|
|
101
|
+
self.logger.info(f"Acquired gateway lock (PID: {os.getpid()})")
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
except (OSError, IOError) as e:
|
|
105
|
+
if self._lock_fd:
|
|
106
|
+
try:
|
|
107
|
+
os.close(self._lock_fd)
|
|
108
|
+
except:
|
|
109
|
+
pass
|
|
110
|
+
self._lock_fd = None
|
|
111
|
+
|
|
112
|
+
self.logger.debug(f"Failed to acquire gateway lock: {e}")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
def release_lock(self):
|
|
116
|
+
"""Release the gateway lock."""
|
|
117
|
+
if self._lock_fd:
|
|
118
|
+
try:
|
|
119
|
+
fcntl.flock(self._lock_fd, fcntl.LOCK_UN)
|
|
120
|
+
os.close(self._lock_fd)
|
|
121
|
+
self._lock_fd = None
|
|
122
|
+
|
|
123
|
+
# Remove lock file
|
|
124
|
+
if self.lock_file.exists():
|
|
125
|
+
self.lock_file.unlink()
|
|
126
|
+
|
|
127
|
+
self.logger.info("Released gateway lock")
|
|
128
|
+
except Exception as e:
|
|
129
|
+
self.logger.warning(f"Error releasing lock: {e}")
|
|
130
|
+
|
|
131
|
+
def is_gateway_running(self) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Check if a gateway instance is currently running.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if gateway is running, False otherwise
|
|
137
|
+
"""
|
|
138
|
+
instance_info = self.get_running_instance_info()
|
|
139
|
+
return instance_info is not None
|
|
140
|
+
|
|
141
|
+
def get_running_instance_info(self) -> Optional[Dict[str, Any]]:
|
|
142
|
+
"""
|
|
143
|
+
Get information about the currently running gateway instance.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Instance information dict or None if no instance running
|
|
147
|
+
"""
|
|
148
|
+
if not self.instance_file.exists():
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
with open(self.instance_file, 'r') as f:
|
|
153
|
+
instance_info = json.load(f)
|
|
154
|
+
|
|
155
|
+
# Validate PID is still running
|
|
156
|
+
pid = instance_info.get('pid')
|
|
157
|
+
if pid and self._is_pid_running(pid):
|
|
158
|
+
return instance_info
|
|
159
|
+
else:
|
|
160
|
+
# Stale instance file, remove it
|
|
161
|
+
self.instance_file.unlink()
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
165
|
+
self.logger.warning(f"Error reading instance file: {e}")
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def _is_pid_running(self, pid: int) -> bool:
|
|
169
|
+
"""Check if a PID is currently running."""
|
|
170
|
+
try:
|
|
171
|
+
os.kill(pid, 0) # Signal 0 just checks if process exists
|
|
172
|
+
return True
|
|
173
|
+
except (OSError, ProcessLookupError):
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def register_instance(self, gateway_name: str, version: str) -> bool:
|
|
177
|
+
"""
|
|
178
|
+
Register a new gateway instance.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
gateway_name: Name of the gateway
|
|
182
|
+
version: Gateway version
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
True if registration successful, False otherwise
|
|
186
|
+
"""
|
|
187
|
+
if not self.acquire_lock():
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
instance_info = {
|
|
192
|
+
'pid': os.getpid(),
|
|
193
|
+
'gateway_name': gateway_name,
|
|
194
|
+
'version': version,
|
|
195
|
+
'started_at': time.time(),
|
|
196
|
+
'lock_file': str(self.lock_file),
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
with open(self.instance_file, 'w') as f:
|
|
200
|
+
json.dump(instance_info, f, indent=2)
|
|
201
|
+
|
|
202
|
+
self._current_instance = instance_info
|
|
203
|
+
self.logger.info(f"Registered gateway instance: {gateway_name} v{version}")
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
self.logger.error(f"Failed to register instance: {e}")
|
|
208
|
+
self.release_lock()
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
def cleanup(self):
|
|
212
|
+
"""Clean up gateway instance and release resources."""
|
|
213
|
+
try:
|
|
214
|
+
# Remove instance file
|
|
215
|
+
if self.instance_file.exists():
|
|
216
|
+
self.instance_file.unlink()
|
|
217
|
+
|
|
218
|
+
# Release lock
|
|
219
|
+
self.release_lock()
|
|
220
|
+
|
|
221
|
+
self._current_instance = None
|
|
222
|
+
self.logger.info("Gateway cleanup completed")
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
self.logger.warning(f"Error during cleanup: {e}")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# Global functions for easy access
|
|
229
|
+
_manager: Optional[MCPGatewayManager] = None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_gateway_manager() -> MCPGatewayManager:
|
|
233
|
+
"""Get the global gateway manager instance."""
|
|
234
|
+
global _manager
|
|
235
|
+
if _manager is None:
|
|
236
|
+
_manager = MCPGatewayManager()
|
|
237
|
+
return _manager
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def is_gateway_running() -> bool:
|
|
241
|
+
"""Check if a gateway instance is currently running."""
|
|
242
|
+
return get_gateway_manager().is_gateway_running()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_gateway_status() -> Optional[Dict[str, Any]]:
|
|
246
|
+
"""Get status of running gateway instance."""
|
|
247
|
+
return get_gateway_manager().get_running_instance_info()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def start_global_gateway(gateway_name: str = "claude-mpm-mcp", version: str = "1.0.0") -> bool:
|
|
251
|
+
"""
|
|
252
|
+
Start the global MCP gateway instance.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
gateway_name: Name for the gateway
|
|
256
|
+
version: Gateway version
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
True if started successfully, False otherwise
|
|
260
|
+
"""
|
|
261
|
+
manager = get_gateway_manager()
|
|
262
|
+
|
|
263
|
+
# Check if already running
|
|
264
|
+
if manager.is_gateway_running():
|
|
265
|
+
instance_info = manager.get_running_instance_info()
|
|
266
|
+
manager.logger.info(f"Gateway already running (PID: {instance_info.get('pid')})")
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
# Register new instance
|
|
270
|
+
if not manager.register_instance(gateway_name, version):
|
|
271
|
+
manager.logger.error("Failed to register gateway instance")
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
# Import and start the gateway
|
|
275
|
+
try:
|
|
276
|
+
from ..main import MCPGatewayOrchestrator
|
|
277
|
+
|
|
278
|
+
orchestrator = MCPGatewayOrchestrator()
|
|
279
|
+
|
|
280
|
+
if not await orchestrator.initialize():
|
|
281
|
+
manager.logger.error("Failed to initialize gateway")
|
|
282
|
+
manager.cleanup()
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
if not await orchestrator.start():
|
|
286
|
+
manager.logger.error("Failed to start gateway")
|
|
287
|
+
manager.cleanup()
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
manager.logger.info("Global gateway started successfully")
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
except Exception as e:
|
|
294
|
+
manager.logger.error(f"Error starting gateway: {e}")
|
|
295
|
+
manager.cleanup()
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def run_global_gateway():
|
|
300
|
+
"""Run the global MCP gateway until shutdown."""
|
|
301
|
+
manager = get_gateway_manager()
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
from ..main import MCPGatewayOrchestrator
|
|
305
|
+
|
|
306
|
+
orchestrator = MCPGatewayOrchestrator()
|
|
307
|
+
await orchestrator.run()
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
manager.logger.error(f"Error running gateway: {e}")
|
|
311
|
+
finally:
|
|
312
|
+
manager.cleanup()
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Gateway Startup Verification
|
|
3
|
+
================================
|
|
4
|
+
|
|
5
|
+
Provides startup verification and installation for MCP Gateway configuration.
|
|
6
|
+
Ensures the gateway is properly configured with essential tools on startup.
|
|
7
|
+
|
|
8
|
+
WHY: The MCP gateway should be automatically configured and verified on startup
|
|
9
|
+
to provide a seamless experience with diagnostic tools, file summarizer, and
|
|
10
|
+
ticket service.
|
|
11
|
+
|
|
12
|
+
DESIGN DECISIONS:
|
|
13
|
+
- Automatic configuration detection and installation
|
|
14
|
+
- Essential tools verification (diagnostics, file summarizer, ticket service)
|
|
15
|
+
- Graceful fallback when tools are unavailable
|
|
16
|
+
- Non-blocking startup (warnings instead of failures)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from claude_mpm.config.paths import paths
|
|
24
|
+
from claude_mpm.core.logger import get_logger
|
|
25
|
+
|
|
26
|
+
from .singleton_manager import get_gateway_manager, is_gateway_running
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MCPGatewayStartupVerifier:
|
|
30
|
+
"""
|
|
31
|
+
Verifies and configures MCP Gateway on startup.
|
|
32
|
+
|
|
33
|
+
Ensures the gateway is properly configured with essential tools and
|
|
34
|
+
provides diagnostic information about the gateway state.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
"""Initialize the startup verifier."""
|
|
39
|
+
self.logger = get_logger("MCPGatewayStartupVerifier")
|
|
40
|
+
self.config_dir = paths.claude_mpm_dir_hidden / "mcp"
|
|
41
|
+
self.config_file = self.config_dir / "gateway_config.json"
|
|
42
|
+
|
|
43
|
+
# Essential tools that should be available
|
|
44
|
+
self.essential_tools = [
|
|
45
|
+
"echo", # Basic diagnostic tool
|
|
46
|
+
"calculator", # Math operations
|
|
47
|
+
"system_info", # System diagnostics
|
|
48
|
+
"health_check", # Health diagnostics
|
|
49
|
+
"document_summarizer", # File summarizer
|
|
50
|
+
"ticket", # Ticket service (unified)
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
async def verify_and_configure(self) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Verify MCP gateway configuration and configure if needed.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dictionary with verification results and status
|
|
59
|
+
"""
|
|
60
|
+
self.logger.info("Starting MCP Gateway verification")
|
|
61
|
+
|
|
62
|
+
results = {
|
|
63
|
+
"gateway_configured": False,
|
|
64
|
+
"singleton_manager": False,
|
|
65
|
+
"essential_tools": {},
|
|
66
|
+
"configuration_created": False,
|
|
67
|
+
"warnings": [],
|
|
68
|
+
"errors": [],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# 1. Verify singleton manager
|
|
73
|
+
results["singleton_manager"] = self._verify_singleton_manager()
|
|
74
|
+
|
|
75
|
+
# 2. Ensure configuration directory exists
|
|
76
|
+
self._ensure_config_directory()
|
|
77
|
+
|
|
78
|
+
# 3. Verify or create gateway configuration
|
|
79
|
+
config_created = await self._verify_gateway_configuration()
|
|
80
|
+
results["configuration_created"] = config_created
|
|
81
|
+
|
|
82
|
+
# 4. Verify essential tools
|
|
83
|
+
tools_status = await self._verify_essential_tools()
|
|
84
|
+
results["essential_tools"] = tools_status
|
|
85
|
+
|
|
86
|
+
# 5. Check overall gateway status
|
|
87
|
+
results["gateway_configured"] = self._assess_gateway_status(results)
|
|
88
|
+
|
|
89
|
+
# Log summary
|
|
90
|
+
self._log_verification_summary(results)
|
|
91
|
+
|
|
92
|
+
return results
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
self.logger.error(f"Error during MCP Gateway verification: {e}")
|
|
96
|
+
results["errors"].append(f"Verification failed: {e}")
|
|
97
|
+
return results
|
|
98
|
+
|
|
99
|
+
def _verify_singleton_manager(self) -> bool:
|
|
100
|
+
"""Verify singleton manager is working."""
|
|
101
|
+
try:
|
|
102
|
+
manager = get_gateway_manager()
|
|
103
|
+
# Test basic functionality
|
|
104
|
+
running = is_gateway_running()
|
|
105
|
+
self.logger.debug(f"Singleton manager operational, gateway running: {running}")
|
|
106
|
+
return True
|
|
107
|
+
except Exception as e:
|
|
108
|
+
self.logger.warning(f"Singleton manager issue: {e}")
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def _ensure_config_directory(self):
|
|
112
|
+
"""Ensure MCP configuration directory exists."""
|
|
113
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
self.logger.debug(f"MCP config directory: {self.config_dir}")
|
|
115
|
+
|
|
116
|
+
async def _verify_gateway_configuration(self) -> bool:
|
|
117
|
+
"""Verify or create gateway configuration."""
|
|
118
|
+
if self.config_file.exists():
|
|
119
|
+
self.logger.debug("Gateway configuration file exists")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
# Create default configuration
|
|
123
|
+
default_config = self._create_default_configuration()
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
import json
|
|
127
|
+
with open(self.config_file, 'w') as f:
|
|
128
|
+
json.dump(default_config, f, indent=2)
|
|
129
|
+
|
|
130
|
+
self.logger.info(f"Created default gateway configuration: {self.config_file}")
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
self.logger.warning(f"Failed to create configuration: {e}")
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
def _create_default_configuration(self) -> Dict[str, Any]:
|
|
138
|
+
"""Create default MCP gateway configuration."""
|
|
139
|
+
return {
|
|
140
|
+
"mcp": {
|
|
141
|
+
"server": {
|
|
142
|
+
"name": "claude-mpm-mcp-gateway",
|
|
143
|
+
"version": "1.0.0",
|
|
144
|
+
"description": "Claude MPM MCP Gateway with essential tools"
|
|
145
|
+
},
|
|
146
|
+
"tools": {
|
|
147
|
+
"enabled": True,
|
|
148
|
+
"auto_discover": True,
|
|
149
|
+
"timeout_default": 30,
|
|
150
|
+
"max_concurrent": 10,
|
|
151
|
+
"essential_tools": self.essential_tools
|
|
152
|
+
},
|
|
153
|
+
"logging": {
|
|
154
|
+
"level": "INFO",
|
|
155
|
+
"file": str(paths.logs_dir / "mcp_gateway.log")
|
|
156
|
+
},
|
|
157
|
+
"security": {
|
|
158
|
+
"validate_schemas": True,
|
|
159
|
+
"sanitize_inputs": True
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async def _verify_essential_tools(self) -> Dict[str, Dict[str, Any]]:
|
|
165
|
+
"""Verify essential tools are available."""
|
|
166
|
+
tools_status = {}
|
|
167
|
+
|
|
168
|
+
for tool_name in self.essential_tools:
|
|
169
|
+
status = await self._verify_tool(tool_name)
|
|
170
|
+
tools_status[tool_name] = status
|
|
171
|
+
|
|
172
|
+
return tools_status
|
|
173
|
+
|
|
174
|
+
async def _verify_tool(self, tool_name: str) -> Dict[str, Any]:
|
|
175
|
+
"""Verify a specific tool is available."""
|
|
176
|
+
status = {
|
|
177
|
+
"available": False,
|
|
178
|
+
"initialized": False,
|
|
179
|
+
"error": None,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Try to import and initialize the tool
|
|
184
|
+
tool_instance = await self._create_tool_instance(tool_name)
|
|
185
|
+
|
|
186
|
+
if tool_instance:
|
|
187
|
+
status["available"] = True
|
|
188
|
+
|
|
189
|
+
# Try to initialize
|
|
190
|
+
if hasattr(tool_instance, 'initialize'):
|
|
191
|
+
initialized = await tool_instance.initialize()
|
|
192
|
+
status["initialized"] = initialized
|
|
193
|
+
else:
|
|
194
|
+
status["initialized"] = True
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
status["error"] = str(e)
|
|
198
|
+
self.logger.debug(f"Tool {tool_name} verification failed: {e}")
|
|
199
|
+
|
|
200
|
+
return status
|
|
201
|
+
|
|
202
|
+
async def _create_tool_instance(self, tool_name: str):
|
|
203
|
+
"""Create an instance of the specified tool."""
|
|
204
|
+
tool_map = {
|
|
205
|
+
"echo": ("..tools.base_adapter", "EchoToolAdapter"),
|
|
206
|
+
"calculator": ("..tools.base_adapter", "CalculatorToolAdapter"),
|
|
207
|
+
"system_info": ("..tools.base_adapter", "SystemInfoToolAdapter"),
|
|
208
|
+
"health_check": ("..tools.health_check_tool", "HealthCheckTool"),
|
|
209
|
+
"document_summarizer": ("..tools.document_summarizer", "DocumentSummarizerTool"),
|
|
210
|
+
"ticket": ("..tools.unified_ticket_tool", "UnifiedTicketTool"),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if tool_name not in tool_map:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
module_path, class_name = tool_map[tool_name]
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
# Dynamic import
|
|
220
|
+
from importlib import import_module
|
|
221
|
+
module = import_module(module_path, package=__package__)
|
|
222
|
+
tool_class = getattr(module, class_name)
|
|
223
|
+
return tool_class()
|
|
224
|
+
except (ImportError, AttributeError) as e:
|
|
225
|
+
self.logger.debug(f"Could not import {tool_name}: {e}")
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
def _assess_gateway_status(self, results: Dict[str, Any]) -> bool:
|
|
229
|
+
"""Assess overall gateway configuration status."""
|
|
230
|
+
# Gateway is considered configured if:
|
|
231
|
+
# 1. Singleton manager works
|
|
232
|
+
# 2. At least basic tools are available
|
|
233
|
+
# 3. No critical errors
|
|
234
|
+
|
|
235
|
+
if not results["singleton_manager"]:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
if results["errors"]:
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
# Check if at least basic diagnostic tools are available
|
|
242
|
+
essential_available = 0
|
|
243
|
+
for tool_name, status in results["essential_tools"].items():
|
|
244
|
+
if status.get("available", False):
|
|
245
|
+
essential_available += 1
|
|
246
|
+
|
|
247
|
+
# Consider configured if at least 3 essential tools are available
|
|
248
|
+
return essential_available >= 3
|
|
249
|
+
|
|
250
|
+
def _log_verification_summary(self, results: Dict[str, Any]):
|
|
251
|
+
"""Log verification summary."""
|
|
252
|
+
if results["gateway_configured"]:
|
|
253
|
+
self.logger.info("✅ MCP Gateway verification completed successfully")
|
|
254
|
+
else:
|
|
255
|
+
self.logger.warning("⚠️ MCP Gateway verification completed with issues")
|
|
256
|
+
|
|
257
|
+
# Log tool status
|
|
258
|
+
available_tools = []
|
|
259
|
+
unavailable_tools = []
|
|
260
|
+
|
|
261
|
+
for tool_name, status in results["essential_tools"].items():
|
|
262
|
+
if status.get("available", False):
|
|
263
|
+
available_tools.append(tool_name)
|
|
264
|
+
else:
|
|
265
|
+
unavailable_tools.append(tool_name)
|
|
266
|
+
|
|
267
|
+
if available_tools:
|
|
268
|
+
self.logger.info(f"Available tools: {', '.join(available_tools)}")
|
|
269
|
+
|
|
270
|
+
if unavailable_tools:
|
|
271
|
+
self.logger.warning(f"Unavailable tools: {', '.join(unavailable_tools)}")
|
|
272
|
+
|
|
273
|
+
# Log warnings and errors
|
|
274
|
+
for warning in results.get("warnings", []):
|
|
275
|
+
self.logger.warning(warning)
|
|
276
|
+
|
|
277
|
+
for error in results.get("errors", []):
|
|
278
|
+
self.logger.error(error)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Global verification function
|
|
282
|
+
async def verify_mcp_gateway_on_startup() -> Dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
Verify MCP Gateway configuration on startup.
|
|
285
|
+
|
|
286
|
+
This function should be called during application startup to ensure
|
|
287
|
+
the MCP gateway is properly configured.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Dictionary with verification results
|
|
291
|
+
"""
|
|
292
|
+
verifier = MCPGatewayStartupVerifier()
|
|
293
|
+
return await verifier.verify_and_configure()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def is_mcp_gateway_configured() -> bool:
|
|
297
|
+
"""
|
|
298
|
+
Quick check if MCP gateway appears to be configured.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
True if gateway appears configured, False otherwise
|
|
302
|
+
"""
|
|
303
|
+
try:
|
|
304
|
+
# Check if singleton manager works
|
|
305
|
+
manager = get_gateway_manager()
|
|
306
|
+
|
|
307
|
+
# Check if config directory exists
|
|
308
|
+
config_dir = paths.claude_mpm_dir_hidden / "mcp"
|
|
309
|
+
if not config_dir.exists():
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
return True
|
|
313
|
+
|
|
314
|
+
except Exception:
|
|
315
|
+
return False
|
|
@@ -256,6 +256,13 @@ class MCPGatewayOrchestrator:
|
|
|
256
256
|
except Exception as e:
|
|
257
257
|
self.logger.warning(f"Could not load basic tools: {e}")
|
|
258
258
|
|
|
259
|
+
# Optional: Health check tool
|
|
260
|
+
try:
|
|
261
|
+
from .tools.health_check_tool import HealthCheckTool
|
|
262
|
+
tools.append(HealthCheckTool())
|
|
263
|
+
except Exception as e:
|
|
264
|
+
self.logger.warning(f"Could not load health check tool: {e}")
|
|
265
|
+
|
|
259
266
|
# Optional: Document summarizer
|
|
260
267
|
if DocumentSummarizerTool is not None:
|
|
261
268
|
try:
|