claude-mpm 4.4.0__py3-none-any.whl → 4.4.4__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/WORKFLOW.md +2 -14
- claude_mpm/agents/agent_loader.py +3 -2
- claude_mpm/agents/agent_loader_integration.py +2 -1
- claude_mpm/agents/async_agent_loader.py +2 -2
- claude_mpm/agents/base_agent_loader.py +2 -2
- claude_mpm/agents/frontmatter_validator.py +1 -0
- claude_mpm/agents/system_agent_config.py +2 -1
- claude_mpm/cli/commands/configure.py +2 -29
- claude_mpm/cli/commands/doctor.py +44 -5
- claude_mpm/cli/commands/mpm_init.py +117 -63
- claude_mpm/cli/parsers/configure_parser.py +6 -15
- claude_mpm/cli/startup_logging.py +1 -3
- claude_mpm/config/agent_config.py +1 -1
- claude_mpm/config/paths.py +2 -1
- claude_mpm/core/agent_name_normalizer.py +1 -0
- claude_mpm/core/config.py +2 -1
- claude_mpm/core/config_aliases.py +2 -1
- claude_mpm/core/file_utils.py +0 -1
- claude_mpm/core/framework/__init__.py +38 -0
- claude_mpm/core/framework/formatters/__init__.py +11 -0
- claude_mpm/core/framework/formatters/capability_generator.py +367 -0
- claude_mpm/core/framework/formatters/content_formatter.py +288 -0
- claude_mpm/core/framework/formatters/context_generator.py +184 -0
- claude_mpm/core/framework/loaders/__init__.py +13 -0
- claude_mpm/core/framework/loaders/agent_loader.py +206 -0
- claude_mpm/core/framework/loaders/file_loader.py +223 -0
- claude_mpm/core/framework/loaders/instruction_loader.py +161 -0
- claude_mpm/core/framework/loaders/packaged_loader.py +232 -0
- claude_mpm/core/framework/processors/__init__.py +11 -0
- claude_mpm/core/framework/processors/memory_processor.py +230 -0
- claude_mpm/core/framework/processors/metadata_processor.py +146 -0
- claude_mpm/core/framework/processors/template_processor.py +244 -0
- claude_mpm/core/framework_loader.py +298 -1795
- claude_mpm/core/log_manager.py +2 -1
- claude_mpm/core/tool_access_control.py +1 -0
- claude_mpm/core/unified_agent_registry.py +2 -1
- claude_mpm/core/unified_paths.py +1 -0
- claude_mpm/experimental/cli_enhancements.py +1 -0
- claude_mpm/hooks/__init__.py +9 -1
- claude_mpm/hooks/base_hook.py +1 -0
- claude_mpm/hooks/instruction_reinforcement.py +1 -0
- claude_mpm/hooks/kuzu_memory_hook.py +359 -0
- claude_mpm/hooks/validation_hooks.py +1 -1
- claude_mpm/scripts/mpm_doctor.py +1 -0
- claude_mpm/services/agents/loading/agent_profile_loader.py +1 -1
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -1
- claude_mpm/services/agents/loading/framework_agent_loader.py +1 -1
- claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -0
- claude_mpm/services/agents/management/agent_management_service.py +1 -1
- claude_mpm/services/agents/memory/memory_categorization_service.py +0 -1
- claude_mpm/services/agents/memory/memory_file_service.py +6 -2
- claude_mpm/services/agents/memory/memory_format_service.py +0 -1
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
- claude_mpm/services/async_session_logger.py +1 -1
- claude_mpm/services/claude_session_logger.py +1 -0
- claude_mpm/services/core/path_resolver.py +2 -0
- claude_mpm/services/diagnostics/checks/__init__.py +2 -0
- claude_mpm/services/diagnostics/checks/installation_check.py +126 -25
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +399 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +4 -0
- claude_mpm/services/diagnostics/doctor_reporter.py +259 -32
- claude_mpm/services/event_bus/direct_relay.py +2 -1
- claude_mpm/services/event_bus/event_bus.py +1 -0
- claude_mpm/services/event_bus/relay.py +3 -2
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +1 -1
- claude_mpm/services/infrastructure/daemon_manager.py +1 -1
- claude_mpm/services/mcp_config_manager.py +67 -4
- claude_mpm/services/mcp_gateway/core/process_pool.py +320 -0
- claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
- claude_mpm/services/mcp_gateway/main.py +3 -13
- claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
- claude_mpm/services/mcp_gateway/tools/__init__.py +14 -2
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +38 -6
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +527 -0
- claude_mpm/services/memory/cache/simple_cache.py +1 -1
- claude_mpm/services/project/archive_manager.py +159 -96
- claude_mpm/services/project/documentation_manager.py +64 -45
- claude_mpm/services/project/enhanced_analyzer.py +132 -89
- claude_mpm/services/project/project_organizer.py +225 -131
- claude_mpm/services/response_tracker.py +1 -1
- claude_mpm/services/shared/__init__.py +2 -1
- claude_mpm/services/shared/service_factory.py +8 -5
- claude_mpm/services/socketio/server/eventbus_integration.py +1 -1
- claude_mpm/services/unified/__init__.py +1 -1
- claude_mpm/services/unified/analyzer_strategies/__init__.py +3 -3
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +97 -53
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +81 -40
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +277 -178
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +196 -112
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +83 -49
- claude_mpm/services/unified/config_strategies/__init__.py +175 -0
- claude_mpm/services/unified/config_strategies/config_schema.py +735 -0
- claude_mpm/services/unified/config_strategies/context_strategy.py +750 -0
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +1009 -0
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +879 -0
- claude_mpm/services/unified/config_strategies/unified_config_service.py +814 -0
- claude_mpm/services/unified/config_strategies/validation_strategy.py +1144 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +7 -7
- claude_mpm/services/unified/deployment_strategies/base.py +24 -28
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +168 -88
- claude_mpm/services/unified/deployment_strategies/local.py +49 -34
- claude_mpm/services/unified/deployment_strategies/utils.py +39 -43
- claude_mpm/services/unified/deployment_strategies/vercel.py +30 -24
- claude_mpm/services/unified/interfaces.py +0 -26
- claude_mpm/services/unified/migration.py +17 -40
- claude_mpm/services/unified/strategies.py +9 -26
- claude_mpm/services/unified/unified_analyzer.py +48 -44
- claude_mpm/services/unified/unified_config.py +21 -19
- claude_mpm/services/unified/unified_deployment.py +21 -26
- claude_mpm/storage/state_storage.py +1 -0
- claude_mpm/utils/agent_dependency_loader.py +18 -6
- claude_mpm/utils/common.py +14 -12
- claude_mpm/utils/database_connector.py +15 -12
- claude_mpm/utils/error_handler.py +1 -0
- claude_mpm/utils/log_cleanup.py +1 -0
- claude_mpm/utils/path_operations.py +1 -0
- claude_mpm/utils/session_logging.py +1 -1
- claude_mpm/utils/subprocess_utils.py +1 -0
- claude_mpm/validation/agent_validator.py +1 -1
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/METADATA +23 -17
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/RECORD +126 -105
- claude_mpm/cli/commands/configure_tui.py +0 -1927
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/WHEEL +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/top_level.txt +0 -0
@@ -5,9 +5,11 @@ WHY: Verify that claude-mpm is properly installed with correct Python version,
|
|
5
5
|
dependencies, and installation method.
|
6
6
|
"""
|
7
7
|
|
8
|
+
import os
|
8
9
|
import subprocess
|
9
10
|
import sys
|
10
11
|
from pathlib import Path
|
12
|
+
from typing import Optional
|
11
13
|
|
12
14
|
from ..models import DiagnosticResult, DiagnosticStatus
|
13
15
|
from .base_check import BaseDiagnosticCheck
|
@@ -145,31 +147,41 @@ class InstallationCheck(BaseDiagnosticCheck):
|
|
145
147
|
exe_path = sys.executable
|
146
148
|
details["python_executable"] = exe_path
|
147
149
|
|
148
|
-
# 2. Check
|
150
|
+
# 2. Check for container environment
|
151
|
+
container_type = self._detect_container_environment()
|
152
|
+
if container_type:
|
153
|
+
details["container_type"] = container_type
|
154
|
+
methods_found.append("container")
|
155
|
+
|
156
|
+
# 3. Check if we're in a virtual environment
|
149
157
|
in_venv = hasattr(sys, "real_prefix") or (
|
150
158
|
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
|
151
159
|
)
|
152
160
|
|
153
|
-
#
|
161
|
+
# 4. Check if running from pipx environment
|
154
162
|
# Pipx creates venvs in specific locations
|
155
163
|
is_pipx_venv = False
|
156
164
|
if in_venv and (".local/pipx/venvs" in exe_path or "pipx/venvs" in exe_path):
|
157
165
|
is_pipx_venv = True
|
158
166
|
methods_found.append("pipx")
|
159
167
|
details["pipx_venv"] = sys.prefix
|
168
|
+
# Get pipx metadata if available
|
169
|
+
pipx_metadata = self._get_pipx_metadata()
|
170
|
+
if pipx_metadata:
|
171
|
+
details["pipx_metadata"] = pipx_metadata
|
160
172
|
elif in_venv:
|
161
173
|
# Regular virtual environment (not pipx)
|
162
174
|
methods_found.append("venv")
|
163
175
|
details["venv_path"] = sys.prefix
|
164
176
|
|
165
|
-
#
|
177
|
+
# 5. Check if running from source (development mode)
|
166
178
|
claude_mpm_path = Path(__file__).parent.parent.parent.parent.parent
|
167
179
|
if (claude_mpm_path / "pyproject.toml").exists():
|
168
180
|
if (claude_mpm_path / ".git").exists():
|
169
181
|
methods_found.append("development")
|
170
182
|
details["source_path"] = str(claude_mpm_path)
|
171
183
|
|
172
|
-
#
|
184
|
+
# 6. Check Homebrew Python
|
173
185
|
if not in_venv and "/opt/homebrew" in exe_path:
|
174
186
|
methods_found.append("homebrew")
|
175
187
|
details["homebrew_python"] = exe_path
|
@@ -178,33 +190,24 @@ class InstallationCheck(BaseDiagnosticCheck):
|
|
178
190
|
methods_found.append("homebrew")
|
179
191
|
details["homebrew_python"] = exe_path
|
180
192
|
|
181
|
-
#
|
193
|
+
# 7. Check for system Python
|
182
194
|
if not in_venv and not methods_found:
|
183
195
|
if "/usr/bin/python" in exe_path or "/usr/local/bin/python" in exe_path:
|
184
196
|
methods_found.append("system")
|
185
197
|
details["system_python"] = exe_path
|
186
198
|
|
187
|
-
#
|
199
|
+
# 8. Additional check for pipx if not detected via venv
|
188
200
|
if "pipx" not in methods_found:
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
# Pipx is installed but we're not running from it
|
200
|
-
details["pipx_installed"] = True
|
201
|
-
details["pipx_not_active"] = (
|
202
|
-
"claude-mpm is installed via pipx but not currently running from pipx environment"
|
203
|
-
)
|
204
|
-
except (subprocess.SubprocessError, FileNotFoundError):
|
205
|
-
pass
|
206
|
-
|
207
|
-
# 8. Check pip installation status
|
201
|
+
pipx_check = self._check_pipx_installation_status()
|
202
|
+
if pipx_check:
|
203
|
+
if not is_pipx_venv:
|
204
|
+
# Pipx is installed but we're not running from it
|
205
|
+
details["pipx_installed"] = True
|
206
|
+
details["pipx_not_active"] = (
|
207
|
+
"claude-mpm is installed via pipx but not currently running from pipx environment"
|
208
|
+
)
|
209
|
+
|
210
|
+
# 9. Check pip installation status
|
208
211
|
try:
|
209
212
|
result = subprocess.run(
|
210
213
|
[sys.executable, "-m", "pip", "show", "claude-mpm"],
|
@@ -242,6 +245,22 @@ class InstallationCheck(BaseDiagnosticCheck):
|
|
242
245
|
details=details,
|
243
246
|
)
|
244
247
|
|
248
|
+
# Container environments are special
|
249
|
+
if "container" in methods_found:
|
250
|
+
container_msg = (
|
251
|
+
f"Running in {details.get('container_type', 'container')} environment"
|
252
|
+
)
|
253
|
+
if "pipx" in methods_found:
|
254
|
+
container_msg += " with pipx"
|
255
|
+
elif "venv" in methods_found:
|
256
|
+
container_msg += " with virtual environment"
|
257
|
+
return DiagnosticResult(
|
258
|
+
category="Installation Method",
|
259
|
+
status=DiagnosticStatus.OK,
|
260
|
+
message=container_msg,
|
261
|
+
details=details,
|
262
|
+
)
|
263
|
+
|
245
264
|
# Pipx is the recommended method
|
246
265
|
if "pipx" in methods_found:
|
247
266
|
return DiagnosticResult(
|
@@ -413,3 +432,85 @@ class InstallationCheck(BaseDiagnosticCheck):
|
|
413
432
|
"in_venv": in_venv,
|
414
433
|
},
|
415
434
|
)
|
435
|
+
|
436
|
+
def _detect_container_environment(self) -> Optional[str]:
|
437
|
+
"""Detect if running in a container environment."""
|
438
|
+
# Check for Docker
|
439
|
+
if Path("/.dockerenv").exists():
|
440
|
+
return "Docker"
|
441
|
+
|
442
|
+
# Check for Kubernetes
|
443
|
+
if Path("/var/run/secrets/kubernetes.io").exists():
|
444
|
+
return "Kubernetes"
|
445
|
+
|
446
|
+
# Check cgroup for container indicators
|
447
|
+
try:
|
448
|
+
with open("/proc/1/cgroup") as f:
|
449
|
+
cgroup = f.read()
|
450
|
+
if "docker" in cgroup:
|
451
|
+
return "Docker"
|
452
|
+
if "kubepods" in cgroup:
|
453
|
+
return "Kubernetes"
|
454
|
+
if "containerd" in cgroup:
|
455
|
+
return "containerd"
|
456
|
+
if "lxc" in cgroup:
|
457
|
+
return "LXC"
|
458
|
+
except (FileNotFoundError, PermissionError):
|
459
|
+
pass
|
460
|
+
|
461
|
+
# Check environment variables
|
462
|
+
if os.environ.get("CONTAINER"):
|
463
|
+
return os.environ.get("CONTAINER_ENGINE", "Container")
|
464
|
+
|
465
|
+
# Check for Podman
|
466
|
+
if Path("/run/.containerenv").exists():
|
467
|
+
return "Podman"
|
468
|
+
|
469
|
+
# Check for WSL
|
470
|
+
if Path("/proc/sys/fs/binfmt_misc/WSLInterop").exists():
|
471
|
+
return "WSL"
|
472
|
+
|
473
|
+
return None
|
474
|
+
|
475
|
+
def _get_pipx_metadata(self) -> Optional[dict]:
|
476
|
+
"""Get pipx metadata for the current installation."""
|
477
|
+
try:
|
478
|
+
import json
|
479
|
+
|
480
|
+
result = subprocess.run(
|
481
|
+
["pipx", "list", "--json"],
|
482
|
+
capture_output=True,
|
483
|
+
text=True,
|
484
|
+
timeout=5,
|
485
|
+
check=False,
|
486
|
+
)
|
487
|
+
if result.returncode == 0:
|
488
|
+
data = json.loads(result.stdout)
|
489
|
+
venvs = data.get("venvs", {})
|
490
|
+
if "claude-mpm" in venvs:
|
491
|
+
return {
|
492
|
+
"version": venvs["claude-mpm"]
|
493
|
+
.get("metadata", {})
|
494
|
+
.get("main_package", {})
|
495
|
+
.get("package_version"),
|
496
|
+
"python": venvs["claude-mpm"]
|
497
|
+
.get("metadata", {})
|
498
|
+
.get("python_version"),
|
499
|
+
}
|
500
|
+
except Exception:
|
501
|
+
pass
|
502
|
+
return None
|
503
|
+
|
504
|
+
def _check_pipx_installation_status(self) -> bool:
|
505
|
+
"""Check if claude-mpm is installed via pipx."""
|
506
|
+
try:
|
507
|
+
result = subprocess.run(
|
508
|
+
["pipx", "list"],
|
509
|
+
capture_output=True,
|
510
|
+
text=True,
|
511
|
+
timeout=5,
|
512
|
+
check=False,
|
513
|
+
)
|
514
|
+
return "claude-mpm" in result.stdout
|
515
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
516
|
+
return False
|
@@ -0,0 +1,399 @@
|
|
1
|
+
"""
|
2
|
+
Check MCP external services installation and health.
|
3
|
+
|
4
|
+
WHY: Verify that MCP services (mcp-vector-search, mcp-browser, mcp-ticketer, kuzu-memory)
|
5
|
+
are properly installed and accessible for enhanced Claude Desktop capabilities.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import subprocess
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Dict, List, Optional, Tuple
|
12
|
+
|
13
|
+
from ..models import DiagnosticResult, DiagnosticStatus
|
14
|
+
from .base_check import BaseDiagnosticCheck
|
15
|
+
|
16
|
+
|
17
|
+
class MCPServicesCheck(BaseDiagnosticCheck):
|
18
|
+
"""Check MCP external services installation and health."""
|
19
|
+
|
20
|
+
# Define MCP services to check
|
21
|
+
MCP_SERVICES = {
|
22
|
+
"mcp-vector-search": {
|
23
|
+
"package": "mcp-vector-search",
|
24
|
+
"command": ["mcp-vector-search", "--help"],
|
25
|
+
"description": "Vector search for semantic code navigation",
|
26
|
+
"check_health": True,
|
27
|
+
"health_command": ["mcp-vector-search", "--version"],
|
28
|
+
},
|
29
|
+
"mcp-browser": {
|
30
|
+
"package": "mcp-browser",
|
31
|
+
"command": ["mcp-browser", "--help"],
|
32
|
+
"description": "Browser automation and web interaction",
|
33
|
+
"check_health": True,
|
34
|
+
"health_command": ["mcp-browser", "--version"],
|
35
|
+
},
|
36
|
+
"mcp-ticketer": {
|
37
|
+
"package": "mcp-ticketer",
|
38
|
+
"command": ["mcp-ticketer", "--help"],
|
39
|
+
"description": "Ticket and task management",
|
40
|
+
"check_health": True,
|
41
|
+
"health_command": ["mcp-ticketer", "--version"],
|
42
|
+
},
|
43
|
+
"kuzu-memory": {
|
44
|
+
"package": "kuzu-memory",
|
45
|
+
"command": ["kuzu-memory", "--help"],
|
46
|
+
"description": "Graph-based memory system",
|
47
|
+
"check_health": False, # May not have version command
|
48
|
+
},
|
49
|
+
}
|
50
|
+
|
51
|
+
@property
|
52
|
+
def name(self) -> str:
|
53
|
+
return "mcp_services_check"
|
54
|
+
|
55
|
+
@property
|
56
|
+
def category(self) -> str:
|
57
|
+
return "MCP Services"
|
58
|
+
|
59
|
+
def run(self) -> DiagnosticResult:
|
60
|
+
"""Run MCP services diagnostics."""
|
61
|
+
try:
|
62
|
+
details = {}
|
63
|
+
sub_results = []
|
64
|
+
services_status = {}
|
65
|
+
|
66
|
+
# Check each MCP service
|
67
|
+
for service_name, service_config in self.MCP_SERVICES.items():
|
68
|
+
service_result = self._check_service(service_name, service_config)
|
69
|
+
sub_results.append(service_result)
|
70
|
+
services_status[service_name] = {
|
71
|
+
"status": service_result.status.value,
|
72
|
+
"installed": service_result.details.get("installed", False),
|
73
|
+
"accessible": service_result.details.get("accessible", False),
|
74
|
+
"version": service_result.details.get("version"),
|
75
|
+
}
|
76
|
+
|
77
|
+
# Check MCP gateway configuration for services
|
78
|
+
gateway_result = self._check_gateway_configuration()
|
79
|
+
sub_results.append(gateway_result)
|
80
|
+
|
81
|
+
# Count service statuses
|
82
|
+
installed_count = sum(1 for s in services_status.values() if s["installed"])
|
83
|
+
accessible_count = sum(
|
84
|
+
1 for s in services_status.values() if s["accessible"]
|
85
|
+
)
|
86
|
+
total_services = len(self.MCP_SERVICES)
|
87
|
+
|
88
|
+
details["services"] = services_status
|
89
|
+
details["installed_count"] = installed_count
|
90
|
+
details["accessible_count"] = accessible_count
|
91
|
+
details["total_services"] = total_services
|
92
|
+
details["gateway_configured"] = gateway_result.status == DiagnosticStatus.OK
|
93
|
+
|
94
|
+
# Determine overall status
|
95
|
+
errors = [r for r in sub_results if r.status == DiagnosticStatus.ERROR]
|
96
|
+
warnings = [r for r in sub_results if r.status == DiagnosticStatus.WARNING]
|
97
|
+
|
98
|
+
if errors:
|
99
|
+
status = DiagnosticStatus.ERROR
|
100
|
+
message = f"Critical issues with {len(errors)} MCP service(s)"
|
101
|
+
elif installed_count == 0:
|
102
|
+
status = DiagnosticStatus.WARNING
|
103
|
+
message = "No MCP services installed"
|
104
|
+
elif accessible_count < installed_count:
|
105
|
+
status = DiagnosticStatus.WARNING
|
106
|
+
message = f"{installed_count}/{total_services} services installed, {accessible_count} accessible"
|
107
|
+
elif installed_count < total_services:
|
108
|
+
status = DiagnosticStatus.WARNING
|
109
|
+
message = f"{installed_count}/{total_services} MCP services installed"
|
110
|
+
else:
|
111
|
+
status = DiagnosticStatus.OK
|
112
|
+
message = f"All {total_services} MCP services installed and accessible"
|
113
|
+
|
114
|
+
return DiagnosticResult(
|
115
|
+
category=self.category,
|
116
|
+
status=status,
|
117
|
+
message=message,
|
118
|
+
details=details,
|
119
|
+
sub_results=sub_results if self.verbose else [],
|
120
|
+
)
|
121
|
+
|
122
|
+
except Exception as e:
|
123
|
+
return DiagnosticResult(
|
124
|
+
category=self.category,
|
125
|
+
status=DiagnosticStatus.ERROR,
|
126
|
+
message=f"MCP services check failed: {e!s}",
|
127
|
+
details={"error": str(e)},
|
128
|
+
)
|
129
|
+
|
130
|
+
def _check_service(self, service_name: str, config: Dict) -> DiagnosticResult:
|
131
|
+
"""Check a specific MCP service."""
|
132
|
+
details = {"service": service_name}
|
133
|
+
|
134
|
+
# Check if installed via pipx
|
135
|
+
pipx_installed, pipx_path = self._check_pipx_installation(config["package"])
|
136
|
+
details["pipx_installed"] = pipx_installed
|
137
|
+
if pipx_path:
|
138
|
+
details["pipx_path"] = pipx_path
|
139
|
+
|
140
|
+
# Check if accessible in PATH
|
141
|
+
accessible, command_path = self._check_command_accessible(config["command"])
|
142
|
+
details["accessible"] = accessible
|
143
|
+
if command_path:
|
144
|
+
details["command_path"] = command_path
|
145
|
+
|
146
|
+
# Check for installation in various locations
|
147
|
+
if not pipx_installed and not accessible:
|
148
|
+
# Try common installation locations
|
149
|
+
alt_installed, alt_path = self._check_alternative_installations(
|
150
|
+
service_name
|
151
|
+
)
|
152
|
+
if alt_installed:
|
153
|
+
details["alternative_installation"] = alt_path
|
154
|
+
accessible = alt_installed
|
155
|
+
|
156
|
+
details["installed"] = pipx_installed or accessible
|
157
|
+
|
158
|
+
# Check service health/version if accessible
|
159
|
+
if accessible and config.get("check_health"):
|
160
|
+
version = self._get_service_version(
|
161
|
+
config.get("health_command", config["command"])
|
162
|
+
)
|
163
|
+
if version:
|
164
|
+
details["version"] = version
|
165
|
+
|
166
|
+
# Determine status
|
167
|
+
if not (pipx_installed or accessible):
|
168
|
+
return DiagnosticResult(
|
169
|
+
category=f"MCP Service: {service_name}",
|
170
|
+
status=DiagnosticStatus.WARNING,
|
171
|
+
message=f"Not installed: {config['description']}",
|
172
|
+
details=details,
|
173
|
+
fix_command=f"pipx install {config['package']}",
|
174
|
+
fix_description=f"Install {service_name} for {config['description']}",
|
175
|
+
)
|
176
|
+
|
177
|
+
if pipx_installed and not accessible:
|
178
|
+
return DiagnosticResult(
|
179
|
+
category=f"MCP Service: {service_name}",
|
180
|
+
status=DiagnosticStatus.WARNING,
|
181
|
+
message="Installed via pipx but not in PATH",
|
182
|
+
details=details,
|
183
|
+
fix_command="pipx ensurepath",
|
184
|
+
fix_description="Ensure pipx bin directory is in PATH",
|
185
|
+
)
|
186
|
+
|
187
|
+
return DiagnosticResult(
|
188
|
+
category=f"MCP Service: {service_name}",
|
189
|
+
status=DiagnosticStatus.OK,
|
190
|
+
message="Installed and accessible",
|
191
|
+
details=details,
|
192
|
+
)
|
193
|
+
|
194
|
+
def _check_pipx_installation(self, package_name: str) -> Tuple[bool, Optional[str]]:
|
195
|
+
"""Check if a package is installed via pipx."""
|
196
|
+
try:
|
197
|
+
result = subprocess.run(
|
198
|
+
["pipx", "list", "--json"],
|
199
|
+
capture_output=True,
|
200
|
+
text=True,
|
201
|
+
timeout=5,
|
202
|
+
check=False,
|
203
|
+
)
|
204
|
+
|
205
|
+
if result.returncode == 0:
|
206
|
+
try:
|
207
|
+
data = json.loads(result.stdout)
|
208
|
+
venvs = data.get("venvs", {})
|
209
|
+
|
210
|
+
if package_name in venvs:
|
211
|
+
venv_info = venvs[package_name]
|
212
|
+
# Get the main app path
|
213
|
+
apps = (
|
214
|
+
venv_info.get("metadata", {})
|
215
|
+
.get("main_package", {})
|
216
|
+
.get("apps", [])
|
217
|
+
)
|
218
|
+
if apps:
|
219
|
+
app_path = (
|
220
|
+
venv_info.get("metadata", {})
|
221
|
+
.get("main_package", {})
|
222
|
+
.get("app_paths", [])
|
223
|
+
)
|
224
|
+
if app_path:
|
225
|
+
return True, app_path[0]
|
226
|
+
return True, None
|
227
|
+
except json.JSONDecodeError:
|
228
|
+
pass
|
229
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
230
|
+
pass
|
231
|
+
|
232
|
+
return False, None
|
233
|
+
|
234
|
+
def _check_command_accessible(
|
235
|
+
self, command: List[str]
|
236
|
+
) -> Tuple[bool, Optional[str]]:
|
237
|
+
"""Check if a command is accessible in PATH."""
|
238
|
+
try:
|
239
|
+
# Use 'which' on Unix-like systems
|
240
|
+
result = subprocess.run(
|
241
|
+
["which", command[0]],
|
242
|
+
capture_output=True,
|
243
|
+
text=True,
|
244
|
+
timeout=2,
|
245
|
+
check=False,
|
246
|
+
)
|
247
|
+
|
248
|
+
if result.returncode == 0:
|
249
|
+
path = result.stdout.strip()
|
250
|
+
return True, path
|
251
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
252
|
+
pass
|
253
|
+
|
254
|
+
# Try direct execution
|
255
|
+
try:
|
256
|
+
result = subprocess.run(
|
257
|
+
command,
|
258
|
+
capture_output=True,
|
259
|
+
text=True,
|
260
|
+
timeout=2,
|
261
|
+
check=False,
|
262
|
+
)
|
263
|
+
if (
|
264
|
+
result.returncode == 0
|
265
|
+
or "help" in result.stdout.lower()
|
266
|
+
or "usage" in result.stdout.lower()
|
267
|
+
):
|
268
|
+
return True, None
|
269
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
270
|
+
pass
|
271
|
+
|
272
|
+
return False, None
|
273
|
+
|
274
|
+
def _check_alternative_installations(
|
275
|
+
self, service_name: str
|
276
|
+
) -> Tuple[bool, Optional[str]]:
|
277
|
+
"""Check for alternative installation locations."""
|
278
|
+
# Common installation paths
|
279
|
+
paths_to_check = [
|
280
|
+
Path.home() / ".local" / "bin" / service_name,
|
281
|
+
Path("/usr/local/bin") / service_name,
|
282
|
+
Path("/opt") / service_name / "bin" / service_name,
|
283
|
+
Path.home() / ".npm" / "bin" / service_name, # For npm-based services
|
284
|
+
Path.home() / ".cargo" / "bin" / service_name, # For Rust-based services
|
285
|
+
]
|
286
|
+
|
287
|
+
for path in paths_to_check:
|
288
|
+
if path.exists():
|
289
|
+
return True, str(path)
|
290
|
+
|
291
|
+
return False, None
|
292
|
+
|
293
|
+
def _get_service_version(self, command: List[str]) -> Optional[str]:
|
294
|
+
"""Get version information for a service."""
|
295
|
+
try:
|
296
|
+
result = subprocess.run(
|
297
|
+
command,
|
298
|
+
capture_output=True,
|
299
|
+
text=True,
|
300
|
+
timeout=2,
|
301
|
+
check=False,
|
302
|
+
)
|
303
|
+
|
304
|
+
if result.returncode == 0:
|
305
|
+
output = result.stdout.strip()
|
306
|
+
# Try to extract version from output
|
307
|
+
lines = output.split("\n")
|
308
|
+
for line in lines:
|
309
|
+
if "version" in line.lower() or "v" in line.lower():
|
310
|
+
return line.strip()
|
311
|
+
# Return first line if no version line found
|
312
|
+
if lines:
|
313
|
+
return lines[0].strip()
|
314
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
315
|
+
pass
|
316
|
+
|
317
|
+
return None
|
318
|
+
|
319
|
+
def _check_gateway_configuration(self) -> DiagnosticResult:
|
320
|
+
"""Check if MCP services are configured in the gateway."""
|
321
|
+
try:
|
322
|
+
# Check MCP config file
|
323
|
+
config_dir = Path.home() / ".claude" / "mcp"
|
324
|
+
config_file = config_dir / "config.json"
|
325
|
+
|
326
|
+
if not config_file.exists():
|
327
|
+
return DiagnosticResult(
|
328
|
+
category="MCP Gateway Configuration",
|
329
|
+
status=DiagnosticStatus.WARNING,
|
330
|
+
message="MCP configuration file not found",
|
331
|
+
details={"config_path": str(config_file), "exists": False},
|
332
|
+
fix_command="claude-mpm configure --mcp",
|
333
|
+
fix_description="Initialize MCP configuration",
|
334
|
+
)
|
335
|
+
|
336
|
+
with open(config_file) as f:
|
337
|
+
config = json.load(f)
|
338
|
+
|
339
|
+
# Check for external services configuration
|
340
|
+
external_services = config.get("external_services", {})
|
341
|
+
configured_services = []
|
342
|
+
missing_services = []
|
343
|
+
|
344
|
+
for service_name in self.MCP_SERVICES:
|
345
|
+
if service_name in external_services:
|
346
|
+
configured_services.append(service_name)
|
347
|
+
else:
|
348
|
+
# Also check if it's in the services list directly
|
349
|
+
services = config.get("services", [])
|
350
|
+
if any(s.get("name") == service_name for s in services):
|
351
|
+
configured_services.append(service_name)
|
352
|
+
else:
|
353
|
+
missing_services.append(service_name)
|
354
|
+
|
355
|
+
details = {
|
356
|
+
"config_path": str(config_file),
|
357
|
+
"configured_services": configured_services,
|
358
|
+
"missing_services": missing_services,
|
359
|
+
}
|
360
|
+
|
361
|
+
if not configured_services:
|
362
|
+
return DiagnosticResult(
|
363
|
+
category="MCP Gateway Configuration",
|
364
|
+
status=DiagnosticStatus.WARNING,
|
365
|
+
message="No MCP services configured in gateway",
|
366
|
+
details=details,
|
367
|
+
fix_command="claude-mpm configure --mcp --add-services",
|
368
|
+
fix_description="Add MCP services to gateway configuration",
|
369
|
+
)
|
370
|
+
|
371
|
+
if missing_services:
|
372
|
+
return DiagnosticResult(
|
373
|
+
category="MCP Gateway Configuration",
|
374
|
+
status=DiagnosticStatus.WARNING,
|
375
|
+
message=f"{len(configured_services)} services configured, {len(missing_services)} missing",
|
376
|
+
details=details,
|
377
|
+
)
|
378
|
+
|
379
|
+
return DiagnosticResult(
|
380
|
+
category="MCP Gateway Configuration",
|
381
|
+
status=DiagnosticStatus.OK,
|
382
|
+
message=f"All {len(configured_services)} services configured",
|
383
|
+
details=details,
|
384
|
+
)
|
385
|
+
|
386
|
+
except json.JSONDecodeError as e:
|
387
|
+
return DiagnosticResult(
|
388
|
+
category="MCP Gateway Configuration",
|
389
|
+
status=DiagnosticStatus.ERROR,
|
390
|
+
message="Invalid JSON in MCP configuration",
|
391
|
+
details={"error": str(e)},
|
392
|
+
)
|
393
|
+
except Exception as e:
|
394
|
+
return DiagnosticResult(
|
395
|
+
category="MCP Gateway Configuration",
|
396
|
+
status=DiagnosticStatus.WARNING,
|
397
|
+
message=f"Could not check configuration: {e!s}",
|
398
|
+
details={"error": str(e)},
|
399
|
+
)
|
@@ -21,6 +21,7 @@ from .checks import (
|
|
21
21
|
InstallationCheck,
|
22
22
|
InstructionsCheck,
|
23
23
|
MCPCheck,
|
24
|
+
MCPServicesCheck,
|
24
25
|
MonitorCheck,
|
25
26
|
StartupLogCheck,
|
26
27
|
)
|
@@ -45,6 +46,7 @@ class DiagnosticRunner:
|
|
45
46
|
"""
|
46
47
|
self.verbose = verbose
|
47
48
|
self.fix = fix
|
49
|
+
self.logger = logger # Add logger initialization
|
48
50
|
# Define check order (dependencies first)
|
49
51
|
self.check_classes: List[Type[BaseDiagnosticCheck]] = [
|
50
52
|
InstallationCheck,
|
@@ -54,6 +56,7 @@ class DiagnosticRunner:
|
|
54
56
|
ClaudeDesktopCheck,
|
55
57
|
AgentCheck,
|
56
58
|
MCPCheck,
|
59
|
+
MCPServicesCheck, # Check external MCP services
|
57
60
|
MonitorCheck,
|
58
61
|
StartupLogCheck, # Check startup logs for recent issues
|
59
62
|
CommonIssuesCheck,
|
@@ -121,6 +124,7 @@ class DiagnosticRunner:
|
|
121
124
|
ClaudeDesktopCheck,
|
122
125
|
AgentCheck,
|
123
126
|
MCPCheck,
|
127
|
+
MCPServicesCheck,
|
124
128
|
MonitorCheck,
|
125
129
|
StartupLogCheck,
|
126
130
|
]
|