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
claude_mpm/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
4.4.
|
1
|
+
4.4.10
|
claude_mpm/scripts/mcp_server.py
CHANGED
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.
|
131
|
-
message = f"All {total_services} MCP services installed
|
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=
|
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=
|
232
|
-
message=
|
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
|
-
|
749
|
+
# The correct args for kuzu-memory v1.1.0+ are ["mcp", "serve"]
|
750
|
+
correct_args = ["mcp", "serve"]
|
427
751
|
|
428
|
-
# Check for
|
429
|
-
if args
|
752
|
+
# Check for any configuration that is NOT the correct one
|
753
|
+
if args != correct_args:
|
430
754
|
needs_fix = True
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
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
|
-
#
|
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,
|
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":
|
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":
|
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
|