claude-mpm 4.4.5__py3-none-any.whl → 4.4.7__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/local_ops_agent.json +273 -0
- claude_mpm/cli/__init__.py +21 -0
- claude_mpm/cli/commands/mcp_external_commands.py +7 -7
- claude_mpm/cli/commands/mcp_install_commands.py +9 -9
- claude_mpm/cli/commands/mcp_setup_external.py +6 -6
- claude_mpm/cli/commands/verify.py +118 -0
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/hooks/kuzu_memory_hook.py +4 -2
- claude_mpm/services/agents/deployment/agent_deployment.py +10 -6
- claude_mpm/services/diagnostics/checks/__init__.py +2 -2
- claude_mpm/services/diagnostics/checks/{claude_desktop_check.py → claude_code_check.py} +95 -112
- claude_mpm/services/diagnostics/checks/mcp_check.py +6 -6
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +29 -6
- claude_mpm/services/diagnostics/diagnostic_runner.py +5 -5
- claude_mpm/services/diagnostics/doctor_reporter.py +4 -4
- claude_mpm/services/mcp_config_manager.py +46 -26
- claude_mpm/services/mcp_gateway/core/process_pool.py +11 -8
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +4 -4
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +8 -4
- claude_mpm/services/mcp_service_verifier.py +690 -0
- claude_mpm/services/project/project_organizer.py +8 -1
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +1 -2
- claude_mpm/services/unified/config_strategies/context_strategy.py +1 -3
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +3 -1
- claude_mpm/validation/frontmatter_validator.py +1 -1
- {claude_mpm-4.4.5.dist-info → claude_mpm-4.4.7.dist-info}/METADATA +20 -5
- {claude_mpm-4.4.5.dist-info → claude_mpm-4.4.7.dist-info}/RECORD +32 -29
- {claude_mpm-4.4.5.dist-info → claude_mpm-4.4.7.dist-info}/WHEEL +0 -0
- {claude_mpm-4.4.5.dist-info → claude_mpm-4.4.7.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.4.5.dist-info → claude_mpm-4.4.7.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.4.5.dist-info → claude_mpm-4.4.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,690 @@
|
|
1
|
+
"""
|
2
|
+
MCP Service Verifier
|
3
|
+
====================
|
4
|
+
|
5
|
+
Comprehensive verification system for MCP services that checks installation,
|
6
|
+
configuration, and runtime functionality. Provides detailed diagnostics and
|
7
|
+
automated fixes for common issues.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import json
|
11
|
+
import os
|
12
|
+
import shutil
|
13
|
+
import subprocess
|
14
|
+
import sys
|
15
|
+
import time
|
16
|
+
from dataclasses import dataclass
|
17
|
+
from enum import Enum
|
18
|
+
from pathlib import Path
|
19
|
+
from typing import Dict, List, Optional, Tuple
|
20
|
+
|
21
|
+
from ..core.logger import get_logger
|
22
|
+
|
23
|
+
|
24
|
+
class ServiceStatus(Enum):
|
25
|
+
"""MCP service health status levels."""
|
26
|
+
|
27
|
+
WORKING = "✅" # Fully operational
|
28
|
+
MISCONFIGURED = "⚠️" # Installed but configuration issues
|
29
|
+
NOT_INSTALLED = "❌" # Not installed at all
|
30
|
+
PERMISSION_DENIED = "🔒" # Permissions issue
|
31
|
+
VERSION_MISMATCH = "🔄" # Needs upgrade
|
32
|
+
UNKNOWN = "❓" # Unknown status
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class ServiceDiagnostic:
|
37
|
+
"""Detailed diagnostic information for a service."""
|
38
|
+
|
39
|
+
name: str
|
40
|
+
status: ServiceStatus
|
41
|
+
message: str
|
42
|
+
installed_path: Optional[str] = None
|
43
|
+
configured_command: Optional[str] = None
|
44
|
+
fix_command: Optional[str] = None
|
45
|
+
details: Optional[Dict] = None
|
46
|
+
|
47
|
+
|
48
|
+
class MCPServiceVerifier:
|
49
|
+
"""
|
50
|
+
Comprehensive MCP service verification and auto-fix system.
|
51
|
+
|
52
|
+
This verifier performs deep health checks on MCP services including:
|
53
|
+
- Installation verification (pipx, uvx, system)
|
54
|
+
- Configuration validation in ~/.claude.json
|
55
|
+
- Permission checks on executables
|
56
|
+
- Command format verification
|
57
|
+
- Runtime functionality testing
|
58
|
+
- Auto-fix capabilities for common issues
|
59
|
+
"""
|
60
|
+
|
61
|
+
# Known MCP services and their requirements
|
62
|
+
SERVICE_REQUIREMENTS = {
|
63
|
+
"mcp-vector-search": {
|
64
|
+
"pipx_package": "mcp-vector-search",
|
65
|
+
"test_args": ["--version"],
|
66
|
+
"required_args": ["-m", "mcp_vector_search.mcp.server"],
|
67
|
+
"needs_project_path": True,
|
68
|
+
},
|
69
|
+
"mcp-browser": {
|
70
|
+
"pipx_package": "mcp-browser",
|
71
|
+
"test_args": ["--version"],
|
72
|
+
"required_args": ["mcp"],
|
73
|
+
"env_vars": {"MCP_BROWSER_HOME": "~/.mcp-browser"},
|
74
|
+
},
|
75
|
+
"mcp-ticketer": {
|
76
|
+
"pipx_package": "mcp-ticketer",
|
77
|
+
"test_args": ["--version"],
|
78
|
+
"required_args": ["mcp"],
|
79
|
+
},
|
80
|
+
"kuzu-memory": {
|
81
|
+
"pipx_package": "kuzu-memory",
|
82
|
+
"test_args": ["--help"], # kuzu-memory uses --help not --version
|
83
|
+
"required_args": ["mcp", "serve"], # Modern format
|
84
|
+
"min_version": "1.1.0", # Minimum version for MCP support
|
85
|
+
"version_check_pattern": ["mcp", "serve", "claude"], # Pattern to check in help
|
86
|
+
},
|
87
|
+
}
|
88
|
+
|
89
|
+
def __init__(self):
|
90
|
+
"""Initialize the MCP service verifier."""
|
91
|
+
self.logger = get_logger(__name__)
|
92
|
+
self.project_root = Path.cwd()
|
93
|
+
self.claude_config_path = Path.home() / ".claude.json"
|
94
|
+
self.diagnostics: Dict[str, ServiceDiagnostic] = {}
|
95
|
+
|
96
|
+
def verify_all_services(self, auto_fix: bool = False) -> Dict[str, ServiceDiagnostic]:
|
97
|
+
"""
|
98
|
+
Perform comprehensive verification of all MCP services.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
auto_fix: Whether to attempt automatic fixes for issues
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
Dictionary mapping service names to diagnostic results
|
105
|
+
"""
|
106
|
+
self.logger.info("Starting MCP service verification...")
|
107
|
+
|
108
|
+
for service_name in self.SERVICE_REQUIREMENTS:
|
109
|
+
diagnostic = self._verify_service(service_name)
|
110
|
+
self.diagnostics[service_name] = diagnostic
|
111
|
+
|
112
|
+
# Attempt auto-fix if requested and fixable
|
113
|
+
if auto_fix and diagnostic.fix_command and diagnostic.status != ServiceStatus.WORKING:
|
114
|
+
self._attempt_auto_fix(service_name, diagnostic)
|
115
|
+
# Re-verify after fix
|
116
|
+
self.diagnostics[service_name] = self._verify_service(service_name)
|
117
|
+
|
118
|
+
return self.diagnostics
|
119
|
+
|
120
|
+
def _verify_service(self, service_name: str) -> ServiceDiagnostic:
|
121
|
+
"""
|
122
|
+
Perform deep verification of a single MCP service.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
service_name: Name of the service to verify
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
Diagnostic result for the service
|
129
|
+
"""
|
130
|
+
requirements = self.SERVICE_REQUIREMENTS[service_name]
|
131
|
+
|
132
|
+
# Step 1: Check if service is installed
|
133
|
+
installed_path = self._find_service_installation(service_name)
|
134
|
+
|
135
|
+
if not installed_path:
|
136
|
+
return ServiceDiagnostic(
|
137
|
+
name=service_name,
|
138
|
+
status=ServiceStatus.NOT_INSTALLED,
|
139
|
+
message=f"{service_name} is not installed",
|
140
|
+
fix_command=f"pipx install {requirements['pipx_package']}"
|
141
|
+
)
|
142
|
+
|
143
|
+
# Step 2: Check executable permissions
|
144
|
+
if not self._check_permissions(installed_path):
|
145
|
+
return ServiceDiagnostic(
|
146
|
+
name=service_name,
|
147
|
+
status=ServiceStatus.PERMISSION_DENIED,
|
148
|
+
message=f"Permission denied for {service_name}",
|
149
|
+
installed_path=installed_path,
|
150
|
+
fix_command=f"chmod +x {installed_path}"
|
151
|
+
)
|
152
|
+
|
153
|
+
# Step 3: Test basic functionality
|
154
|
+
if not self._test_service_functionality(service_name, installed_path):
|
155
|
+
# Check if it's a version issue for kuzu-memory
|
156
|
+
if service_name == "kuzu-memory":
|
157
|
+
version_info = self._check_kuzu_version(installed_path)
|
158
|
+
if not version_info["has_mcp_support"]:
|
159
|
+
return ServiceDiagnostic(
|
160
|
+
name=service_name,
|
161
|
+
status=ServiceStatus.VERSION_MISMATCH,
|
162
|
+
message=f"kuzu-memory needs upgrade to v1.1.0+ for MCP support",
|
163
|
+
installed_path=installed_path,
|
164
|
+
fix_command="pipx upgrade kuzu-memory",
|
165
|
+
details=version_info
|
166
|
+
)
|
167
|
+
|
168
|
+
return ServiceDiagnostic(
|
169
|
+
name=service_name,
|
170
|
+
status=ServiceStatus.MISCONFIGURED,
|
171
|
+
message=f"{service_name} installed but not functioning",
|
172
|
+
installed_path=installed_path,
|
173
|
+
fix_command=f"pipx reinstall {requirements['pipx_package']}"
|
174
|
+
)
|
175
|
+
|
176
|
+
# Step 4: Verify configuration in ~/.claude.json
|
177
|
+
config_status = self._verify_configuration(service_name, installed_path)
|
178
|
+
|
179
|
+
if not config_status["configured"]:
|
180
|
+
return ServiceDiagnostic(
|
181
|
+
name=service_name,
|
182
|
+
status=ServiceStatus.MISCONFIGURED,
|
183
|
+
message=f"{service_name} not configured in ~/.claude.json",
|
184
|
+
installed_path=installed_path,
|
185
|
+
configured_command=None,
|
186
|
+
fix_command="Run 'claude-mpm configure' to update configuration"
|
187
|
+
)
|
188
|
+
|
189
|
+
if not config_status["correct"]:
|
190
|
+
return ServiceDiagnostic(
|
191
|
+
name=service_name,
|
192
|
+
status=ServiceStatus.MISCONFIGURED,
|
193
|
+
message=f"{service_name} configuration needs update",
|
194
|
+
installed_path=installed_path,
|
195
|
+
configured_command=config_status.get("command"),
|
196
|
+
fix_command="Run 'claude-mpm configure' to fix configuration",
|
197
|
+
details={"config_issue": config_status.get("issue")}
|
198
|
+
)
|
199
|
+
|
200
|
+
# Step 5: Test actual MCP command execution
|
201
|
+
if not self._test_mcp_command(service_name, config_status.get("command"), config_status.get("args", [])):
|
202
|
+
return ServiceDiagnostic(
|
203
|
+
name=service_name,
|
204
|
+
status=ServiceStatus.MISCONFIGURED,
|
205
|
+
message=f"{service_name} command format issue",
|
206
|
+
installed_path=installed_path,
|
207
|
+
configured_command=config_status.get("command"),
|
208
|
+
fix_command="Run 'claude-mpm configure' to update command format",
|
209
|
+
details={"command": config_status.get("command"), "args": config_status.get("args")}
|
210
|
+
)
|
211
|
+
|
212
|
+
# All checks passed!
|
213
|
+
return ServiceDiagnostic(
|
214
|
+
name=service_name,
|
215
|
+
status=ServiceStatus.WORKING,
|
216
|
+
message=f"{service_name} is fully operational",
|
217
|
+
installed_path=installed_path,
|
218
|
+
configured_command=config_status.get("command")
|
219
|
+
)
|
220
|
+
|
221
|
+
def _find_service_installation(self, service_name: str) -> Optional[str]:
|
222
|
+
"""
|
223
|
+
Find where a service is installed.
|
224
|
+
|
225
|
+
Checks in order:
|
226
|
+
1. pipx installation
|
227
|
+
2. uvx installation
|
228
|
+
3. System PATH
|
229
|
+
4. User pip installation
|
230
|
+
|
231
|
+
Args:
|
232
|
+
service_name: Name of the service
|
233
|
+
|
234
|
+
Returns:
|
235
|
+
Path to the service executable or None
|
236
|
+
"""
|
237
|
+
# Check pipx
|
238
|
+
pipx_path = Path.home() / ".local" / "pipx" / "venvs" / service_name / "bin" / service_name
|
239
|
+
if pipx_path.exists():
|
240
|
+
return str(pipx_path)
|
241
|
+
|
242
|
+
# Special case for mcp-vector-search (uses Python interpreter)
|
243
|
+
if service_name == "mcp-vector-search":
|
244
|
+
pipx_python = pipx_path.parent / "python"
|
245
|
+
if pipx_python.exists():
|
246
|
+
return str(pipx_python)
|
247
|
+
|
248
|
+
# Check system PATH
|
249
|
+
system_path = shutil.which(service_name)
|
250
|
+
if system_path:
|
251
|
+
return system_path
|
252
|
+
|
253
|
+
# Check user pip installation
|
254
|
+
user_bin = Path.home() / ".local" / "bin" / service_name
|
255
|
+
if user_bin.exists():
|
256
|
+
return str(user_bin)
|
257
|
+
|
258
|
+
return None
|
259
|
+
|
260
|
+
def _check_permissions(self, path: str) -> bool:
|
261
|
+
"""
|
262
|
+
Check if a file has execute permissions.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
path: Path to the executable
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
True if executable, False otherwise
|
269
|
+
"""
|
270
|
+
try:
|
271
|
+
return os.access(path, os.X_OK)
|
272
|
+
except Exception as e:
|
273
|
+
self.logger.debug(f"Permission check failed for {path}: {e}")
|
274
|
+
return False
|
275
|
+
|
276
|
+
def _test_service_functionality(self, service_name: str, path: str) -> bool:
|
277
|
+
"""
|
278
|
+
Test if a service can execute basic commands.
|
279
|
+
|
280
|
+
Args:
|
281
|
+
service_name: Name of the service
|
282
|
+
path: Path to the executable
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
True if service is functional, False otherwise
|
286
|
+
"""
|
287
|
+
requirements = self.SERVICE_REQUIREMENTS[service_name]
|
288
|
+
test_args = requirements.get("test_args", ["--help"])
|
289
|
+
|
290
|
+
try:
|
291
|
+
# First try direct execution
|
292
|
+
result = subprocess.run(
|
293
|
+
[path] + test_args,
|
294
|
+
capture_output=True,
|
295
|
+
text=True,
|
296
|
+
timeout=10,
|
297
|
+
check=False
|
298
|
+
)
|
299
|
+
|
300
|
+
output = (result.stdout + result.stderr).lower()
|
301
|
+
|
302
|
+
# Check for success indicators
|
303
|
+
if result.returncode == 0:
|
304
|
+
return True
|
305
|
+
|
306
|
+
# Some tools return non-zero but still work
|
307
|
+
if any(word in output for word in ["version", "usage", "help", service_name.lower()]):
|
308
|
+
# Make sure it's not an error
|
309
|
+
if not any(error in output for error in ["error", "not found", "traceback", "no module"]):
|
310
|
+
return True
|
311
|
+
|
312
|
+
# Try pipx run as fallback
|
313
|
+
if shutil.which("pipx"):
|
314
|
+
result = subprocess.run(
|
315
|
+
["pipx", "run", service_name] + test_args,
|
316
|
+
capture_output=True,
|
317
|
+
text=True,
|
318
|
+
timeout=10,
|
319
|
+
check=False
|
320
|
+
)
|
321
|
+
if result.returncode == 0 or "version" in result.stdout.lower():
|
322
|
+
return True
|
323
|
+
|
324
|
+
except subprocess.TimeoutExpired:
|
325
|
+
self.logger.warning(f"Service {service_name} timed out during functionality test")
|
326
|
+
except Exception as e:
|
327
|
+
self.logger.debug(f"Functionality test failed for {service_name}: {e}")
|
328
|
+
|
329
|
+
return False
|
330
|
+
|
331
|
+
def _check_kuzu_version(self, path: str) -> Dict:
|
332
|
+
"""
|
333
|
+
Check kuzu-memory version and MCP support.
|
334
|
+
|
335
|
+
Args:
|
336
|
+
path: Path to kuzu-memory executable
|
337
|
+
|
338
|
+
Returns:
|
339
|
+
Dictionary with version information
|
340
|
+
"""
|
341
|
+
version_info = {
|
342
|
+
"has_mcp_support": False,
|
343
|
+
"version": "unknown",
|
344
|
+
"command_format": None
|
345
|
+
}
|
346
|
+
|
347
|
+
try:
|
348
|
+
# Check help output for MCP support
|
349
|
+
result = subprocess.run(
|
350
|
+
[path, "--help"],
|
351
|
+
capture_output=True,
|
352
|
+
text=True,
|
353
|
+
timeout=10,
|
354
|
+
check=False
|
355
|
+
)
|
356
|
+
|
357
|
+
help_text = (result.stdout + result.stderr).lower()
|
358
|
+
|
359
|
+
# Check for modern "mcp serve" command
|
360
|
+
if "mcp serve" in help_text or ("mcp" in help_text and "serve" in help_text):
|
361
|
+
version_info["has_mcp_support"] = True
|
362
|
+
version_info["command_format"] = "mcp serve"
|
363
|
+
# Check for legacy "serve" only
|
364
|
+
elif "serve" in help_text and "mcp" not in help_text:
|
365
|
+
version_info["has_mcp_support"] = False
|
366
|
+
version_info["command_format"] = "serve"
|
367
|
+
|
368
|
+
# Try to extract version
|
369
|
+
version_result = subprocess.run(
|
370
|
+
[path, "--version"],
|
371
|
+
capture_output=True,
|
372
|
+
text=True,
|
373
|
+
timeout=5,
|
374
|
+
check=False
|
375
|
+
)
|
376
|
+
if version_result.returncode == 0:
|
377
|
+
version_info["version"] = version_result.stdout.strip()
|
378
|
+
|
379
|
+
except Exception as e:
|
380
|
+
self.logger.debug(f"Failed to check kuzu-memory version: {e}")
|
381
|
+
|
382
|
+
return version_info
|
383
|
+
|
384
|
+
def _verify_configuration(self, service_name: str, installed_path: str) -> Dict:
|
385
|
+
"""
|
386
|
+
Verify service configuration in ~/.claude.json.
|
387
|
+
|
388
|
+
Args:
|
389
|
+
service_name: Name of the service
|
390
|
+
installed_path: Path where service is installed
|
391
|
+
|
392
|
+
Returns:
|
393
|
+
Dictionary with configuration status
|
394
|
+
"""
|
395
|
+
project_key = str(self.project_root)
|
396
|
+
|
397
|
+
if not self.claude_config_path.exists():
|
398
|
+
return {"configured": False, "correct": False}
|
399
|
+
|
400
|
+
try:
|
401
|
+
with open(self.claude_config_path) as f:
|
402
|
+
config = json.load(f)
|
403
|
+
|
404
|
+
# Check if project is configured
|
405
|
+
if "projects" not in config or project_key not in config["projects"]:
|
406
|
+
return {"configured": False, "correct": False}
|
407
|
+
|
408
|
+
project_config = config["projects"][project_key]
|
409
|
+
mcp_servers = project_config.get("mcpServers", {})
|
410
|
+
|
411
|
+
# Check if service is configured
|
412
|
+
if service_name not in mcp_servers:
|
413
|
+
return {"configured": False, "correct": False}
|
414
|
+
|
415
|
+
service_config = mcp_servers[service_name]
|
416
|
+
command = service_config.get("command", "")
|
417
|
+
args = service_config.get("args", [])
|
418
|
+
|
419
|
+
# Validate command configuration
|
420
|
+
requirements = self.SERVICE_REQUIREMENTS[service_name]
|
421
|
+
required_args = requirements.get("required_args", [])
|
422
|
+
|
423
|
+
# Check if using pipx run or direct execution
|
424
|
+
if command == "pipx" and args and args[0] == "run":
|
425
|
+
# pipx run format
|
426
|
+
if service_name not in args:
|
427
|
+
return {
|
428
|
+
"configured": True,
|
429
|
+
"correct": False,
|
430
|
+
"command": command,
|
431
|
+
"args": args,
|
432
|
+
"issue": "Service name missing in pipx run command"
|
433
|
+
}
|
434
|
+
# Check required args are present
|
435
|
+
for req_arg in required_args:
|
436
|
+
if req_arg not in args[2:]: # Skip "run" and service name
|
437
|
+
return {
|
438
|
+
"configured": True,
|
439
|
+
"correct": False,
|
440
|
+
"command": command,
|
441
|
+
"args": args,
|
442
|
+
"issue": f"Missing required argument: {req_arg}"
|
443
|
+
}
|
444
|
+
elif command == "uvx" and args and args[0] == service_name:
|
445
|
+
# uvx format - similar validation
|
446
|
+
for req_arg in required_args:
|
447
|
+
if req_arg not in args[1:]:
|
448
|
+
return {
|
449
|
+
"configured": True,
|
450
|
+
"correct": False,
|
451
|
+
"command": command,
|
452
|
+
"args": args,
|
453
|
+
"issue": f"Missing required argument: {req_arg}"
|
454
|
+
}
|
455
|
+
else:
|
456
|
+
# Direct execution - command should be a valid path
|
457
|
+
if not Path(command).exists() and command != installed_path:
|
458
|
+
# Allow for relative paths that might resolve differently
|
459
|
+
if not shutil.which(command):
|
460
|
+
return {
|
461
|
+
"configured": True,
|
462
|
+
"correct": False,
|
463
|
+
"command": command,
|
464
|
+
"args": args,
|
465
|
+
"issue": f"Command path does not exist: {command}"
|
466
|
+
}
|
467
|
+
|
468
|
+
# Check required args
|
469
|
+
for req_arg in required_args:
|
470
|
+
if req_arg not in args:
|
471
|
+
return {
|
472
|
+
"configured": True,
|
473
|
+
"correct": False,
|
474
|
+
"command": command,
|
475
|
+
"args": args,
|
476
|
+
"issue": f"Missing required argument: {req_arg}"
|
477
|
+
}
|
478
|
+
|
479
|
+
# Special validation for kuzu-memory command format
|
480
|
+
if service_name == "kuzu-memory":
|
481
|
+
# Should use "mcp serve" format for modern versions
|
482
|
+
if args and "serve" in args and "mcp" not in args:
|
483
|
+
return {
|
484
|
+
"configured": True,
|
485
|
+
"correct": False,
|
486
|
+
"command": command,
|
487
|
+
"args": args,
|
488
|
+
"issue": "Using legacy 'serve' format, should use 'mcp serve'"
|
489
|
+
}
|
490
|
+
|
491
|
+
return {
|
492
|
+
"configured": True,
|
493
|
+
"correct": True,
|
494
|
+
"command": command,
|
495
|
+
"args": args
|
496
|
+
}
|
497
|
+
|
498
|
+
except Exception as e:
|
499
|
+
self.logger.error(f"Failed to verify configuration: {e}")
|
500
|
+
return {"configured": False, "correct": False, "error": str(e)}
|
501
|
+
|
502
|
+
def _test_mcp_command(self, service_name: str, command: str, args: List[str]) -> bool:
|
503
|
+
"""
|
504
|
+
Test if the configured MCP command actually works.
|
505
|
+
|
506
|
+
Args:
|
507
|
+
service_name: Name of the service
|
508
|
+
command: Configured command
|
509
|
+
args: Configured arguments
|
510
|
+
|
511
|
+
Returns:
|
512
|
+
True if command executes successfully
|
513
|
+
"""
|
514
|
+
if not command:
|
515
|
+
return False
|
516
|
+
|
517
|
+
try:
|
518
|
+
# Build test command - add --help to test without side effects
|
519
|
+
test_cmd = [command] + args[:2] if args else [command] # Include base args
|
520
|
+
test_cmd.append("--help")
|
521
|
+
|
522
|
+
result = subprocess.run(
|
523
|
+
test_cmd,
|
524
|
+
capture_output=True,
|
525
|
+
text=True,
|
526
|
+
timeout=10,
|
527
|
+
check=False,
|
528
|
+
cwd=str(self.project_root) # Run in project context
|
529
|
+
)
|
530
|
+
|
531
|
+
# Check for success or expected output
|
532
|
+
output = (result.stdout + result.stderr).lower()
|
533
|
+
if result.returncode == 0:
|
534
|
+
return True
|
535
|
+
|
536
|
+
# Check for expected patterns
|
537
|
+
if service_name == "kuzu-memory" and "mcp" in output and "serve" in output:
|
538
|
+
return True
|
539
|
+
if service_name in output or "usage" in output or "help" in output:
|
540
|
+
if not any(error in output for error in ["error", "not found", "traceback"]):
|
541
|
+
return True
|
542
|
+
|
543
|
+
except subprocess.TimeoutExpired:
|
544
|
+
self.logger.warning(f"Command test timed out for {service_name}")
|
545
|
+
except Exception as e:
|
546
|
+
self.logger.debug(f"Command test failed for {service_name}: {e}")
|
547
|
+
|
548
|
+
return False
|
549
|
+
|
550
|
+
def _attempt_auto_fix(self, service_name: str, diagnostic: ServiceDiagnostic) -> bool:
|
551
|
+
"""
|
552
|
+
Attempt to automatically fix a service issue.
|
553
|
+
|
554
|
+
Args:
|
555
|
+
service_name: Name of the service
|
556
|
+
diagnostic: Current diagnostic information
|
557
|
+
|
558
|
+
Returns:
|
559
|
+
True if fix was successful
|
560
|
+
"""
|
561
|
+
if not diagnostic.fix_command:
|
562
|
+
return False
|
563
|
+
|
564
|
+
self.logger.info(f"Attempting auto-fix for {service_name}: {diagnostic.fix_command}")
|
565
|
+
|
566
|
+
try:
|
567
|
+
# Handle different types of fix commands
|
568
|
+
if diagnostic.fix_command.startswith("pipx "):
|
569
|
+
# Execute pipx command
|
570
|
+
cmd_parts = diagnostic.fix_command.split()
|
571
|
+
result = subprocess.run(
|
572
|
+
cmd_parts,
|
573
|
+
capture_output=True,
|
574
|
+
text=True,
|
575
|
+
timeout=120,
|
576
|
+
check=False
|
577
|
+
)
|
578
|
+
return result.returncode == 0
|
579
|
+
|
580
|
+
elif diagnostic.fix_command.startswith("chmod "):
|
581
|
+
# Fix permissions
|
582
|
+
path = diagnostic.fix_command.replace("chmod +x ", "")
|
583
|
+
os.chmod(path, 0o755)
|
584
|
+
return True
|
585
|
+
|
586
|
+
elif "claude-mpm configure" in diagnostic.fix_command:
|
587
|
+
# Trigger configuration update
|
588
|
+
from .mcp_config_manager import MCPConfigManager
|
589
|
+
manager = MCPConfigManager()
|
590
|
+
success, _ = manager.ensure_mcp_services_configured()
|
591
|
+
return success
|
592
|
+
|
593
|
+
except Exception as e:
|
594
|
+
self.logger.error(f"Auto-fix failed for {service_name}: {e}")
|
595
|
+
|
596
|
+
return False
|
597
|
+
|
598
|
+
def print_diagnostics(self, diagnostics: Optional[Dict[str, ServiceDiagnostic]] = None) -> None:
|
599
|
+
"""
|
600
|
+
Print formatted diagnostic results to console.
|
601
|
+
|
602
|
+
Args:
|
603
|
+
diagnostics: Diagnostic results to print (uses self.diagnostics if None)
|
604
|
+
"""
|
605
|
+
if diagnostics is None:
|
606
|
+
diagnostics = self.diagnostics
|
607
|
+
|
608
|
+
if not diagnostics:
|
609
|
+
print("\n📋 No services verified yet")
|
610
|
+
return
|
611
|
+
|
612
|
+
print("\n" + "=" * 60)
|
613
|
+
print("📋 MCP Service Verification Report")
|
614
|
+
print("=" * 60)
|
615
|
+
|
616
|
+
# Group by status
|
617
|
+
working = []
|
618
|
+
issues = []
|
619
|
+
|
620
|
+
for name, diag in diagnostics.items():
|
621
|
+
if diag.status == ServiceStatus.WORKING:
|
622
|
+
working.append(diag)
|
623
|
+
else:
|
624
|
+
issues.append(diag)
|
625
|
+
|
626
|
+
# Print working services
|
627
|
+
if working:
|
628
|
+
print("\n✅ Fully Operational Services:")
|
629
|
+
for diag in working:
|
630
|
+
print(f" • {diag.name}: {diag.message}")
|
631
|
+
if diag.configured_command:
|
632
|
+
print(f" Command: {diag.configured_command}")
|
633
|
+
|
634
|
+
# Print services with issues
|
635
|
+
if issues:
|
636
|
+
print("\n⚠️ Services Requiring Attention:")
|
637
|
+
for diag in issues:
|
638
|
+
print(f"\n {diag.status.value} {diag.name}:")
|
639
|
+
print(f" Issue: {diag.message}")
|
640
|
+
if diag.installed_path:
|
641
|
+
print(f" Path: {diag.installed_path}")
|
642
|
+
if diag.fix_command:
|
643
|
+
print(f" Fix: {diag.fix_command}")
|
644
|
+
if diag.details:
|
645
|
+
print(f" Details: {json.dumps(diag.details, indent=6)}")
|
646
|
+
|
647
|
+
# Summary
|
648
|
+
print("\n" + "=" * 60)
|
649
|
+
print(f"Summary: {len(working)}/{len(diagnostics)} services operational")
|
650
|
+
|
651
|
+
if issues:
|
652
|
+
print("\n💡 Quick Fix Commands:")
|
653
|
+
seen_fixes = set()
|
654
|
+
for diag in issues:
|
655
|
+
if diag.fix_command and diag.fix_command not in seen_fixes:
|
656
|
+
print(f" {diag.fix_command}")
|
657
|
+
seen_fixes.add(diag.fix_command)
|
658
|
+
|
659
|
+
print("\nOr run: claude-mpm verify --fix")
|
660
|
+
|
661
|
+
print("=" * 60 + "\n")
|
662
|
+
|
663
|
+
|
664
|
+
def verify_mcp_services_on_startup() -> Tuple[bool, str]:
|
665
|
+
"""
|
666
|
+
Quick verification check for MCP services during startup.
|
667
|
+
|
668
|
+
This is a lightweight check that runs during CLI initialization
|
669
|
+
to warn users of potential issues without blocking startup.
|
670
|
+
|
671
|
+
Returns:
|
672
|
+
Tuple of (all_working, summary_message)
|
673
|
+
"""
|
674
|
+
verifier = MCPServiceVerifier()
|
675
|
+
logger = get_logger(__name__)
|
676
|
+
|
677
|
+
# Do quick checks only (don't block startup)
|
678
|
+
issues = []
|
679
|
+
for service_name in MCPServiceVerifier.SERVICE_REQUIREMENTS:
|
680
|
+
path = verifier._find_service_installation(service_name)
|
681
|
+
if not path:
|
682
|
+
issues.append(f"{service_name} not installed")
|
683
|
+
elif not verifier._check_permissions(path):
|
684
|
+
issues.append(f"{service_name} permission issue")
|
685
|
+
|
686
|
+
if issues:
|
687
|
+
message = f"MCP service issues detected: {', '.join(issues)}. Run 'claude-mpm verify' for details."
|
688
|
+
return False, message
|
689
|
+
|
690
|
+
return True, "All MCP services appear operational"
|