claude-mpm 4.5.5__py3-none-any.whl → 4.5.8__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.
@@ -41,6 +41,14 @@ class MCPConfigManager:
41
41
  "kuzu-memory",
42
42
  }
43
43
 
44
+ # Known missing dependencies for MCP services that pipx doesn't handle automatically
45
+ # Maps service names to list of missing dependencies that need injection
46
+ SERVICE_MISSING_DEPENDENCIES = {
47
+ "mcp-ticketer": ["gql"], # mcp-ticketer v0.1.8+ needs gql but doesn't declare it
48
+ # Add more services here as needed, e.g.:
49
+ # "another-service": ["dep1", "dep2"],
50
+ }
51
+
44
52
  # Static known-good MCP service configurations
45
53
  # These are the correct, tested configurations that work reliably
46
54
  # Note: Commands will be resolved to full paths dynamically in get_static_service_config()
@@ -943,6 +951,11 @@ class MCPConfigManager:
943
951
  )
944
952
 
945
953
  if result.returncode == 0:
954
+ # Inject any missing dependencies if needed
955
+ if service_name in self.SERVICE_MISSING_DEPENDENCIES:
956
+ self.logger.debug(f"Injecting missing dependencies for newly installed {service_name}...")
957
+ self._inject_missing_dependencies(service_name)
958
+
946
959
  # Verify installation worked
947
960
  if self._verify_service_installed(service_name, "pipx"):
948
961
  return True, "pipx"
@@ -1092,18 +1105,39 @@ class MCPConfigManager:
1092
1105
  failed_services.append(f"{service_name} (reinstall failed)")
1093
1106
 
1094
1107
  elif issue_type == "missing_dependency":
1095
- # Fix missing dependencies by automatically reinstalling
1108
+ # Fix missing dependencies - try injection first, then reinstall if needed
1096
1109
  self.logger.info(
1097
- f" {service_name} has missing dependencies - auto-reinstalling..."
1110
+ f" {service_name} has missing dependencies - attempting fix..."
1098
1111
  )
1112
+
1113
+ # First try to inject dependencies without reinstalling
1114
+ injection_success = self._inject_missing_dependencies(service_name)
1115
+
1116
+ if injection_success:
1117
+ # Verify the fix worked
1118
+ issue_after_injection = self._detect_service_issue(service_name)
1119
+ if issue_after_injection is None:
1120
+ fixed_services.append(f"{service_name} (dependencies injected)")
1121
+ self.logger.info(f" ✅ Fixed {service_name} with dependency injection")
1122
+ continue # Move to next service
1123
+
1124
+ # If injection alone didn't work, try full reinstall
1125
+ self.logger.info(" Dependency injection insufficient, trying full reinstall...")
1099
1126
  success = self._auto_reinstall_mcp_service(service_name)
1100
1127
  if success:
1101
- fixed_services.append(f"{service_name} (auto-reinstalled)")
1128
+ fixed_services.append(f"{service_name} (auto-reinstalled with dependencies)")
1102
1129
  else:
1103
- self.logger.warning(
1104
- f" Auto-reinstall failed for {service_name}. Manual fix: "
1105
- f"pipx uninstall {service_name} && pipx install {service_name}"
1106
- )
1130
+ # Provide specific manual fix for known services
1131
+ if service_name == "mcp-ticketer":
1132
+ self.logger.warning(
1133
+ f" Auto-fix failed for {service_name}. Manual fix: "
1134
+ f"pipx uninstall {service_name} && pipx install {service_name} && pipx inject {service_name} gql"
1135
+ )
1136
+ else:
1137
+ self.logger.warning(
1138
+ f" Auto-reinstall failed for {service_name}. Manual fix: "
1139
+ f"pipx uninstall {service_name} && pipx install {service_name}"
1140
+ )
1107
1141
  failed_services.append(f"{service_name} (auto-reinstall failed)")
1108
1142
 
1109
1143
  elif issue_type == "path_issue":
