claude-mpm 4.4.7__py3-none-any.whl → 4.4.9__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 CHANGED
@@ -1 +1 @@
1
- 4.4.7
1
+ 4.4.9
@@ -5,11 +5,14 @@ WHY: Verify that MCP services (mcp-vector-search, mcp-browser, mcp-ticketer, kuz
5
5
  are properly installed and accessible for enhanced Claude Code capabilities.
6
6
  """
7
7
 
8
+ import asyncio
8
9
  import json
9
10
  import subprocess
11
+ import time
10
12
  from pathlib import Path
11
13
  from typing import Dict, List, Optional, Tuple
12
14
 
15
+ from claude_mpm.core.logger import get_logger
13
16
  from ..models import DiagnosticResult, DiagnosticStatus
14
17
  from .base_check import BaseDiagnosticCheck
15
18
 
@@ -17,6 +20,11 @@ from .base_check import BaseDiagnosticCheck
17
20
  class MCPServicesCheck(BaseDiagnosticCheck):
18
21
  """Check MCP external services installation and health."""
19
22
 
23
+ def __init__(self, verbose: bool = False):
24
+ """Initialize the MCP services check."""
25
+ super().__init__(verbose)
26
+ self.logger = get_logger(self.__class__.__name__)
27
+
20
28
  # Define MCP services to check
21
29
  MCP_SERVICES = {
22
30
  "mcp-vector-search": {
@@ -29,6 +37,8 @@ class MCPServicesCheck(BaseDiagnosticCheck):
29
37
  "check_health": True,
30
38
  "health_command": ["mcp-vector-search", "--version"],
31
39
  "pipx_run_command": ["pipx", "run", "mcp-vector-search", "--version"],
40
+ "mcp_command": ["python", "-m", "mcp_vector_search.mcp.server"], # Command to run as MCP server
41
+ "pipx_mcp_command": ["pipx", "run", "--spec", "mcp-vector-search", "python", "-m", "mcp_vector_search.mcp.server"],
32
42
  },
33
43
  "mcp-browser": {
34
44
  "package": "mcp-browser",
@@ -37,6 +47,7 @@ class MCPServicesCheck(BaseDiagnosticCheck):
37
47
  "check_health": True,
38
48
  "health_command": ["mcp-browser", "--version"],
39
49
  "pipx_run_command": ["pipx", "run", "mcp-browser", "--version"],
50
+ "mcp_command": ["mcp-browser", "mcp"], # Command to run as MCP server
40
51
  },
41
52
  "mcp-ticketer": {
42
53
  "package": "mcp-ticketer",
@@ -45,6 +56,7 @@ class MCPServicesCheck(BaseDiagnosticCheck):
45
56
  "check_health": True,
46
57
  "health_command": ["mcp-ticketer", "--version"],
47
58
  "pipx_run_command": ["pipx", "run", "mcp-ticketer", "--version"],
59
+ "mcp_command": ["mcp-ticketer", "mcp"], # Command to run as MCP server
48
60
  },
49
61
  "kuzu-memory": {
50
62
  "package": "kuzu-memory",
@@ -53,6 +65,7 @@ class MCPServicesCheck(BaseDiagnosticCheck):
53
65
  "check_health": True, # v1.1.0+ has version command
54
66
  "health_command": ["kuzu-memory", "--version"],
55
67
  "pipx_run_command": ["pipx", "run", "kuzu-memory", "--version"],
68
+ "mcp_command": ["kuzu-memory", "mcp", "serve"], # v1.1.0+ uses 'mcp serve' args
56
69
  },
57
70
  }
58
71
 
@@ -71,15 +84,29 @@ class MCPServicesCheck(BaseDiagnosticCheck):
71
84
  sub_results = []
72
85
  services_status = {}
73
86
 
87
+ # Check for kuzu-memory configuration issues and offer auto-fix
88
+ kuzu_config_result = self._check_and_fix_kuzu_memory_config()
89
+ if kuzu_config_result:
90
+ sub_results.append(kuzu_config_result)
91
+
74
92
  # Check each MCP service
75
93
  for service_name, service_config in self.MCP_SERVICES.items():
76
94
  service_result = self._check_service(service_name, service_config)
77
95
  sub_results.append(service_result)
96
+
97
+ # Extract connection test info if available
98
+ connection_test = service_result.details.get("connection_test", {})
99
+
78
100
  services_status[service_name] = {
79
101
  "status": service_result.status.value,
80
102
  "installed": service_result.details.get("installed", False),
81
103
  "accessible": service_result.details.get("accessible", False),
82
104
  "version": service_result.details.get("version"),
105
+ "connection_tested": bool(connection_test),
106
+ "connected": connection_test.get("connected", False),
107
+ "response_time_ms": connection_test.get("response_time_ms"),
108
+ "tools_discovered": connection_test.get("tools_discovered", 0),
109
+ "connection_error": connection_test.get("error"),
83
110
  }
84
111
 
85
112
  # Check MCP gateway configuration for services
@@ -91,12 +118,18 @@ class MCPServicesCheck(BaseDiagnosticCheck):
91
118
  accessible_count = sum(
92
119
  1 for s in services_status.values() if s["accessible"]
93
120
  )
121
+ connected_count = sum(1 for s in services_status.values() if s["connected"])
94
122
  total_services = len(self.MCP_SERVICES)
95
123
 
124
+ # Calculate total tools discovered
125
+ total_tools = sum(s.get("tools_discovered", 0) for s in services_status.values())
126
+
96
127
  details["services"] = services_status
97
128
  details["installed_count"] = installed_count
98
129
  details["accessible_count"] = accessible_count
130
+ details["connected_count"] = connected_count
99
131
  details["total_services"] = total_services
132
+ details["total_tools_discovered"] = total_tools
100
133
  details["gateway_configured"] = gateway_result.status == DiagnosticStatus.OK
101
134
 
102
135
  # Determine overall status
@@ -109,6 +142,12 @@ class MCPServicesCheck(BaseDiagnosticCheck):
109
142
  elif installed_count == 0:
110
143
  status = DiagnosticStatus.WARNING
111
144
  message = "No MCP services installed"
145
+ elif connected_count == total_services:
146
+ status = DiagnosticStatus.OK
147
+ message = f"All {total_services} MCP services connected ({total_tools} tools available)"
148
+ elif connected_count > 0:
149
+ status = DiagnosticStatus.WARNING
150
+ message = f"{connected_count}/{total_services} MCP services connected, {installed_count} installed"
112
151
  elif accessible_count < installed_count:
113
152
  status = DiagnosticStatus.WARNING
114
153
  message = f"{installed_count}/{total_services} services installed, {accessible_count} accessible"
@@ -116,8 +155,8 @@ class MCPServicesCheck(BaseDiagnosticCheck):
116
155
  status = DiagnosticStatus.WARNING
117
156
  message = f"{installed_count}/{total_services} MCP services installed"
118
157
  else:
119
- status = DiagnosticStatus.OK
120
- message = f"All {total_services} MCP services installed and accessible"
158
+ status = DiagnosticStatus.WARNING
159
+ message = f"All {total_services} MCP services installed but connections not tested"
121
160
 
122
161
  return DiagnosticResult(
123
162
  category=self.category,
@@ -135,6 +174,185 @@ class MCPServicesCheck(BaseDiagnosticCheck):
135
174
  details={"error": str(e)},
136
175
  )
137
176
 
177
+ async def _test_mcp_connection(
178
+ self, service_name: str, command: List[str]
179
+ ) -> Dict:
180
+ """Test MCP server connection by sending JSON-RPC requests."""
181
+ result = {
182
+ "connected": False,
183
+ "response_time": None,
184
+ "tools_count": 0,
185
+ "tools": [],
186
+ "error": None,
187
+ }
188
+
189
+ process = None
190
+ try:
191
+ # Start the MCP server process
192
+ start_time = time.time()
193
+ process = await asyncio.create_subprocess_exec(
194
+ *command,
195
+ stdin=asyncio.subprocess.PIPE,
196
+ stdout=asyncio.subprocess.PIPE,
197
+ stderr=asyncio.subprocess.PIPE,
198
+ )
199
+
200
+ # Give the server a moment to initialize
201
+ await asyncio.sleep(0.1)
202
+
203
+ # Prepare initialize request
204
+ init_request = {
205
+ "jsonrpc": "2.0",
206
+ "method": "initialize",
207
+ "params": {
208
+ "protocolVersion": "2024-11-05",
209
+ "capabilities": {},
210
+ "clientInfo": {
211
+ "name": "mpm-doctor",
212
+ "version": "1.0.0"
213
+ }
214
+ },
215
+ "id": 1
216
+ }
217
+
218
+ # Send initialize request
219
+ request_line = json.dumps(init_request) + "\n"
220
+ process.stdin.write(request_line.encode())
221
+ await process.stdin.drain()
222
+
223
+ # Read response with timeout
224
+ try:
225
+ response_line = await asyncio.wait_for(
226
+ process.stdout.readline(), timeout=5.0
227
+ )
228
+ response_time = time.time() - start_time
229
+
230
+ if response_line:
231
+ # Some MCP servers may output non-JSON before the actual response
232
+ # Try to find JSON in the response
233
+ response_text = response_line.decode().strip()
234
+
235
+ # Skip empty lines or non-JSON lines
236
+ while response_text and not response_text.startswith('{'):
237
+ # Try to read the next line
238
+ try:
239
+ response_line = await asyncio.wait_for(
240
+ process.stdout.readline(), timeout=1.0
241
+ )
242
+ if response_line:
243
+ response_text = response_line.decode().strip()
244
+ else:
245
+ break
246
+ except asyncio.TimeoutError:
247
+ break
248
+
249
+ if not response_text or not response_text.startswith('{'):
250
+ result["error"] = "No valid JSON response received"
251
+ return result
252
+
253
+ response = json.loads(response_text)
254
+
255
+ # Check for valid JSON-RPC response
256
+ if "result" in response and response.get("id") == 1:
257
+ result["connected"] = True
258
+ result["response_time"] = round(response_time * 1000, 2) # ms
259
+
260
+ # Send tools/list request
261
+ tools_request = {
262
+ "jsonrpc": "2.0",
263
+ "method": "tools/list",
264
+ "params": {},
265
+ "id": 2
266
+ }
267
+
268
+ request_line = json.dumps(tools_request) + "\n"
269
+ process.stdin.write(request_line.encode())
270
+ await process.stdin.drain()
271
+
272
+ # Read tools response
273
+ try:
274
+ tools_response_line = await asyncio.wait_for(
275
+ process.stdout.readline(), timeout=3.0
276
+ )
277
+
278
+ if tools_response_line:
279
+ tools_response = json.loads(tools_response_line.decode())
280
+ if "result" in tools_response:
281
+ tools = tools_response["result"].get("tools", [])
282
+ result["tools_count"] = len(tools)
283
+ # Store first 5 tool names for display
284
+ result["tools"] = [
285
+ tool.get("name", "unknown")
286
+ for tool in tools[:5]
287
+ ]
288
+ except asyncio.TimeoutError:
289
+ # Connection successful but tools query timed out
290
+ pass
291
+ except (json.JSONDecodeError, KeyError):
292
+ # Connection successful but tools response invalid
293
+ pass
294
+
295
+ elif "error" in response:
296
+ result["error"] = f"MCP error: {response['error'].get('message', 'Unknown error')}"
297
+ else:
298
+ result["error"] = "Invalid JSON-RPC response format"
299
+
300
+ except asyncio.TimeoutError:
301
+ # Try to get any error output from stderr
302
+ stderr_output = ""
303
+ if process and process.stderr:
304
+ try:
305
+ stderr_data = await asyncio.wait_for(
306
+ process.stderr.read(1000), timeout=0.5
307
+ )
308
+ if stderr_data:
309
+ stderr_output = stderr_data.decode('utf-8', errors='ignore')[:200]
310
+ except:
311
+ pass
312
+
313
+ if stderr_output:
314
+ result["error"] = f"Connection timeout (5s). Server output: {stderr_output}"
315
+ else:
316
+ result["error"] = "Connection timeout (5s)"
317
+
318
+ except json.JSONDecodeError as e:
319
+ # Try to get stderr for more context
320
+ stderr_output = ""
321
+ if process and process.stderr:
322
+ try:
323
+ stderr_data = await asyncio.wait_for(
324
+ process.stderr.read(1000), timeout=0.5
325
+ )
326
+ if stderr_data:
327
+ stderr_output = stderr_data.decode('utf-8', errors='ignore')[:200]
328
+ except:
329
+ pass
330
+
331
+ if stderr_output:
332
+ result["error"] = f"Invalid JSON response: {str(e)}. Server error: {stderr_output}"
333
+ else:
334
+ result["error"] = f"Invalid JSON response: {str(e)}"
335
+
336
+ except FileNotFoundError:
337
+ result["error"] = f"Command not found: {command[0]}"
338
+ except PermissionError:
339
+ result["error"] = f"Permission denied: {command[0]}"
340
+ except Exception as e:
341
+ result["error"] = f"Connection failed: {str(e)}"
342
+ finally:
343
+ # Clean up process
344
+ if process:
345
+ try:
346
+ process.terminate()
347
+ await asyncio.wait_for(process.wait(), timeout=2.0)
348
+ except asyncio.TimeoutError:
349
+ process.kill()
350
+ await process.wait()
351
+ except Exception:
352
+ pass
353
+
354
+ return result
355
+
138
356
  def _check_service(self, service_name: str, config: Dict) -> DiagnosticResult:
139
357
  """Check a specific MCP service."""
140
358
  details = {"service": service_name}
@@ -145,6 +363,12 @@ class MCPServicesCheck(BaseDiagnosticCheck):
145
363
  if pipx_path:
146
364
  details["pipx_path"] = pipx_path
147
365
 
366
+ # Special check for mcp-ticketer: ensure gql dependency
367
+ if service_name == "mcp-ticketer" and pipx_installed:
368
+ gql_fixed = self._ensure_mcp_ticketer_gql_dependency()
369
+ if gql_fixed:
370
+ details["gql_dependency_fixed"] = True
371
+
148
372
  # Check if accessible in PATH
149
373
  accessible, command_path = self._check_command_accessible(config["command"])
150
374
  details["accessible"] = accessible
@@ -186,6 +410,64 @@ class MCPServicesCheck(BaseDiagnosticCheck):
186
410
  details["version"] = version
187
411
  break
188
412
 
413
+ # Test MCP connection if installed (accessible or pipx) and has mcp_command
414
+ if (accessible or pipx_installed) and "mcp_command" in config:
415
+ # Determine which command to use for MCP connection test
416
+ mcp_command = None
417
+ if pipx_installed and not accessible:
418
+ # Service is installed via pipx but not in PATH
419
+ if "pipx_mcp_command" in config:
420
+ # Use special pipx MCP command if available (e.g., for mcp-vector-search)
421
+ mcp_command = config["pipx_mcp_command"]
422
+ else:
423
+ # Build pipx run command based on package
424
+ base_cmd = config["mcp_command"]
425
+ if len(base_cmd) > 0 and base_cmd[0] == config["package"]:
426
+ # Simple case where first command is the package name
427
+ mcp_command = ["pipx", "run", config["package"]] + base_cmd[1:]
428
+ else:
429
+ # Complex case - just try running the package with mcp arg
430
+ mcp_command = ["pipx", "run", config["package"], "mcp"]
431
+ elif details.get("accessible_via_pipx_run"):
432
+ # Use pipx run for the MCP command
433
+ if "pipx_mcp_command" in config:
434
+ # Use special pipx MCP command if available (e.g., for mcp-vector-search)
435
+ mcp_command = config["pipx_mcp_command"]
436
+ else:
437
+ # Build pipx run command
438
+ base_cmd = config["mcp_command"]
439
+ if service_name == "kuzu-memory":
440
+ # Special case for kuzu-memory with args
441
+ mcp_command = ["pipx", "run", base_cmd[0]] + base_cmd[1:]
442
+ else:
443
+ mcp_command = ["pipx", "run"] + base_cmd
444
+ else:
445
+ mcp_command = config["mcp_command"]
446
+
447
+ if mcp_command:
448
+ # Run async connection test
449
+ try:
450
+ loop = asyncio.new_event_loop()
451
+ asyncio.set_event_loop(loop)
452
+ connection_result = loop.run_until_complete(
453
+ self._test_mcp_connection(service_name, mcp_command)
454
+ )
455
+ loop.close()
456
+
457
+ # Add connection test results to details
458
+ details["connection_test"] = {
459
+ "connected": connection_result["connected"],
460
+ "response_time_ms": connection_result["response_time"],
461
+ "tools_discovered": connection_result["tools_count"],
462
+ "tools_sample": connection_result["tools"],
463
+ "error": connection_result["error"]
464
+ }
465
+ except Exception as e:
466
+ details["connection_test"] = {
467
+ "connected": False,
468
+ "error": f"Test failed: {str(e)}"
469
+ }
470
+
189
471
  # Determine status
190
472
  if not (pipx_installed or accessible):
191
473
  return DiagnosticResult(
@@ -200,10 +482,19 @@ class MCPServicesCheck(BaseDiagnosticCheck):
200
482
  if pipx_installed and not accessible:
201
483
  # Check if pipx run works
202
484
  if details.get("pipx_run_available"):
485
+ # Include connection test info if available
486
+ connection_info = details.get("connection_test", {})
487
+ if connection_info.get("connected"):
488
+ message = f"Installed via pipx, connection OK ({connection_info.get('tools_discovered', 0)} tools)"
489
+ elif connection_info.get("error"):
490
+ message = f"Installed via pipx, connection failed: {connection_info['error']}"
491
+ else:
492
+ message = "Installed via pipx (use 'pipx run' to execute)"
493
+
203
494
  return DiagnosticResult(
204
495
  category=f"MCP Service: {service_name}",
205
- status=DiagnosticStatus.OK,
206
- message="Installed via pipx (use 'pipx run' to execute)",
496
+ status=DiagnosticStatus.OK if connection_info.get("connected") else DiagnosticStatus.WARNING,
497
+ message=message,
207
498
  details=details,
208
499
  )
209
500
  return DiagnosticResult(
@@ -215,10 +506,26 @@ class MCPServicesCheck(BaseDiagnosticCheck):
215
506
  fix_description="Ensure pipx bin directory is in PATH",
216
507
  )
217
508
 
509
+ # Service is accessible - check connection test results
510
+ connection_info = details.get("connection_test", {})
511
+ if connection_info:
512
+ if connection_info.get("connected"):
513
+ response_time = connection_info.get("response_time_ms")
514
+ tools_count = connection_info.get("tools_discovered", 0)
515
+ message = f"Installed, accessible, connection OK ({tools_count} tools, {response_time}ms)"
516
+ status = DiagnosticStatus.OK
517
+ else:
518
+ error = connection_info.get("error", "Unknown error")
519
+ message = f"Installed but connection failed: {error}"
520
+ status = DiagnosticStatus.WARNING
521
+ else:
522
+ message = "Installed and accessible"
523
+ status = DiagnosticStatus.OK
524
+
218
525
  return DiagnosticResult(
219
526
  category=f"MCP Service: {service_name}",
220
- status=DiagnosticStatus.OK,
221
- message="Installed and accessible",
527
+ status=status,
528
+ message=message,
222
529
  details=details,
223
530
  )
224
531
 
@@ -391,6 +698,149 @@ class MCPServicesCheck(BaseDiagnosticCheck):
391
698
 
392
699
  return None
393
700
 
701
+ def _check_and_fix_kuzu_memory_config(self) -> Optional[DiagnosticResult]:
702
+ """Check for incorrect kuzu-memory configuration in .claude.json and offer auto-fix."""
703
+ claude_config_path = Path.home() / ".claude.json"
704
+
705
+ if not claude_config_path.exists():
706
+ return None
707
+
708
+ try:
709
+ with open(claude_config_path) as f:
710
+ config = json.load(f)
711
+
712
+ mcp_servers = config.get("mcpServers", {})
713
+ kuzu_config = mcp_servers.get("kuzu-memory")
714
+
715
+ if not kuzu_config:
716
+ return None
717
+
718
+ # Check if kuzu-memory has incorrect args
719
+ args = kuzu_config.get("args", [])
720
+ needs_fix = False
721
+ fix_reason = ""
722
+ # The correct args for kuzu-memory v1.1.0+ are ["mcp", "serve"]
723
+ correct_args = ["mcp", "serve"]
724
+
725
+ # Check for any configuration that is NOT the correct one
726
+ if args != correct_args:
727
+ needs_fix = True
728
+ # Identify the specific issue
729
+ if args == ["claude", "mcp-server"]:
730
+ fix_reason = "Outdated 'claude mcp-server' format (pre-v1.1.0)"
731
+ elif args == ["serve"]:
732
+ fix_reason = "Legacy 'serve' format"
733
+ elif args == ["mcp-server"]:
734
+ fix_reason = "Incorrect 'mcp-server' format"
735
+ elif args == []:
736
+ fix_reason = "Empty args list"
737
+ else:
738
+ fix_reason = f"Incorrect args format: {args}"
739
+
740
+ if needs_fix:
741
+ # Log the issue for debugging
742
+ self.logger.warning(
743
+ f"Found incorrect kuzu-memory configuration: {fix_reason}. "
744
+ f"Current args: {args}, should be: {correct_args}"
745
+ )
746
+
747
+ # Auto-fix the configuration
748
+ fixed = self._fix_kuzu_memory_args(claude_config_path, config, correct_args)
749
+
750
+ if fixed:
751
+ return DiagnosticResult(
752
+ category="kuzu-memory Configuration Fix",
753
+ status=DiagnosticStatus.OK,
754
+ message=f"Fixed kuzu-memory configuration to use correct args",
755
+ details={
756
+ "old_args": args,
757
+ "new_args": correct_args,
758
+ "reason": fix_reason,
759
+ "auto_fixed": True,
760
+ },
761
+ )
762
+ else:
763
+ return DiagnosticResult(
764
+ category="kuzu-memory Configuration",
765
+ status=DiagnosticStatus.WARNING,
766
+ message="kuzu-memory has incorrect configuration",
767
+ details={
768
+ "current_args": args,
769
+ "correct_args": correct_args,
770
+ "reason": fix_reason,
771
+ "auto_fix_failed": True,
772
+ },
773
+ fix_command="claude-mpm configure --mcp --fix-kuzu",
774
+ fix_description="Fix kuzu-memory configuration manually",
775
+ )
776
+
777
+ # Configuration is correct - args match ["mcp", "serve"]
778
+ return None
779
+
780
+ except (json.JSONDecodeError, Exception) as e:
781
+ self.logger.debug(f"Could not check kuzu-memory config: {e}")
782
+ return None
783
+
784
+ def _fix_kuzu_memory_args(self, config_path: Path, config: Dict, new_args: List[str]) -> bool:
785
+ """Fix kuzu-memory args in the configuration."""
786
+ try:
787
+ # Save old args before updating
788
+ old_args = config["mcpServers"]["kuzu-memory"].get("args", [])
789
+
790
+ # Log the exact change we're about to make
791
+ self.logger.debug(
792
+ f"Fixing kuzu-memory args: old={old_args}, new={new_args}"
793
+ )
794
+
795
+ # Create backup
796
+ backup_path = config_path.with_suffix(".json.backup")
797
+ with open(backup_path, "w") as f:
798
+ json.dump(config, f, indent=2)
799
+
800
+ # Update the configuration - ensure we're setting the exact new_args
801
+ config["mcpServers"]["kuzu-memory"]["args"] = new_args
802
+
803
+ # Verify the update in memory before writing
804
+ if config["mcpServers"]["kuzu-memory"]["args"] != new_args:
805
+ self.logger.error(
806
+ f"Failed to update args in memory! "
807
+ f"Expected {new_args}, got {config['mcpServers']['kuzu-memory']['args']}"
808
+ )
809
+ return False
810
+
811
+ # Write updated configuration
812
+ with open(config_path, "w") as f:
813
+ json.dump(config, f, indent=2)
814
+
815
+ # Verify the file was written correctly
816
+ with open(config_path) as f:
817
+ verify_config = json.load(f)
818
+ verify_args = verify_config.get("mcpServers", {}).get("kuzu-memory", {}).get("args", [])
819
+
820
+ if verify_args != new_args:
821
+ self.logger.error(
822
+ f"Configuration write verification failed! "
823
+ f"Expected {new_args}, got {verify_args}"
824
+ )
825
+ # Restore backup
826
+ with open(backup_path) as bf:
827
+ backup_config = json.load(bf)
828
+ with open(config_path, "w") as f:
829
+ json.dump(backup_config, f, indent=2)
830
+ return False
831
+
832
+ self.logger.info(
833
+ f"✅ Fixed kuzu-memory configuration in {config_path}\n"
834
+ f" Changed args from {old_args} to {new_args}\n"
835
+ f" Backup saved to {backup_path}"
836
+ )
837
+
838
+ return True
839
+
840
+ except Exception as e:
841
+ self.logger.error(f"Failed to fix kuzu-memory configuration: {e}")
842
+ return False
843
+
394
844
  def _check_gateway_configuration(self) -> DiagnosticResult:
395
845
  """Check if MCP services are configured in the gateway."""
396
846
  try:
@@ -472,3 +922,41 @@ class MCPServicesCheck(BaseDiagnosticCheck):
472
922
  message=f"Could not check configuration: {e!s}",
473
923
  details={"error": str(e)},
474
924
  )
925
+
926
+ def _ensure_mcp_ticketer_gql_dependency(self) -> bool:
927
+ """Ensure mcp-ticketer has the gql dependency injected."""
928
+ try:
929
+ # First check if mcp-ticketer can import gql
930
+ result = subprocess.run(
931
+ ["pipx", "run", "--spec", "mcp-ticketer", "python", "-c", "import gql"],
932
+ capture_output=True,
933
+ text=True,
934
+ timeout=5,
935
+ check=False,
936
+ )
937
+
938
+ # If import fails, inject the dependency
939
+ if result.returncode != 0:
940
+ self.logger.info("🔧 mcp-ticketer missing gql dependency, fixing...")
941
+
942
+ inject_result = subprocess.run(
943
+ ["pipx", "inject", "mcp-ticketer", "gql"],
944
+ capture_output=True,
945
+ text=True,
946
+ timeout=30,
947
+ check=False,
948
+ )
949
+
950
+ if inject_result.returncode == 0:
951
+ self.logger.info("✅ Successfully injected gql dependency into mcp-ticketer")
952
+ return True
953
+ else:
954
+ self.logger.warning(f"Failed to inject gql dependency: {inject_result.stderr}")
955
+ return False
956
+
957
+ # Dependency already present
958
+ return False
959
+
960
+ except Exception as e:
961
+ self.logger.debug(f"Could not check/fix mcp-ticketer gql dependency: {e}")
962
+ return False
@@ -214,6 +214,9 @@ class DiagnosticRunner:
214
214
  "agents": AgentCheck,
215
215
  "agent": AgentCheck,
216
216
  "mcp": MCPCheck,
217
+ "mcp_services": MCPServicesCheck,
218
+ "mcp-services": MCPServicesCheck,
219
+ "external": MCPServicesCheck,
217
220
  "monitor": MonitorCheck,
218
221
  "monitoring": MonitorCheck,
219
222
  "common": CommonIssuesCheck,
@@ -41,6 +41,33 @@ 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": "pipx",
50
+ "args": ["run", "kuzu-memory", "mcp", "serve"]
51
+ },
52
+ "mcp-ticketer": {
53
+ "type": "stdio",
54
+ "command": "pipx",
55
+ "args": ["run", "mcp-ticketer", "mcp"]
56
+ },
57
+ "mcp-browser": {
58
+ "type": "stdio",
59
+ "command": "pipx",
60
+ "args": ["run", "mcp-browser", "mcp"],
61
+ "env": {"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")}
62
+ },
63
+ "mcp-vector-search": {
64
+ "type": "stdio",
65
+ "command": "pipx",
66
+ "args": ["run", "mcp-vector-search", "-m", "mcp_vector_search.mcp.server", "{project_root}"],
67
+ "env": {}
68
+ }
69
+ }
70
+
44
71
  def __init__(self):
45
72
  """Initialize the MCP configuration manager."""
46
73
  self.logger = get_logger(__name__)
@@ -195,11 +222,36 @@ class MCPConfigManager:
195
222
 
196
223
  return None
197
224
 
225
+ def get_static_service_config(self, service_name: str) -> Optional[Dict]:
226
+ """
227
+ Get the static, known-good configuration for an MCP service.
228
+
229
+ Args:
230
+ service_name: Name of the MCP service
231
+
232
+ Returns:
233
+ Static service configuration dict or None if service not known
234
+ """
235
+ if service_name not in self.STATIC_MCP_CONFIGS:
236
+ return None
237
+
238
+ config = self.STATIC_MCP_CONFIGS[service_name].copy()
239
+
240
+ # Special handling for mcp-vector-search: replace {project_root} placeholder
241
+ if service_name == "mcp-vector-search":
242
+ config["args"] = [
243
+ arg.replace("{project_root}", str(self.project_root)) if "{project_root}" in arg else arg
244
+ for arg in config["args"]
245
+ ]
246
+
247
+ return config
248
+
198
249
  def generate_service_config(self, service_name: str) -> Optional[Dict]:
199
250
  """
200
251
  Generate configuration for a specific MCP service.
201
252
 
202
- Prefers 'pipx run' or 'uvx' commands over direct execution for better isolation.
253
+ Prefers static configurations over detection. Falls back to detection
254
+ only for unknown services.
203
255
 
204
256
  Args:
205
257
  service_name: Name of the MCP service
@@ -207,6 +259,12 @@ class MCPConfigManager:
207
259
  Returns:
208
260
  Service configuration dict or None if service not found
209
261
  """
262
+ # First try to get static configuration
263
+ static_config = self.get_static_service_config(service_name)
264
+ if static_config:
265
+ return static_config
266
+
267
+ # Fall back to detection-based configuration for unknown services
210
268
  import shutil
211
269
 
212
270
  # Check for pipx run first (preferred for isolation)
@@ -308,7 +366,7 @@ class MCPConfigManager:
308
366
 
309
367
  elif service_name == "kuzu-memory":
310
368
  # Determine kuzu-memory command version
311
- kuzu_args = ["mcp", "serve"] # Default to the correct modern format
369
+ kuzu_args = ["mcp", "serve"] # Default to the standard v1.1.0+ format
312
370
  test_cmd = None
313
371
 
314
372
  if use_pipx_run:
@@ -330,20 +388,21 @@ class MCPConfigManager:
330
388
  # Check for MCP support in help output
331
389
  help_output = result.stdout.lower() + result.stderr.lower()
332
390
 
333
- # Modern version detection - look for "mcp serve" command
391
+ # Standard version detection - look for "mcp serve" command (v1.1.0+)
392
+ # This is the correct format for kuzu-memory v1.1.0 and later
334
393
  if "mcp serve" in help_output or ("mcp" in help_output and "serve" in help_output):
335
- # Modern version with mcp serve command
394
+ # Standard v1.1.0+ version with mcp serve command
336
395
  kuzu_args = ["mcp", "serve"]
337
396
  # Legacy version detection - only "serve" without "mcp"
338
397
  elif "serve" in help_output and "mcp" not in help_output:
339
398
  # Very old version that only has serve command
340
399
  kuzu_args = ["serve"]
341
- # Note: "claude mcp-server" format is deprecated and not used
342
400
  else:
343
- # Default to the correct modern format
401
+ # Default to the standard mcp serve format (v1.1.0+)
402
+ # Note: "claude mcp-server" format is deprecated and does not work
344
403
  kuzu_args = ["mcp", "serve"]
345
404
  except Exception:
346
- # Default to the correct mcp serve command on any error
405
+ # Default to the standard mcp serve command on any error
347
406
  kuzu_args = ["mcp", "serve"]
348
407
 
349
408
  if use_pipx_run:
@@ -371,17 +430,20 @@ class MCPConfigManager:
371
430
 
372
431
  def ensure_mcp_services_configured(self) -> Tuple[bool, str]:
373
432
  """
374
- Ensure MCP services are configured in ~/.claude.json on startup.
433
+ Ensure MCP services are configured correctly in ~/.claude.json on startup.
375
434
 
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.
435
+ This method checks ALL projects in ~/.claude.json and ensures each has
436
+ the correct, static MCP service configurations. It will:
437
+ 1. Add missing services
438
+ 2. Fix incorrect configurations
439
+ 3. Update all projects, not just the current one
378
440
 
379
441
  Returns:
380
442
  Tuple of (success, message)
381
443
  """
382
444
  updated = False
445
+ fixed_services = []
383
446
  added_services = []
384
- project_key = str(self.project_root)
385
447
 
386
448
  # Load existing Claude config or create minimal structure
387
449
  claude_config = {}
@@ -396,9 +458,23 @@ class MCPConfigManager:
396
458
  # Ensure projects structure exists
397
459
  if "projects" not in claude_config:
398
460
  claude_config["projects"] = {}
461
+ updated = True
399
462
 
400
- if project_key not in claude_config["projects"]:
401
- claude_config["projects"][project_key] = {
463
+ # Check and fix mcp-ticketer dependencies ONCE before processing projects
464
+ # This avoids running the same pipx inject command 100+ times
465
+ mcp_ticketer_fixed = False
466
+ if "mcp-ticketer" in self.PIPX_SERVICES:
467
+ mcp_ticketer_fixed = self._check_and_fix_mcp_ticketer_dependencies()
468
+
469
+ # Process ALL projects in the config, not just current one
470
+ projects_to_update = list(claude_config.get("projects", {}).keys())
471
+
472
+ # Also add the current project if not in list
473
+ current_project_key = str(self.project_root)
474
+ if current_project_key not in projects_to_update:
475
+ projects_to_update.append(current_project_key)
476
+ # Initialize new project structure
477
+ claude_config["projects"][current_project_key] = {
402
478
  "allowedTools": [],
403
479
  "history": [],
404
480
  "mcpContextUris": [],
@@ -412,29 +488,51 @@ class MCPConfigManager:
412
488
  }
413
489
  updated = True
414
490
 
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
- )
491
+ # Update each project's MCP configurations
492
+ for project_key in projects_to_update:
493
+ project_config = claude_config["projects"][project_key]
494
+
495
+ # Ensure mcpServers section exists
496
+ if "mcpServers" not in project_config:
497
+ project_config["mcpServers"] = {}
498
+ updated = True
499
+
500
+ # Check and fix each service configuration
501
+ for service_name in self.PIPX_SERVICES:
502
+ # Get the correct static configuration
503
+ correct_config = self.STATIC_MCP_CONFIGS[service_name].copy()
504
+
505
+ # Special handling for mcp-vector-search: replace {project_root} placeholder
506
+ if service_name == "mcp-vector-search":
507
+ # Use the project key as the project root for each project
508
+ correct_config["args"] = [
509
+ arg.replace("{project_root}", project_key) if "{project_root}" in arg else arg
510
+ for arg in correct_config["args"]
511
+ ]
512
+
513
+ # Check if service exists and has correct configuration
514
+ existing_config = project_config["mcpServers"].get(service_name)
515
+
516
+ # Determine if we need to update
517
+ needs_update = False
518
+ if not existing_config:
519
+ # Service is missing
520
+ needs_update = True
521
+ added_services.append(f"{service_name} in {Path(project_key).name}")
435
522
  else:
523
+ # Service exists, check if configuration is correct
524
+ # Compare command and args (the most critical parts)
525
+ if (existing_config.get("command") != correct_config.get("command") or
526
+ existing_config.get("args") != correct_config.get("args")):
527
+ needs_update = True
528
+ fixed_services.append(f"{service_name} in {Path(project_key).name}")
529
+
530
+ # Update configuration if needed
531
+ if needs_update:
532
+ project_config["mcpServers"][service_name] = correct_config
533
+ updated = True
436
534
  self.logger.debug(
437
- f"MCP service {service_name} not found for auto-configuration"
535
+ f"Updated MCP service config for {service_name} in project {Path(project_key).name}"
438
536
  )
439
537
 
440
538
  # Write updated config if changes were made
@@ -448,7 +546,6 @@ class MCPConfigManager:
448
546
  f".backup.{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.json"
449
547
  )
450
548
  import shutil
451
-
452
549
  shutil.copy2(self.claude_config_path, backup_path)
453
550
  self.logger.debug(f"Created backup: {backup_path}")
454
551
 
@@ -456,18 +553,20 @@ class MCPConfigManager:
456
553
  with open(self.claude_config_path, "w") as f:
457
554
  json.dump(claude_config, f, indent=2)
458
555
 
556
+ messages = []
459
557
  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"
558
+ messages.append(f"Added MCP services: {', '.join(added_services[:3])}")
559
+ if fixed_services:
560
+ messages.append(f"Fixed MCP services: {', '.join(fixed_services[:3])}")
561
+
562
+ if messages:
563
+ return True, "; ".join(messages)
564
+ return True, "All MCP services already configured correctly"
466
565
  except Exception as e:
467
566
  self.logger.error(f"Failed to write Claude config: {e}")
468
567
  return False, f"Failed to write configuration: {e}"
469
568
 
470
- return True, "All MCP services already configured"
569
+ return True, "All MCP services already configured correctly"
471
570
 
472
571
  def update_mcp_config(self, force_pipx: bool = True) -> Tuple[bool, str]:
473
572
  """
@@ -695,6 +794,49 @@ class MCPConfigManager:
695
794
 
696
795
  return False, "none"
697
796
 
797
+ def _check_and_fix_mcp_ticketer_dependencies(self) -> bool:
798
+ """Check and fix mcp-ticketer missing gql dependency.
799
+
800
+ Note: This is a workaround for mcp-ticketer <= 0.1.8 which is missing
801
+ the gql dependency in its package metadata. Future versions (> 0.1.8)
802
+ should include 'gql[httpx]>=3.0.0' as a dependency, making this fix
803
+ unnecessary. We keep this for backward compatibility with older versions.
804
+ """
805
+ try:
806
+ # Test if gql is available in mcp-ticketer's environment
807
+ test_result = subprocess.run(
808
+ ["pipx", "run", "--spec", "mcp-ticketer", "python", "-c", "import gql"],
809
+ capture_output=True,
810
+ text=True,
811
+ timeout=5,
812
+ check=False,
813
+ )
814
+
815
+ # If import fails, inject the dependency
816
+ if test_result.returncode != 0:
817
+ self.logger.info("🔧 mcp-ticketer missing gql dependency, fixing...")
818
+
819
+ inject_result = subprocess.run(
820
+ ["pipx", "inject", "mcp-ticketer", "gql"],
821
+ capture_output=True,
822
+ text=True,
823
+ timeout=30,
824
+ check=False,
825
+ )
826
+
827
+ if inject_result.returncode == 0:
828
+ self.logger.info("✅ Successfully injected gql dependency into mcp-ticketer")
829
+ return True
830
+ else:
831
+ self.logger.warning(f"Failed to inject gql: {inject_result.stderr}")
832
+ return False
833
+
834
+ return False
835
+
836
+ except Exception as e:
837
+ self.logger.debug(f"Could not check/fix mcp-ticketer dependencies: {e}")
838
+ return False
839
+
698
840
  def _verify_service_installed(self, service_name: str, method: str) -> bool:
699
841
  """
700
842
  Verify that a service was successfully installed and is functional.
@@ -711,6 +853,9 @@ class MCPConfigManager:
711
853
  # Give the installation a moment to settle
712
854
  time.sleep(1)
713
855
 
856
+ # Note: mcp-ticketer dependency fix is now handled once in ensure_mcp_services_configured()
857
+ # to avoid running the same pipx inject command multiple times
858
+
714
859
  # Check if we can find the service
715
860
  service_path = self.detect_service_path(service_name)
716
861
  if not service_path:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.4.7
3
+ Version: 4.4.9
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
@@ -1,5 +1,5 @@
1
1
  claude_mpm/BUILD_NUMBER,sha256=toytnNjkIKPgQaGwDqQdC1rpNTAdSEc6Vja50d7Ovug,4
2
- claude_mpm/VERSION,sha256=R8T1NAm61Ztbc_VglNJtvUE8VEkNwCNRHjh4rXfFP_M,6
2
+ claude_mpm/VERSION,sha256=k8-5xMoy2UlbzZa4MVGVy1j0Ym4rIg_47OvMJE5eNKg,6
3
3
  claude_mpm/__init__.py,sha256=lyTZAYGH4DTaFGLRNWJKk5Q5oTjzN5I6AXmfVX-Jff0,1512
4
4
  claude_mpm/__main__.py,sha256=Ro5UBWBoQaSAIoSqWAr7zkbLyvi4sSy28WShqAhKJG0,723
5
5
  claude_mpm/constants.py,sha256=cChN3myrAcF3jC-6DvHnBFTEnwlDk-TAsIXPvUZr_yw,5953
@@ -411,7 +411,7 @@ claude_mpm/services/event_aggregator.py,sha256=DDcehIZVpiEDzs9o18gDZyvjMBHCq2H8H
411
411
  claude_mpm/services/exceptions.py,sha256=5lVZETr_6-xk0ItH7BTfYUiX5RlckS1e8ah_UalYG9c,26475
412
412
  claude_mpm/services/hook_installer_service.py,sha256=z3kKeriEY1Y9bFesuGlHBxhCtc0Wzd3Zv02k2_rEyGo,19727
413
413
  claude_mpm/services/hook_service.py,sha256=rZnMn_4qxX5g9KAn0IQdoG50WmySNfsTmfG0XHuRHXk,15737
414
- claude_mpm/services/mcp_config_manager.py,sha256=bMV3vkDahRjH94Ht91sUg1Ha8H1Sz63Q1AM9SddfvBY,28957
414
+ claude_mpm/services/mcp_config_manager.py,sha256=iEBnl9rPFR-PccOol8ZdufyCvhZvVN47wYtTteQVpBY,35112
415
415
  claude_mpm/services/mcp_service_verifier.py,sha256=ngiegCngX18AFehfyJdvqQAvscoIBvFN_DeOoGTjxj0,25164
416
416
  claude_mpm/services/memory_hook_service.py,sha256=pRlTClkRcw30Jhwbha4BC8IMdzKZxF8aWqf52JlntgY,11600
417
417
  claude_mpm/services/monitor_build_service.py,sha256=8gWR9CaqgXdG6-OjOFXGpk28GCcJTlHhojkUYnMCebI,12160
@@ -547,7 +547,7 @@ claude_mpm/services/core/interfaces/communication.py,sha256=evwtLbYCFa3Zb8kEfL10
547
547
  claude_mpm/services/core/interfaces/infrastructure.py,sha256=eLtr_dFhA3Ux3mPOV_4DbWhGjHpfpGnj6xOhfQcgZGk,10037
548
548
  claude_mpm/services/core/interfaces/service.py,sha256=hNfHXe45LcPCp_dToOmZCfnUZBF5axMf_TdxqCSm2-I,11536
549
549
  claude_mpm/services/diagnostics/__init__.py,sha256=WTRucANR9EwNi53rotjkeE4k75s18RjHJ8s1BfBj7ic,614
550
- claude_mpm/services/diagnostics/diagnostic_runner.py,sha256=bfF3QQfaJPv2fyo61AstHZ139nHvY0fcjXXbXFcNdZo,9336
550
+ claude_mpm/services/diagnostics/diagnostic_runner.py,sha256=PJje2C3_20JAeDBf2LpZ_rfi3rwSWk73EXJmZEkDcvs,9470
551
551
  claude_mpm/services/diagnostics/doctor_reporter.py,sha256=WhlHBWy-KI8OhAWujOu77VAgSvkBvtF3sDrjacYZhvg,19649
552
552
  claude_mpm/services/diagnostics/models.py,sha256=nqOQLllZyZmw3Zt5eFJfE1Al7C3Vrn3REgFlARtT3jQ,3831
553
553
  claude_mpm/services/diagnostics/checks/__init__.py,sha256=aNdOeJHZVIpEqqzr6xWUOiyZCIrN4vckfRxkW70cqeo,987
@@ -560,7 +560,7 @@ claude_mpm/services/diagnostics/checks/filesystem_check.py,sha256=V5HoHDYlSuoK2l
560
560
  claude_mpm/services/diagnostics/checks/installation_check.py,sha256=WoTt15R8Wg-6k2JZFAtmffFuih1AIyCX71QOHEFH-Ro,19562
561
561
  claude_mpm/services/diagnostics/checks/instructions_check.py,sha256=VbgBorl0RpFvxKQ_SC1gibTmGSiXaKSp-vVZt6hbH1g,16290
562
562
  claude_mpm/services/diagnostics/checks/mcp_check.py,sha256=SftuhP70abopyMD8GlLA_K3XHEYnBAeITggUQI0cYP4,12173
563
- claude_mpm/services/diagnostics/checks/mcp_services_check.py,sha256=aH1YgyAAj3rj5H02adWUjM00KdZuOz4Qm9oyY4bgELc,19007
563
+ claude_mpm/services/diagnostics/checks/mcp_services_check.py,sha256=SrHdpRbEc62e7mkfC_m8aGFtQIkhNOmiiCQZ78MME98,40800
564
564
  claude_mpm/services/diagnostics/checks/monitor_check.py,sha256=NUx5G1yjHWlukZmwhUz4o8STRWgsQEx01YjIMReNC0A,10096
565
565
  claude_mpm/services/diagnostics/checks/startup_log_check.py,sha256=DrXdml2rHvmhFBdb_sntE3xmwaP_DZIKjdVbCn8Dy7E,12258
566
566
  claude_mpm/services/event_bus/__init__.py,sha256=ETCo4a6puIeyVWAv55uCDjjhzNyUwbVAHEcAVkVapx8,688
@@ -773,9 +773,9 @@ claude_mpm/utils/subprocess_utils.py,sha256=D0izRT8anjiUb_JG72zlJR_JAw1cDkb7kalN
773
773
  claude_mpm/validation/__init__.py,sha256=YZhwE3mhit-lslvRLuwfX82xJ_k4haZeKmh4IWaVwtk,156
774
774
  claude_mpm/validation/agent_validator.py,sha256=Nm2WmcbCb0EwOG4nFcikc3wVdiiAfjGBBI3YoR6ainQ,20915
775
775
  claude_mpm/validation/frontmatter_validator.py,sha256=IDBOCBweO6umydSnUJjBh81sKk3cy9hRFYm61DCiXbI,7020
776
- claude_mpm-4.4.7.dist-info/licenses/LICENSE,sha256=lpaivOlPuBZW1ds05uQLJJswy8Rp_HMNieJEbFlqvLk,1072
777
- claude_mpm-4.4.7.dist-info/METADATA,sha256=dxh7pWMT-QL9jjSQ4oI4tYrYfjuYfJAMucrSK30Jn70,17517
778
- claude_mpm-4.4.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
779
- claude_mpm-4.4.7.dist-info/entry_points.txt,sha256=FDPZgz8JOvD-6iuXY2l9Zbo9zYVRuE4uz4Qr0vLeGOk,471
780
- claude_mpm-4.4.7.dist-info/top_level.txt,sha256=1nUg3FEaBySgm8t-s54jK5zoPnu3_eY6EP6IOlekyHA,11
781
- claude_mpm-4.4.7.dist-info/RECORD,,
776
+ claude_mpm-4.4.9.dist-info/licenses/LICENSE,sha256=lpaivOlPuBZW1ds05uQLJJswy8Rp_HMNieJEbFlqvLk,1072
777
+ claude_mpm-4.4.9.dist-info/METADATA,sha256=RSN3ddmTCsiu9uTUoaSFYFeB7O56BR7awyIOpLkSVsA,17517
778
+ claude_mpm-4.4.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
779
+ claude_mpm-4.4.9.dist-info/entry_points.txt,sha256=FDPZgz8JOvD-6iuXY2l9Zbo9zYVRuE4uz4Qr0vLeGOk,471
780
+ claude_mpm-4.4.9.dist-info/top_level.txt,sha256=1nUg3FEaBySgm8t-s54jK5zoPnu3_eY6EP6IOlekyHA,11
781
+ claude_mpm-4.4.9.dist-info/RECORD,,