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
claude_mpm/VERSION CHANGED
@@ -1 +1 @@
1
- 4.4.8
1
+ 4.4.10
File without changes
File without changes
@@ -5,8 +5,10 @@ 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
 
@@ -35,6 +37,8 @@ class MCPServicesCheck(BaseDiagnosticCheck):
35
37
  "check_health": True,
36
38
  "health_command": ["mcp-vector-search", "--version"],
37
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"],
38
42
  },
39
43
  "mcp-browser": {
40
44
  "package": "mcp-browser",
@@ -43,6 +47,7 @@ class MCPServicesCheck(BaseDiagnosticCheck):
43
47
  "check_health": True,
44
48
  "health_command": ["mcp-browser", "--version"],
45
49
  "pipx_run_command": ["pipx", "run", "mcp-browser", "--version"],
50
+ "mcp_command": ["mcp-browser", "mcp"], # Command to run as MCP server
46
51
  },
47
52
  "mcp-ticketer": {
48
53
  "package": "mcp-ticketer",
@@ -51,6 +56,7 @@ class MCPServicesCheck(BaseDiagnosticCheck):
51
56
  "check_health": True,
52
57
  "health_command": ["mcp-ticketer", "--version"],
53
58
  "pipx_run_command": ["pipx", "run", "mcp-ticketer", "--version"],
59
+ "mcp_command": ["mcp-ticketer", "mcp"], # Command to run as MCP server
54
60
  },
55
61
  "kuzu-memory": {
56
62
  "package": "kuzu-memory",
@@ -59,6 +65,7 @@ class MCPServicesCheck(BaseDiagnosticCheck):
59
65
  "check_health": True, # v1.1.0+ has version command
60
66
  "health_command": ["kuzu-memory", "--version"],
61
67
  "pipx_run_command": ["pipx", "run", "kuzu-memory", "--version"],
68
+ "mcp_command": ["kuzu-memory", "mcp", "serve"], # v1.1.0+ uses 'mcp serve' args
62
69
  },
63
70
  }
64
71
 
@@ -77,6 +84,33 @@ class MCPServicesCheck(BaseDiagnosticCheck):
77
84
  sub_results = []
78
85
  services_status = {}
79
86
 
87
+ # Use MCPConfigManager to detect and fix corrupted installations
88
+ from claude_mpm.services.mcp_config_manager import MCPConfigManager
89
+ mcp_manager = MCPConfigManager()
90
+
91
+ # Run comprehensive fix for all MCP service issues
92
+ fix_success, fix_message = mcp_manager.fix_mcp_service_issues()
93
+ if fix_message and fix_message != "All MCP services are functioning correctly":
94
+ # Create diagnostic result for the fixes
95
+ fix_result = DiagnosticResult(
96
+ category="MCP Service Fixes",
97
+ status=DiagnosticStatus.OK if fix_success else DiagnosticStatus.WARNING,
98
+ message=fix_message,
99
+ details={"auto_fix_applied": True}
100
+ )
101
+ sub_results.append(fix_result)
102
+
103
+ # Also ensure configurations are updated for all projects
104
+ config_success, config_message = mcp_manager.ensure_mcp_services_configured()
105
+ if config_message and config_message != "All MCP services already configured correctly":
106
+ config_result = DiagnosticResult(
107
+ category="MCP Configuration Update",
108
+ status=DiagnosticStatus.OK if config_success else DiagnosticStatus.WARNING,
109
+ message=config_message,
110
+ details={"auto_config_applied": True}
111
+ )
112
+ sub_results.append(config_result)
113
+
80
114
  # Check for kuzu-memory configuration issues and offer auto-fix
81
115
  kuzu_config_result = self._check_and_fix_kuzu_memory_config()
82
116
  if kuzu_config_result:
@@ -86,11 +120,20 @@ class MCPServicesCheck(BaseDiagnosticCheck):
86
120
  for service_name, service_config in self.MCP_SERVICES.items():