@@ -1129,7 +1163,11 @@ class MCPConfigManager:
1129
1163
  message += "\n\n💡 Manual fix instructions:"
1130
1164
  for failed in failed_services:
1131
1165
  service = failed.split(" ")[0]
1132
- message += f"\n • {service}: pipx uninstall {service} && pipx install {service}"
1166
+ if service in self.SERVICE_MISSING_DEPENDENCIES:
1167
+ deps = " ".join([f"&& pipx inject {service} {dep}" for dep in self.SERVICE_MISSING_DEPENDENCIES[service]])
1168
+ message += f"\n • {service}: pipx uninstall {service} && pipx install {service} {deps}"
1169
+ else:
1170
+ message += f"\n • {service}: pipx uninstall {service} && pipx install {service}"
1133
1171
 
1134
1172
  return success, message
1135
1173
 
@@ -1148,7 +1186,58 @@ class MCPConfigManager:
1148
1186
 
1149
1187
  # Try to run the service with --help to detect issues
1150
1188
  try:
1151
- # Test with pipx run
1189
+ # First check if service is installed in pipx venv
1190
+ pipx_venv_bin = self.pipx_base / service_name / "bin" / service_name
1191
+ if pipx_venv_bin.exists():
1192
+ # Test the installed version directly (has injected dependencies)
1193
+ # This avoids using pipx run which downloads a fresh cache copy without dependencies
1194
+ self.logger.debug(f"Testing {service_name} from installed pipx venv: {pipx_venv_bin}")
1195
+ result = subprocess.run(
1196
+ [str(pipx_venv_bin), "--help"],
1197
+ capture_output=True,
1198
+ text=True,
1199
+ timeout=10,
1200
+ check=False,
1201
+ )
1202
+
1203
+ # Check for specific error patterns in installed version
1204
+ stderr_lower = result.stderr.lower()
1205
+ stdout_lower = result.stdout.lower()
1206
+ combined_output = stderr_lower + stdout_lower
1207
+
1208
+ # Import errors in installed version (should be rare if dependencies injected)
1209
+ if (
1210
+ "modulenotfounderror" in combined_output
1211
+ or "importerror" in combined_output
1212
+ ):
1213
+ # Check if it's specifically the gql dependency for mcp-ticketer
1214
+ if service_name == "mcp-ticketer" and "gql" in combined_output:
1215
+ return "missing_dependency"
1216
+ return "import_error"
1217
+
1218
+ # Path issues
1219
+ if "no such file or directory" in combined_output:
1220
+ return "path_issue"
1221
+
1222
+ # If help text appears, service is working
1223
+ if (
1224
+ "usage:" in combined_output
1225
+ or "help" in combined_output
1226
+ or result.returncode in [0, 1]
1227
+ ):
1228
+ self.logger.debug(f"{service_name} is working correctly")
1229
+ return None # Service is working
1230
+
1231
+ # Unknown issue
1232
+ if result.returncode not in [0, 1]:
1233
+ self.logger.debug(f"{service_name} returned unexpected exit code: {result.returncode}")
1234
+ return "unknown_error"
1235
+
1236
+ return None # Default to working if no issues detected
1237
+
1238
+ # Service not installed in pipx venv - use pipx run for detection
1239
+ # Note: pipx run uses cache which may not have injected dependencies
1240
+ self.logger.debug(f"Testing {service_name} via pipx run (not installed in venv)")
1152
1241
  result = subprocess.run(
1153
1242
  ["pipx", "run", service_name, "--help"],
1154
1243
  capture_output=True,
@@ -1169,15 +1258,15 @@ class MCPConfigManager:
1169
1258
  ):
1170
1259
  return "not_installed"
1171
1260
 
1172
- # Import errors (like mcp-ticketer's corrupted state)
1261
+ # Import errors when using pipx run (cache version)
1173
1262
  if (
1174
1263
  "modulenotfounderror" in combined_output
1175
1264
  or "importerror" in combined_output
1176
1265
  ):
