codegraph-engine 3.0.0__2-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 (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +868 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. 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
+ }