87
121
  service_result = self._check_service(service_name, service_config)
88
122
  sub_results.append(service_result)
123
+
124
+ # Extract connection test info if available
125
+ connection_test = service_result.details.get("connection_test", {})
126
+
89
127
  services_status[service_name] = {
90
128
  "status": service_result.status.value,
91
129
  "installed": service_result.details.get("installed", False),
92
130
  "accessible": service_result.details.get("accessible", False),
93
131
  "version": service_result.details.get("version"),
132
+ "connection_tested": bool(connection_test),
133
+ "connected": connection_test.get("connected", False),
134
+ "response_time_ms": connection_test.get("response_time_ms"),
135
+ "tools_discovered": connection_test.get("tools_discovered", 0),
136
+ "connection_error": connection_test.get("error"),
94
137
  }
95
138
 
96
139
  # Check MCP gateway configuration for services
@@ -102,12 +145,18 @@ class MCPServicesCheck(BaseDiagnosticCheck):
102
145
  accessible_count = sum(
103
146
  1 for s in services_status.values() if s["accessible"]
104
147
  )
148
+ connected_count = sum(1 for s in services_status.values() if s["connected"])
105
149
  total_services = len(self.MCP_SERVICES)
106
150
 
151
+ # Calculate total tools discovered
152
+ total_tools = sum(s.get("tools_discovered", 0) for s in services_status.values())
153
+
107
154
  details["services"] = services_status
108
155
  details["installed_count"] = installed_count
109
156
  details["accessible_count"] = accessible_count
157
+ details["connected_count"] = connected_count
110
158
  details["total_services"] = total_services
159
+ details["total_tools_discovered"] = total_tools
111
160
  details["gateway_configured"] = gateway_result.status == DiagnosticStatus.OK
112
161
 
113
162
  # Determine overall status
@@ -120,6 +169,12 @@ class MCPServicesCheck(BaseDiagnosticCheck):
120
169
  elif installed_count == 0:
121
170
  status = DiagnosticStatus.WARNING
122
171
  message = "No MCP services installed"
172
+ elif connected_count == total_services:
173
+ status = DiagnosticStatus.OK
174
+ message = f"All {total_services} MCP services connected ({total_tools} tools available)"
175
+ elif connected_count > 0:
176
+ status = DiagnosticStatus.WARNING
177
+ message = f"{connected_count}/{total_services} MCP services connected, {installed_count} installed"
123
178
  elif accessible_count < installed_count:
124
179
  status = DiagnosticStatus.WARNING
125
180
  message = f"{installed_count}/{total_services} services installed, {accessible_count} accessible"
@@ -127,8 +182,8 @@ class MCPServicesCheck(BaseDiagnosticCheck):
127
182
  status = DiagnosticStatus.WARNING
128
183
  message = f"{installed_count}/{total_services} MCP services installed"
129
184
  else:
130
- status = DiagnosticStatus.OK
131
- message = f"All {total_services} MCP services installed and accessible"
185
+ status = DiagnosticStatus.WARNING
186
+ message = f"All {total_services} MCP services installed but connections not tested"
132
187
 
133
188
  return DiagnosticResult(
134
189
  category=self.category,
@@ -146,6 +201,185 @@ class MCPServicesCheck(BaseDiagnosticCheck):
146
201
  details={"error": str(e)},
147
202
  )
148
203
 
