claude-mpm 4.4.11__py3-none-any.whl → 4.5.0__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.
@@ -43,30 +43,31 @@ class MCPConfigManager:
43
43
 
44
44
  # Static known-good MCP service configurations
45
45
  # These are the correct, tested configurations that work reliably
46
+ # Note: Commands will be resolved to full paths dynamically in get_static_service_config()
46
47
  STATIC_MCP_CONFIGS = {
47
48
  "kuzu-memory": {
48
49
  "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
50
+ "command": "kuzu-memory", # Will be resolved to full path
51
+ "args": ["mcp", "serve"], # v1.1.0+ uses 'mcp serve' command
51
52
  },
52
53
  "mcp-ticketer": {
53
54
  "type": "stdio",
54
- "command": "mcp-ticketer", # Use direct binary to preserve injected dependencies
55
- "args": ["mcp"]
55
+ "command": "mcp-ticketer", # Will be resolved to full path
56
+ "args": ["mcp"],
56
57
  },
57
58
  "mcp-browser": {
58
59
  "type": "stdio",
59
- "command": "mcp-browser", # Use direct binary
60
+ "command": "mcp-browser", # Will be resolved to full path
60
61
  "args": ["mcp"],
61
- "env": {"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")}
62
+ "env": {"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")},
62
63
  },
63
64
  "mcp-vector-search": {
64
65
  "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"),
66
+ # Special handling: needs Python interpreter from pipx venv
67
+ "command": "python", # Will be resolved to pipx venv Python
67
68
  "args": ["-m", "mcp_vector_search.mcp.server", "{project_root}"],
68
- "env": {}
69
- }
69
+ "env": {},
70
+ },
70
71
  }
71
72
 
72
73
  def __init__(self):
@@ -264,7 +265,11 @@ class MCPConfigManager:
264
265
  test_args = config["args"].copy()
265
266
  # Replace project root placeholder for testing
266
267
  test_args = [
267
- arg.replace("{project_root}", str(self.project_root)) if "{project_root}" in arg else arg
268
+ (
269
+ arg.replace("{project_root}", str(self.project_root))
270
+ if "{project_root}" in arg
271
+ else arg
272
+ )
268
273
  for arg in test_args
269
274
  ]
270
275
 
@@ -286,13 +291,16 @@ class MCPConfigManager:
286
291
  text=True,
287
292
  timeout=5,
288
293
  check=False,
289
- env=config.get("env", {})
294
+ env=config.get("env", {}),
290
295
  )
291
296
 
292
297
  # Check if command executed (exit code 0 or 1 for help)
293
298
  if result.returncode in [0, 1]:
294
299
  # Additional check for import errors in stderr
295
- if "ModuleNotFoundError" in result.stderr or "ImportError" in result.stderr:
300
+ if (
301
+ "ModuleNotFoundError" in result.stderr
302
+ or "ImportError" in result.stderr
303
+ ):
296
304
  self.logger.debug(f"Service {service_name} has import errors")
297
305
  return False
298
306
  return True
@@ -305,7 +313,9 @@ class MCPConfigManager:
305
313
 
306
314
  return False
307
315
 