1177
- # Check if it's specifically the gql dependency for mcp-ticketer
1178
- if service_name == "mcp-ticketer" and "gql" in combined_output:
1179
- return "missing_dependency"
1180
- return "import_error"
1266
+ # Don't report missing_dependency for cache version - it may be missing injected deps
1267
+ # Just report that service needs to be installed properly
1268
+ self.logger.debug(f"{service_name} has import errors in pipx run cache - needs proper installation")
1269
+ return "not_installed"
1181
1270
 
1182
1271
  # Path issues
1183
1272
  if "no such file or directory" in combined_output:
@@ -1240,6 +1329,11 @@ class MCPConfigManager:
1240
1329
  )
1241
1330
 
1242
1331
  if install_result.returncode == 0:
1332
+ # Inject any missing dependencies if needed
1333
+ if service_name in self.SERVICE_MISSING_DEPENDENCIES:
1334
+ self.logger.debug(f"Injecting missing dependencies for {service_name}...")
1335
+ self._inject_missing_dependencies(service_name)
1336
+
1243
1337
  # Verify the reinstall worked
1244
1338
  issue = self._detect_service_issue(service_name)
1245
1339
  if issue is None:
@@ -1258,6 +1352,65 @@ class MCPConfigManager:
1258
1352
  self.logger.error(f"Error reinstalling {service_name}: {e}")
1259
1353
  return False
1260
1354
 
1355
+ def _inject_missing_dependencies(self, service_name: str) -> bool:
1356
+ """
1357
+ Inject missing dependencies into a pipx-installed MCP service.
1358
+
1359
+ Some MCP services don't properly declare all their dependencies in their
1360
+ package metadata, which causes import errors when pipx creates isolated
1361
+ virtual environments. This method injects the missing dependencies using
1362
+ pipx inject.
1363
+
1364
+ Args:
1365
+ service_name: Name of the MCP service to fix
1366
+
1367
+ Returns:
1368
+ True if dependencies were injected successfully or no injection needed, False otherwise
1369
+ """
1370
+ # Check if this service has known missing dependencies
1371
+ if service_name not in self.SERVICE_MISSING_DEPENDENCIES:
1372
+ return True # No dependencies to inject
1373
+
1374
+ missing_deps = self.SERVICE_MISSING_DEPENDENCIES[service_name]
1375
+ if not missing_deps:
1376
+ return True # No dependencies to inject
1377
+
1378
+ self.logger.info(
1379
+ f" → Injecting missing dependencies for {service_name}: {', '.join(missing_deps)}"
1380
+ )
1381
+
1382
+ all_successful = True
1383
+ for dep in missing_deps:
1384
+ try:
1385
+ self.logger.debug(f" Injecting {dep} into {service_name}...")
1386
+ result = subprocess.run(
1387
+ ["pipx", "inject", service_name, dep],
1388
+ capture_output=True,
1389
+ text=True,
1390
+ timeout=60,
1391
+ check=False,
1392
+ )
1393
+
1394
+ if result.returncode == 0:
1395
+ self.logger.info(f" ✅ Successfully injected {dep}")
1396
+ # Check if already injected (pipx will complain if package already exists)
1397
+ elif "already satisfied" in result.stderr.lower() or "already installed" in result.stderr.lower():
1398
+ self.logger.debug(f" {dep} already present in {service_name}")
1399
+ else:
1400
+ self.logger.error(
1401
+ f" Failed to inject {dep}: {result.stderr}"
1402
+ )
1403
+ all_successful = False
1404
+
1405
+ except subprocess.TimeoutExpired:
1406
+ self.logger.error(f" Timeout while injecting {dep}")
1407
+ all_successful = False
1408
+ except Exception as e:
1409
+ self.logger.error(f" Error injecting {dep}: {e}")
1410
+ all_successful = False
1411
+
1412
+ return all_successful
1413
+
1261
1414
  def _auto_reinstall_mcp_service(self, service_name: str) -> bool:
1262
1415
  """
1263
1416
  Automatically reinstall an MCP service with missing dependencies.
@@ -1312,6 +1465,14 @@ class MCPConfigManager:
1312
1465
  )
1313
1466
  return False
1314
1467
 
1468
+ # Inject any missing dependencies that pipx doesn't handle automatically
1469
+ if service_name in self.SERVICE_MISSING_DEPENDENCIES:
1470
+ self.logger.info(f" → Fixing missing dependencies for {service_name}...")
1471
+ if not self._inject_missing_dependencies(service_name):
1472
+ self.logger.warning(
1473
+ f"Failed to inject all dependencies for {service_name}, but continuing..."
1474
+ )
1475
+
1315
1476
  # Verify the reinstall worked
1316
1477
  self.logger.info(f" → Verifying {service_name} installation...")
1317
1478
  issue = self._detect_service_issue(service_name)
@@ -1319,9 +1480,17 @@ class MCPConfigManager:
1319
1480
  if issue is None:
1320
1481
  self.logger.info(f" ✅ Successfully reinstalled {service_name}")
1321
1482
  return True
1322
- self.logger.warning(
1323
- f"Reinstalled {service_name} but still has issue: {issue}"
1324
- )
1483
+
1484
+ # If still has missing dependency issue after injection, log specific instructions
1485
+ if issue == "missing_dependency" and service_name == "mcp-ticketer":
1486
+ self.logger.error(
1487
+ f" {service_name} still has missing dependencies after injection. "
1488
+ f"Manual fix: pipx inject {service_name} gql"
1489
+ )
1490
+ else:
1491
+ self.logger.warning(
1492
+ f"Reinstalled {service_name} but still has issue: {issue}"
1493
+ )
1325
1494
  return False
1326
1495
 
1327
1496
  except subprocess.TimeoutExpired:
@@ -961,7 +961,6 @@ Generated by Claude MPM Archive Manager
961
961
  content,
962
962
  )
963
963
 
964
-
965
964
  def display_review_summary(self, review: Dict) -> None:
966
965
  """Display a formatted summary of the documentation review."""
967
966
  console.print("\n[bold cyan]📚 Documentation Review Summary[/bold cyan]\n")
@@ -233,7 +233,6 @@ class DocumentationManager:
233
233
  # Add metadata
234
234
  return self._add_metadata(merged)
235
235
 
236
-
237
236
  def _parse_into_sections(self, content: str) -> Dict[str, str]:
238
237
  """Parse markdown content into a dictionary of sections."""
239
238
  sections = {}
@@ -979,10 +979,7 @@ This directory is used for {description.lower()}.
979
979
  for file in root_files:
980
980
  if file.is_file() and (
981
981
  ("test" in file.name.lower() and file.suffix == ".py")
982
- or (
983
- file.suffix in [".sh", ".bash"]
984
- and file.name not in ["Makefile"]
985
- )
982
+ or (file.suffix in [".sh", ".bash"] and file.name not in ["Makefile"])
986
983
  or file.suffix in [".log", ".tmp", ".cache"]
987
984
  ):
988
985
  misplaced_count += 1
@@ -19,6 +19,7 @@ DESIGN DECISIONS:
19
19
  """
20
20
 
21
21
  from datetime import datetime, timezone
22
+ from threading import Lock
22
23
  from typing import Any, Dict, Optional
23
24
 
24
25
  from claude_mpm.core.config import Config
@@ -214,12 +215,15 @@ class ResponseTracker:
214
215
  logger.info(f"Response tracker session ID set to: {session_id}")
215
216
 
216
217
 
217
- # Singleton instance for consistency
218
+ # Singleton instance with thread-safe initialization
218
219
  _tracker_instance = None
220
+ _tracker_lock = Lock()
219
221
 
220
222
 
221
223
  def get_response_tracker(config: Optional[Config] = None) -> ResponseTracker:
222
- """Get the singleton response tracker instance.
224
+ """Get the singleton response tracker instance with thread-safe initialization.
225
+
226
+ Uses double-checked locking pattern to ensure thread safety.
223
227
 