204
+ async def _test_mcp_connection(
205
+ self, service_name: str, command: List[str]
206
+ ) -> Dict:
207
+ """Test MCP server connection by sending JSON-RPC requests."""
208
+ result = {
209
+ "connected": False,
210
+ "response_time": None,
211
+ "tools_count": 0,
212
+ "tools": [],
213
+ "error": None,
214
+ }
215
+
216
+ process = None
217
+ try:
218
+ # Start the MCP server process
219
+ start_time = time.time()
220
+ process = await asyncio.create_subprocess_exec(
221
+ *command,
222
+ stdin=asyncio.subprocess.PIPE,
223
+ stdout=asyncio.subprocess.PIPE,
224
+ stderr=asyncio.subprocess.PIPE,
225
+ )
226
+
227
+ # Give the server a moment to initialize
228
+ await asyncio.sleep(0.1)
229
+
230
+ # Prepare initialize request
231
+ init_request = {
232
+ "jsonrpc": "2.0",
233
+ "method": "initialize",
234
+ "params": {
235
+ "protocolVersion": "2024-11-05",
236
+ "capabilities": {},
237
+ "clientInfo": {
238
+ "name": "mpm-doctor",
239
+ "version": "1.0.0"
240
+ }
241
+ },
242
+ "id": 1
243
+ }
244
+
245
+ # Send initialize request
246
+ request_line = json.dumps(init_request) + "\n"
247
+ process.stdin.write(request_line.encode())
248
+ await process.stdin.drain()
249
+
250
+ # Read response with timeout
251
+ try:
252
+ response_line = await asyncio.wait_for(
253
+ process.stdout.readline(), timeout=5.0
254
+ )
255
+ response_time = time.time() - start_time
256
+
257
+ if response_line:
258
+ # Some MCP servers may output non-JSON before the actual response
259
+ # Try to find JSON in the response
260
+ response_text = response_line.decode().strip()
261
+
262
+ # Skip empty lines or non-JSON lines
263
+ while response_text and not response_text.startswith('{'):
264
+ # Try to read the next line
265
+ try:
266
+ response_line = await asyncio.wait_for(
267
+ process.stdout.readline(), timeout=1.0
268
+ )
269
+ if response_line:
270
+ response_text = response_line.decode().strip()
271
+ else:
272
+ break
273
+ except asyncio.TimeoutError:
274
+ break
275
+
276
+ if not response_text or not response_text.startswith('{'):
277
+ result["error"] = "No valid JSON response received"
278
+ return result
279
+
280
+ response = json.loads(response_text)
281
+
282
+ # Check for valid JSON-RPC response
283
+ if "result" in response and response.get("id") == 1:
284
+ result["connected"] = True
285
+ result["response_time"] = round(response_time * 1000, 2) # ms
286
+
287
+ # Send tools/list request
288
+ tools_request = {
289
+ "jsonrpc": "2.0",
290
+ "method": "tools/list",
291
+ "params": {},
292
+ "id": 2
293
+ }
294
+
295
+ request_line = json.dumps(tools_request) + "\n"
296
+ process.stdin.write(request_line.encode())
297
+ await process.stdin.drain()
298
+
299
+ # Read tools response
300
+ try:
301
+ tools_response_line = await asyncio.wait_for(
302
+ process.stdout.readline(), timeout=3.0
303
+ )
304
+
305
+ if tools_response_line:
306
+ tools_response = json.loads(tools_response_line.decode())
307
+ if "result" in tools_response:
308
+ tools = tools_response["result"].get("tools", [])
309
+ result["tools_count"] = len(tools)
310
+ # Store first 5 tool names for display
311
+ result["tools"] = [
312
+ tool.get("name", "unknown")
313
+ for tool in tools[:5]
314
+ ]
315
+ except asyncio.TimeoutError:
316
+ # Connection successful but tools query timed out
317
+ pass
318
+ except (json.JSONDecodeError, KeyError):
319
+ # Connection successful but tools response invalid
320
+ pass
321
+
322
+ elif "error" in response:
323
+ result["error"] = f"MCP error: {response['error'].get('message', 'Unknown error')}"
324
+ else:
325
+ result["error"] = "Invalid JSON-RPC response format"
326
+
327
+ except asyncio.TimeoutError:
328
+ # Try to get any error output from stderr
329
+ stderr_output = ""
330
+ if process and process.stderr:
331
+ try:
332
+ stderr_data = await asyncio.wait_for(
333
+ process.stderr.read(1000), timeout=0.5
334
+ )
335
+ if stderr_data:
336
+ stderr_output = stderr_data.decode('utf-8', errors='ignore')[:200]
337
+ except:
338
+ pass
339
+
340
+ if stderr_output:
341
+ result["error"] = f"Connection timeout (5s). Server output: {stderr_output}"
342
+ else:
343
+ result["error"] = "Connection timeout (5s)"
344
+
345
+ except json.JSONDecodeError as e:
346
+ # Try to get stderr for more context
347
+ stderr_output = ""
348
+ if process and process.stderr:
349
+ try:
350
+ stderr_data = await asyncio.wait_for(
351
+ process.stderr.read(1000), timeout=0.5
352
+ )
353
+ if stderr_data:
354
+ stderr_output = stderr_data.decode('utf-8', errors='ignore')[:200]
355
+ except:
356
+ pass
357
+
358
+ if stderr_output:
359
+ result["error"] = f"Invalid JSON response: {str(e)}. Server error: {stderr_output}"
360
+ else:
361
+ result["error"] = f"Invalid JSON response: {str(e)}"
362
+
363
+ except FileNotFoundError:
364
+ result["error"] = f"Command not found: {command[0]}"
365
+ except PermissionError:
366
+ result["error"] = f"Permission denied: {command[0]}"
367
+ except Exception as e:
368
+ result["error"] = f"Connection failed: {str(e)}"
369
+ finally:
370
+ # Clean up process
371
+ if process:
372
+ try:
373
+ process.terminate()
374
+ await asyncio.wait_for(process.wait(), timeout=2.0)
375
+ except asyncio.TimeoutError:
376
+ process.kill()
377
+ await process.wait()
378
+ except Exception:
379
+ pass
380
+
381
+ return result
382
+
149
383
  def _check_service(self, service_name: str, config: Dict) -> DiagnosticResult:
150
384
  """Check a specific MCP service."""
151
385
  details = {"service": service_name}
@@ -156,6 +390,12 @@ class MCPServicesCheck(BaseDiagnosticCheck):
156
390
  if pipx_path:
157
391
  details["pipx_path"] = pipx_path
158
392
 
393
+ # Special check for mcp-ticketer: ensure gql dependency
394
+ if service_name == "mcp-ticketer" and pipx_installed:
395
+ gql_fixed = self._ensure_mcp_ticketer_gql_dependency()
396
+ if gql_fixed:
397
+ details["gql_dependency_fixed"] = True
398
+
159
399
  # Check if accessible in PATH
160
400
  accessible, command_path = self._check_command_accessible(config["command"])
161
401
  details["accessible"] = accessible
@@ -197,6 +437,64 @@ class MCPServicesCheck(BaseDiagnosticCheck):
197
437
  details["version"] = version
198
438
  break
199
439
 
440
+ # Test MCP connection if installed (accessible or pipx) and has mcp_command
441
+ if (accessible or pipx_installed) and "mcp_command" in config:
442
+ # Determine which command to use for MCP connection test
443
+ mcp_command = None
444
+ if pipx_installed and not accessible:
445
+ # Service is installed via pipx but not in PATH
446
+ if "pipx_mcp_command" in config:
447
+ # Use special pipx MCP command if available (e.g., for mcp-vector-search)
448
+ mcp_command = config["pipx_mcp_command"]
449
+ else:
450
+ # Build pipx run command based on package
451
+ base_cmd = config["mcp_command"]
452
+ if len(base_cmd) > 0 and base_cmd[0] == config["package"]:
453
+ # Simple case where first command is the package name
454
+ mcp_command = ["pipx", "run", config["package"]] + base_cmd[1:]
455
+ else:
456
+ # Complex case - just try running the package with mcp arg
457
+ mcp_command = ["pipx", "run", config["package"], "mcp"]
458
+ elif details.get("accessible_via_pipx_run"):
459
+ # Use pipx run for the MCP command
460
+ if "pipx_mcp_command" in config:
461
+ # Use special pipx MCP command if available (e.g., for mcp-vector-search)
462
+ mcp_command = config["pipx_mcp_command"]
463
+ else:
464
+ # Build pipx run command
465
+ base_cmd = config["mcp_command"]
466
+ if service_name == "kuzu-memory":
467
+ # Special case for kuzu-memory with args
468
+ mcp_command = ["pipx", "run", base_cmd[0]] + base_cmd[1:]
469
+ else:
470
+ mcp_command = ["pipx", "run"] + base_cmd
471
+ else:
472
+ mcp_command = config["mcp_command"]
473
+
474
+ if mcp_command:
475
+ # Run async connection test
476
+ try:
477
+ loop = asyncio.new_event_loop()
478
+ asyncio.set_event_loop(loop)
479
+ connection_result = loop.run_until_complete(
480
+ self._test_mcp_connection(service_name, mcp_command)
481
+ )
482
+ loop.close()
483
+
484
+ # Add connection test results to details
485
+ details["connection_test"] = {
486
+ "connected": connection_result["connected"],
487
+ "response_time_ms": connection_result["response_time"],
488
+ "tools_discovered": connection_result["tools_count"],
489
+ "tools_sample": connection_result["tools"],
490
+ "error": connection_result["error"]
491
+ }
492
+ except Exception as e:
493
+ details["connection_test"] = {
494
+ "connected": False,
495
+ "error": f"Test failed: {str(e)}"
496
+ }
497
+
200
498
  # Determine status
