claude-mpm 4.4.8__py3-none-any.whl → 4.4.10__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 (24) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/scripts/mcp_server.py +0 -0
  3. claude_mpm/scripts/start_activity_logging.py +0 -0
  4. claude_mpm/services/diagnostics/checks/mcp_services_check.py +420 -27
  5. claude_mpm/services/mcp_config_manager.py +563 -42
  6. {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/METADATA +1 -1
  7. {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/RECORD +9 -22
  8. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  9. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  10. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  11. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-313.pyc +0 -0
  12. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  13. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  14. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  15. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  16. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-313.pyc +0 -0
  17. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  18. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  19. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  20. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  21. {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/WHEEL +0 -0
  22. {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/entry_points.txt +0 -0
  23. {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/licenses/LICENSE +0 -0
  24. {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/top_level.txt +0 -0
@@ -41,6 +41,34 @@ class MCPConfigManager:
41
41
  "kuzu-memory",
42
42
  }
43
43
 
44
+ # Static known-good MCP service configurations
45
+ # These are the correct, tested configurations that work reliably
46
+ STATIC_MCP_CONFIGS = {
47
+ "kuzu-memory": {
48
+ "type": "stdio",
49
+ "command": "kuzu-memory", # Use direct binary, will be resolved to full path
50
+ "args": ["mcp", "serve"] # v1.1.0+ uses 'mcp serve' command
51
+ },
52
+ "mcp-ticketer": {
53
+ "type": "stdio",
54
+ "command": "mcp-ticketer", # Use direct binary to preserve injected dependencies
55
+ "args": ["mcp"]
56
+ },
57
+ "mcp-browser": {
58
+ "type": "stdio",
59
+ "command": "mcp-browser", # Use direct binary
60
+ "args": ["mcp"],
61
+ "env": {"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")}
62
+ },
63
+ "mcp-vector-search": {
64
+ "type": "stdio",
65
+ # Use pipx venv's Python directly for module execution
66
+ "command": str(Path.home() / ".local" / "pipx" / "venvs" / "mcp-vector-search" / "bin" / "python"),
67
+ "args": ["-m", "mcp_vector_search.mcp.server", "{project_root}"],
68
+ "env": {}
69
+ }
70
+ }
71
+
44
72
  def __init__(self):
45
73
  """Initialize the MCP configuration manager."""
46
74
  self.logger = get_logger(__name__)
@@ -195,11 +223,187 @@ class MCPConfigManager:
195
223
 
196
224
  return None
197
225
 
226
+ def test_service_command(self, service_name: str, config: Dict) -> bool:
227
+ """
228
+ Test if a service configuration actually works.
229
+
230
+ Args:
231
+ service_name: Name of the MCP service
232
+ config: Service configuration to test
233
+
234
+ Returns:
235
+ True if service responds correctly, False otherwise
236
+ """
237
+ try:
238
+ import shutil
239
+
240
+ # Build command - handle pipx PATH issues
241
+ command = config["command"]
242
+
243
+ # If command is pipx and not found, try common paths
244
+ if command == "pipx":
245
+ pipx_path = shutil.which("pipx")
246
+ if not pipx_path:
247
+ # Try common pipx locations
248
+ for possible_path in [
249
+ "/opt/homebrew/bin/pipx",
250
+ "/usr/local/bin/pipx",
251
+ str(Path.home() / ".local" / "bin" / "pipx"),
252
+ ]:
253
+ if Path(possible_path).exists():
254
+ command = possible_path
255
+ break
256
+ else:
257
+ command = pipx_path
258
+
259
+ cmd = [command]
260
+
261
+ # Add test args (--help or --version)
262
+ if "args" in config:
263
+ # For MCP services, test with --help after the subcommand
264
+ test_args = config["args"].copy()
265
+ # Replace project root placeholder for testing
266
+ test_args = [
267
+ arg.replace("{project_root}", str(self.project_root)) if "{project_root}" in arg else arg
268
+ for arg in test_args
269
+ ]
270
+
271
+ # Add --help at the end
272
+ if service_name == "mcp-vector-search":
273
+ # For Python module invocation, just test if Python can import the module
274
+ cmd.extend(test_args[:2]) # Just python -m module_name
275
+ cmd.extend(["--help"])
276
+ else:
277
+ cmd.extend(test_args)
278
+ cmd.append("--help")
279
+ else:
280
+ cmd.append("--help")
281
+
282
+ # Run test command with timeout
283
+ result = subprocess.run(
284
+ cmd,
285
+ capture_output=True,
286
+ text=True,
287
+ timeout=5,
288
+ check=False,
289
+ env=config.get("env", {})
290
+ )
291
+
292
+ # Check if command executed (exit code 0 or 1 for help)
293
+ if result.returncode in [0, 1]:
294
+ # Additional check for import errors in stderr
295
+ if "ModuleNotFoundError" in result.stderr or "ImportError" in result.stderr:
296
+ self.logger.debug(f"Service {service_name} has import errors")
297
+ return False
298
+ return True
299
+
300
+ except subprocess.TimeoutExpired:
301
+ # Timeout might mean the service started successfully and is waiting for input
302
+ return True
303
+ except Exception as e:
304
+ self.logger.debug(f"Error testing {service_name}: {e}")
305
+
306
+ return False
307
+
308
+ def get_static_service_config(self, service_name: str, project_path: Optional[str] = None) -> Optional[Dict]:
309
+ """
310
+ Get the static, known-good configuration for an MCP service.
311
+
312
+ Args:
313
+ service_name: Name of the MCP service
314
+ project_path: Optional project path to use (defaults to current project)
315
+
316
+ Returns:
317
+ Static service configuration dict or None if service not known
318
+ """
319
+ if service_name not in self.STATIC_MCP_CONFIGS:
320
+ return None
321
+
322
+ config = self.STATIC_MCP_CONFIGS[service_name].copy()
323
+ import shutil
324
+
325
+ # Resolve service binary commands to full paths
326
+ if service_name in ["kuzu-memory", "mcp-ticketer", "mcp-browser"]:
327
+ # Try to find the full path of the binary
328
+ binary_name = config["command"]
329
+ binary_path = shutil.which(binary_name)
330
+
331
+ if not binary_path:
332
+ # Try common installation locations
333
+ possible_paths = [
334
+ f"/opt/homebrew/bin/{binary_name}",
335
+ f"/usr/local/bin/{binary_name}",
336
+ str(Path.home() / ".local" / "bin" / binary_name),
337
+ ]
338
+ for path in possible_paths:
339
+ if Path(path).exists():
340
+ binary_path = path
341
+ break
342
+
343
+ if binary_path:
344
+ config["command"] = binary_path
345
+ # If still not found, keep the binary name and hope it's in PATH
346
+
347
+ # Resolve pipx command to full path if needed
348
+ elif config.get("command") == "pipx":
349
+ pipx_path = shutil.which("pipx")
350
+ if not pipx_path:
351
+ # Try common pipx locations
352
+ for possible_path in [
353
+ "/opt/homebrew/bin/pipx",
354
+ "/usr/local/bin/pipx",
355
+ str(Path.home() / ".local" / "bin" / "pipx"),
356
+ ]:
357
+ if Path(possible_path).exists():
358
+ pipx_path = possible_path
359
+ break
360
+ if pipx_path:
361
+ config["command"] = pipx_path
362
+ else:
363
+ # Keep as "pipx" and hope it's in PATH when executed
364
+ config["command"] = "pipx"
365
+
366
+ # Handle user-specific paths for mcp-vector-search
367
+ if service_name == "mcp-vector-search":
368
+ # Get the correct pipx venv path for the current user
369
+ home = Path.home()
370
+ python_path = home / ".local" / "pipx" / "venvs" / "mcp-vector-search" / "bin" / "python"
371
+
372
+ # Check if the Python interpreter exists, if not fallback to pipx run
373
+ if python_path.exists():
374
+ config["command"] = str(python_path)
375
+ else:
376
+ # Fallback to pipx run method
377
+ import shutil
378
+ pipx_path = shutil.which("pipx")
379
+ if not pipx_path:
380
+ # Try common pipx locations
381
+ for possible_path in [
382
+ "/opt/homebrew/bin/pipx",
383
+ "/usr/local/bin/pipx",
384
+ str(Path.home() / ".local" / "bin" / "pipx"),
385
+ ]:
386
+ if Path(possible_path).exists():
387
+ pipx_path = possible_path
388
+ break
389
+ config["command"] = pipx_path if pipx_path else "pipx"
390
+ config["args"] = ["run", "--spec", "mcp-vector-search", "python"] + config["args"]
391
+
392
+ # Use provided project path or current project
393
+ project_root = project_path if project_path else str(self.project_root)
394
+ config["args"] = [
395
+ arg.replace("{project_root}", project_root) if "{project_root}" in arg else arg
396
+ for arg in config["args"]
397
+ ]
398
+
399
+ return config
400
+
198
401
  def generate_service_config(self, service_name: str) -> Optional[Dict]:
199
402
  """
200
403
  Generate configuration for a specific MCP service.
201
404
 
202
- Prefers 'pipx run' or 'uvx' commands over direct execution for better isolation.
405
+ Prefers static configurations over detection. Falls back to detection
406
+ only for unknown services.
203
407
 
204
408
  Args:
205
409
  service_name: Name of the MCP service
@@ -207,6 +411,17 @@ class MCPConfigManager:
207
411
  Returns:
208
412
  Service configuration dict or None if service not found
209
413
  """
414
+ # First try to get static configuration
415
+ static_config = self.get_static_service_config(service_name)
416
+ if static_config:
417
+ # Validate that the static config actually works
418
+ if self.test_service_command(service_name, static_config):
419
+ self.logger.debug(f"Static config for {service_name} validated successfully")
420
+ return static_config
421
+ else:
422
+ self.logger.warning(f"Static config for {service_name} failed validation, trying fallback")
423
+
424
+ # Fall back to detection-based configuration for unknown services
210
425
  import shutil
211
426
 
212
427
  # Check for pipx run first (preferred for isolation)
@@ -308,7 +523,7 @@ class MCPConfigManager:
308
523
 
309
524
  elif service_name == "kuzu-memory":
310
525
  # Determine kuzu-memory command version
311
- kuzu_args = ["mcp", "serve"] # Default to the correct modern format
526
+ kuzu_args = ["mcp", "serve"] # Default to the standard v1.1.0+ format
312
527
  test_cmd = None
313
528
 
314
529
  if use_pipx_run:
@@ -330,20 +545,21 @@ class MCPConfigManager:
330
545
  # Check for MCP support in help output
331
546
  help_output = result.stdout.lower() + result.stderr.lower()
332
547
 
333
- # Modern version detection - look for "mcp serve" command
548
+ # Standard version detection - look for "mcp serve" command (v1.1.0+)
549
+ # This is the correct format for kuzu-memory v1.1.0 and later
334
550
  if "mcp serve" in help_output or ("mcp" in help_output and "serve" in help_output):
335
- # Modern version with mcp serve command
551
+ # Standard v1.1.0+ version with mcp serve command
336
552
  kuzu_args = ["mcp", "serve"]
337
553
  # Legacy version detection - only "serve" without "mcp"
338
554
  elif "serve" in help_output and "mcp" not in help_output:
339
555
  # Very old version that only has serve command
340
556
  kuzu_args = ["serve"]
341
- # Note: "claude mcp-server" format is deprecated and not used
342
557
  else:
343
- # Default to the correct modern format
558
+ # Default to the standard mcp serve format (v1.1.0+)
559
+ # Note: "claude mcp-server" format is deprecated and does not work
344
560
  kuzu_args = ["mcp", "serve"]
345
561
  except Exception:
346
- # Default to the correct mcp serve command on any error
562
+ # Default to the standard mcp serve command on any error
347
563
  kuzu_args = ["mcp", "serve"]
348
564
 
349
565
  if use_pipx_run:
@@ -371,17 +587,20 @@ class MCPConfigManager:
371
587
 
372
588
  def ensure_mcp_services_configured(self) -> Tuple[bool, str]:
373
589
  """
374
- Ensure MCP services are configured in ~/.claude.json on startup.
590
+ Ensure MCP services are configured correctly in ~/.claude.json on startup.
375
591
 
376
- This method checks if the core MCP services are configured in the
377
- current project's mcpServers section and automatically adds them if missing.
592
+ This method checks ALL projects in ~/.claude.json and ensures each has
593
+ the correct, static MCP service configurations. It will:
594
+ 1. Add missing services
595
+ 2. Fix incorrect configurations
596
+ 3. Update all projects, not just the current one
378
597
 
379
598
  Returns:
380
599
  Tuple of (success, message)
381
600
  """
382
601
  updated = False
602
+ fixed_services = []
383
603
  added_services = []
384
- project_key = str(self.project_root)
385
604
 
386
605
  # Load existing Claude config or create minimal structure
387
606
  claude_config = {}
@@ -396,9 +615,22 @@ class MCPConfigManager:
396
615
  # Ensure projects structure exists
397
616
  if "projects" not in claude_config:
398
617
  claude_config["projects"] = {}
618
+ updated = True
619
+
620
+ # Fix any corrupted MCP service installations first
621
+ fix_success, fix_message = self.fix_mcp_service_issues()
622
+ if not fix_success:
623
+ self.logger.warning(f"Some MCP services could not be fixed: {fix_message}")
624
+
625
+ # Process ALL projects in the config, not just current one
626
+ projects_to_update = list(claude_config.get("projects", {}).keys())
399
627
 
400
- if project_key not in claude_config["projects"]:
401
- claude_config["projects"][project_key] = {
628
+ # Also add the current project if not in list
629
+ current_project_key = str(self.project_root)
630
+ if current_project_key not in projects_to_update:
631
+ projects_to_update.append(current_project_key)
632
+ # Initialize new project structure
633
+ claude_config["projects"][current_project_key] = {
402
634
  "allowedTools": [],
403
635
  "history": [],
404
636
  "mcpContextUris": [],
@@ -412,29 +644,47 @@ class MCPConfigManager:
412
644
  }
413
645
  updated = True
414
646
 
415
- # Get the project's mcpServers section
416
- project_config = claude_config["projects"][project_key]
417
- if "mcpServers" not in project_config:
418
- project_config["mcpServers"] = {}
419
- updated = True
420
-
421
- # Check each service and add if missing
422
- for service_name in self.PIPX_SERVICES:
423
- if service_name not in project_config["mcpServers"]:
424
- # Try to detect and configure the service
425
- service_path = self.detect_service_path(service_name)
426
- if service_path:
427
- config = self.generate_service_config(service_name)
428
- if config:
429
- project_config["mcpServers"][service_name] = config
430
- added_services.append(service_name)
431
- updated = True
432
- self.logger.debug(
433
- f"Added MCP service to config: {service_name}"
434
- )
647
+ # Update each project's MCP configurations
648
+ for project_key in projects_to_update:
649
+ project_config = claude_config["projects"][project_key]
650
+
651
+ # Ensure mcpServers section exists
652
+ if "mcpServers" not in project_config:
653
+ project_config["mcpServers"] = {}
654
+ updated = True
655
+
656
+ # Check and fix each service configuration
657
+ for service_name in self.PIPX_SERVICES:
658
+ # Get the correct static configuration with project-specific paths
659
+ correct_config = self.get_static_service_config(service_name, project_key)
660
+
661
+ if not correct_config:
662
+ self.logger.warning(f"No static config available for {service_name}")
663
+ continue
664
+
665
+ # Check if service exists and has correct configuration
666
+ existing_config = project_config["mcpServers"].get(service_name)
667
+
668
+ # Determine if we need to update
669
+ needs_update = False
670
+ if not existing_config:
671
+ # Service is missing
672
+ needs_update = True
673
+ added_services.append(f"{service_name} in {Path(project_key).name}")
435
674
  else:
675
+ # Service exists, check if configuration is correct
676
+ # Compare command and args (the most critical parts)
677
+ if (existing_config.get("command") != correct_config.get("command") or
678
+ existing_config.get("args") != correct_config.get("args")):
679
+ needs_update = True
680
+ fixed_services.append(f"{service_name} in {Path(project_key).name}")
681
+
682
+ # Update configuration if needed
683
+ if needs_update:
684
+ project_config["mcpServers"][service_name] = correct_config
685
+ updated = True
436
686
  self.logger.debug(
437
- f"MCP service {service_name} not found for auto-configuration"
687
+ f"Updated MCP service config for {service_name} in project {Path(project_key).name}"
438
688
  )
439
689
 
440
690
  # Write updated config if changes were made
@@ -448,7 +698,6 @@ class MCPConfigManager:
448
698
  f".backup.{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.json"
449
699
  )
450
700
  import shutil
451
-
452
701
  shutil.copy2(self.claude_config_path, backup_path)
453
702
  self.logger.debug(f"Created backup: {backup_path}")
454
703
 
@@ -456,18 +705,20 @@ class MCPConfigManager:
456
705
  with open(self.claude_config_path, "w") as f:
457
706
  json.dump(claude_config, f, indent=2)
458
707
 
708
+ messages = []
459
709
  if added_services:
460
- message = (
461
- f"Auto-configured MCP services: {', '.join(added_services)}"
462
- )
463
- # Don't log here - let the caller handle logging to avoid duplicates
464
- return True, message
465
- return True, "All MCP services already configured"
710
+ messages.append(f"Added MCP services: {', '.join(added_services[:3])}")
711
+ if fixed_services:
712
+ messages.append(f"Fixed MCP services: {', '.join(fixed_services[:3])}")
713
+
714
+ if messages:
715
+ return True, "; ".join(messages)
716
+ return True, "All MCP services already configured correctly"
466
717
  except Exception as e:
467
718
  self.logger.error(f"Failed to write Claude config: {e}")
468
719
  return False, f"Failed to write configuration: {e}"
469
720
 
470
- return True, "All MCP services already configured"
721
+ return True, "All MCP services already configured correctly"
471
722
 
472
723
  def update_mcp_config(self, force_pipx: bool = True) -> Tuple[bool, str]:
473
724
  """
@@ -695,6 +946,250 @@ class MCPConfigManager:
695
946
 
696
947
  return False, "none"
697
948
 
949
+ def _check_and_fix_mcp_ticketer_dependencies(self) -> bool:
950
+ """Check and fix mcp-ticketer missing gql dependency.
951
+
952
+ Note: This is a workaround for mcp-ticketer <= 0.1.8 which is missing
953
+ the gql dependency in its package metadata. Future versions (> 0.1.8)
954
+ should include 'gql[httpx]>=3.0.0' as a dependency, making this fix
955
+ unnecessary. We keep this for backward compatibility with older versions.
956
+ """
957
+ try:
958
+ # Test if gql is available in mcp-ticketer's environment
959
+ test_result = subprocess.run(
960
+ ["pipx", "run", "--spec", "mcp-ticketer", "python", "-c", "import gql"],
961
+ capture_output=True,
962
+ text=True,
963
+ timeout=5,
964
+ check=False,
965
+ )
966
+
967
+ # If import fails, inject the dependency
968
+ if test_result.returncode != 0:
969
+ self.logger.info("🔧 mcp-ticketer missing gql dependency, fixing...")
970
+
971
+ inject_result = subprocess.run(
972
+ ["pipx", "inject", "mcp-ticketer", "gql"],
973
+ capture_output=True,
974
+ text=True,
975
+ timeout=30,
976
+ check=False,
977
+ )
978
+
979
+ if inject_result.returncode == 0:
980
+ self.logger.info("✅ Successfully injected gql dependency into mcp-ticketer")
981
+ return True
982
+ else:
983
+ self.logger.warning(f"Failed to inject gql: {inject_result.stderr}")
984
+ return False
985
+
986
+ return False
987
+
988
+ except Exception as e:
989
+ self.logger.debug(f"Could not check/fix mcp-ticketer dependencies: {e}")
990
+ return False
991
+
992
+ def fix_mcp_service_issues(self) -> Tuple[bool, str]:
993
+ """
994
+ Detect and fix corrupted MCP service installations.
995
+
996
+ This method:
997
+ 1. Tests each MCP service for import/execution errors
998
+ 2. Automatically reinstalls corrupted services
999
+ 3. Fixes missing dependencies (like mcp-ticketer's gql)
1000
+ 4. Validates fixes worked
1001
+
1002
+ Returns:
1003
+ Tuple of (success, message)
1004
+ """
1005
+ self.logger.info("🔍 Checking MCP services for issues...")
1006
+
1007
+ services_to_fix = []
1008
+ fixed_services = []
1009
+ failed_services = []
1010
+
1011
+ # Check each service for issues
1012
+ for service_name in self.PIPX_SERVICES:
1013
+ issue_type = self._detect_service_issue(service_name)
1014
+ if issue_type:
1015
+ services_to_fix.append((service_name, issue_type))
1016
+ self.logger.debug(f"Found issue with {service_name}: {issue_type}")
1017
+
1018
+ if not services_to_fix:
1019
+ return True, "All MCP services are functioning correctly"
1020
+
1021
+ # Fix each problematic service
1022
+ for service_name, issue_type in services_to_fix:
1023
+ self.logger.info(f"🔧 Fixing {service_name}: {issue_type}")
1024
+
1025
+ if issue_type == "not_installed":
1026
+ # Install the service
1027
+ success, method = self._install_service_with_fallback(service_name)
1028
+ if success:
1029
+ fixed_services.append(f"{service_name} (installed via {method})")
1030
+ else:
1031
+ failed_services.append(f"{service_name} (installation failed)")
1032
+
1033
+ elif issue_type == "import_error":
1034
+ # Reinstall to fix corrupted installation
1035
+ self.logger.info(f" Reinstalling {service_name} to fix import errors...")
1036
+ success = self._reinstall_service(service_name)
1037
+ if success:
1038
+ # Special handling for mcp-ticketer - inject missing gql dependency
1039
+ if service_name == "mcp-ticketer":
1040
+ self._check_and_fix_mcp_ticketer_dependencies()
1041
+ fixed_services.append(f"{service_name} (reinstalled)")
1042
+ else:
1043
+ failed_services.append(f"{service_name} (reinstall failed)")
1044
+
1045
+ elif issue_type == "missing_dependency":
1046
+ # Fix missing dependencies
1047
+ if service_name == "mcp-ticketer":
1048
+ if self._check_and_fix_mcp_ticketer_dependencies():
1049
+ fixed_services.append(f"{service_name} (dependency fixed)")
1050
+ else:
1051
+ failed_services.append(f"{service_name} (dependency fix failed)")
1052
+ else:
1053
+ failed_services.append(f"{service_name} (unknown dependency issue)")
1054
+
1055
+ elif issue_type == "path_issue":
1056
+ # Path issues are handled by config updates
1057
+ self.logger.info(f" Path issue for {service_name} will be fixed by config update")
1058
+ fixed_services.append(f"{service_name} (config updated)")
1059
+
1060
+ # Build result message
1061
+ messages = []
1062
+ if fixed_services:
1063
+ messages.append(f"✅ Fixed: {', '.join(fixed_services)}")
1064
+ if failed_services:
1065
+ messages.append(f"❌ Failed: {', '.join(failed_services)}")
1066
+
1067
+ # Return success if at least some services were fixed
1068
+ success = len(fixed_services) > 0 or len(failed_services) == 0
1069
+ message = " | ".join(messages) if messages else "No services needed fixing"
1070
+
1071
+ # Provide manual fix instructions if auto-fix failed
1072
+ if failed_services:
1073
+ message += "\n\n💡 Manual fix instructions:"
1074
+ for failed in failed_services:
1075
+ service = failed.split(" ")[0]
1076
+ message += f"\n • {service}: pipx uninstall {service} && pipx install {service}"
1077
+
1078
+ return success, message
1079
+
1080
+ def _detect_service_issue(self, service_name: str) -> Optional[str]:
1081
+ """
1082
+ Detect what type of issue a service has.
1083
+
1084
+ Returns:
1085
+ Issue type: 'not_installed', 'import_error', 'missing_dependency', 'path_issue', or None
1086
+ """
1087
+ import shutil
1088
+
1089
+ # First check if pipx is available
1090
+ if not shutil.which("pipx"):
1091
+ return "not_installed" # Can't use pipx services without pipx
1092
+
1093
+ # Try to run the service with --help to detect issues
1094
+ try:
1095
+ # Test with pipx run
1096
+ result = subprocess.run(
1097
+ ["pipx", "run", service_name, "--help"],
1098
+ capture_output=True,
1099
+ text=True,
1100
+ timeout=10,
1101
+ check=False
1102
+ )
1103
+
1104
+ # Check for specific error patterns
1105
+ stderr_lower = result.stderr.lower()
1106
+ stdout_lower = result.stdout.lower()
1107
+ combined_output = stderr_lower + stdout_lower
1108
+
1109
+ # Not installed
1110
+ if "no apps associated" in combined_output or "not found" in combined_output:
1111
+ return "not_installed"
1112
+
1113
+ # Import errors (like mcp-ticketer's corrupted state)
1114
+ if "modulenotfounderror" in combined_output or "importerror" in combined_output:
1115
+ # Check if it's specifically the gql dependency for mcp-ticketer
1116
+ if service_name == "mcp-ticketer" and "gql" in combined_output:
1117
+ return "missing_dependency"
1118
+ return "import_error"
1119
+
1120
+ # Path issues
1121
+ if "no such file or directory" in combined_output:
1122
+ return "path_issue"
1123
+
1124
+ # If help text appears, service is working
1125
+ if "usage:" in combined_output or "help" in combined_output or result.returncode in [0, 1]:
1126
+ return None # Service is working
1127
+
1128
+ # Unknown issue
1129
+ if result.returncode not in [0, 1]:
1130
+ return "unknown_error"
1131
+
1132
+ except subprocess.TimeoutExpired:
1133
+ # Timeout might mean service is actually working but waiting for input
1134
+ return None
1135
+ except Exception as e:
1136
+ self.logger.debug(f"Error detecting issue for {service_name}: {e}")
1137
+ return "unknown_error"
1138
+
1139
+ return None
1140
+
1141
+ def _reinstall_service(self, service_name: str) -> bool:
1142
+ """
1143
+ Reinstall a corrupted MCP service.
1144
+
1145
+ Args:
1146
+ service_name: Name of the service to reinstall
1147
+
1148
+ Returns:
1149
+ True if successful, False otherwise
1150
+ """
1151
+ try:
1152
+ self.logger.debug(f"Uninstalling {service_name}...")
1153
+
1154
+ # First uninstall the corrupted version
1155
+ uninstall_result = subprocess.run(
1156
+ ["pipx", "uninstall", service_name],
1157
+ capture_output=True,
1158
+ text=True,
1159
+ timeout=30,
1160
+ check=False
1161
+ )
1162
+
1163
+ # Don't check return code - uninstall might fail if partially corrupted
1164
+ self.logger.debug(f"Uninstall result: {uninstall_result.returncode}")
1165
+
1166
+ # Now reinstall
1167
+ self.logger.debug(f"Installing fresh {service_name}...")
1168
+ install_result = subprocess.run(
1169
+ ["pipx", "install", service_name],
1170
+ capture_output=True,
1171
+ text=True,
1172
+ timeout=120,
1173
+ check=False
1174
+ )
1175
+
1176
+ if install_result.returncode == 0:
1177
+ # Verify the reinstall worked
1178
+ issue = self._detect_service_issue(service_name)
1179
+ if issue is None:
1180
+ self.logger.info(f"✅ Successfully reinstalled {service_name}")
1181
+ return True
1182
+ else:
1183
+ self.logger.warning(f"Reinstalled {service_name} but still has issue: {issue}")
1184
+ return False
1185
+ else:
1186
+ self.logger.error(f"Failed to reinstall {service_name}: {install_result.stderr}")
1187
+ return False
1188
+
1189
+ except Exception as e:
1190
+ self.logger.error(f"Error reinstalling {service_name}: {e}")
1191
+ return False
1192
+
698
1193
  def _verify_service_installed(self, service_name: str, method: str) -> bool:
699
1194
  """
700
1195
  Verify that a service was successfully installed and is functional.
@@ -711,6 +1206,9 @@ class MCPConfigManager:
711
1206
  # Give the installation a moment to settle
712
1207
  time.sleep(1)
713
1208
 
1209
+ # Note: mcp-ticketer dependency fix is now handled once in ensure_mcp_services_configured()
1210
+ # to avoid running the same pipx inject command multiple times
1211
+
714
1212
  # Check if we can find the service
715
1213
  service_path = self.detect_service_path(service_name)
716
1214
  if not service_path:
@@ -767,3 +1265,26 @@ class MCPConfigManager:
767
1265
  self.logger.debug(f"Verification error for {service_name}: {e}")
768
1266
 
769
1267
  return False
1268
+
1269
+ def _get_fallback_config(self, service_name: str, project_path: str) -> Optional[Dict]:
1270
+ """
1271
+ Get a fallback configuration for a service if the primary config fails.
1272
+
1273
+ Args:
1274
+ service_name: Name of the MCP service
1275
+ project_path: Project path to use
1276
+
1277
+ Returns:
1278
+ Fallback configuration or None
1279
+ """
1280
+ # Special fallback for mcp-vector-search using pipx run
1281
+ if service_name == "mcp-vector-search":
1282
+ return {
1283
+ "type": "stdio",
1284
+ "command": "pipx",
1285
+ "args": ["run", "--spec", "mcp-vector-search", "python", "-m", "mcp_vector_search.mcp.server", project_path],
1286
+ "env": {}
1287
+ }
1288
+
1289
+ # For other services, try pipx run
1290
+ return None