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.
- claude_mpm/VERSION +1 -1
- claude_mpm/scripts/mcp_server.py +0 -0
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +420 -27
- claude_mpm/services/mcp_config_manager.py +563 -42
- {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/METADATA +1 -1
- {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/RECORD +9 -22
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/WHEEL +0 -0
- {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.4.8.dist-info → claude_mpm-4.4.10.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
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
|
377
|
-
|
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
|
401
|
-
|
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
|
-
#
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
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}
|
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
|
-
|
461
|
-
|
462
|
-
)
|
463
|
-
|
464
|
-
|
465
|
-
|
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
|