claude-mpm 4.4.3__py3-none-any.whl → 4.4.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/agent_loader.py +3 -2
  3. claude_mpm/agents/agent_loader_integration.py +2 -1
  4. claude_mpm/agents/async_agent_loader.py +2 -2
  5. claude_mpm/agents/base_agent_loader.py +2 -2
  6. claude_mpm/agents/frontmatter_validator.py +1 -0
  7. claude_mpm/agents/system_agent_config.py +2 -1
  8. claude_mpm/cli/commands/doctor.py +44 -5
  9. claude_mpm/cli/commands/mpm_init.py +116 -62
  10. claude_mpm/cli/parsers/configure_parser.py +3 -1
  11. claude_mpm/cli/startup_logging.py +1 -3
  12. claude_mpm/config/agent_config.py +1 -1
  13. claude_mpm/config/paths.py +2 -1
  14. claude_mpm/core/agent_name_normalizer.py +1 -0
  15. claude_mpm/core/config.py +2 -1
  16. claude_mpm/core/config_aliases.py +2 -1
  17. claude_mpm/core/file_utils.py +0 -1
  18. claude_mpm/core/framework/__init__.py +6 -6
  19. claude_mpm/core/framework/formatters/__init__.py +2 -2
  20. claude_mpm/core/framework/formatters/capability_generator.py +19 -8
  21. claude_mpm/core/framework/formatters/content_formatter.py +8 -3
  22. claude_mpm/core/framework/formatters/context_generator.py +7 -3
  23. claude_mpm/core/framework/loaders/__init__.py +3 -3
  24. claude_mpm/core/framework/loaders/agent_loader.py +7 -3
  25. claude_mpm/core/framework/loaders/file_loader.py +16 -6
  26. claude_mpm/core/framework/loaders/instruction_loader.py +16 -6
  27. claude_mpm/core/framework/loaders/packaged_loader.py +36 -12
  28. claude_mpm/core/framework/processors/__init__.py +2 -2
  29. claude_mpm/core/framework/processors/memory_processor.py +14 -6
  30. claude_mpm/core/framework/processors/metadata_processor.py +5 -5
  31. claude_mpm/core/framework/processors/template_processor.py +12 -6
  32. claude_mpm/core/framework_loader.py +44 -20
  33. claude_mpm/core/log_manager.py +2 -1
  34. claude_mpm/core/tool_access_control.py +1 -0
  35. claude_mpm/core/unified_agent_registry.py +2 -1
  36. claude_mpm/core/unified_paths.py +1 -0
  37. claude_mpm/experimental/cli_enhancements.py +1 -0
  38. claude_mpm/hooks/base_hook.py +1 -0
  39. claude_mpm/hooks/instruction_reinforcement.py +1 -0
  40. claude_mpm/hooks/kuzu_memory_hook.py +20 -13
  41. claude_mpm/hooks/validation_hooks.py +1 -1
  42. claude_mpm/scripts/mpm_doctor.py +1 -0
  43. claude_mpm/services/agents/loading/agent_profile_loader.py +1 -1
  44. claude_mpm/services/agents/loading/base_agent_manager.py +1 -1
  45. claude_mpm/services/agents/loading/framework_agent_loader.py +1 -1
  46. claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -0
  47. claude_mpm/services/agents/management/agent_management_service.py +1 -1
  48. claude_mpm/services/agents/memory/memory_categorization_service.py +0 -1
  49. claude_mpm/services/agents/memory/memory_file_service.py +6 -2
  50. claude_mpm/services/agents/memory/memory_format_service.py +0 -1
  51. claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
  52. claude_mpm/services/async_session_logger.py +1 -1
  53. claude_mpm/services/claude_session_logger.py +1 -0
  54. claude_mpm/services/core/path_resolver.py +1 -0
  55. claude_mpm/services/diagnostics/checks/__init__.py +2 -0
  56. claude_mpm/services/diagnostics/checks/installation_check.py +126 -25
  57. claude_mpm/services/diagnostics/checks/mcp_services_check.py +451 -0
  58. claude_mpm/services/diagnostics/diagnostic_runner.py +3 -0
  59. claude_mpm/services/diagnostics/doctor_reporter.py +259 -32
  60. claude_mpm/services/event_bus/direct_relay.py +2 -1
  61. claude_mpm/services/event_bus/event_bus.py +1 -0
  62. claude_mpm/services/event_bus/relay.py +3 -2
  63. claude_mpm/services/framework_claude_md_generator/content_assembler.py +1 -1
  64. claude_mpm/services/infrastructure/daemon_manager.py +1 -1
  65. claude_mpm/services/mcp_config_manager.py +301 -54
  66. claude_mpm/services/mcp_gateway/core/process_pool.py +62 -23
  67. claude_mpm/services/mcp_gateway/tools/__init__.py +6 -5
  68. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +3 -1
  69. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +16 -31
  70. claude_mpm/services/memory/cache/simple_cache.py +1 -1
  71. claude_mpm/services/project/archive_manager.py +159 -96
  72. claude_mpm/services/project/documentation_manager.py +64 -45
  73. claude_mpm/services/project/enhanced_analyzer.py +132 -89
  74. claude_mpm/services/project/project_organizer.py +225 -131
  75. claude_mpm/services/response_tracker.py +1 -1
  76. claude_mpm/services/socketio/server/eventbus_integration.py +1 -1
  77. claude_mpm/services/unified/__init__.py +1 -1
  78. claude_mpm/services/unified/analyzer_strategies/__init__.py +3 -3
  79. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +97 -53
  80. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +81 -40
  81. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +277 -178
  82. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +196 -112
  83. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +83 -49
  84. claude_mpm/services/unified/config_strategies/__init__.py +111 -126
  85. claude_mpm/services/unified/config_strategies/config_schema.py +157 -111
  86. claude_mpm/services/unified/config_strategies/context_strategy.py +91 -89
  87. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +183 -173
  88. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +160 -152
  89. claude_mpm/services/unified/config_strategies/unified_config_service.py +124 -112
  90. claude_mpm/services/unified/config_strategies/validation_strategy.py +298 -259
  91. claude_mpm/services/unified/deployment_strategies/__init__.py +7 -7
  92. claude_mpm/services/unified/deployment_strategies/base.py +24 -28
  93. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +168 -88
  94. claude_mpm/services/unified/deployment_strategies/local.py +49 -34
  95. claude_mpm/services/unified/deployment_strategies/utils.py +39 -43
  96. claude_mpm/services/unified/deployment_strategies/vercel.py +30 -24
  97. claude_mpm/services/unified/interfaces.py +0 -26
  98. claude_mpm/services/unified/migration.py +17 -40
  99. claude_mpm/services/unified/strategies.py +9 -26
  100. claude_mpm/services/unified/unified_analyzer.py +48 -44
  101. claude_mpm/services/unified/unified_config.py +21 -19
  102. claude_mpm/services/unified/unified_deployment.py +21 -26
  103. claude_mpm/storage/state_storage.py +1 -0
  104. claude_mpm/utils/agent_dependency_loader.py +18 -6
  105. claude_mpm/utils/common.py +14 -12
  106. claude_mpm/utils/database_connector.py +15 -12
  107. claude_mpm/utils/error_handler.py +1 -0
  108. claude_mpm/utils/log_cleanup.py +1 -0
  109. claude_mpm/utils/path_operations.py +1 -0
  110. claude_mpm/utils/session_logging.py +1 -1
  111. claude_mpm/utils/subprocess_utils.py +1 -0
  112. claude_mpm/validation/agent_validator.py +1 -1
  113. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.dist-info}/METADATA +35 -15
  114. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.dist-info}/RECORD +118 -117
  115. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.dist-info}/WHEEL +0 -0
  116. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.dist-info}/entry_points.txt +0 -0
  117. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.dist-info}/licenses/LICENSE +0 -0
  118. {claude_mpm-4.4.3.dist-info → claude_mpm-4.4.5.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 if we're in a virtual environment
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
- # 3. Check if running from pipx environment
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
- # 4. Check if running from source (development mode)
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
- # 5. Check Homebrew Python
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
- # 6. Check for system Python
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
- # 7. Additional check for pipx if not detected via venv
199
+ # 8. Additional check for pipx if not detected via venv
188
200
  if "pipx" not in methods_found:
189
- try:
190
- result = subprocess.run(
191
- ["pipx", "list"],
192
- capture_output=True,
193
- text=True,
194
- timeout=5,
195
- check=False,
196
- )
197
- if "claude-mpm" in result.stdout:
198
- if not is_pipx_venv:
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,451 @@
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", "--version"], # Use --version for proper check
25
+ "description": "Vector search for semantic code navigation",
26
+ "check_health": True,
27
+ "health_command": ["mcp-vector-search", "--version"],
28
+ "pipx_run_command": ["pipx", "run", "mcp-vector-search", "--version"],
29
+ },
30
+ "mcp-browser": {
31
+ "package": "mcp-browser",
32
+ "command": ["mcp-browser", "--version"], # Use --version for proper check
33
+ "description": "Browser automation and web interaction",
34
+ "check_health": True,
35
+ "health_command": ["mcp-browser", "--version"],
36
+ "pipx_run_command": ["pipx", "run", "mcp-browser", "--version"],
37
+ },
38
+ "mcp-ticketer": {
39
+ "package": "mcp-ticketer",
40
+ "command": ["mcp-ticketer", "--version"], # Use --version for proper check
41
+ "description": "Ticket and task management",
42
+ "check_health": True,
43
+ "health_command": ["mcp-ticketer", "--version"],
44
+ "pipx_run_command": ["pipx", "run", "mcp-ticketer", "--version"],
45
+ },
46
+ "kuzu-memory": {
47
+ "package": "kuzu-memory",
48
+ "command": ["kuzu-memory", "--version"], # Use --version for proper check
49
+ "description": "Graph-based memory system",
50
+ "check_health": True, # v1.1.0+ has version command
51
+ "health_command": ["kuzu-memory", "--version"],
52
+ "pipx_run_command": ["pipx", "run", "kuzu-memory", "--version"],
53
+ },
54
+ }
55
+
56
+ @property
57
+ def name(self) -> str:
58
+ return "mcp_services_check"
59
+
60
+ @property
61
+ def category(self) -> str:
62
+ return "MCP Services"
63
+
64
+ def run(self) -> DiagnosticResult:
65
+ """Run MCP services diagnostics."""
66
+ try:
67
+ details = {}
68
+ sub_results = []
69
+ services_status = {}
70
+
71
+ # Check each MCP service
72
+ for service_name, service_config in self.MCP_SERVICES.items():
73
+ service_result = self._check_service(service_name, service_config)
74
+ sub_results.append(service_result)
75
+ services_status[service_name] = {
76
+ "status": service_result.status.value,
77
+ "installed": service_result.details.get("installed", False),
78
+ "accessible": service_result.details.get("accessible", False),
79
+ "version": service_result.details.get("version"),
80
+ }
81
+
82
+ # Check MCP gateway configuration for services
83
+ gateway_result = self._check_gateway_configuration()
84
+ sub_results.append(gateway_result)
85
+
86
+ # Count service statuses
87
+ installed_count = sum(1 for s in services_status.values() if s["installed"])
88
+ accessible_count = sum(
89
+ 1 for s in services_status.values() if s["accessible"]
90
+ )
91
+ total_services = len(self.MCP_SERVICES)
92
+
93
+ details["services"] = services_status
94
+ details["installed_count"] = installed_count
95
+ details["accessible_count"] = accessible_count
96
+ details["total_services"] = total_services
97
+ details["gateway_configured"] = gateway_result.status == DiagnosticStatus.OK
98
+
99
+ # Determine overall status
100
+ errors = [r for r in sub_results if r.status == DiagnosticStatus.ERROR]
101
+ warnings = [r for r in sub_results if r.status == DiagnosticStatus.WARNING]
102
+
103
+ if errors:
104
+ status = DiagnosticStatus.ERROR
105
+ message = f"Critical issues with {len(errors)} MCP service(s)"
106
+ elif installed_count == 0:
107
+ status = DiagnosticStatus.WARNING
108
+ message = "No MCP services installed"
109
+ elif accessible_count < installed_count:
110
+ status = DiagnosticStatus.WARNING
111
+ message = f"{installed_count}/{total_services} services installed, {accessible_count} accessible"
112
+ elif installed_count < total_services:
113
+ status = DiagnosticStatus.WARNING
114
+ message = f"{installed_count}/{total_services} MCP services installed"
115
+ else:
116
+ status = DiagnosticStatus.OK
117
+ message = f"All {total_services} MCP services installed and accessible"
118
+
119
+ return DiagnosticResult(
120
+ category=self.category,
121
+ status=status,
122
+ message=message,
123
+ details=details,
124
+ sub_results=sub_results if self.verbose else [],
125
+ )
126
+
127
+ except Exception as e:
128
+ return DiagnosticResult(
129
+ category=self.category,
130
+ status=DiagnosticStatus.ERROR,
131
+ message=f"MCP services check failed: {e!s}",
132
+ details={"error": str(e)},
133
+ )
134
+
135
+ def _check_service(self, service_name: str, config: Dict) -> DiagnosticResult:
136
+ """Check a specific MCP service."""
137
+ details = {"service": service_name}
138
+
139
+ # Check if installed via pipx
140
+ pipx_installed, pipx_path = self._check_pipx_installation(config["package"])
141
+ details["pipx_installed"] = pipx_installed
142
+ if pipx_path:
143
+ details["pipx_path"] = pipx_path
144
+
145
+ # Check if accessible in PATH
146
+ accessible, command_path = self._check_command_accessible(config["command"])
147
+ details["accessible"] = accessible
148
+ if command_path:
149
+ details["command_path"] = command_path
150
+
151
+ # If not directly accessible, try pipx run command
152
+ if not accessible and "pipx_run_command" in config:
153
+ if self._verify_command_works(config["pipx_run_command"]):
154
+ accessible = True
155
+ details["accessible_via_pipx_run"] = True
156
+ details["pipx_run_available"] = True
157
+
158
+ # Check for installation in various locations
159
+ if not pipx_installed and not accessible:
160
+ # Try common installation locations
161
+ alt_installed, alt_path = self._check_alternative_installations(
162
+ service_name
163
+ )
164
+ if alt_installed:
165
+ details["alternative_installation"] = alt_path
166
+ accessible = alt_installed
167
+
168
+ details["installed"] = pipx_installed or accessible
169
+
170
+ # Check service health/version if accessible
171
+ if accessible and config.get("check_health"):
172
+ # Try different version commands in order of preference
173
+ version_commands = []
174
+ if details.get("accessible_via_pipx_run") and "pipx_run_command" in config:
175
+ version_commands.append(config["pipx_run_command"])
176
+ if "health_command" in config:
177
+ version_commands.append(config["health_command"])
178
+ version_commands.append(config["command"])
179
+
180
+ for cmd in version_commands:
181
+ version = self._get_service_version(cmd)
182
+ if version:
183
+ details["version"] = version
184
+ break
185
+
186
+ # Determine status
187
+ if not (pipx_installed or accessible):
188
+ return DiagnosticResult(
189
+ category=f"MCP Service: {service_name}",
190
+ status=DiagnosticStatus.WARNING,
191
+ message=f"Not installed: {config['description']}",
192
+ details=details,
193
+ fix_command=f"pipx install {config['package']}",
194
+ fix_description=f"Install {service_name} for {config['description']}",
195
+ )
196
+
197
+ if pipx_installed and not accessible:
198
+ # Check if pipx run works
199
+ if details.get("pipx_run_available"):
200
+ return DiagnosticResult(
201
+ category=f"MCP Service: {service_name}",
202
+ status=DiagnosticStatus.OK,
203
+ message="Installed via pipx (use 'pipx run' to execute)",
204
+ details=details,
205
+ )
206
+ return DiagnosticResult(
207
+ category=f"MCP Service: {service_name}",
208
+ status=DiagnosticStatus.WARNING,
209
+ message="Installed via pipx but not in PATH",
210
+ details=details,
211
+ fix_command="pipx ensurepath",
212
+ fix_description="Ensure pipx bin directory is in PATH",
213
+ )
214
+
215
+ return DiagnosticResult(
216
+ category=f"MCP Service: {service_name}",
217
+ status=DiagnosticStatus.OK,
218
+ message="Installed and accessible",
219
+ details=details,
220
+ )
221
+
222
+ def _check_pipx_installation(self, package_name: str) -> Tuple[bool, Optional[str]]:
223
+ """Check if a package is installed via pipx."""
224
+ try:
225
+ result = subprocess.run(
226
+ ["pipx", "list", "--json"],
227
+ capture_output=True,
228
+ text=True,
229
+ timeout=5,
230
+ check=False,
231
+ )
232
+
233
+ if result.returncode == 0:
234
+ try:
235
+ data = json.loads(result.stdout)
236
+ venvs = data.get("venvs", {})
237
+
238
+ if package_name in venvs:
239
+ venv_info = venvs[package_name]
240
+ # Get the main app path
241
+ apps = (
242
+ venv_info.get("metadata", {})
243
+ .get("main_package", {})
244
+ .get("apps", [])
245
+ )
246
+ if apps:
247
+ app_path = (
248
+ venv_info.get("metadata", {})
249
+ .get("main_package", {})
250
+ .get("app_paths", [])
251
+ )
252
+ if app_path:
253
+ return True, app_path[0]
254
+ return True, None
255
+ except json.JSONDecodeError:
256
+ pass
257
+ except (subprocess.SubprocessError, FileNotFoundError):
258
+ pass
259
+
260
+ return False, None
261
+
262
+ def _check_command_accessible(
263
+ self, command: List[str]
264
+ ) -> Tuple[bool, Optional[str]]:
265
+ """Check if a command is accessible in PATH."""
266
+ try:
267
+ # Use 'which' on Unix-like systems
268
+ result = subprocess.run(
269
+ ["which", command[0]],
270
+ capture_output=True,
271
+ text=True,
272
+ timeout=2,
273
+ check=False,
274
+ )
275
+
276
+ if result.returncode == 0:
277
+ path = result.stdout.strip()
278
+ # Verify the command actually works with --version
279
+ if self._verify_command_works(command):
280
+ return True, path
281
+ return False, path
282
+ except (subprocess.SubprocessError, FileNotFoundError):
283
+ pass
284
+
285
+ # Try direct execution with --version
286
+ if self._verify_command_works(command):
287
+ return True, None
288
+
289
+ return False, None
290
+
291
+ def _verify_command_works(self, command: List[str]) -> bool:
292
+ """Verify a command actually works by checking its --version output."""
293
+ try:
294
+ result = subprocess.run(
295
+ command,
296
+ capture_output=True,
297
+ text=True,
298
+ timeout=5,
299
+ check=False,
300
+ )
301
+
302
+ # Check for successful execution or version output
303
+ # Don't accept error messages containing "help" or "usage" as success
304
+ if result.returncode == 0:
305
+ # Look for actual version information
306
+ output = (result.stdout + result.stderr).lower()
307
+ # Check for version indicators
308
+ if any(keyword in output for keyword in ["version", "v1.", "v0.", "1.", "0."]):
309
+ # But reject if it's an error message
310
+ if not any(error in output for error in ["error", "not found", "no such", "command not found"]):
311
+ return True
312
+
313
+ # For some tools, non-zero return code is OK if version is shown
314
+ elif "--version" in command or "--help" in command:
315
+ output = (result.stdout + result.stderr).lower()
316
+ # Must have version info and no error indicators
317
+ if ("version" in output or "v1." in output or "v0." in output):
318
+ if not any(error in output for error in ["error", "not found", "no such", "command not found", "traceback"]):
319
+ return True
320
+
321
+ except (subprocess.SubprocessError, FileNotFoundError, OSError):
322
+ pass
323
+
324
+ return False
325
+
326
+ def _check_alternative_installations(
327
+ self, service_name: str
328
+ ) -> Tuple[bool, Optional[str]]:
329
+ """Check for alternative installation locations."""
330
+ # Common installation paths
331
+ paths_to_check = [
332
+ Path.home() / ".local" / "bin" / service_name,
333
+ Path("/usr/local/bin") / service_name,
334
+ Path("/opt") / service_name / "bin" / service_name,
335
+ Path.home() / ".npm" / "bin" / service_name, # For npm-based services
336
+ Path.home() / ".cargo" / "bin" / service_name, # For Rust-based services
337
+ ]
338
+
339
+ for path in paths_to_check:
340
+ if path.exists():
341
+ return True, str(path)
342
+
343
+ return False, None
344
+
345
+ def _get_service_version(self, command: List[str]) -> Optional[str]:
346
+ """Get version information for a service."""
347
+ try:
348
+ result = subprocess.run(
349
+ command,
350
+ capture_output=True,
351
+ text=True,
352
+ timeout=2,
353
+ check=False,
354
+ )
355
+
356
+ if result.returncode == 0:
357
+ output = result.stdout.strip()
358
+ # Try to extract version from output
359
+ lines = output.split("\n")
360
+ for line in lines:
361
+ if "version" in line.lower() or "v" in line.lower():
362
+ return line.strip()
363
+ # Return first line if no version line found
364
+ if lines:
365
+ return lines[0].strip()
366
+ except (subprocess.SubprocessError, FileNotFoundError):
367
+ pass
368
+
369
+ return None
370
+
371
+ def _check_gateway_configuration(self) -> DiagnosticResult:
372
+ """Check if MCP services are configured in the gateway."""
373
+ try:
374
+ # Check MCP config file
375
+ config_dir = Path.home() / ".claude" / "mcp"
376
+ config_file = config_dir / "config.json"
377
+
378
+ if not config_file.exists():
379
+ return DiagnosticResult(
380
+ category="MCP Gateway Configuration",
381
+ status=DiagnosticStatus.WARNING,
382
+ message="MCP configuration file not found",
383
+ details={"config_path": str(config_file), "exists": False},
384
+ fix_command="claude-mpm configure --mcp",
385
+ fix_description="Initialize MCP configuration",
386
+ )
387
+
388
+ with open(config_file) as f:
389
+ config = json.load(f)
390
+
391
+ # Check for external services configuration
392
+ external_services = config.get("external_services", {})
393
+ configured_services = []
394
+ missing_services = []
395
+
396
+ for service_name in self.MCP_SERVICES:
397
+ if service_name in external_services:
398
+ configured_services.append(service_name)
399
+ else:
400
+ # Also check if it's in the services list directly
401
+ services = config.get("services", [])
402
+ if any(s.get("name") == service_name for s in services):
403
+ configured_services.append(service_name)
404
+ else:
405
+ missing_services.append(service_name)
406
+
407
+ details = {
408
+ "config_path": str(config_file),
409
+ "configured_services": configured_services,
410
+ "missing_services": missing_services,
411
+ }
412
+
413
+ if not configured_services:
414
+ return DiagnosticResult(
415
+ category="MCP Gateway Configuration",
416
+ status=DiagnosticStatus.WARNING,
417
+ message="No MCP services configured in gateway",
418
+ details=details,
419
+ fix_command="claude-mpm configure --mcp --add-services",
420
+ fix_description="Add MCP services to gateway configuration",
421
+ )
422
+
423
+ if missing_services:
424
+ return DiagnosticResult(
425
+ category="MCP Gateway Configuration",
426
+ status=DiagnosticStatus.WARNING,
427
+ message=f"{len(configured_services)} services configured, {len(missing_services)} missing",
428
+ details=details,
429
+ )
430
+
431
+ return DiagnosticResult(
432
+ category="MCP Gateway Configuration",
433
+ status=DiagnosticStatus.OK,
434
+ message=f"All {len(configured_services)} services configured",
435
+ details=details,
436
+ )
437
+
438
+ except json.JSONDecodeError as e:
439
+ return DiagnosticResult(
440
+ category="MCP Gateway Configuration",
441
+ status=DiagnosticStatus.ERROR,
442
+ message="Invalid JSON in MCP configuration",
443
+ details={"error": str(e)},
444
+ )
445
+ except Exception as e:
446
+ return DiagnosticResult(
447
+ category="MCP Gateway Configuration",
448
+ status=DiagnosticStatus.WARNING,
449
+ message=f"Could not check configuration: {e!s}",
450
+ details={"error": str(e)},
451
+ )
@@ -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
  )
@@ -55,6 +56,7 @@ class DiagnosticRunner:
55
56
  ClaudeDesktopCheck,
56
57
  AgentCheck,
57
58
  MCPCheck,
59
+ MCPServicesCheck, # Check external MCP services
58
60
  MonitorCheck,
59
61
  StartupLogCheck, # Check startup logs for recent issues
60
62
  CommonIssuesCheck,
@@ -122,6 +124,7 @@ class DiagnosticRunner:
122
124
  ClaudeDesktopCheck,
123
125
  AgentCheck,
124
126
  MCPCheck,
127
+ MCPServicesCheck,
125
128
  MonitorCheck,
126
129
  StartupLogCheck,
127
130
  ]