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.
Files changed (129) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/WORKFLOW.md +2 -14
  3. claude_mpm/agents/agent_loader.py +3 -2
  4. claude_mpm/agents/agent_loader_integration.py +2 -1
  5. claude_mpm/agents/async_agent_loader.py +2 -2
  6. claude_mpm/agents/base_agent_loader.py +2 -2
  7. claude_mpm/agents/frontmatter_validator.py +1 -0
  8. claude_mpm/agents/system_agent_config.py +2 -1
  9. claude_mpm/cli/commands/configure.py +2 -29
  10. claude_mpm/cli/commands/doctor.py +44 -5
  11. claude_mpm/cli/commands/mpm_init.py +117 -63
  12. claude_mpm/cli/parsers/configure_parser.py +6 -15
  13. claude_mpm/cli/startup_logging.py +1 -3
  14. claude_mpm/config/agent_config.py +1 -1
  15. claude_mpm/config/paths.py +2 -1
  16. claude_mpm/core/agent_name_normalizer.py +1 -0
  17. claude_mpm/core/config.py +2 -1
  18. claude_mpm/core/config_aliases.py +2 -1
  19. claude_mpm/core/file_utils.py +0 -1
  20. claude_mpm/core/framework/__init__.py +38 -0
  21. claude_mpm/core/framework/formatters/__init__.py +11 -0
  22. claude_mpm/core/framework/formatters/capability_generator.py +367 -0
  23. claude_mpm/core/framework/formatters/content_formatter.py +288 -0
  24. claude_mpm/core/framework/formatters/context_generator.py +184 -0
  25. claude_mpm/core/framework/loaders/__init__.py +13 -0
  26. claude_mpm/core/framework/loaders/agent_loader.py +206 -0
  27. claude_mpm/core/framework/loaders/file_loader.py +223 -0
  28. claude_mpm/core/framework/loaders/instruction_loader.py +161 -0
  29. claude_mpm/core/framework/loaders/packaged_loader.py +232 -0
  30. claude_mpm/core/framework/processors/__init__.py +11 -0
  31. claude_mpm/core/framework/processors/memory_processor.py +230 -0
  32. claude_mpm/core/framework/processors/metadata_processor.py +146 -0
  33. claude_mpm/core/framework/processors/template_processor.py +244 -0
  34. claude_mpm/core/framework_loader.py +298 -1795
  35. claude_mpm/core/log_manager.py +2 -1
  36. claude_mpm/core/tool_access_control.py +1 -0
  37. claude_mpm/core/unified_agent_registry.py +2 -1
  38. claude_mpm/core/unified_paths.py +1 -0
  39. claude_mpm/experimental/cli_enhancements.py +1 -0
  40. claude_mpm/hooks/__init__.py +9 -1
  41. claude_mpm/hooks/base_hook.py +1 -0
  42. claude_mpm/hooks/instruction_reinforcement.py +1 -0
  43. claude_mpm/hooks/kuzu_memory_hook.py +359 -0
  44. claude_mpm/hooks/validation_hooks.py +1 -1
  45. claude_mpm/scripts/mpm_doctor.py +1 -0
  46. claude_mpm/services/agents/loading/agent_profile_loader.py +1 -1
  47. claude_mpm/services/agents/loading/base_agent_manager.py +1 -1
  48. claude_mpm/services/agents/loading/framework_agent_loader.py +1 -1
  49. claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -0
  50. claude_mpm/services/agents/management/agent_management_service.py +1 -1
  51. claude_mpm/services/agents/memory/memory_categorization_service.py +0 -1
  52. claude_mpm/services/agents/memory/memory_file_service.py +6 -2
  53. claude_mpm/services/agents/memory/memory_format_service.py +0 -1
  54. claude_mpm/services/agents/registry/deployed_agent_discovery.py +1 -1
  55. claude_mpm/services/async_session_logger.py +1 -1
  56. claude_mpm/services/claude_session_logger.py +1 -0
  57. claude_mpm/services/core/path_resolver.py +2 -0
  58. claude_mpm/services/diagnostics/checks/__init__.py +2 -0
  59. claude_mpm/services/diagnostics/checks/installation_check.py +126 -25
  60. claude_mpm/services/diagnostics/checks/mcp_services_check.py +399 -0
  61. claude_mpm/services/diagnostics/diagnostic_runner.py +4 -0
  62. claude_mpm/services/diagnostics/doctor_reporter.py +259 -32
  63. claude_mpm/services/event_bus/direct_relay.py +2 -1
  64. claude_mpm/services/event_bus/event_bus.py +1 -0
  65. claude_mpm/services/event_bus/relay.py +3 -2
  66. claude_mpm/services/framework_claude_md_generator/content_assembler.py +1 -1
  67. claude_mpm/services/infrastructure/daemon_manager.py +1 -1
  68. claude_mpm/services/mcp_config_manager.py +67 -4
  69. claude_mpm/services/mcp_gateway/core/process_pool.py +320 -0
  70. claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
  71. claude_mpm/services/mcp_gateway/main.py +3 -13
  72. claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
  73. claude_mpm/services/mcp_gateway/tools/__init__.py +14 -2
  74. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +38 -6
  75. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +527 -0
  76. claude_mpm/services/memory/cache/simple_cache.py +1 -1
  77. claude_mpm/services/project/archive_manager.py +159 -96
  78. claude_mpm/services/project/documentation_manager.py +64 -45
  79. claude_mpm/services/project/enhanced_analyzer.py +132 -89
  80. claude_mpm/services/project/project_organizer.py +225 -131
  81. claude_mpm/services/response_tracker.py +1 -1
  82. claude_mpm/services/shared/__init__.py +2 -1
  83. claude_mpm/services/shared/service_factory.py +8 -5
  84. claude_mpm/services/socketio/server/eventbus_integration.py +1 -1
  85. claude_mpm/services/unified/__init__.py +1 -1
  86. claude_mpm/services/unified/analyzer_strategies/__init__.py +3 -3
  87. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +97 -53
  88. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +81 -40
  89. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +277 -178
  90. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +196 -112
  91. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +83 -49
  92. claude_mpm/services/unified/config_strategies/__init__.py +175 -0
  93. claude_mpm/services/unified/config_strategies/config_schema.py +735 -0
  94. claude_mpm/services/unified/config_strategies/context_strategy.py +750 -0
  95. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +1009 -0
  96. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +879 -0
  97. claude_mpm/services/unified/config_strategies/unified_config_service.py +814 -0
  98. claude_mpm/services/unified/config_strategies/validation_strategy.py +1144 -0
  99. claude_mpm/services/unified/deployment_strategies/__init__.py +7 -7
  100. claude_mpm/services/unified/deployment_strategies/base.py +24 -28
  101. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +168 -88
  102. claude_mpm/services/unified/deployment_strategies/local.py +49 -34
  103. claude_mpm/services/unified/deployment_strategies/utils.py +39 -43
  104. claude_mpm/services/unified/deployment_strategies/vercel.py +30 -24
  105. claude_mpm/services/unified/interfaces.py +0 -26
  106. claude_mpm/services/unified/migration.py +17 -40
  107. claude_mpm/services/unified/strategies.py +9 -26
  108. claude_mpm/services/unified/unified_analyzer.py +48 -44
  109. claude_mpm/services/unified/unified_config.py +21 -19
  110. claude_mpm/services/unified/unified_deployment.py +21 -26
  111. claude_mpm/storage/state_storage.py +1 -0
  112. claude_mpm/utils/agent_dependency_loader.py +18 -6
  113. claude_mpm/utils/common.py +14 -12
  114. claude_mpm/utils/database_connector.py +15 -12
  115. claude_mpm/utils/error_handler.py +1 -0
  116. claude_mpm/utils/log_cleanup.py +1 -0
  117. claude_mpm/utils/path_operations.py +1 -0
  118. claude_mpm/utils/session_logging.py +1 -1
  119. claude_mpm/utils/subprocess_utils.py +1 -0
  120. claude_mpm/validation/agent_validator.py +1 -1
  121. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/METADATA +23 -17
  122. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/RECORD +126 -105
  123. claude_mpm/cli/commands/configure_tui.py +0 -1927
  124. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
  125. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
  126. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/WHEEL +0 -0
  127. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/entry_points.txt +0 -0
  128. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.4.dist-info}/licenses/LICENSE +0 -0
  129. {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 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,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
  ]