308
- def get_static_service_config(self, service_name: str, project_path: Optional[str] = None) -> Optional[Dict]:
316
+ def get_static_service_config(
317
+ self, service_name: str, project_path: Optional[str] = None
318
+ ) -> Optional[Dict]:
309
319
  """
310
320
  Get the static, known-good configuration for an MCP service.
311
321
 
@@ -326,73 +336,118 @@ class MCPConfigManager:
326
336
  if service_name in ["kuzu-memory", "mcp-ticketer", "mcp-browser"]:
327
337
  # Try to find the full path of the binary
328
338
  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
339
+
340
+ # First check pipx location
341
+ pipx_bin = (
342
+ Path.home()
343
+ / ".local"
344
+ / "pipx"
345
+ / "venvs"
346
+ / service_name
347
+ / "bin"
348
+ / binary_name
349
+ )
350
+ if pipx_bin.exists():
351
+ binary_path = str(pipx_bin)
352
+ else:
353
+ # Try which command
354
+ binary_path = shutil.which(binary_name)
355
+
356
+ if not binary_path:
357
+ # Try common installation locations
358
+ possible_paths = [
359
+ Path.home() / ".local" / "bin" / binary_name,
360
+ Path("/opt/homebrew/bin") / binary_name,
361
+ Path("/usr/local/bin") / binary_name,
362
+ ]
363
+ for path in possible_paths:
364
+ if path.exists():
365
+ binary_path = str(path)
366
+ break
342
367
 
343
368
  if binary_path:
344
369
  config["command"] = binary_path
345
- # If still not found, keep the binary name and hope it's in PATH
370
+ else:
371
+ # Fall back to pipx run method if binary not found
372
+ self.logger.debug(
373
+ f"Could not find {binary_name}, using pipx run fallback"
374
+ )
375
+ config["command"] = "pipx"
376
+ config["args"] = ["run", service_name] + config["args"]
346
377
 
347
- # Resolve pipx command to full path if needed
348
- elif config.get("command") == "pipx":
378
+ # Resolve pipx command to full path if needed (for fallback configs)
379
+ if config.get("command") == "pipx":
349
380
  pipx_path = shutil.which("pipx")
350
381
  if not pipx_path:
351
382
  # 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
383
+ possible_pipx_paths = [
384
+ Path.home() / ".local" / "bin" / "pipx",
385
+ Path("/opt/homebrew/bin/pipx"),
386
+ Path("/usr/local/bin/pipx"),
387
+ ]
388
+ for path in possible_pipx_paths:
389
+ if path.exists():
390
+ pipx_path = str(path)
359
391
  break
360
392
  if pipx_path:
361
393
  config["command"] = pipx_path
362
- else:
363
- # Keep as "pipx" and hope it's in PATH when executed
364
- config["command"] = "pipx"
365
394
 
366
395
  # Handle user-specific paths for mcp-vector-search
367
396
  if service_name == "mcp-vector-search":
368
397
  # Get the correct pipx venv path for the current user
369
398
  home = Path.home()
370
- python_path = home / ".local" / "pipx" / "venvs" / "mcp-vector-search" / "bin" / "python"
399
+ python_path = (
400
+ home
401
+ / ".local"
402
+ / "pipx"
403
+ / "venvs"
404
+ / "mcp-vector-search"
405
+ / "bin"
406
+ / "python"
407
+ )
371
408
 
372
- # Check if the Python interpreter exists, if not fallback to pipx run
409
+ # Check if the Python interpreter exists
373
410
  if python_path.exists():
374
411
  config["command"] = str(python_path)
375
412
  else:
376
413
  # Fallback to pipx run method
377
- import shutil
378
414
  pipx_path = shutil.which("pipx")
379
415
  if not pipx_path:
380
416
  # 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
417
+ possible_pipx_paths = [
418
+ Path.home() / ".local" / "bin" / "pipx",
419
+ Path("/opt/homebrew/bin/pipx"),
420
+ Path("/usr/local/bin/pipx"),
421
+ ]
422
+ for path in possible_pipx_paths:
423
+ if path.exists():
424
+ pipx_path = str(path)
388
425
  break
389
- config["command"] = pipx_path if pipx_path else "pipx"
390
- config["args"] = ["run", "--spec", "mcp-vector-search", "python"] + config["args"]
426
+
427
+ if pipx_path:
428
+ config["command"] = pipx_path
429
+ else:
430
+ config["command"] = "pipx" # Hope it's in PATH
431
+
432
+ # Use pipx run with the spec argument
433
+ config["args"] = [
434
+ "run",
435
+ "--spec",
436
+ "mcp-vector-search",
437
+ "python",
438
+ "-m",
439
+ "mcp_vector_search.mcp.server",
440
+ "{project_root}",
441
+ ]
391
442
 
392
443
  # Use provided project path or current project
393
444
  project_root = project_path if project_path else str(self.project_root)
394
445
  config["args"] = [
395
- arg.replace("{project_root}", project_root) if "{project_root}" in arg else arg
446
+ (
447
+ arg.replace("{project_root}", project_root)
448
+ if "{project_root}" in arg
449
+ else arg
450
+ )
396
451
  for arg in config["args"]
397
452
  ]
398
453
 
@@ -416,10 +471,13 @@ class MCPConfigManager:
416
471
  if static_config:
417
472
  # Validate that the static config actually works
418
473
  if self.test_service_command(service_name, static_config):
419
- self.logger.debug(f"Static config for {service_name} validated successfully")
474
+ self.logger.debug(
475
+ f"Static config for {service_name} validated successfully"
476
+ )
420
477
  return static_config
421
- else:
422
- self.logger.warning(f"Static config for {service_name} failed validation, trying fallback")
478
+ self.logger.warning(
479
+ f"Static config for {service_name} failed validation, trying fallback"
480
+ )
423
481
 
424
482
  # Fall back to detection-based configuration for unknown services
425
483
  import shutil
@@ -547,7 +605,9 @@ class MCPConfigManager:
547
605
 
548
606
  # Standard version detection - look for "mcp serve" command (v1.1.0+)
549
607
  # This is the correct format for kuzu-memory v1.1.0 and later
550
- if "mcp serve" in help_output or ("mcp" in help_output and "serve" in help_output):
608
+ if "mcp serve" in help_output or (
609
+ "mcp" in help_output and "serve" in help_output
610
+ ):
551
611
  # Standard v1.1.0+ version with mcp serve command
552
612
  kuzu_args = ["mcp", "serve"]
553
613
  # Legacy version detection - only "serve" without "mcp"
@@ -656,10 +716,14 @@ class MCPConfigManager:
656
716
  # Check and fix each service configuration
657
717
  for service_name in self.PIPX_SERVICES:
658
718
  # Get the correct static configuration with project-specific paths
659
- correct_config = self.get_static_service_config(service_name, project_key)
719
+ correct_config = self.get_static_service_config(
720
+ service_name, project_key
721
+ )
660
722
 
661
723
  if not correct_config:
662
- self.logger.warning(f"No static config available for {service_name}")
724
+ self.logger.warning(
725
+ f"No static config available for {service_name}"
726
+ )
663
727
  continue
664
728
 
665
729
  # Check if service exists and has correct configuration
@@ -671,13 +735,15 @@ class MCPConfigManager:
671
735
  # Service is missing
672
736
  needs_update = True
673
737
  added_services.append(f"{service_name} in {Path(project_key).name}")
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}")
738
+ # Service exists, check if configuration is correct
739
+ # Compare command and args (the most critical parts)
740
+ elif existing_config.get("command") != correct_config.get(
741
+ "command"
742
+ ) or existing_config.get("args") != correct_config.get("args"):
743
+ needs_update = True
744
+ fixed_services.append(
745
+ f"{service_name} in {Path(project_key).name}"
746
+ )
681
747
 
682
748
  # Update configuration if needed
683
749
  if needs_update:
@@ -698,6 +764,7 @@ class MCPConfigManager:
698
764
  f".backup.{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.json"
699
765
  )
700
766
  import shutil
767
+
701
768
  shutil.copy2(self.claude_config_path, backup_path)
702
769
  self.logger.debug(f"Created backup: {backup_path}")
703
770
 
@@ -707,9 +774,13 @@ class MCPConfigManager:
707
774
 
708
775
  messages = []
709
776
  if added_services:
710
- messages.append(f"Added MCP services: {', '.join(added_services[:3])}")
777
+ messages.append(
778
+ f"Added MCP services: {', '.join(added_services[:3])}"
779
+ )
711
780
  if fixed_services:
712
- messages.append(f"Fixed MCP services: {', '.join(fixed_services[:3])}")
781
+ messages.append(
782
+ f"Fixed MCP services: {', '.join(fixed_services[:3])}"
783
+ )
713
784
 
714
785
  if messages:
715
786
  return True, "; ".join(messages)
@@ -977,11 +1048,12 @@ class MCPConfigManager:
977
1048
  )
978
1049
 
979
1050
  if inject_result.returncode == 0:
980
- self.logger.info("✅ Successfully injected gql dependency into mcp-ticketer")
1051
+ self.logger.info(
1052
+ "✅ Successfully injected gql dependency into mcp-ticketer"
1053
+ )
981
1054
  return True
982
- else:
983
- self.logger.warning(f"Failed to inject gql: {inject_result.stderr}")
984
- return False
1055
+ self.logger.warning(f"Failed to inject gql: {inject_result.stderr}")
1056
+ return False
985
1057
 
986
1058
  return False
987
1059
 
@@ -1032,7 +1104,9 @@ class MCPConfigManager:
1032
1104
 
1033
1105
  elif issue_type == "import_error":
1034
1106
  # Reinstall to fix corrupted installation
1035
- self.logger.info(f" Reinstalling {service_name} to fix import errors...")
1107
+ self.logger.info(
1108
+ f" Reinstalling {service_name} to fix import errors..."
1109
+ )
1036
1110
  success = self._reinstall_service(service_name)
1037
1111
  if success:
1038
1112
  # Special handling for mcp-ticketer - inject missing gql dependency
@@ -1048,13 +1122,17 @@ class MCPConfigManager:
1048
1122
  if self._check_and_fix_mcp_ticketer_dependencies():
1049
1123
  fixed_services.append(f"{service_name} (dependency fixed)")
1050
1124
  else:
1051
- failed_services.append(f"{service_name} (dependency fix failed)")
1125
+ failed_services.append(
1126
+ f"{service_name} (dependency fix failed)"
1127
+ )
1052
1128
  else:
1053
1129
  failed_services.append(f"{service_name} (unknown dependency issue)")
1054
1130
 
1055
1131
  elif issue_type == "path_issue":
1056
1132
  # Path issues are handled by config updates
1057
- self.logger.info(f" Path issue for {service_name} will be fixed by config update")
1133
+ self.logger.info(
1134
+ f" Path issue for {service_name} will be fixed by config update"
1135
+ )
1058
1136
  fixed_services.append(f"{service_name} (config updated)")
1059
1137
 
1060
1138
  # Build result message
@@ -1098,7 +1176,7 @@ class MCPConfigManager:
1098
1176
  capture_output=True,
1099
1177
  text=True,
1100
1178
  timeout=10,
1101
- check=False
1179
+ check=False,
1102
1180
  )
1103
1181
 
1104
1182
  # Check for specific error patterns
@@ -1107,11 +1185,17 @@ class MCPConfigManager:
1107
1185
  combined_output = stderr_lower + stdout_lower
1108
1186
 
1109
1187
  # Not installed
1110
- if "no apps associated" in combined_output or "not found" in combined_output:
1188
+ if (
1189
+ "no apps associated" in combined_output
1190
+ or "not found" in combined_output
1191
+ ):
1111
1192
  return "not_installed"
1112
1193
 
1113
1194
  # Import errors (like mcp-ticketer's corrupted state)
1114
- if "modulenotfounderror" in combined_output or "importerror" in combined_output:
1195
+ if (
1196
+ "modulenotfounderror" in combined_output
1197
+ or "importerror" in combined_output
1198
+ ):
1115
1199
  # Check if it's specifically the gql dependency for mcp-ticketer
1116
1200
  if service_name == "mcp-ticketer" and "gql" in combined_output:
1117
1201
  return "missing_dependency"
@@ -1122,7 +1206,11 @@ class MCPConfigManager:
1122
1206
  return "path_issue"
1123
1207
 
1124
1208
  # If help text appears, service is working
1125
- if "usage:" in combined_output or "help" in combined_output or result.returncode in [0, 1]:
1209
+ if (
1210
+ "usage:" in combined_output
1211
+ or "help" in combined_output
1212
+ or result.returncode in [0, 1]
1213
+ ):
1126
1214
  return None # Service is working
1127
1215
 
1128
1216
  # Unknown issue
@@ -1157,7 +1245,7 @@ class MCPConfigManager:
1157
1245
  capture_output=True,
1158
1246
  text=True,
1159
1247
  timeout=30,
1160
- check=False
1248
+ check=False,
1161
1249
  )
1162
1250
 
1163
1251
  # Don't check return code - uninstall might fail if partially corrupted
@@ -1170,7 +1258,7 @@ class MCPConfigManager:
1170
1258
  capture_output=True,
1171
1259
  text=True,
1172
1260
  timeout=120,
1173
- check=False
1261
+ check=False,
1174
1262
  )
1175
1263
 
1176
1264
  if install_result.returncode == 0:
@@ -1179,12 +1267,14 @@ class MCPConfigManager:
1179
1267
  if issue is None:
1180
1268
  self.logger.info(f"✅ Successfully reinstalled {service_name}")
1181
1269
  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}")
1270
+ self.logger.warning(
1271
+ f"Reinstalled {service_name} but still has issue: {issue}"
1272
+ )
1187
1273
  return False
1274
+ self.logger.error(
1275
+ f"Failed to reinstall {service_name}: {install_result.stderr}"
1276
+ )
1277
+ return False
1188
1278
 
1189
1279
  except Exception as e:
1190
1280
  self.logger.error(f"Error reinstalling {service_name}: {e}")
@@ -1266,7 +1356,9 @@ class MCPConfigManager:
1266
1356
 
1267
1357
  return False
1268
1358
 
1269
- def _get_fallback_config(self, service_name: str, project_path: str) -> Optional[Dict]:
1359
+ def _get_fallback_config(
1360
+ self, service_name: str, project_path: str
1361
+ ) -> Optional[Dict]:
1270
1362
  """
1271
1363
  Get a fallback configuration for a service if the primary config fails.
1272
1364
 
@@ -1282,8 +1374,16 @@ class MCPConfigManager:
1282
1374
  return {
1283
1375
  "type": "stdio",
1284
1376
  "command": "pipx",
1285
- "args": ["run", "--spec", "mcp-vector-search", "python", "-m", "mcp_vector_search.mcp.server", project_path],
1286
- "env": {}
1377
+ "args": [
1378
+ "run",
1379
+ "--spec",
1380
+ "mcp-vector-search",
1381
+ "python",
1382
+ "-m",
1383
+ "mcp_vector_search.mcp.server",
1384
+ project_path,
1385
+ ],
1386
+ "env": {},
1287
1387
  }
1288
1388
 
1289
1389
  # For other services, try pipx run