224
228
  Args:
225
229
  config: Optional configuration instance
@@ -228,9 +232,16 @@ def get_response_tracker(config: Optional[Config] = None) -> ResponseTracker:
228
232
  The shared ResponseTracker instance
229
233
  """
230
234
  global _tracker_instance
231
- if _tracker_instance is None:
232
- _tracker_instance = ResponseTracker(config=config)
233
- return _tracker_instance
235
+
236
+ # Fast path - check without lock
237
+ if _tracker_instance is not None:
238
+ return _tracker_instance
239
+
240
+ # Slow path - acquire lock and double-check
241
+ with _tracker_lock:
242
+ if _tracker_instance is None:
243
+ _tracker_instance = ResponseTracker(config=config)
244
+ return _tracker_instance
234
245
 
235
246
 
236
247
  def track_response(
@@ -0,0 +1,174 @@
1
+ """
2
+ Session Manager Service
3
+
4
+ Centralized session ID management with thread-safe singleton pattern.
5
+ Ensures a single session ID is generated and used across all components.
6
+
7
+ This service addresses race conditions and duplicate session ID generation
8
+ by providing a single source of truth for session identifiers.
9
+ """
10
+
11
+ import os
12
+ from datetime import datetime, timezone
13
+ from threading import Lock
14
+ from typing import Optional
15
+
16
+ from claude_mpm.core.logging_utils import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class SessionManager:
22
+ """
23
+ Thread-safe singleton session manager.
24
+
25
+ Provides centralized session ID generation and management to prevent
26
+ duplicate session IDs across different components.
27
+
28
+ Uses double-checked locking pattern for thread-safe singleton initialization.
29
+ """
30
+
31
+ _instance: Optional["SessionManager"] = None
32
+ _lock = Lock()
33
+ _initialized = False
34
+
35
+ def __new__(cls) -> "SessionManager":
36
+ """
37
+ Create or return the singleton instance using double-checked locking.
38
+
39
+ Returns:
40
+ The singleton SessionManager instance
41
+ """
42
+ # First check without lock (fast path)
43
+ if cls._instance is None:
44
+ # Acquire lock for thread safety
45
+ with cls._lock:
46
+ # Double-check inside lock
47
+ if cls._instance is None:
48
+ cls._instance = super().__new__(cls)
49
+ return cls._instance
50
+
51
+ def __init__(self):
52
+ """
53
+ Initialize the session manager (only once).
54
+
55
+ This method uses an initialization flag to ensure it only
56
+ runs once, even if __init__ is called multiple times.
57
+ """
58
+ # Use class-level lock to ensure thread-safe initialization
59
+ with self.__class__._lock:
60
+ if self.__class__._initialized:
61
+ return
62
+
63
+ # Generate session ID once during initialization
64
+ self._session_id = self._generate_session_id()
65
+ self._session_start_time = datetime.now(timezone.utc)
66
+
67
+ # Mark as initialized
68
+ self.__class__._initialized = True
69
+
70
+ logger.info(
71
+ f"SessionManager initialized with session ID: {self._session_id}"
72
+ )
73
+
74
+ def _generate_session_id(self) -> str:
75
+ """
76
+ Generate or retrieve a session ID.
77
+
78
+ Checks environment variables first, then generates a timestamp-based ID.
79
+
80
+ Returns:
81
+ A unique session identifier
82
+ """
83
+ # Check environment variables in order of preference
84
+ env_vars = ["CLAUDE_SESSION_ID", "ANTHROPIC_SESSION_ID", "SESSION_ID"]
85
+
86
+ for env_var in env_vars:
87
+ session_id = os.environ.get(env_var)
88
+ if session_id:
89
+ logger.info(f"Using session ID from {env_var}: {session_id}")
90
+ return session_id
91
+
92
+ # Generate timestamp-based session ID
93
+ session_id = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
94
+ logger.info(f"Generated new session ID: {session_id}")
95
+ return session_id
96
+
97
+ def get_session_id(self) -> str:
98
+ """
99
+ Get the current session ID.
100
+
101
+ Thread-safe method to retrieve the session ID.
102
+
103
+ Returns:
104
+ The current session ID
105
+ """
106
+ return self._session_id
107
+
108
+ def get_session_start_time(self) -> datetime:
109
+ """
110
+ Get the session start time.
111
+
112
+ Returns:
113
+ The datetime when the session was initialized
114
+ """
115
+ return self._session_start_time
116
+
117
+ def set_session_id(self, session_id: str) -> None:
118
+ """
119
+ Override the session ID.
120
+
121
+ This should only be used in special circumstances, as it can
122
+ break the single session ID guarantee.
123
+
124
+ Args:
125
+ session_id: The new session ID to use
126
+ """
127
+ with self.__class__._lock:
128
+ old_id = self._session_id
129
+ if old_id != session_id:
130
+ self._session_id = session_id
131
+ logger.warning(f"Session ID changed from {old_id} to {session_id}")
132
+ else:
133
+ logger.debug(f"Session ID already set to {session_id}, no change needed")
134
+
135
+ @classmethod
136
+ def reset(cls) -> None:
137
+ """
138
+ Reset the singleton instance (mainly for testing).
139
+
140
+ This method should not be used in production code.
141
+ """
142
+ with cls._lock:
143
+ cls._instance = None
144
+ cls._initialized = False
145
+ logger.debug("SessionManager singleton reset")
146
+
147
+
148
+ # Global accessor function
149
+ _manager: Optional[SessionManager] = None
150
+
151
+
152
+ def get_session_manager() -> SessionManager:
153
+ """
154
+ Get the global SessionManager instance.
155
+
156
+ Thread-safe accessor that ensures a single SessionManager exists.
157
+
158
+ Returns:
159
+ The singleton SessionManager instance
160
+ """
161
+ global _manager
162
+ if _manager is None:
163
+ _manager = SessionManager()
164
+ return _manager
165
+
166
+
167
+ def get_session_id() -> str:
168
+ """
169
+ Convenience function to get the current session ID.
170
+
171
+ Returns:
172
+ The current session ID
173
+ """
174
+ return get_session_manager().get_session_id()
@@ -580,7 +580,6 @@ class DependencyAnalyzerStrategy(AnalyzerStrategy):
580
580
  # In production, you would integrate with vulnerability databases
581
581
  # like npm audit, pip-audit, or safety
582
582
 
583
-
584
583
  def _calculate_statistics(self, results: Dict[str, Any]) -> Dict[str, Any]:
585
584
  """Calculate dependency statistics."""
586
585
  all_deps = self._flatten_dependencies(results.get("dependencies", {}))
@@ -716,9 +716,13 @@ class StructureAnalyzerStrategy(AnalyzerStrategy):
716
716
  }
717
717
 
718
718
  # Compare architecture
719
- if "architecture" in baseline and "architecture" in current and (
720
- baseline["architecture"]["pattern"]
721
- != current["architecture"]["pattern"]
719
+ if (
720
+ "architecture" in baseline
721
+ and "architecture" in current
722
+ and (
723
+ baseline["architecture"]["pattern"]
724
+ != current["architecture"]["pattern"]
725
+ )
722
726
  ):
723
727
  comparison["architecture_change"] = {
724
728
  "baseline": baseline["architecture"]["pattern"],
@@ -509,9 +509,7 @@ class CachingContextManager:
509
509
  def invalidate_context(self, context_id: str):
510
510
  """Invalidate all cached values for context"""
511
511
  with self._lock:
512
- keys_to_remove = [
513
- k for k in self.cache if k.startswith(f"{context_id}:")
514
- ]
512
+ keys_to_remove = [k for k in self.cache if k.startswith(f"{context_id}:")]
515
513
 
516
514
  for key in keys_to_remove:
517
515
  del self.cache[key]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.5.5
3
+ Version: 4.5.8
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team