201
499
  if not (pipx_installed or accessible):
202
500
  return DiagnosticResult(
@@ -211,10 +509,19 @@ class MCPServicesCheck(BaseDiagnosticCheck):
211
509
  if pipx_installed and not accessible:
212
510
  # Check if pipx run works
213
511
  if details.get("pipx_run_available"):
512
+ # Include connection test info if available
513
+ connection_info = details.get("connection_test", {})
514
+ if connection_info.get("connected"):
515
+ message = f"Installed via pipx, connection OK ({connection_info.get('tools_discovered', 0)} tools)"
516
+ elif connection_info.get("error"):
517
+ message = f"Installed via pipx, connection failed: {connection_info['error']}"
518
+ else:
519
+ message = "Installed via pipx (use 'pipx run' to execute)"
520
+
214
521
  return DiagnosticResult(
215
522
  category=f"MCP Service: {service_name}",
216
- status=DiagnosticStatus.OK,
217
- message="Installed via pipx (use 'pipx run' to execute)",
523
+ status=DiagnosticStatus.OK if connection_info.get("connected") else DiagnosticStatus.WARNING,
524
+ message=message,
218
525
  details=details,
219
526
  )
220
527
  return DiagnosticResult(
@@ -226,10 +533,26 @@ class MCPServicesCheck(BaseDiagnosticCheck):
226
533
  fix_description="Ensure pipx bin directory is in PATH",
227
534
  )
228
535
 
536
+ # Service is accessible - check connection test results
537
+ connection_info = details.get("connection_test", {})
538
+ if connection_info:
539
+ if connection_info.get("connected"):
540
+ response_time = connection_info.get("response_time_ms")
541
+ tools_count = connection_info.get("tools_discovered", 0)
542
+ message = f"Installed, accessible, connection OK ({tools_count} tools, {response_time}ms)"
543
+ status = DiagnosticStatus.OK
544
+ else:
545
+ error = connection_info.get("error", "Unknown error")
546
+ message = f"Installed but connection failed: {error}"
547
+ status = DiagnosticStatus.WARNING
548
+ else:
549
+ message = "Installed and accessible"
550
+ status = DiagnosticStatus.OK
551
+
229
552
  return DiagnosticResult(
230
553
  category=f"MCP Service: {service_name}",
231
- status=DiagnosticStatus.OK,
232
- message="Installed and accessible",
554
+ status=status,
555
+ message=message,
233
556
  details=details,
234
557
  )
235
558
 
@@ -423,40 +746,42 @@ class MCPServicesCheck(BaseDiagnosticCheck):
423
746
  args = kuzu_config.get("args", [])
424
747
  needs_fix = False
425
748
  fix_reason = ""
426
- new_args = None
749
+ # The correct args for kuzu-memory v1.1.0+ are ["mcp", "serve"]
750
+ correct_args = ["mcp", "serve"]
427
751
 
428
- # Check for outdated configurations
429
- if args == ["claude", "mcp-server"]:
752
+ # Check for any configuration that is NOT the correct one
753
+ if args != correct_args:
430
754
  needs_fix = True
431
- fix_reason = "Outdated 'claude mcp-server' format"
432
- new_args = ["mcp", "serve"]
433
- elif args == ["serve"]:
434
- needs_fix = True
435
- fix_reason = "Legacy 'serve' format"
436
- new_args = ["mcp", "serve"]
437
- elif args == ["mcp-server"]:
438
- needs_fix = True
439
- fix_reason = "Incorrect 'mcp-server' format"
440
- new_args = ["mcp", "serve"]
755
+ # Identify the specific issue
756
+ if args == ["claude", "mcp-server"]:
757
+ fix_reason = "Outdated 'claude mcp-server' format (pre-v1.1.0)"
758
+ elif args == ["serve"]:
759
+ fix_reason = "Legacy 'serve' format"
760
+ elif args == ["mcp-server"]:
761
+ fix_reason = "Incorrect 'mcp-server' format"
762
+ elif args == []:
763
+ fix_reason = "Empty args list"
764
+ else:
765
+ fix_reason = f"Incorrect args format: {args}"
441
766
 
442
767
  if needs_fix:
443
- # Offer to auto-fix
768
+ # Log the issue for debugging
444
769
  self.logger.warning(
445
770
  f"Found incorrect kuzu-memory configuration: {fix_reason}. "
446
- f"Current args: {args}"
771
+ f"Current args: {args}, should be: {correct_args}"
447
772
  )
448
773
 
449
774
  # Auto-fix the configuration
450
- fixed = self._fix_kuzu_memory_args(claude_config_path, config, new_args)
775
+ fixed = self._fix_kuzu_memory_args(claude_config_path, config, correct_args)
451
776
 
452
777
  if fixed:
453
778
  return DiagnosticResult(
454
779
  category="kuzu-memory Configuration Fix",
455
780
  status=DiagnosticStatus.OK,
456
- message="Fixed kuzu-memory configuration",
781
+ message=f"Fixed kuzu-memory configuration to use correct args",
457
782
  details={
458
783
  "old_args": args,
459
- "new_args": new_args,
784
+ "new_args": correct_args,
460
785
  "reason": fix_reason,
461
786
  "auto_fixed": True,
462
787
  },
@@ -468,7 +793,7 @@ class MCPServicesCheck(BaseDiagnosticCheck):
468
793
  message="kuzu-memory has incorrect configuration",
469
794
  details={
470
795
  "current_args": args,
471
- "correct_args": new_args,
796
+ "correct_args": correct_args,
472
797
  "reason": fix_reason,
473
798
  "auto_fix_failed": True,
474
799
  },
@@ -476,7 +801,7 @@ class MCPServicesCheck(BaseDiagnosticCheck):
476
801
  fix_description="Fix kuzu-memory configuration manually",
477
802
  )
478
803
 
479
- # Configuration is correct
804
+ # Configuration is correct - args match ["mcp", "serve"]
480
805
  return None
481
806
 
482
807
  except (json.JSONDecodeError, Exception) as e:
@@ -489,18 +814,48 @@ class MCPServicesCheck(BaseDiagnosticCheck):
489
814
  # Save old args before updating
490
815
  old_args = config["mcpServers"]["kuzu-memory"].get("args", [])
491
816
 
817
+ # Log the exact change we're about to make
818
+ self.logger.debug(
819
+ f"Fixing kuzu-memory args: old={old_args}, new={new_args}"
820
+ )
821
+
492
822
  # Create backup
493
823
  backup_path = config_path.with_suffix(".json.backup")
494
824
  with open(backup_path, "w") as f:
495
825
  json.dump(config, f, indent=2)
496
826
 
497
- # Update the configuration
827
+ # Update the configuration - ensure we're setting the exact new_args
498
828
  config["mcpServers"]["kuzu-memory"]["args"] = new_args
499
829
 
830
+ # Verify the update in memory before writing
831
+ if config["mcpServers"]["kuzu-memory"]["args"] != new_args:
832
+ self.logger.error(
833
+ f"Failed to update args in memory! "
834
+ f"Expected {new_args}, got {config['mcpServers']['kuzu-memory']['args']}"
835
+ )
836
+ return False
837
+
500
838
  # Write updated configuration
501
839
  with open(config_path, "w") as f:
502
840
  json.dump(config, f, indent=2)
503
841
 
842
+ # Verify the file was written correctly
843
+ with open(config_path) as f:
844
+ verify_config = json.load(f)
845
+ verify_args = verify_config.get("mcpServers", {}).get("kuzu-memory", {}).get("args", [])
846
+
847
+ if verify_args != new_args:
848
+ self.logger.error(
849
+ f"Configuration write verification failed! "
850
+ f"Expected {new_args}, got {verify_args}"
851
+ )
852
+ # Restore backup
853
+ with open(backup_path) as bf:
854
+ backup_config = json.load(bf)
855
+ with open(config_path, "w") as f:
856
+ json.dump(backup_config, f, indent=2)
857
+ return False
858
+
504
859
  self.logger.info(
505
860
  f"✅ Fixed kuzu-memory configuration in {config_path}\n"
506
861
  f" Changed args from {old_args} to {new_args}\n"
@@ -594,3 +949,41 @@ class MCPServicesCheck(BaseDiagnosticCheck):
594
949
  message=f"Could not check configuration: {e!s}",
595
950
  details={"error": str(e)},
596
951
  )
952
+
953
+ def _ensure_mcp_ticketer_gql_dependency(self) -> bool:
954
+ """Ensure mcp-ticketer has the gql dependency injected."""
955
+ try:
956
+ # First check if mcp-ticketer can import gql
957
+ result = subprocess.run(
958
+ ["pipx", "run", "--spec", "mcp-ticketer", "python", "-c", "import gql"],
959
+ capture_output=True,
960
+ text=True,
961
+ timeout=5,
962
+ check=False,
963
+ )
964
+
965
+ # If import fails, inject the dependency
966
+ if result.returncode != 0:
967
+ self.logger.info("🔧 mcp-ticketer missing gql dependency, fixing...")
968
+
969
+ inject_result = subprocess.run(
970
+ ["pipx", "inject", "mcp-ticketer", "gql"],
971
+ capture_output=True,
972
+ text=True,
973
+ timeout=30,
974
+ check=False,
975
+ )
976
+
977
+ if inject_result.returncode == 0:
978
+ self.logger.info("✅ Successfully injected gql dependency into mcp-ticketer")
979
+ return True
980
+ else:
981
+ self.logger.warning(f"Failed to inject gql dependency: {inject_result.stderr}")
982
+ return False
983
+
984
+ # Dependency already present
985
+ return False
986
+
987
+ except Exception as e:
988
+ self.logger.debug(f"Could not check/fix mcp-ticketer gql dependency: {e}")
989
+ return False