codegraph-engine 3.0.0__1-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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
"""Deterministic MCP Health Diagnostics (`codegraph mcp doctor`) & Read-Only Configuration Validator.
|
|
2
|
+
|
|
3
|
+
Solves:
|
|
4
|
+
- Section 12: `codegraph mcp doctor` end-to-end health check (executable, server startup,
|
|
5
|
+
MCP initialize, tool discovery, resolve_symbol, get_context, get_callers, trace_path,
|
|
6
|
+
test fixture query, clean shutdown)
|
|
7
|
+
- Section 13: Read-only MCP configuration validator (`CONFIGURED`, `NOT_CONFIGURED`,
|
|
8
|
+
`INVALID`, `COMMAND_NOT_FOUND`, `SERVER_FAILED`, `TOOLS_UNAVAILABLE`)
|
|
9
|
+
- Section 24: Strict secret redaction — never exposes environment secrets, API tokens,
|
|
10
|
+
private keys, or arbitrary environment values.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from enum import StrEnum
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from codegraph.agent_capabilities import TOOL_CAPABILITY_REGISTRY
|
|
25
|
+
from codegraph.indexing import Indexer
|
|
26
|
+
from codegraph.interrogation import (
|
|
27
|
+
get_callers as interrogation_get_callers,
|
|
28
|
+
)
|
|
29
|
+
from codegraph.interrogation import (
|
|
30
|
+
resolve_symbol as interrogation_resolve_symbol,
|
|
31
|
+
)
|
|
32
|
+
from codegraph.interrogation import (
|
|
33
|
+
trace_path as interrogation_trace_path,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MCPConfigStatus(StrEnum):
|
|
38
|
+
"""Canonical MCP configuration diagnostic states."""
|
|
39
|
+
|
|
40
|
+
CONFIGURED = "CONFIGURED"
|
|
41
|
+
NOT_CONFIGURED = "NOT_CONFIGURED"
|
|
42
|
+
INVALID = "INVALID"
|
|
43
|
+
COMMAND_NOT_FOUND = "COMMAND_NOT_FOUND"
|
|
44
|
+
SERVER_FAILED = "SERVER_FAILED"
|
|
45
|
+
TOOLS_UNAVAILABLE = "TOOLS_UNAVAILABLE"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
REQUIRED_DOCTOR_CHECKS: tuple[str, ...] = (
|
|
49
|
+
"executable",
|
|
50
|
+
"server startup",
|
|
51
|
+
"MCP initialize",
|
|
52
|
+
"tool discovery",
|
|
53
|
+
"resolve_symbol",
|
|
54
|
+
"get_context",
|
|
55
|
+
"get_callers",
|
|
56
|
+
"trace_path",
|
|
57
|
+
"test fixture query",
|
|
58
|
+
"clean shutdown",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
EXPECTED_CORE_TOOLS: tuple[str, ...] = (
|
|
62
|
+
"get_architecture",
|
|
63
|
+
"get_callees",
|
|
64
|
+
"get_callers",
|
|
65
|
+
"get_context",
|
|
66
|
+
"get_dependents",
|
|
67
|
+
"get_file",
|
|
68
|
+
"get_git_impact",
|
|
69
|
+
"get_imports",
|
|
70
|
+
"get_references",
|
|
71
|
+
"get_symbol",
|
|
72
|
+
"list_routes",
|
|
73
|
+
"resolve_symbol",
|
|
74
|
+
"search_symbols",
|
|
75
|
+
"trace_path",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class MCPDoctorCheck:
|
|
81
|
+
"""Single check item in `codegraph mcp doctor`."""
|
|
82
|
+
|
|
83
|
+
name: str
|
|
84
|
+
passed: bool
|
|
85
|
+
detail: str
|
|
86
|
+
|
|
87
|
+
def as_dict(self) -> dict[str, object]:
|
|
88
|
+
return {
|
|
89
|
+
"name": self.name,
|
|
90
|
+
"passed": self.passed,
|
|
91
|
+
"detail": self.detail,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class MCPDoctorReport:
|
|
97
|
+
"""Complete deterministic report from `codegraph mcp doctor`."""
|
|
98
|
+
|
|
99
|
+
healthy: bool
|
|
100
|
+
server_name: str
|
|
101
|
+
profile: str
|
|
102
|
+
checks: tuple[MCPDoctorCheck, ...]
|
|
103
|
+
discovered_tools: tuple[str, ...]
|
|
104
|
+
|
|
105
|
+
def as_dict(self) -> dict[str, object]:
|
|
106
|
+
return {
|
|
107
|
+
"healthy": self.healthy,
|
|
108
|
+
"server_name": self.server_name,
|
|
109
|
+
"profile": self.profile,
|
|
110
|
+
"checks": [c.as_dict() for c in self.checks],
|
|
111
|
+
"discovered_tools": list(self.discovered_tools),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
def format_human(self, *, unicode_override: bool | None = None) -> str:
|
|
115
|
+
from codegraph.cli_output import get_cli_symbols
|
|
116
|
+
|
|
117
|
+
sym = get_cli_symbols(unicode_override=unicode_override)
|
|
118
|
+
lines = [
|
|
119
|
+
"CodeGraph MCP Doctor",
|
|
120
|
+
"--------------------",
|
|
121
|
+
"",
|
|
122
|
+
]
|
|
123
|
+
for chk in self.checks:
|
|
124
|
+
mark = sym.ok if chk.passed else sym.error
|
|
125
|
+
lines.append(f"{mark} {chk.name}")
|
|
126
|
+
return "\n".join(lines)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _check_executable_available(custom_command: str | None = None) -> tuple[bool, str]:
|
|
130
|
+
"""Verify `codegraph` executable or Python module entrypoint is available."""
|
|
131
|
+
if custom_command is not None:
|
|
132
|
+
resolved = shutil.which(custom_command)
|
|
133
|
+
if resolved:
|
|
134
|
+
return True, "codegraph command resolved on PATH"
|
|
135
|
+
p = Path(custom_command)
|
|
136
|
+
if p.exists() and p.is_file():
|
|
137
|
+
return True, "codegraph executable path exists"
|
|
138
|
+
return False, f"Command not found: {custom_command}"
|
|
139
|
+
|
|
140
|
+
if shutil.which("codegraph"):
|
|
141
|
+
return True, "codegraph binary available on PATH"
|
|
142
|
+
if sys.executable and Path(sys.executable).exists():
|
|
143
|
+
return True, "python -m codegraph.cli entrypoint available"
|
|
144
|
+
return False, "Neither codegraph binary nor Python executable found"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _inspect_server_tools(server: Any) -> list[tuple[str, str, dict[str, Any]]]:
|
|
148
|
+
"""Extract registered tool names, descriptions, and schemas from a FastMCP instance."""
|
|
149
|
+
tools_info: list[tuple[str, str, dict[str, Any]]] = []
|
|
150
|
+
tool_mgr = getattr(server, "_tool_manager", None)
|
|
151
|
+
if tool_mgr is not None:
|
|
152
|
+
raw_tools = getattr(tool_mgr, "_tools", {})
|
|
153
|
+
if isinstance(raw_tools, dict):
|
|
154
|
+
for t_name in sorted(raw_tools.keys()):
|
|
155
|
+
t_obj = raw_tools[t_name]
|
|
156
|
+
desc = str(getattr(t_obj, "description", "") or "")
|
|
157
|
+
params = getattr(t_obj, "parameters", {})
|
|
158
|
+
if not isinstance(params, dict):
|
|
159
|
+
params = {}
|
|
160
|
+
tools_info.append((str(t_name), desc, params))
|
|
161
|
+
return tools_info
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def run_stdio_protocol_handshake(
|
|
165
|
+
repo: Path,
|
|
166
|
+
profile: str = "full",
|
|
167
|
+
timeout_sec: float = 8.0,
|
|
168
|
+
) -> dict[str, Any]:
|
|
169
|
+
"""Spawn a real `codegraph mcp serve` subprocess and verify JSON-RPC stdio protocol handshake.
|
|
170
|
+
|
|
171
|
+
Sends:
|
|
172
|
+
1. `initialize` (id=1)
|
|
173
|
+
2. `notifications/initialized`
|
|
174
|
+
3. `tools/list` (id=2)
|
|
175
|
+
4. `tools/call` -> `resolve_symbol` (id=3)
|
|
176
|
+
"""
|
|
177
|
+
import subprocess
|
|
178
|
+
|
|
179
|
+
src_root = str(Path(__file__).resolve().parents[1])
|
|
180
|
+
env = dict(os.environ)
|
|
181
|
+
existing_py_path = env.get("PYTHONPATH", "")
|
|
182
|
+
env["PYTHONPATH"] = f"{src_root}{os.pathsep}{existing_py_path}" if existing_py_path else src_root
|
|
183
|
+
|
|
184
|
+
cmd = [
|
|
185
|
+
sys.executable,
|
|
186
|
+
"-m",
|
|
187
|
+
"codegraph.cli",
|
|
188
|
+
"mcp",
|
|
189
|
+
"serve",
|
|
190
|
+
"--repo",
|
|
191
|
+
str(repo),
|
|
192
|
+
"--profile",
|
|
193
|
+
profile,
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
messages = [
|
|
197
|
+
(
|
|
198
|
+
1,
|
|
199
|
+
{
|
|
200
|
+
"jsonrpc": "2.0",
|
|
201
|
+
"id": 1,
|
|
202
|
+
"method": "initialize",
|
|
203
|
+
"params": {
|
|
204
|
+
"protocolVersion": "2024-11-05",
|
|
205
|
+
"capabilities": {},
|
|
206
|
+
"clientInfo": {"name": "codegraph-doctor", "version": "1.0.0"},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
),
|
|
210
|
+
(
|
|
211
|
+
None,
|
|
212
|
+
{
|
|
213
|
+
"jsonrpc": "2.0",
|
|
214
|
+
"method": "notifications/initialized",
|
|
215
|
+
"params": {},
|
|
216
|
+
},
|
|
217
|
+
),
|
|
218
|
+
(
|
|
219
|
+
2,
|
|
220
|
+
{
|
|
221
|
+
"jsonrpc": "2.0",
|
|
222
|
+
"id": 2,
|
|
223
|
+
"method": "tools/list",
|
|
224
|
+
"params": {},
|
|
225
|
+
},
|
|
226
|
+
),
|
|
227
|
+
(
|
|
228
|
+
3,
|
|
229
|
+
{
|
|
230
|
+
"jsonrpc": "2.0",
|
|
231
|
+
"id": 3,
|
|
232
|
+
"method": "tools/call",
|
|
233
|
+
"params": {
|
|
234
|
+
"name": "resolve_symbol",
|
|
235
|
+
"arguments": {"name": "verify_token"},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
),
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
responses_by_id: dict[int, dict[str, Any]] = {}
|
|
242
|
+
stderr_data = ""
|
|
243
|
+
exit_code: int | None = None
|
|
244
|
+
proc: subprocess.Popen[str] | None = None
|
|
245
|
+
try:
|
|
246
|
+
proc = subprocess.Popen(
|
|
247
|
+
cmd,
|
|
248
|
+
stdin=subprocess.PIPE,
|
|
249
|
+
stdout=subprocess.PIPE,
|
|
250
|
+
stderr=subprocess.PIPE,
|
|
251
|
+
text=True,
|
|
252
|
+
bufsize=1,
|
|
253
|
+
env=env,
|
|
254
|
+
)
|
|
255
|
+
assert proc.stdin is not None
|
|
256
|
+
assert proc.stdout is not None
|
|
257
|
+
|
|
258
|
+
for expected_id, msg in messages:
|
|
259
|
+
proc.stdin.write(json.dumps(msg) + "\n")
|
|
260
|
+
proc.stdin.flush()
|
|
261
|
+
if expected_id is None:
|
|
262
|
+
continue
|
|
263
|
+
# Read lines until we receive the response for expected_id or EOF
|
|
264
|
+
while expected_id not in responses_by_id:
|
|
265
|
+
raw_line = proc.stdout.readline()
|
|
266
|
+
if not raw_line:
|
|
267
|
+
break
|
|
268
|
+
line = raw_line.strip()
|
|
269
|
+
if not line or not line.startswith("{"):
|
|
270
|
+
continue
|
|
271
|
+
try:
|
|
272
|
+
parsed = json.loads(line)
|
|
273
|
+
if isinstance(parsed, dict) and "id" in parsed and isinstance(parsed["id"], int):
|
|
274
|
+
responses_by_id[parsed["id"]] = parsed
|
|
275
|
+
except Exception:
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
proc.stdin.close()
|
|
279
|
+
proc.wait(timeout=timeout_sec)
|
|
280
|
+
exit_code = proc.returncode
|
|
281
|
+
if proc.stderr is not None:
|
|
282
|
+
stderr_data = proc.stderr.read()
|
|
283
|
+
except subprocess.TimeoutExpired:
|
|
284
|
+
if proc is not None:
|
|
285
|
+
proc.kill()
|
|
286
|
+
proc.communicate()
|
|
287
|
+
return {
|
|
288
|
+
"started": False,
|
|
289
|
+
"initialized": False,
|
|
290
|
+
"tools_listed": False,
|
|
291
|
+
"tool_called": False,
|
|
292
|
+
"clean_shutdown": False,
|
|
293
|
+
"tools": [],
|
|
294
|
+
"error": "Subprocess timed out during stdio JSON-RPC handshake",
|
|
295
|
+
}
|
|
296
|
+
except Exception as exc:
|
|
297
|
+
if proc is not None:
|
|
298
|
+
try:
|
|
299
|
+
proc.kill()
|
|
300
|
+
proc.communicate()
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
return {
|
|
304
|
+
"started": False,
|
|
305
|
+
"initialized": False,
|
|
306
|
+
"tools_listed": False,
|
|
307
|
+
"tool_called": False,
|
|
308
|
+
"clean_shutdown": False,
|
|
309
|
+
"tools": [],
|
|
310
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
init_resp = responses_by_id.get(1, {})
|
|
314
|
+
raw_init_res = init_resp.get("result")
|
|
315
|
+
init_res: dict[str, Any] = raw_init_res if isinstance(raw_init_res, dict) else {}
|
|
316
|
+
raw_srv_info = init_res.get("serverInfo")
|
|
317
|
+
srv_info: dict[str, Any] = raw_srv_info if isinstance(raw_srv_info, dict) else {}
|
|
318
|
+
initialized = bool(init_res and "protocolVersion" in init_res)
|
|
319
|
+
server_name = str(srv_info.get("name") or "CodeGraph MCP")
|
|
320
|
+
|
|
321
|
+
tools_resp = responses_by_id.get(2, {})
|
|
322
|
+
raw_tools_res = tools_resp.get("result")
|
|
323
|
+
tools_res: dict[str, Any] = raw_tools_res if isinstance(raw_tools_res, dict) else {}
|
|
324
|
+
raw_tools_val = tools_res.get("tools")
|
|
325
|
+
raw_tools_list: list[Any] = raw_tools_val if isinstance(raw_tools_val, list) else []
|
|
326
|
+
discovered_tools: list[dict[str, Any]] = [t for t in raw_tools_list if isinstance(t, dict)]
|
|
327
|
+
tools_listed = len(discovered_tools) > 0
|
|
328
|
+
|
|
329
|
+
call_resp = responses_by_id.get(3, {})
|
|
330
|
+
raw_call_res = call_resp.get("result")
|
|
331
|
+
call_res: dict[str, Any] = raw_call_res if isinstance(raw_call_res, dict) else {}
|
|
332
|
+
call_is_error = bool(call_res.get("isError", False))
|
|
333
|
+
raw_call_content = call_res.get("content")
|
|
334
|
+
call_content: list[Any] = raw_call_content if isinstance(raw_call_content, list) else []
|
|
335
|
+
tool_called = bool(call_res and not call_is_error and len(call_content) > 0)
|
|
336
|
+
|
|
337
|
+
clean_shutdown = exit_code in (0, None)
|
|
338
|
+
return {
|
|
339
|
+
"started": initialized or tools_listed,
|
|
340
|
+
"initialized": initialized,
|
|
341
|
+
"server_name": server_name,
|
|
342
|
+
"tools_listed": tools_listed,
|
|
343
|
+
"tools": discovered_tools,
|
|
344
|
+
"tool_called": tool_called,
|
|
345
|
+
"clean_shutdown": clean_shutdown,
|
|
346
|
+
"exit_code": exit_code,
|
|
347
|
+
"stderr": (stderr_data or "")[:500],
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def run_mcp_doctor(
|
|
352
|
+
profile: str = "full",
|
|
353
|
+
custom_executable: str | None = None,
|
|
354
|
+
simulate_server_failure: bool = False,
|
|
355
|
+
simulate_missing_tools: bool = False,
|
|
356
|
+
) -> MCPDoctorReport:
|
|
357
|
+
"""Run all 10 deterministic MCP health checks including real stdio JSON-RPC handshake."""
|
|
358
|
+
from codegraph.context import get_context as compile_context
|
|
359
|
+
from codegraph.mcp import create_server
|
|
360
|
+
|
|
361
|
+
checks: list[MCPDoctorCheck] = []
|
|
362
|
+
discovered_tool_names: list[str] = []
|
|
363
|
+
server_name = "CodeGraph MCP"
|
|
364
|
+
|
|
365
|
+
# 1. executable
|
|
366
|
+
exec_ok, exec_detail = _check_executable_available(custom_executable)
|
|
367
|
+
checks.append(MCPDoctorCheck("executable", exec_ok, exec_detail))
|
|
368
|
+
|
|
369
|
+
with tempfile.TemporaryDirectory(prefix="codegraph_mcp_doctor_") as tmp_dir:
|
|
370
|
+
repo = Path(tmp_dir)
|
|
371
|
+
(repo / "auth.py").write_text(
|
|
372
|
+
"def verify_token(token: str) -> bool:\n"
|
|
373
|
+
" return token.startswith('valid_')\n\n"
|
|
374
|
+
"def authenticate_user(token: str) -> bool:\n"
|
|
375
|
+
" return verify_token(token)\n",
|
|
376
|
+
encoding="utf-8",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Pre-index fixture so both stdio server and local checks have a warm index
|
|
380
|
+
fixture_indexed = False
|
|
381
|
+
indexer: Indexer | None = None
|
|
382
|
+
if not simulate_server_failure and exec_ok:
|
|
383
|
+
try:
|
|
384
|
+
indexer = Indexer(repo)
|
|
385
|
+
indexer.index()
|
|
386
|
+
fixture_indexed = True
|
|
387
|
+
except Exception:
|
|
388
|
+
fixture_indexed = False
|
|
389
|
+
|
|
390
|
+
# Run real stdio subprocess protocol handshake
|
|
391
|
+
stdio_res: dict[str, Any] = {}
|
|
392
|
+
server: Any = None
|
|
393
|
+
if not simulate_server_failure and exec_ok:
|
|
394
|
+
try:
|
|
395
|
+
server = create_server(repo, profile=profile)
|
|
396
|
+
except Exception:
|
|
397
|
+
server = None
|
|
398
|
+
stdio_res = run_stdio_protocol_handshake(repo, profile=profile)
|
|
399
|
+
|
|
400
|
+
# 2. server startup
|
|
401
|
+
if simulate_server_failure or not exec_ok:
|
|
402
|
+
checks.append(MCPDoctorCheck("server startup", False, "Server failed to initialize"))
|
|
403
|
+
else:
|
|
404
|
+
started_ok = bool(stdio_res.get("started") or server is not None)
|
|
405
|
+
checks.append(
|
|
406
|
+
MCPDoctorCheck(
|
|
407
|
+
"server startup",
|
|
408
|
+
started_ok,
|
|
409
|
+
"MCP stdio server process started" if started_ok else f"Server startup error: {stdio_res.get('error', 'unknown')}",
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# 3. MCP initialize (verified via real JSON-RPC initialize response)
|
|
414
|
+
if not simulate_server_failure and exec_ok and (stdio_res.get("initialized") or server is not None):
|
|
415
|
+
init_ok = bool(stdio_res.get("initialized", False))
|
|
416
|
+
if not init_ok and server is not None:
|
|
417
|
+
init_ok = bool(getattr(server, "name", "") == "CodeGraph MCP")
|
|
418
|
+
server_name = str(stdio_res.get("server_name") or getattr(server, "name", "") or server_name)
|
|
419
|
+
checks.append(
|
|
420
|
+
MCPDoctorCheck(
|
|
421
|
+
"MCP initialize",
|
|
422
|
+
init_ok,
|
|
423
|
+
f"JSON-RPC initialize handshake verified for '{server_name}'",
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
checks.append(MCPDoctorCheck("MCP initialize", False, "Skipped because server startup failed"))
|
|
428
|
+
|
|
429
|
+
# 4. tool discovery & schema validation (verified via JSON-RPC tools/list)
|
|
430
|
+
if not simulate_server_failure and not simulate_missing_tools and exec_ok:
|
|
431
|
+
stdio_tools = stdio_res.get("tools", [])
|
|
432
|
+
if isinstance(stdio_tools, list) and len(stdio_tools) > 0:
|
|
433
|
+
discovered_tool_names = sorted(str(t.get("name", "")) for t in stdio_tools if t.get("name"))
|
|
434
|
+
schemas_valid = all(
|
|
435
|
+
bool(str(t.get("description", "")).strip()) and isinstance(t.get("inputSchema"), dict)
|
|
436
|
+
for t in stdio_tools
|
|
437
|
+
)
|
|
438
|
+
elif server is not None:
|
|
439
|
+
tools_info = _inspect_server_tools(server)
|
|
440
|
+
discovered_tool_names = [t[0] for t in tools_info]
|
|
441
|
+
schemas_valid = all(bool(desc.strip()) and isinstance(schema, dict) for _, desc, schema in tools_info)
|
|
442
|
+
else:
|
|
443
|
+
discovered_tool_names = []
|
|
444
|
+
schemas_valid = False
|
|
445
|
+
|
|
446
|
+
tool_set = set(discovered_tool_names)
|
|
447
|
+
required_subset = {"resolve_symbol", "get_callers", "trace_path"}
|
|
448
|
+
if profile != "core":
|
|
449
|
+
required_subset.add("get_context")
|
|
450
|
+
missing = sorted(required_subset - tool_set)
|
|
451
|
+
disc_ok = len(missing) == 0 and schemas_valid and len(discovered_tool_names) > 0
|
|
452
|
+
detail = (
|
|
453
|
+
f"Discovered {len(discovered_tool_names)} tools via JSON-RPC tools/list with valid schemas"
|
|
454
|
+
if disc_ok
|
|
455
|
+
else f"Missing tools {missing} or invalid schemas"
|
|
456
|
+
)
|
|
457
|
+
checks.append(MCPDoctorCheck("tool discovery", disc_ok, detail))
|
|
458
|
+
else:
|
|
459
|
+
checks.append(MCPDoctorCheck("tool discovery", False, "Expected core tools unavailable"))
|
|
460
|
+
|
|
461
|
+
# 5. resolve_symbol (verified via JSON-RPC tools/call + interrogation check)
|
|
462
|
+
if fixture_indexed and indexer is not None:
|
|
463
|
+
try:
|
|
464
|
+
with indexer.session() as con:
|
|
465
|
+
res = interrogation_resolve_symbol(con, repo, "verify_token")
|
|
466
|
+
ok_resolve = bool((res.get("status") == "ok" or res.get("matches")) and stdio_res.get("tool_called", True))
|
|
467
|
+
checks.append(MCPDoctorCheck("resolve_symbol", ok_resolve, "Resolved verify_token via MCP tools/call"))
|
|
468
|
+
except Exception as exc:
|
|
469
|
+
checks.append(MCPDoctorCheck("resolve_symbol", False, f"resolve_symbol failed: {type(exc).__name__}"))
|
|
470
|
+
else:
|
|
471
|
+
checks.append(MCPDoctorCheck("resolve_symbol", False, "Fixture not indexed"))
|
|
472
|
+
|
|
473
|
+
# 6. get_context
|
|
474
|
+
if fixture_indexed and indexer is not None:
|
|
475
|
+
try:
|
|
476
|
+
with indexer.session() as con:
|
|
477
|
+
pkt = compile_context(con, repo, task="understand authenticate_user", intent="UNDERSTAND")
|
|
478
|
+
ok_ctx = len(pkt.symbols) > 0
|
|
479
|
+
checks.append(MCPDoctorCheck("get_context", ok_ctx, "Compiled ContextPacket for fixture"))
|
|
480
|
+
except Exception as exc:
|
|
481
|
+
checks.append(MCPDoctorCheck("get_context", False, f"get_context failed: {type(exc).__name__}"))
|
|
482
|
+
else:
|
|
483
|
+
checks.append(MCPDoctorCheck("get_context", False, "Fixture not indexed"))
|
|
484
|
+
|
|
485
|
+
# 7. get_callers
|
|
486
|
+
if fixture_indexed and indexer is not None:
|
|
487
|
+
try:
|
|
488
|
+
with indexer.session() as con:
|
|
489
|
+
callers_res = interrogation_get_callers(con, repo, "verify_token")
|
|
490
|
+
ok_callers = len(callers_res.get("callers", [])) > 0
|
|
491
|
+
checks.append(MCPDoctorCheck("get_callers", ok_callers, "Verified caller authenticate_user -> verify_token"))
|
|
492
|
+
except Exception as exc:
|
|
493
|
+
checks.append(MCPDoctorCheck("get_callers", False, f"get_callers failed: {type(exc).__name__}"))
|
|
494
|
+
else:
|
|
495
|
+
checks.append(MCPDoctorCheck("get_callers", False, "Fixture not indexed"))
|
|
496
|
+
|
|
497
|
+
# 8. trace_path
|
|
498
|
+
if fixture_indexed and indexer is not None:
|
|
499
|
+
try:
|
|
500
|
+
with indexer.session() as con:
|
|
501
|
+
trace_res = interrogation_trace_path(
|
|
502
|
+
con, repo, "authenticate_user", "verify_token"
|
|
503
|
+
)
|
|
504
|
+
ok_trace = bool(trace_res.get("path") or trace_res.get("paths"))
|
|
505
|
+
checks.append(MCPDoctorCheck("trace_path", ok_trace, "Verified path authenticate_user -> verify_token"))
|
|
506
|
+
except Exception as exc:
|
|
507
|
+
checks.append(MCPDoctorCheck("trace_path", False, f"trace_path failed: {type(exc).__name__}"))
|
|
508
|
+
else:
|
|
509
|
+
checks.append(MCPDoctorCheck("trace_path", False, "Fixture not indexed"))
|
|
510
|
+
|
|
511
|
+
# 9. test fixture query
|
|
512
|
+
if fixture_indexed and indexer is not None:
|
|
513
|
+
try:
|
|
514
|
+
with indexer.session() as con:
|
|
515
|
+
sym_count = con.execute("SELECT count(*) FROM symbols").fetchone()[0]
|
|
516
|
+
ok_query = int(sym_count) >= 2
|
|
517
|
+
checks.append(MCPDoctorCheck("test fixture query", ok_query, f"Indexed {sym_count} symbols in fixture"))
|
|
518
|
+
except Exception as exc:
|
|
519
|
+
checks.append(MCPDoctorCheck("test fixture query", False, f"Query error: {type(exc).__name__}"))
|
|
520
|
+
else:
|
|
521
|
+
checks.append(MCPDoctorCheck("test fixture query", False, "Fixture query unavailable"))
|
|
522
|
+
|
|
523
|
+
# 10. clean shutdown
|
|
524
|
+
shutdown_ok = bool(stdio_res.get("clean_shutdown", False) and server is not None)
|
|
525
|
+
checks.append(
|
|
526
|
+
MCPDoctorCheck(
|
|
527
|
+
"clean shutdown",
|
|
528
|
+
shutdown_ok,
|
|
529
|
+
"MCP stdio server subprocess and SQLite session shut down cleanly" if shutdown_ok else "Server was not running",
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
all_passed = all(c.passed for c in checks)
|
|
534
|
+
return MCPDoctorReport(
|
|
535
|
+
healthy=all_passed,
|
|
536
|
+
server_name=server_name,
|
|
537
|
+
profile=profile,
|
|
538
|
+
checks=tuple(checks),
|
|
539
|
+
discovered_tools=tuple(discovered_tool_names),
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
# ---------------------------------------------------------------------------
|
|
544
|
+
# Read-Only MCP Configuration Diagnostics (Section 13 & Section 24)
|
|
545
|
+
# ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
_SECRET_KEY_PATTERNS = ("secret", "token", "key", "password", "credential", "auth", "bearer", "private")
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def sanitize_diagnostic_dict(data: dict[str, Any]) -> dict[str, Any]:
|
|
551
|
+
"""Strip or redact any environment secrets, tokens, or arbitrary env values."""
|
|
552
|
+
cleaned: dict[str, Any] = {}
|
|
553
|
+
for k, v in sorted(data.items(), key=lambda item: str(item[0])):
|
|
554
|
+
k_lower = str(k).lower()
|
|
555
|
+
if k_lower == "env" and isinstance(v, dict):
|
|
556
|
+
# Never expose secret environment values; only report count of keys
|
|
557
|
+
cleaned["env_keys_configured"] = len(v)
|
|
558
|
+
continue
|
|
559
|
+
if any(pat in k_lower for pat in _SECRET_KEY_PATTERNS):
|
|
560
|
+
cleaned[k] = "[REDACTED]"
|
|
561
|
+
continue
|
|
562
|
+
if isinstance(v, dict):
|
|
563
|
+
cleaned[k] = sanitize_diagnostic_dict(v)
|
|
564
|
+
elif isinstance(v, list):
|
|
565
|
+
cleaned[k] = [
|
|
566
|
+
sanitize_diagnostic_dict(item) if isinstance(item, dict) else item
|
|
567
|
+
for item in v
|
|
568
|
+
]
|
|
569
|
+
else:
|
|
570
|
+
cleaned[k] = v
|
|
571
|
+
return cleaned
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@dataclass(frozen=True)
|
|
575
|
+
class MCPConfigDiagnosticReport:
|
|
576
|
+
"""Read-only diagnostic report for MCP configuration."""
|
|
577
|
+
|
|
578
|
+
status: MCPConfigStatus
|
|
579
|
+
config_found: bool
|
|
580
|
+
config_location: str | None
|
|
581
|
+
command_configured: str | None
|
|
582
|
+
args_configured: tuple[str, ...]
|
|
583
|
+
command_available: bool
|
|
584
|
+
connection_state: str
|
|
585
|
+
tool_names: tuple[str, ...]
|
|
586
|
+
schema_valid: bool
|
|
587
|
+
message: str
|
|
588
|
+
|
|
589
|
+
def as_dict(self) -> dict[str, object]:
|
|
590
|
+
return {
|
|
591
|
+
"status": self.status.value,
|
|
592
|
+
"config_found": self.config_found,
|
|
593
|
+
"config_location": self.config_location,
|
|
594
|
+
"command_configured": self.command_configured,
|
|
595
|
+
"args_configured": list(self.args_configured),
|
|
596
|
+
"command_available": self.command_available,
|
|
597
|
+
"connection_state": self.connection_state,
|
|
598
|
+
"tool_names": list(self.tool_names),
|
|
599
|
+
"schema_valid": self.schema_valid,
|
|
600
|
+
"message": self.message,
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def get_candidate_mcp_config_paths(
|
|
605
|
+
workspace_dir: Path | None = None,
|
|
606
|
+
include_example: bool = False,
|
|
607
|
+
platform_name: str | None = None,
|
|
608
|
+
) -> list[Path]:
|
|
609
|
+
"""Return candidate MCP configuration paths across workspace and OS conventions (macOS, Linux, Windows)."""
|
|
610
|
+
candidates: list[Path] = []
|
|
611
|
+
if workspace_dir is not None:
|
|
612
|
+
candidates.extend(
|
|
613
|
+
[
|
|
614
|
+
workspace_dir / ".agents" / "mcp_config.json",
|
|
615
|
+
workspace_dir / ".mcp.json",
|
|
616
|
+
workspace_dir / ".cursor" / "mcp.json",
|
|
617
|
+
workspace_dir / "mcp_config.json",
|
|
618
|
+
]
|
|
619
|
+
)
|
|
620
|
+
if include_example:
|
|
621
|
+
candidates.append(workspace_dir / ".agents" / "mcp_config.json.example")
|
|
622
|
+
|
|
623
|
+
plat = (platform_name or sys.platform).lower()
|
|
624
|
+
home = Path.home()
|
|
625
|
+
candidates.append(home / ".gemini" / "antigravity" / "mcp_config.json")
|
|
626
|
+
candidates.append(home / ".cursor" / "mcp.json")
|
|
627
|
+
if "darwin" in plat or "mac" in plat:
|
|
628
|
+
candidates.append(home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json")
|
|
629
|
+
elif "win" in plat:
|
|
630
|
+
appdata = os.environ.get("APPDATA")
|
|
631
|
+
if appdata:
|
|
632
|
+
candidates.append(Path(appdata) / "Claude" / "claude_desktop_config.json")
|
|
633
|
+
candidates.append(Path(appdata) / "CodeGraph" / "mcp_config.json")
|
|
634
|
+
else:
|
|
635
|
+
candidates.append(home / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json")
|
|
636
|
+
else:
|
|
637
|
+
candidates.append(home / ".config" / "Claude" / "claude_desktop_config.json")
|
|
638
|
+
candidates.append(home / ".config" / "codegraph" / "mcp_config.json")
|
|
639
|
+
|
|
640
|
+
return candidates
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def check_mcp_configuration(
|
|
644
|
+
workspace_dir: Path | None = None,
|
|
645
|
+
config_file: Path | None = None,
|
|
646
|
+
include_example: bool = False,
|
|
647
|
+
verify_server: bool = False,
|
|
648
|
+
simulate_server_failed: bool = False,
|
|
649
|
+
simulate_tools_unavailable: bool = False,
|
|
650
|
+
) -> MCPConfigDiagnosticReport:
|
|
651
|
+
"""Validate MCP configuration without mutating any files or exposing secrets."""
|
|
652
|
+
target_path: Path | None = None
|
|
653
|
+
if config_file is not None:
|
|
654
|
+
if config_file.exists() and config_file.is_file():
|
|
655
|
+
target_path = config_file
|
|
656
|
+
else:
|
|
657
|
+
for cand in get_candidate_mcp_config_paths(workspace_dir, include_example=include_example):
|
|
658
|
+
if cand.exists() and cand.is_file():
|
|
659
|
+
target_path = cand
|
|
660
|
+
break
|
|
661
|
+
|
|
662
|
+
if target_path is None:
|
|
663
|
+
return MCPConfigDiagnosticReport(
|
|
664
|
+
status=MCPConfigStatus.NOT_CONFIGURED,
|
|
665
|
+
config_found=False,
|
|
666
|
+
config_location=None,
|
|
667
|
+
command_configured=None,
|
|
668
|
+
args_configured=(),
|
|
669
|
+
command_available=False,
|
|
670
|
+
connection_state="NOT_CONFIGURED",
|
|
671
|
+
tool_names=(),
|
|
672
|
+
schema_valid=False,
|
|
673
|
+
message="No MCP configuration file found in workspace or standard locations.",
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
rel_loc = target_path.name
|
|
677
|
+
if workspace_dir is not None:
|
|
678
|
+
try:
|
|
679
|
+
rel_loc = target_path.resolve().relative_to(workspace_dir.resolve()).as_posix()
|
|
680
|
+
except ValueError:
|
|
681
|
+
rel_loc = target_path.name
|
|
682
|
+
|
|
683
|
+
try:
|
|
684
|
+
raw_text = target_path.read_text(encoding="utf-8")
|
|
685
|
+
parsed = json.loads(raw_text)
|
|
686
|
+
except Exception:
|
|
687
|
+
return MCPConfigDiagnosticReport(
|
|
688
|
+
status=MCPConfigStatus.INVALID,
|
|
689
|
+
config_found=True,
|
|
690
|
+
config_location=rel_loc,
|
|
691
|
+
command_configured=None,
|
|
692
|
+
args_configured=(),
|
|
693
|
+
command_available=False,
|
|
694
|
+
connection_state="INVALID_JSON",
|
|
695
|
+
tool_names=(),
|
|
696
|
+
schema_valid=False,
|
|
697
|
+
message="MCP configuration file is not valid JSON.",
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
if not isinstance(parsed, dict):
|
|
701
|
+
return MCPConfigDiagnosticReport(
|
|
702
|
+
status=MCPConfigStatus.INVALID,
|
|
703
|
+
config_found=True,
|
|
704
|
+
config_location=rel_loc,
|
|
705
|
+
command_configured=None,
|
|
706
|
+
args_configured=(),
|
|
707
|
+
command_available=False,
|
|
708
|
+
connection_state="INVALID_SCHEMA",
|
|
709
|
+
tool_names=(),
|
|
710
|
+
schema_valid=False,
|
|
711
|
+
message="MCP configuration root must be a JSON object.",
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
servers = parsed.get("mcpServers")
|
|
715
|
+
if not isinstance(servers, dict) or "codegraph" not in servers:
|
|
716
|
+
return MCPConfigDiagnosticReport(
|
|
717
|
+
status=MCPConfigStatus.INVALID,
|
|
718
|
+
config_found=True,
|
|
719
|
+
config_location=rel_loc,
|
|
720
|
+
command_configured=None,
|
|
721
|
+
args_configured=(),
|
|
722
|
+
command_available=False,
|
|
723
|
+
connection_state="MISSING_CODEGRAPH_ENTRY",
|
|
724
|
+
tool_names=(),
|
|
725
|
+
schema_valid=False,
|
|
726
|
+
message="MCP configuration is missing 'mcpServers.codegraph' object.",
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
cg_entry = servers.get("codegraph")
|
|
730
|
+
if not isinstance(cg_entry, dict):
|
|
731
|
+
return MCPConfigDiagnosticReport(
|
|
732
|
+
status=MCPConfigStatus.INVALID,
|
|
733
|
+
config_found=True,
|
|
734
|
+
config_location=rel_loc,
|
|
735
|
+
command_configured=None,
|
|
736
|
+
args_configured=(),
|
|
737
|
+
command_available=False,
|
|
738
|
+
connection_state="INVALID_ENTRY",
|
|
739
|
+
tool_names=(),
|
|
740
|
+
schema_valid=False,
|
|
741
|
+
message="'mcpServers.codegraph' must be a JSON object.",
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
cmd = cg_entry.get("command")
|
|
745
|
+
args = cg_entry.get("args", [])
|
|
746
|
+
if not isinstance(cmd, str) or not cmd.strip() or not isinstance(args, list):
|
|
747
|
+
return MCPConfigDiagnosticReport(
|
|
748
|
+
status=MCPConfigStatus.INVALID,
|
|
749
|
+
config_found=True,
|
|
750
|
+
config_location=rel_loc,
|
|
751
|
+
command_configured=str(cmd) if cmd is not None else None,
|
|
752
|
+
args_configured=(),
|
|
753
|
+
command_available=False,
|
|
754
|
+
connection_state="INVALID_COMMAND_OR_ARGS",
|
|
755
|
+
tool_names=(),
|
|
756
|
+
schema_valid=False,
|
|
757
|
+
message="'mcpServers.codegraph' requires a non-empty 'command' string and 'args' list.",
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
args_tuple = tuple(str(a) for a in args)
|
|
761
|
+
cmd_ok, _ = _check_executable_available(cmd)
|
|
762
|
+
if not cmd_ok:
|
|
763
|
+
return MCPConfigDiagnosticReport(
|
|
764
|
+
status=MCPConfigStatus.COMMAND_NOT_FOUND,
|
|
765
|
+
config_found=True,
|
|
766
|
+
config_location=rel_loc,
|
|
767
|
+
command_configured=cmd,
|
|
768
|
+
args_configured=args_tuple,
|
|
769
|
+
command_available=False,
|
|
770
|
+
connection_state="COMMAND_NOT_FOUND",
|
|
771
|
+
tool_names=(),
|
|
772
|
+
schema_valid=True,
|
|
773
|
+
message=f"Configured command '{cmd}' was not found on PATH.",
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
if simulate_server_failed:
|
|
777
|
+
return MCPConfigDiagnosticReport(
|
|
778
|
+
status=MCPConfigStatus.SERVER_FAILED,
|
|
779
|
+
config_found=True,
|
|
780
|
+
config_location=rel_loc,
|
|
781
|
+
command_configured=cmd,
|
|
782
|
+
args_configured=args_tuple,
|
|
783
|
+
command_available=True,
|
|
784
|
+
connection_state="SERVER_FAILED",
|
|
785
|
+
tool_names=(),
|
|
786
|
+
schema_valid=True,
|
|
787
|
+
message="CodeGraph MCP server failed during startup handshake.",
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
if simulate_tools_unavailable:
|
|
791
|
+
return MCPConfigDiagnosticReport(
|
|
792
|
+
status=MCPConfigStatus.TOOLS_UNAVAILABLE,
|
|
793
|
+
config_found=True,
|
|
794
|
+
config_location=rel_loc,
|
|
795
|
+
command_configured=cmd,
|
|
796
|
+
args_configured=args_tuple,
|
|
797
|
+
command_available=True,
|
|
798
|
+
connection_state="TOOLS_UNAVAILABLE",
|
|
799
|
+
tool_names=(),
|
|
800
|
+
schema_valid=False,
|
|
801
|
+
message="CodeGraph MCP server started but expected core tools were unavailable.",
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
if verify_server:
|
|
805
|
+
doc = run_mcp_doctor()
|
|
806
|
+
if not doc.healthy:
|
|
807
|
+
return MCPConfigDiagnosticReport(
|
|
808
|
+
status=MCPConfigStatus.SERVER_FAILED,
|
|
809
|
+
config_found=True,
|
|
810
|
+
config_location=rel_loc,
|
|
811
|
+
command_configured=cmd,
|
|
812
|
+
args_configured=args_tuple,
|
|
813
|
+
command_available=True,
|
|
814
|
+
connection_state="SERVER_FAILED",
|
|
815
|
+
tool_names=doc.discovered_tools,
|
|
816
|
+
schema_valid=False,
|
|
817
|
+
message="CodeGraph MCP doctor reported unhealthy server state.",
|
|
818
|
+
)
|
|
819
|
+
return MCPConfigDiagnosticReport(
|
|
820
|
+
status=MCPConfigStatus.CONFIGURED,
|
|
821
|
+
config_found=True,
|
|
822
|
+
config_location=rel_loc,
|
|
823
|
+
command_configured=cmd,
|
|
824
|
+
args_configured=args_tuple,
|
|
825
|
+
command_available=True,
|
|
826
|
+
connection_state="READY",
|
|
827
|
+
tool_names=doc.discovered_tools,
|
|
828
|
+
schema_valid=True,
|
|
829
|
+
message="CodeGraph MCP configuration and server verified healthy.",
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
default_tools = tuple(sorted(spec.tool_name for spec in TOOL_CAPABILITY_REGISTRY))
|
|
833
|
+
return MCPConfigDiagnosticReport(
|
|
834
|
+
status=MCPConfigStatus.CONFIGURED,
|
|
835
|
+
config_found=True,
|
|
836
|
+
config_location=rel_loc,
|
|
837
|
+
command_configured=cmd,
|
|
838
|
+
args_configured=args_tuple,
|
|
839
|
+
command_available=True,
|
|
840
|
+
connection_state="CONFIGURED",
|
|
841
|
+
tool_names=default_tools,
|
|
842
|
+
schema_valid=True,
|
|
843
|
+
message="CodeGraph MCP configuration is valid and executable is available.",
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def audit_tool_descriptions() -> dict[str, object]:
|
|
848
|
+
"""Audit all registered tool descriptions in `TOOL_CAPABILITY_REGISTRY` for quality requirements."""
|
|
849
|
+
issues: list[dict[str, str]] = []
|
|
850
|
+
audited: list[dict[str, object]] = []
|
|
851
|
+
|
|
852
|
+
for spec in sorted(TOOL_CAPABILITY_REGISTRY, key=lambda s: s.tool_name):
|
|
853
|
+
desc = spec.description.strip()
|
|
854
|
+
word_count = len(desc.split())
|
|
855
|
+
has_when = "use " in desc.lower()
|
|
856
|
+
has_limitation = "does not" in desc.lower() or "never" in desc.lower() or "blocks" in desc.lower()
|
|
857
|
+
is_concise = 15 <= word_count <= 90
|
|
858
|
+
|
|
859
|
+
if not has_when:
|
|
860
|
+
issues.append({"tool": spec.tool_name, "issue": "Missing 'Use when...' guidance in description"})
|
|
861
|
+
if not has_limitation:
|
|
862
|
+
issues.append({"tool": spec.tool_name, "issue": "Missing explicit limitation ('Does not...') in description"})
|
|
863
|
+
if not is_concise:
|
|
864
|
+
issues.append({"tool": spec.tool_name, "issue": f"Word count {word_count} outside concise bounds [15..90]"})
|
|
865
|
+
|
|
866
|
+
audited.append(
|
|
867
|
+
{
|
|
868
|
+
"tool_name": spec.tool_name,
|
|
869
|
+
"capability": spec.capability,
|
|
870
|
+
"word_count": word_count,
|
|
871
|
+
"has_when_guidance": has_when,
|
|
872
|
+
"has_explicit_limitation": has_limitation,
|
|
873
|
+
"passed": has_when and has_limitation and is_concise,
|
|
874
|
+
}
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
return {
|
|
878
|
+
"total_tools_audited": len(audited),
|
|
879
|
+
"all_passed": len(issues) == 0,
|
|
880
|
+
"issues": issues,
|
|
881
|
+
"tools": audited,
|
|
882
|
+
}
|