context-mcp-server 1.1.3 → 1.1.4
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.
- package/codegraph/__pycache__/callflow_html.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/export.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/tree_html.cpython-313.pyc +0 -0
- package/codegraph/callflow_html.py +4 -2
- package/codegraph/export.py +24 -9
- package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/ast_extractor.py +34 -7
- package/codegraph/graph/__pycache__/symbol_resolution.cpython-313.pyc +0 -0
- package/codegraph/graph/symbol_resolution.py +10 -2
- package/codegraph/server.py +7 -3
- package/codegraph/tree_html.py +28 -30
- package/package.json +1 -1
- package/pyproject.toml +72 -72
- package/src/db.js +4 -1
- package/src/search.js +4 -9
- package/src/server.js +16 -7
- package/src/templates/antigravity/GEMINI.md +18 -6
- package/src/templates/claude/CLAUDE.md +14 -1
- package/src/templates/claude/skills/SKILL.md +15 -3
- package/src/templates/codex/AGENTS.md +9 -2
- package/src/templates/cursor/cursor-rules.mdc +13 -4
- package/src/templates/gemini/GEMINI.md +14 -3
- package/src/templates/windsurf/windsurf-rules.md +14 -3
- package/src/tools/codegraph.js +4 -1
- package/src/tools/context.js +6 -6
- package/src/tools/search.js +1 -1
- package/src/tools/symbolDetail.js +74 -0
- package/src/tools/toolRegistry.js +77 -0
- package/src/vector.js +7 -2
- package/uv.lock +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -52,8 +52,10 @@ hr{border:none;border-top:1px solid var(--border);margin:40px 0;}
|
|
|
52
52
|
# ── Mermaid helpers ───────────────────────────────────────────────────────────
|
|
53
53
|
|
|
54
54
|
def _mermaid_id(s: str) -> str:
|
|
55
|
-
"""Sanitize
|
|
56
|
-
|
|
55
|
+
"""Sanitize for Mermaid node ID; include hash suffix to prevent collisions."""
|
|
56
|
+
sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", s)[:32] or "node"
|
|
57
|
+
h = format(hash(s) & 0xFFFF, "04x")
|
|
58
|
+
return f"{sanitized}_{h}"
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
def _mermaid_label(s: str) -> str:
|
package/codegraph/export.py
CHANGED
|
@@ -128,6 +128,10 @@ def to_html(graph_dict: dict, output_path: str) -> str:
|
|
|
128
128
|
<body>
|
|
129
129
|
<div id="graph"></div>
|
|
130
130
|
<div id="sidebar">
|
|
131
|
+
<div id="toolbar">
|
|
132
|
+
<button class="tb-btn" onclick="network.fit()">Fit</button>
|
|
133
|
+
<button class="tb-btn" onclick="network.setOptions({{physics:{{enabled:true}}}});setTimeout(()=>network.setOptions({{physics:{{enabled:false}}}}),2000)">Relayout</button>
|
|
134
|
+
</div>
|
|
131
135
|
<div id="search-wrap">
|
|
132
136
|
<input id="search" type="text" placeholder="Search nodes…" autocomplete="off">
|
|
133
137
|
<div id="search-results"></div>
|
|
@@ -184,14 +188,20 @@ def _html_styles() -> str:
|
|
|
184
188
|
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
|
185
189
|
.legend-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
186
190
|
.legend-count { color: #666; font-size: 11px; }
|
|
187
|
-
#stats { padding: 10px 14px; border-top: 1px solid #2a2a4e; font-size: 11px; color: #
|
|
191
|
+
#stats { padding: 10px 14px; border-top: 1px solid #2a2a4e; font-size: 11px; color: #888; }
|
|
192
|
+
#toolbar { padding: 6px 8px; border-bottom: 1px solid #2a2a4e; display: flex; gap: 6px; }
|
|
193
|
+
.tb-btn { background: #1a1a2e; border: 1px solid #3a3a5e; color: #c0c0d0; border-radius: 4px; padding: 3px 10px; font-size: 11px; cursor: pointer; }
|
|
194
|
+
.tb-btn:hover { background: #2a2a4e; border-color: #4E79A7; color: #e0e0e0; }
|
|
188
195
|
#legend-controls { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; padding: 4px 0; }
|
|
189
196
|
#legend-controls label { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; color: #aaa; user-select: none; }
|
|
190
197
|
#legend-controls label:hover { color: #e0e0e0; }
|
|
191
|
-
#select-all-cb { appearance: none; -webkit-appearance: none; width: 14px; height: 14px; border: 1.5px solid #3a3a5e; border-radius: 3px; background: #0f0f1a; cursor: pointer; }
|
|
198
|
+
#select-all-cb { appearance: none; -webkit-appearance: none; width: 14px; height: 14px; border: 1.5px solid #3a3a5e; border-radius: 3px; background: #0f0f1a; cursor: pointer; position: relative; flex-shrink: 0; }
|
|
192
199
|
#select-all-cb:checked { background: #4E79A7; border-color: #4E79A7; }
|
|
200
|
+
#select-all-cb:checked::after { content: "✓"; position: absolute; color: #fff; font-size: 10px; top: -2px; left: 1px; }
|
|
193
201
|
.legend-cb { appearance: none; -webkit-appearance: none; width: 14px; height: 14px; border: 1.5px solid #3a3a5e; border-radius: 3px; background: #0f0f1a; cursor: pointer; position: relative; flex-shrink: 0; }
|
|
194
202
|
.legend-cb:checked { background: #4E79A7; border-color: #4E79A7; }
|
|
203
|
+
.legend-cb:checked::after { content: "✓"; position: absolute; color: #fff; font-size: 10px; top: -2px; left: 1px; }
|
|
204
|
+
.type-badge { display: inline-block; font-size: 10px; padding: 1px 5px; border-radius: 3px; margin-left: 4px; background: #2a2a4e; color: #888; vertical-align: middle; }
|
|
195
205
|
</style>"""
|
|
196
206
|
|
|
197
207
|
|
|
@@ -205,15 +215,21 @@ function esc(s) {{
|
|
|
205
215
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
206
216
|
}}
|
|
207
217
|
|
|
218
|
+
function makeTooltip(html) {{
|
|
219
|
+
const d = document.createElement('div');
|
|
220
|
+
d.style.cssText = 'background:#1a1a2e;border:1px solid #3a3a5e;border-radius:6px;padding:8px 12px;font-size:12px;color:#e0e0e0;max-width:260px;line-height:1.6;';
|
|
221
|
+
d.innerHTML = html;
|
|
222
|
+
return d;
|
|
223
|
+
}}
|
|
208
224
|
const nodesDS = new vis.DataSet(RAW_NODES.map(n => ({{
|
|
209
225
|
id: n.id, label: n.label, color: n.color, size: n.size,
|
|
210
|
-
font: n.font, title: n.title,
|
|
226
|
+
font: n.font, title: makeTooltip(n.title),
|
|
211
227
|
_community: n.community, _community_name: n.community_name,
|
|
212
228
|
_source_file: n.source_file, _file_type: n.file_type, _degree: n.degree,
|
|
213
229
|
}})));
|
|
214
230
|
|
|
215
231
|
const edgesDS = new vis.DataSet(RAW_EDGES.map((e, i) => ({{
|
|
216
|
-
id: i, from: e.from, to: e.to, title: e.title,
|
|
232
|
+
id: i, from: e.from, to: e.to, title: e.title ? makeTooltip(esc(e.title)) : undefined,
|
|
217
233
|
dashes: e.dashes, width: e.width, color: e.color,
|
|
218
234
|
arrows: {{ to: {{ enabled: true, scaleFactor: 0.5 }} }},
|
|
219
235
|
}})));
|
|
@@ -243,11 +259,10 @@ function showInfo(nodeId) {{
|
|
|
243
259
|
return `<span class="neighbor-link" style="border-left-color:${{esc(color)}}" onclick="focusNode(${{JSON.stringify(nid)}})">${{esc(nb ? nb.label : nid)}}</span>`;
|
|
244
260
|
}}).join('');
|
|
245
261
|
document.getElementById('info-content').innerHTML = `
|
|
246
|
-
<div class="field"><b>${{esc(n.label)}}</b></div>
|
|
247
|
-
<div class="field">
|
|
248
|
-
<div class="field">
|
|
249
|
-
<div class="field">
|
|
250
|
-
<div class="field">Degree: ${{n._degree}}</div>
|
|
262
|
+
<div class="field"><b>${{esc(n.label)}}</b><span class="type-badge">${{esc(n._file_type || '?')}}</span></div>
|
|
263
|
+
<div class="field">Community: ${{esc(n._community_name || '—')}}</div>
|
|
264
|
+
<div class="field" title="${{esc(n._source_file || '')}}">File: ${{esc((n._source_file || '—').split('/').pop() || n._source_file || '—')}}</div>
|
|
265
|
+
<div class="field">Connections: ${{n._degree}}</div>
|
|
251
266
|
${{neighborIds.length ? `<div style="margin-top:8px;color:#aaa;font-size:11px">Neighbors (${{neighborIds.length}})</div><div id="neighbors-list">${{neighborItems}}</div>` : ''}}
|
|
252
267
|
`;
|
|
253
268
|
}}
|
|
Binary file
|
|
@@ -326,14 +326,35 @@ def _extract_with_treesitter(source: bytes, rel_path: str, cfg: dict) -> list[di
|
|
|
326
326
|
import_names: list[str] = []
|
|
327
327
|
for node in _walk(root, cfg["import_types"]):
|
|
328
328
|
text = node.text.decode("utf-8", errors="ignore").strip()
|
|
329
|
-
|
|
329
|
+
raw = None
|
|
330
|
+
# Try 'from "module"' or "from 'module'" first (JS/TS/Python)
|
|
331
|
+
m = re.search(r'from\s+["\']([^"\']+)["\']', text)
|
|
330
332
|
if m:
|
|
331
|
-
raw = m.group(1)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
333
|
+
raw = m.group(1)
|
|
334
|
+
if not raw:
|
|
335
|
+
# Plain: import module or import "module"
|
|
336
|
+
m = re.search(r'import\s+["\']?([a-zA-Z_][\w./\\-]*)["\']?', text)
|
|
337
|
+
if m:
|
|
338
|
+
raw = m.group(1)
|
|
339
|
+
if not raw:
|
|
340
|
+
# require("module")
|
|
341
|
+
m = re.search(r'require\s*\(\s*["\']([^"\']+)["\']\s*\)', text)
|
|
342
|
+
if m:
|
|
343
|
+
raw = m.group(1)
|
|
344
|
+
if raw:
|
|
345
|
+
stem = raw.strip("\"'").replace("\\", "/").split("/")[-1].split(".")[0]
|
|
346
|
+
if stem and stem not in import_names:
|
|
347
|
+
import_names.append(stem)
|
|
348
|
+
# For languages with empty import_types (Ruby, Lua), fall back to regex
|
|
349
|
+
if not import_names and not cfg.get("import_types"):
|
|
350
|
+
source_str = source_bytes.decode("utf-8", errors="ignore")
|
|
351
|
+
ext = Path(rel_path).suffix.lower()
|
|
352
|
+
lang_name = _EXT_TO_LANG_NAME.get(ext, "")
|
|
353
|
+
import_names = _collect_imports_regex(source_str, lang_name)
|
|
354
|
+
|
|
355
|
+
# Always assign (empty list is valid — don't gate on non-empty)
|
|
356
|
+
for entry in nodes:
|
|
357
|
+
entry["imports"] = import_names[:]
|
|
337
358
|
|
|
338
359
|
return nodes
|
|
339
360
|
|
|
@@ -435,6 +456,12 @@ _IMPORT_RE: dict[str, re.Pattern] = {
|
|
|
435
456
|
"rust": re.compile(r'use\s+([\w:]+)', re.MULTILINE),
|
|
436
457
|
"java": re.compile(r'import\s+([\w.]+)', re.MULTILINE),
|
|
437
458
|
"csharp": re.compile(r'using\s+([\w.]+)', re.MULTILINE),
|
|
459
|
+
"ruby": re.compile(r'require(?:_relative)?\s*["\']([^"\']+)["\']', re.MULTILINE),
|
|
460
|
+
"lua": re.compile(r'require\s*\(?["\']([^"\']+)["\']\)?', re.MULTILINE),
|
|
461
|
+
"c": re.compile(r'#include\s+[<"]([^>"]+)[>"]', re.MULTILINE),
|
|
462
|
+
"cpp": re.compile(r'#include\s+[<"]([^>"]+)[>"]', re.MULTILINE),
|
|
463
|
+
"dart": re.compile(r'import\s+["\']([^"\']+)["\']', re.MULTILINE),
|
|
464
|
+
"swift": re.compile(r'^import\s+([\w.]+)', re.MULTILINE),
|
|
438
465
|
}
|
|
439
466
|
|
|
440
467
|
# Keywords that look like calls but aren't
|
|
Binary file
|
|
@@ -92,9 +92,17 @@ def resolve_calls(
|
|
|
92
92
|
if not target_ids:
|
|
93
93
|
target_ids = name_index.get(callee_name, [])
|
|
94
94
|
|
|
95
|
-
#
|
|
96
|
-
if len(target_ids)
|
|
95
|
+
# Resolve ambiguity: prefer match in same directory as caller
|
|
96
|
+
if len(target_ids) == 0:
|
|
97
97
|
continue
|
|
98
|
+
if len(target_ids) > 1:
|
|
99
|
+
caller_dir = str(Path(caller_file.replace("\\", "/")).parent)
|
|
100
|
+
same_dir = [t for t in target_ids
|
|
101
|
+
if t.replace("\\", "/").startswith(caller_dir + "/")]
|
|
102
|
+
if len(same_dir) == 1:
|
|
103
|
+
target_ids = same_dir
|
|
104
|
+
else:
|
|
105
|
+
continue # still ambiguous after narrowing
|
|
98
106
|
|
|
99
107
|
target_id = target_ids[0]
|
|
100
108
|
if target_id == caller_id:
|
package/codegraph/server.py
CHANGED
|
@@ -248,10 +248,13 @@ async def _build(args: dict) -> dict:
|
|
|
248
248
|
save_graph(root, graph_dict)
|
|
249
249
|
generate_report(graph_dict, root)
|
|
250
250
|
save_cache(root, cache)
|
|
251
|
+
|
|
252
|
+
cache_dir = str(Path(root) / "codegraph-cache")
|
|
253
|
+
viz = {}
|
|
251
254
|
try:
|
|
252
|
-
export_all(graph_dict,
|
|
253
|
-
except Exception:
|
|
254
|
-
|
|
255
|
+
viz = export_all(graph_dict, cache_dir) or {}
|
|
256
|
+
except Exception as e:
|
|
257
|
+
viz = {"error": str(e)}
|
|
255
258
|
|
|
256
259
|
elapsed_ms = int((time.time() - t0) * 1000)
|
|
257
260
|
result = {
|
|
@@ -264,6 +267,7 @@ async def _build(args: dict) -> dict:
|
|
|
264
267
|
"deleted": len(deleted),
|
|
265
268
|
"time_ms": elapsed_ms,
|
|
266
269
|
"summary": f"Built graph: {len(graph_dict.get('nodes', []))} nodes from code files.",
|
|
270
|
+
"outputs": viz,
|
|
267
271
|
}
|
|
268
272
|
|
|
269
273
|
return result
|
package/codegraph/tree_html.py
CHANGED
|
@@ -112,24 +112,27 @@ _HTML_TEMPLATE = r"""<!DOCTYPE html>
|
|
|
112
112
|
<meta charset="UTF-8">
|
|
113
113
|
<title>{title}</title>
|
|
114
114
|
<style>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
116
|
+
body {{ font-family: 'Segoe UI', system-ui, sans-serif; background: #0f0f1a; color: #e0e0e0; }}
|
|
117
|
+
header {{ padding: 18px 28px 10px; border-bottom: 1px solid #2a2a4e; }}
|
|
118
|
+
h1 {{ font-size: 1.5rem; font-weight: 600; color: #e0e0e0; letter-spacing: -0.01em; }}
|
|
119
|
+
h1 span {{ color: #4E79A7; }}
|
|
120
|
+
.controls {{ padding: 12px 28px; display: flex; gap: 10px; border-bottom: 1px solid #2a2a4e; }}
|
|
121
|
+
button {{ padding: 6px 16px; background: #1a1a2e; color: #c0c0d0; border: 1px solid #3a3a5e; border-radius: 5px; font-size: 0.85rem; cursor: pointer; }}
|
|
122
|
+
button:hover {{ background: #2a2a4e; border-color: #4E79A7; color: #e0e0e0; }}
|
|
123
|
+
#tree-container {{ width: 100vw; height: calc(100vh - 100px); overflow: auto; }}
|
|
124
|
+
svg {{ background: #0f0f1a; display: block; }}
|
|
125
|
+
.node circle {{ stroke-width: 2px; }}
|
|
126
|
+
.node text {{ font: 12px 'Segoe UI', sans-serif; fill: #c8c8d8; paint-order: stroke fill; stroke: #0f0f1a; stroke-width: 3px; stroke-linejoin: round; stroke-opacity: 0.9; }}
|
|
127
|
+
.link {{ fill: none; stroke-opacity: 0.4; stroke-width: 1.5px; }}
|
|
125
128
|
</style>
|
|
126
129
|
</head>
|
|
127
130
|
<body>
|
|
128
|
-
<h1>{header}</h1>
|
|
131
|
+
<header><h1><span>◈</span> {header}</h1></header>
|
|
129
132
|
<div class="controls">
|
|
130
133
|
<button onclick="expandAll()">Expand All</button>
|
|
131
134
|
<button onclick="collapseAll()">Collapse All</button>
|
|
132
|
-
<button onclick="resetView()">Reset
|
|
135
|
+
<button onclick="resetView()">Reset</button>
|
|
133
136
|
</div>
|
|
134
137
|
<div id="tree-container">
|
|
135
138
|
<svg id="tree-svg" width="{svg_width}" height="{svg_height}"></svg>
|
|
@@ -139,33 +142,28 @@ _HTML_TEMPLATE = r"""<!DOCTYPE html>
|
|
|
139
142
|
const initialJsonData = {data_json};
|
|
140
143
|
function transformData(d) {{
|
|
141
144
|
function p(node, parentL1) {{
|
|
142
|
-
|
|
143
|
-
if (node.total_count !== undefined && !/\(Total Count: \d+\)$/.test(dn))
|
|
144
|
-
dn += ` (Total Count: ${{node.total_count}})`;
|
|
145
|
-
const r = {{ name: dn, originalStageName: parentL1 === "Root" ? node.name : parentL1 }};
|
|
145
|
+
const r = {{ name: node.name, count: node.total_count || 0, originalStageName: parentL1 === "Root" ? node.name : parentL1 }};
|
|
146
146
|
if (node.children && node.children.length > 0)
|
|
147
147
|
r.children = node.children.map(c => p(c, parentL1 === "Root" ? node.name : parentL1));
|
|
148
148
|
return r;
|
|
149
149
|
}}
|
|
150
|
-
|
|
151
|
-
if (d.total_count !== undefined && !/\(Total Count: \d+\)$/.test(rn)) rn += ` (Total Count: ${{d.total_count}})`;
|
|
152
|
-
return {{ name: rn, originalStageName: "Root", children: (d.children || []).map(c => p(c, "Root")) }};
|
|
150
|
+
return {{ name: d.name, count: d.total_count || 0, originalStageName: "Root", children: (d.children || []).map(c => p(c, "Root")) }};
|
|
153
151
|
}}
|
|
154
152
|
const treeData = transformData(initialJsonData);
|
|
155
153
|
const PALETTE = [
|
|
156
|
-
["#
|
|
157
|
-
["#
|
|
158
|
-
["#
|
|
159
|
-
["#
|
|
154
|
+
["#4E79A7","#3a5c84","#1e3050"],["#59A14F","#3d7a42","#1e4020"],
|
|
155
|
+
["#E15759","#b03a3c","#6a1820"],["#B07AA1","#845a78","#4a2040"],
|
|
156
|
+
["#F28E2B","#b8681c","#6a3808"],["#76B7B2","#4d8a85","#1e4040"],
|
|
157
|
+
["#EDC948","#b89c20","#6a5808"],["#FF9DA7","#cc6370","#7a1828"],
|
|
160
158
|
];
|
|
161
|
-
const phaseColors = {{ "Root": {{ fill:"#
|
|
162
|
-
(initialJsonData.children||[]).forEach((c,i) => {{ const pal=PALETTE[i%PALETTE.length]; phaseColors[c.name]={{ fill:pal[0],stroke:pal[
|
|
159
|
+
const phaseColors = {{ "Root": {{ fill:"#2a2a4e",stroke:"#4E79A7",collapsedFill:"#1a1a3e" }}, "Default": {{ fill:"#3a3a5e",stroke:"#5a5a8e",collapsedFill:"#2a2a4e" }} }};
|
|
160
|
+
(initialJsonData.children||[]).forEach((c,i) => {{ const pal=PALETTE[i%PALETTE.length]; phaseColors[c.name]={{ fill:pal[0],stroke:pal[0],collapsedFill:pal[2] }}; }});
|
|
163
161
|
const levelPalettes = {{
|
|
164
|
-
0:{{fill:"#
|
|
165
|
-
2:{{fill:"#
|
|
166
|
-
3:{{fill:"#
|
|
167
|
-
4:{{fill:"#
|
|
168
|
-
default:{{fill:"#
|
|
162
|
+
0:{{fill:"#1a1a3e",stroke:"#4E79A7",collapsedFill:"#0f0f2a"}},
|
|
163
|
+
2:{{fill:"#59A14F",stroke:"#3d7a42",collapsedFill:"#1e4020"}},
|
|
164
|
+
3:{{fill:"#F28E2B",stroke:"#b8681c",collapsedFill:"#6a3808"}},
|
|
165
|
+
4:{{fill:"#B07AA1",stroke:"#845a78",collapsedFill:"#4a2040"}},
|
|
166
|
+
default:{{fill:"#3a3a5e",stroke:"#5a5a8e",collapsedFill:"#2a2a4e"}}
|
|
169
167
|
}};
|
|
170
168
|
const svg = d3.select("#tree-svg");
|
|
171
169
|
const margin = {{top:40,right:120,bottom:80,left:450}};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Persistent AI memory + codebase knowledge graph MCP server. Works across Claude Code, Cursor, Gemini CLI, Codex, Windsurf, VS Code Copilot, Claude.ai, and ChatGPT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/pyproject.toml
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "codegraph-mcp"
|
|
7
|
-
version = "1.1.
|
|
8
|
-
description = "Codebase knowledge graph MCP server — AST extraction, graph queries, community detection"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.11"
|
|
11
|
-
license = { text = "MIT" }
|
|
12
|
-
keywords = ["mcp", "ai", "codegraph", "knowledge-graph", "ast"]
|
|
13
|
-
dependencies = [
|
|
14
|
-
"mcp>=1.0.0",
|
|
15
|
-
"networkx>=3.0",
|
|
16
|
-
"pymupdf>=1.24",
|
|
17
|
-
"tree-sitter>=0.23.0",
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
[project.optional-dependencies]
|
|
21
|
-
leiden = [
|
|
22
|
-
"graspologic>=3.3",
|
|
23
|
-
]
|
|
24
|
-
treesitter = [
|
|
25
|
-
"tree-sitter>=0.23.0",
|
|
26
|
-
"tree-sitter-python",
|
|
27
|
-
"tree-sitter-javascript",
|
|
28
|
-
"tree-sitter-typescript",
|
|
29
|
-
"tree-sitter-go",
|
|
30
|
-
"tree-sitter-rust",
|
|
31
|
-
"tree-sitter-java",
|
|
32
|
-
"tree-sitter-c",
|
|
33
|
-
"tree-sitter-cpp",
|
|
34
|
-
"tree-sitter-c-sharp",
|
|
35
|
-
"tree-sitter-ruby",
|
|
36
|
-
"tree-sitter-php",
|
|
37
|
-
"tree-sitter-swift",
|
|
38
|
-
"tree-sitter-lua",
|
|
39
|
-
"tree-sitter-kotlin",
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
[project.scripts]
|
|
43
|
-
codegraph-mcp = "codegraph.server:main"
|
|
44
|
-
|
|
45
|
-
[tool.hatch.build.targets.wheel]
|
|
46
|
-
packages = ["codegraph"]
|
|
47
|
-
|
|
48
|
-
# ── uv ────────────────────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
[dependency-groups]
|
|
51
|
-
dev = [
|
|
52
|
-
"pytest>=8.0",
|
|
53
|
-
"pytest-asyncio>=0.23",
|
|
54
|
-
"ruff>=0.4",
|
|
55
|
-
"mypy>=1.10",
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
# ── ruff ─────────────────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
[tool.ruff]
|
|
61
|
-
target-version = "py311"
|
|
62
|
-
line-length = 100
|
|
63
|
-
|
|
64
|
-
[tool.ruff.lint]
|
|
65
|
-
select = ["E", "F", "I"]
|
|
66
|
-
ignore = ["E501"]
|
|
67
|
-
|
|
68
|
-
# ── pytest ────────────────────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
[tool.pytest.ini_options]
|
|
71
|
-
asyncio_mode = "auto"
|
|
72
|
-
testpaths = ["tests"]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codegraph-mcp"
|
|
7
|
+
version = "1.1.4"
|
|
8
|
+
description = "Codebase knowledge graph MCP server — AST extraction, graph queries, community detection"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["mcp", "ai", "codegraph", "knowledge-graph", "ast"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"mcp>=1.0.0",
|
|
15
|
+
"networkx>=3.0",
|
|
16
|
+
"pymupdf>=1.24",
|
|
17
|
+
"tree-sitter>=0.23.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
leiden = [
|
|
22
|
+
"graspologic>=3.3",
|
|
23
|
+
]
|
|
24
|
+
treesitter = [
|
|
25
|
+
"tree-sitter>=0.23.0",
|
|
26
|
+
"tree-sitter-python",
|
|
27
|
+
"tree-sitter-javascript",
|
|
28
|
+
"tree-sitter-typescript",
|
|
29
|
+
"tree-sitter-go",
|
|
30
|
+
"tree-sitter-rust",
|
|
31
|
+
"tree-sitter-java",
|
|
32
|
+
"tree-sitter-c",
|
|
33
|
+
"tree-sitter-cpp",
|
|
34
|
+
"tree-sitter-c-sharp",
|
|
35
|
+
"tree-sitter-ruby",
|
|
36
|
+
"tree-sitter-php",
|
|
37
|
+
"tree-sitter-swift",
|
|
38
|
+
"tree-sitter-lua",
|
|
39
|
+
"tree-sitter-kotlin",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
codegraph-mcp = "codegraph.server:main"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["codegraph"]
|
|
47
|
+
|
|
48
|
+
# ── uv ────────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest>=8.0",
|
|
53
|
+
"pytest-asyncio>=0.23",
|
|
54
|
+
"ruff>=0.4",
|
|
55
|
+
"mypy>=1.10",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# ── ruff ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
target-version = "py311"
|
|
62
|
+
line-length = 100
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "I"]
|
|
66
|
+
ignore = ["E501"]
|
|
67
|
+
|
|
68
|
+
# ── pytest ────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
asyncio_mode = "auto"
|
|
72
|
+
testpaths = ["tests"]
|
package/src/db.js
CHANGED
|
@@ -463,7 +463,10 @@ export function searchContext({ query, project, limit = 10, compact = false }) {
|
|
|
463
463
|
}
|
|
464
464
|
const scored = results.map(c => {
|
|
465
465
|
const haystack = `${c.title || ''} ${c.content || ''} ${(Array.isArray(c.tags) ? c.tags : []).join(' ')}`.toLowerCase();
|
|
466
|
-
const score = terms.reduce((s, t) =>
|
|
466
|
+
const score = terms.reduce((s, t) => {
|
|
467
|
+
try { return s + (haystack.match(new RegExp(`\\b${t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'))?.length ?? 0); }
|
|
468
|
+
catch { return s; }
|
|
469
|
+
}, 0);
|
|
467
470
|
return { ...c, score };
|
|
468
471
|
}).filter(c => c.score > 0).sort((a, b) => b.score - a.score);
|
|
469
472
|
const sliced = scored.slice(0, limit).map(({ score, ...c }) => c);
|
package/src/search.js
CHANGED
|
@@ -98,15 +98,10 @@ export function search({ query, mode = 'semantic', project, limit = 10, id, comp
|
|
|
98
98
|
const all = getContext({ limit: 1000 });
|
|
99
99
|
const target = all.find(e => e.id === id || e.id.startsWith(id));
|
|
100
100
|
if (!target) throw new Error(`No entry found with id starting "${id}"`);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const explicit = all.filter(e => explicitIds.has(e.id));
|
|
106
|
-
const semantic = explicitIds.size < limit
|
|
107
|
-
? findRelated(target, all.filter(e => !explicitIds.has(e.id) && e.id !== target.id), limit - explicitIds.size)
|
|
108
|
-
: [];
|
|
109
|
-
return { target, results: [...explicit, ...semantic].slice(0, limit) };
|
|
101
|
+
// ponytail: relations/relatedBy never populated — pure semantic fallback
|
|
102
|
+
const others = all.filter(e => e.id !== target.id);
|
|
103
|
+
const results = findRelated(target, others, limit);
|
|
104
|
+
return { target, results };
|
|
110
105
|
}
|
|
111
106
|
default:
|
|
112
107
|
throw new Error(`Unknown search mode: ${mode}. Use: keyword, semantic, related`);
|
package/src/server.js
CHANGED
|
@@ -9,17 +9,20 @@ import { getConfig } from './config.js';
|
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const { version } = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
11
11
|
|
|
12
|
-
import * as contextTool
|
|
13
|
-
import * as searchTool
|
|
14
|
-
import * as planTool
|
|
15
|
-
import * as errorCheckTool
|
|
16
|
-
import * as fileTool
|
|
17
|
-
import * as gitTool
|
|
18
|
-
import * as codegraphTool
|
|
12
|
+
import * as contextTool from './tools/context.js';
|
|
13
|
+
import * as searchTool from './tools/search.js';
|
|
14
|
+
import * as planTool from './tools/plan.js';
|
|
15
|
+
import * as errorCheckTool from './tools/errorCheck.js';
|
|
16
|
+
import * as fileTool from './tools/fileTools.js';
|
|
17
|
+
import * as gitTool from './tools/gitTools.js';
|
|
18
|
+
import * as codegraphTool from './tools/codegraph.js';
|
|
19
|
+
import * as symbolDetailTool from './tools/symbolDetail.js';
|
|
20
|
+
import * as toolRegistryTool from './tools/toolRegistry.js';
|
|
19
21
|
|
|
20
22
|
const FILE_TOOL_NAMES = new Set(fileTool.definitions.map(d => d.name));
|
|
21
23
|
const GIT_TOOL_NAMES = new Set(gitTool.definitions.map(d => d.name));
|
|
22
24
|
const CODEGRAPH_TOOL_NAMES = codegraphTool.TOOL_NAMES;
|
|
25
|
+
const REGISTRY_TOOL_NAMES = toolRegistryTool.TOOL_NAMES;
|
|
23
26
|
|
|
24
27
|
export function createServer({ enableFileTools = false, enableGitTools = getConfig().access_git === true } = {}) {
|
|
25
28
|
const state = {
|
|
@@ -43,6 +46,8 @@ export function createServer({ enableFileTools = false, enableGitTools = getConf
|
|
|
43
46
|
if (enableFileTools) tools.push(...fileTool.definitions);
|
|
44
47
|
if (enableGitTools) tools.push(...gitTool.definitions);
|
|
45
48
|
tools.push(...codegraphTool.definitions);
|
|
49
|
+
tools.push(symbolDetailTool.definition);
|
|
50
|
+
tools.push(...toolRegistryTool.definitions);
|
|
46
51
|
return { tools };
|
|
47
52
|
});
|
|
48
53
|
|
|
@@ -73,6 +78,10 @@ export function createServer({ enableFileTools = false, enableGitTools = getConf
|
|
|
73
78
|
result = await gitTool.handle(name, args, state);
|
|
74
79
|
} else if (CODEGRAPH_TOOL_NAMES.has(name)) {
|
|
75
80
|
result = codegraphTool.handle(name, args, state);
|
|
81
|
+
} else if (name === symbolDetailTool.definition.name) {
|
|
82
|
+
result = await symbolDetailTool.handle(args, state);
|
|
83
|
+
} else if (REGISTRY_TOOL_NAMES.has(name)) {
|
|
84
|
+
result = toolRegistryTool.handle(name);
|
|
76
85
|
} else {
|
|
77
86
|
throw new Error(`Unknown tool: ${name}`);
|
|
78
87
|
}
|
|
@@ -74,14 +74,26 @@ Run `ctx search "<query>" --project <project>` before asking the user to re-expl
|
|
|
74
74
|
## 6. ContextGraph CLI
|
|
75
75
|
|
|
76
76
|
```
|
|
77
|
-
ctx graph build <path>
|
|
78
|
-
ctx graph arch <path>
|
|
79
|
-
ctx graph query <path> "<question>"
|
|
80
|
-
ctx graph nodes <path> <type>
|
|
81
|
-
ctx graph report <path>
|
|
77
|
+
ctx graph build <path> → build AST graph (run once, incremental)
|
|
78
|
+
ctx graph arch <path> → module map: files, exports, imports
|
|
79
|
+
ctx graph query <path> "<question>" → structural question about the codebase
|
|
80
|
+
ctx graph nodes <path> <type> → list all nodes of a type (class|function|module|file|struct|table)
|
|
81
|
+
ctx graph report <path> → god nodes, clusters, surprising connections
|
|
82
|
+
ctx graph affected <path> <node> → blast radius — what breaks if X changes?
|
|
82
83
|
```
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
MCP tools (when available):
|
|
86
|
+
- `get_symbol_detail(name, path)` — source code for one function, no full file read
|
|
87
|
+
- `tool_registry()` — which tools have side effects
|
|
88
|
+
- `safety_policy()` — which actions need confirmation
|
|
89
|
+
|
|
90
|
+
Decision rules:
|
|
91
|
+
- **Unknown codebase**: `ctx graph report` first
|
|
92
|
+
- **Before any refactor**: `ctx graph affected <path> <node>` — FIRST
|
|
93
|
+
- **"Show me function X"**: `get_symbol_detail` or `ctx graph query`
|
|
94
|
+
- **`ctx search`** finds past decisions. **`ctx graph query`** finds code. Different tools.
|
|
95
|
+
|
|
96
|
+
Never read files for structure questions.
|
|
85
97
|
|
|
86
98
|
---
|
|
87
99
|
|
|
@@ -122,10 +122,23 @@ codegraph_html(path, formats?) → regenerate visualizations (auto-run
|
|
|
122
122
|
| Where is function X defined? | `codegraph_query node:"X"` |
|
|
123
123
|
| What does module Y depend on? | `codegraph_query question:"what does Y import?"` |
|
|
124
124
|
| What are all the classes? | `codegraph_nodes type:"class"` |
|
|
125
|
-
| Most connected files? | `codegraph_report` |
|
|
125
|
+
| Most connected files / god nodes? | `codegraph_report` |
|
|
126
126
|
| What breaks if I change X? | `codegraph_affected node:"X"` |
|
|
127
|
+
| Show me only the code for function X? | `get_symbol_detail name:"X"` |
|
|
128
|
+
| Which tools have side effects? | `tool_registry` |
|
|
129
|
+
|
|
130
|
+
### When to reach for each graph tool
|
|
131
|
+
|
|
132
|
+
- **Unknown territory** (first look at any codebase): `codegraph_report` — shows bottlenecks + surprises first
|
|
133
|
+
- **"Where is X defined?"**: `codegraph_query node:"X"` — faster than grep
|
|
134
|
+
- **"What does this module do?"**: `codegraph_arch` — static module map, no reads needed
|
|
135
|
+
- **Before any refactor or rename**: `codegraph_affected node:"X"` — see blast radius FIRST
|
|
136
|
+
- **"List all classes/functions"**: `codegraph_nodes type:"class"` or `type:"function"`
|
|
137
|
+
- **Files changed since last session**: `codegraph_build` is incremental — re-run after adding files
|
|
138
|
+
- **"Show me just that function"**: `get_symbol_detail` — avoids reading the whole file
|
|
127
139
|
|
|
128
140
|
**Never read files for structure questions — use graph tools first.**
|
|
141
|
+
**`search` finds past decisions. `codegraph_query` finds code symbols. Different tools.**
|
|
129
142
|
|
|
130
143
|
---
|
|
131
144
|
|
|
@@ -34,7 +34,7 @@ Call `context` tool **before any tool or response** with:
|
|
|
34
34
|
- `rootPath: "<absolute path to git repo root>"` — required for sandbox + graph lookup
|
|
35
35
|
|
|
36
36
|
Returns:
|
|
37
|
-
- `recentEntries` — last 15 entries; newest
|
|
37
|
+
- `recentEntries` — last 15 entries; newest 2 + high-signal entries have full content, rest have 200-char preview
|
|
38
38
|
- `activePlans` — in-progress plans; read them before starting any new work
|
|
39
39
|
- `codegraph` — `{ built: true/false, nodes, edges, communities }`
|
|
40
40
|
- `stats.totalEntries` — if ≥ 20, write a compaction summary before proceeding (see Rule 4)
|
|
@@ -116,10 +116,13 @@ codegraph_build(path) → AST graph: functions, classes, imports, edges
|
|
|
116
116
|
```
|
|
117
117
|
codegraph_arch(path) → module map: every file, exports, imports
|
|
118
118
|
codegraph_query(path, question?, node?) → find symbol or answer structural question
|
|
119
|
-
codegraph_nodes(path, type) → list all nodes of a type
|
|
119
|
+
codegraph_nodes(path, type) → list all nodes of a type (class|function|module|file|struct|table)
|
|
120
120
|
codegraph_report(path) → god nodes, clusters, structural analysis
|
|
121
121
|
codegraph_affected(path, node, depth?) → blast radius BFS — what breaks if X changes?
|
|
122
122
|
codegraph_html(path, formats?) → regenerate visualizations (auto-runs on every build)
|
|
123
|
+
get_symbol_detail(name, path) → source code for one function/class — no full file read
|
|
124
|
+
tool_registry() → which tools have side effects + approval requirements
|
|
125
|
+
safety_policy() → which actions need user confirmation
|
|
123
126
|
```
|
|
124
127
|
|
|
125
128
|
| Question | Tool |
|
|
@@ -130,6 +133,15 @@ codegraph_html(path, formats?) → regenerate visualizations (auto-run
|
|
|
130
133
|
| List all classes/functions | `codegraph_nodes type:"class"` |
|
|
131
134
|
| Most connected / central files | `codegraph_report` |
|
|
132
135
|
| What breaks if I change X? | `codegraph_affected node:"X"` |
|
|
136
|
+
| Show me just the code for function X | `get_symbol_detail name:"X"` |
|
|
137
|
+
| Which tools are dangerous? | `tool_registry` or `safety_policy` |
|
|
138
|
+
|
|
139
|
+
### When to reach for each graph tool
|
|
140
|
+
|
|
141
|
+
- **Unknown territory**: `codegraph_report` first — god nodes + surprises
|
|
142
|
+
- **Before any refactor or rename**: `codegraph_affected` — blast radius FIRST
|
|
143
|
+
- **"Show me just that function"**: `get_symbol_detail` — avoids reading the whole file
|
|
144
|
+
- **`search`** finds past decisions. **`codegraph_query`** finds code symbols. Different tools.
|
|
133
145
|
|
|
134
146
|
---
|
|
135
147
|
|
|
@@ -141,4 +153,4 @@ codegraph_html(path, formats?) → regenerate visualizations (auto-run
|
|
|
141
153
|
4. Compaction at ≥ 20 entries — before starting task
|
|
142
154
|
5. Plan for multi-file work — `status:"done"` deletes it
|
|
143
155
|
6. Search before asking about past work
|
|
144
|
-
7. Graph tools before files
|
|
156
|
+
7. Graph tools before files — `codegraph_affected` before any refactor
|
|
@@ -90,8 +90,15 @@ codegraph_affected(path, node, depth?) -> blast radius BFS — what breaks if
|
|
|
90
90
|
codegraph_html(path, formats?) -> regenerate visualizations on demand
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
Decision rules:
|
|
94
|
+
- Unknown codebase: `codegraph_report` first (god nodes + surprises)
|
|
95
|
+
- "Where is X?": `codegraph_query node:"X"`
|
|
96
|
+
- Before any refactor: `codegraph_affected node:"X"` (blast radius)
|
|
97
|
+
- List all classes: `codegraph_nodes type:"class"`
|
|
98
|
+
- Just the function body: `get_symbol_detail name:"X"` (no full file read)
|
|
99
|
+
- `search` finds past decisions. `codegraph_query` finds code symbols. Different tools.
|
|
100
|
+
|
|
101
|
+
Read files only when you need exact bug or implementation details not in the graph.
|
|
95
102
|
|
|
96
103
|
---
|
|
97
104
|
|
|
@@ -34,18 +34,27 @@ Build once: `codegraph_build(path)` — auto-generates visualizations, then quer
|
|
|
34
34
|
```
|
|
35
35
|
codegraph_arch(path) → module map (files, exports, imports)
|
|
36
36
|
codegraph_query(path, question?, node?) → find symbol or answer structural question
|
|
37
|
-
codegraph_nodes(path, type) → list all nodes of a type
|
|
37
|
+
codegraph_nodes(path, type) → list all nodes of a type (class|function|module|file|struct|table)
|
|
38
38
|
codegraph_report(path) → god nodes, clusters, structural analysis
|
|
39
39
|
codegraph_affected(path, node, depth?) → blast radius — what breaks if X changes?
|
|
40
40
|
codegraph_html(path, formats?) → regenerate visualizations on demand
|
|
41
|
+
get_symbol_detail(name, path) → source code for one function/class — no full file read
|
|
42
|
+
tool_registry() → which tools have side effects + approval requirements
|
|
43
|
+
safety_policy() → which actions need user confirmation before running
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
Decision rules:
|
|
47
|
+
- **Unknown codebase**: `codegraph_report` first (god nodes + surprises)
|
|
48
|
+
- **"Where is X?"**: `codegraph_query node:"X"` — faster than grep
|
|
49
|
+
- **Before any refactor**: `codegraph_affected node:"X"` — see blast radius FIRST
|
|
50
|
+
- **"Show me function X"**: `get_symbol_detail` — no full file read needed
|
|
51
|
+
- **List all classes**: `codegraph_nodes type:"class"`
|
|
52
|
+
- **`search`** finds past decisions. **`codegraph_query`** finds code symbols. Different tools.
|
|
44
53
|
|
|
45
54
|
## Rules
|
|
46
55
|
|
|
47
56
|
1. `context.resume` first — every conversation
|
|
48
57
|
2. Always pass `project`
|
|
49
58
|
3. `search` before asking the user about past work
|
|
50
|
-
4. `
|
|
51
|
-
5. Files only for bugs and logic
|
|
59
|
+
4. Graph tools before reading files — `codegraph_affected` before any refactor
|
|
60
|
+
5. Files only for bugs and exact logic
|
|
@@ -71,13 +71,24 @@ Call `search` before asking user to re-explain past work.
|
|
|
71
71
|
codegraph_build(path) → build AST graph + auto-generate all visualizations
|
|
72
72
|
codegraph_arch(path, limit?) → module map: files, exports, imports
|
|
73
73
|
codegraph_query(path, question?, node?) → find symbol or answer structural question
|
|
74
|
-
codegraph_nodes(path, type) → list all nodes of a type
|
|
75
|
-
codegraph_report(path) → structural analysis
|
|
74
|
+
codegraph_nodes(path, type) → list all nodes of a type (class|function|module|file|struct|table)
|
|
75
|
+
codegraph_report(path) → structural analysis, god nodes, clusters
|
|
76
76
|
codegraph_affected(path, node, depth?) → blast radius BFS — what breaks if X changes?
|
|
77
77
|
codegraph_html(path, formats?) → regenerate visualizations on demand
|
|
78
|
+
get_symbol_detail(name, path) → source code for one function/class — no full file read
|
|
79
|
+
tool_registry() → which tools have side effects + approval requirements
|
|
80
|
+
safety_policy() → which actions need user confirmation
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
Decision rules:
|
|
84
|
+
- **Unknown codebase**: `codegraph_report` first (god nodes + surprises)
|
|
85
|
+
- **"Where is X?"**: `codegraph_query node:"X"`
|
|
86
|
+
- **Before any refactor**: `codegraph_affected node:"X"` — FIRST
|
|
87
|
+
- **"Show me function X"**: `get_symbol_detail` — avoids full file read
|
|
88
|
+
- **List all classes**: `codegraph_nodes type:"class"`
|
|
89
|
+
- **`search`** finds past decisions. **`codegraph_query`** finds code. Different tools.
|
|
90
|
+
|
|
91
|
+
Never read files for structure questions.
|
|
81
92
|
|
|
82
93
|
---
|
|
83
94
|
|
|
@@ -66,13 +66,24 @@ Call `search` before asking user to re-explain past work.
|
|
|
66
66
|
```
|
|
67
67
|
codegraph_arch(path) → module map (files, exports, imports)
|
|
68
68
|
codegraph_query(path, ...) → find specific function/class/file
|
|
69
|
-
codegraph_nodes(path, type) → list all nodes of a type
|
|
70
|
-
codegraph_report(path) → structural analysis
|
|
69
|
+
codegraph_nodes(path, type) → list all nodes of a type (class|function|module|file|struct|table)
|
|
70
|
+
codegraph_report(path) → structural analysis, god nodes, clusters
|
|
71
71
|
codegraph_affected(path, node, depth?) → blast radius — what breaks if X changes?
|
|
72
72
|
codegraph_html(path, formats?) → regenerate visualizations (auto on every build)
|
|
73
|
+
get_symbol_detail(name, path) → source code for one function/class — no full file read
|
|
74
|
+
tool_registry() → which tools have side effects + approval requirements
|
|
75
|
+
safety_policy() → which actions need user confirmation
|
|
73
76
|
```
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
Decision rules:
|
|
79
|
+
- **Unknown codebase**: `codegraph_report` first
|
|
80
|
+
- **"Where is X?"**: `codegraph_query node:"X"`
|
|
81
|
+
- **Before any refactor**: `codegraph_affected node:"X"` — FIRST
|
|
82
|
+
- **"Show me function X"**: `get_symbol_detail` — avoids full file read
|
|
83
|
+
- **List all classes**: `codegraph_nodes type:"class"`
|
|
84
|
+
- **`search`** finds past decisions. **`codegraph_query`** finds code. Different tools.
|
|
85
|
+
|
|
86
|
+
Never read files for structure questions.
|
|
76
87
|
|
|
77
88
|
---
|
|
78
89
|
|
package/src/tools/codegraph.js
CHANGED
|
@@ -71,7 +71,10 @@ export const definitions = [
|
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
name: 'codegraph_nodes',
|
|
74
|
-
description:
|
|
74
|
+
description:
|
|
75
|
+
'List all nodes of a given type in the graph. ' +
|
|
76
|
+
'type must be one of: class, function, module, concept, service, file, struct, table. ' +
|
|
77
|
+
'Use to enumerate all classes before refactoring, all functions in a module, or all files of a type.',
|
|
75
78
|
inputSchema: {
|
|
76
79
|
type: 'object',
|
|
77
80
|
properties: {
|
package/src/tools/context.js
CHANGED
|
@@ -89,15 +89,15 @@ export async function handle(args, state) {
|
|
|
89
89
|
|
|
90
90
|
const rawEntries = getContext({ project: proj, limit: 15, compact: false })
|
|
91
91
|
.filter(e => e.status !== 'archived');
|
|
92
|
-
//
|
|
92
|
+
// Full content only for: newest 2, or high-signal entries (why+outcome+files)
|
|
93
|
+
// ponytail: avoids dumping 5 full auto-entries (graph builds, etc.) every resume
|
|
93
94
|
const entries = rawEntries.map((e, i) => {
|
|
94
|
-
|
|
95
|
+
const isHighSignal = e.why && e.outcome && Array.isArray(e.files) && e.files.length > 0;
|
|
96
|
+
if (i < 2 || isHighSignal) return e;
|
|
95
97
|
return {
|
|
96
98
|
id: e.id, project: e.project, title: e.title, type: e.type,
|
|
97
99
|
status: e.status, tags: e.tags, source: e.source,
|
|
98
100
|
createdAt: e.createdAt, updatedAt: e.updatedAt,
|
|
99
|
-
...(e.why ? { why: e.why } : {}),
|
|
100
|
-
...(e.outcome ? { outcome: e.outcome } : {}),
|
|
101
101
|
preview: (e.content || '').slice(0, 200),
|
|
102
102
|
};
|
|
103
103
|
});
|
|
@@ -116,12 +116,12 @@ export async function handle(args, state) {
|
|
|
116
116
|
|
|
117
117
|
if (discussions.length === 1) state.discussionId = discussions[0].id;
|
|
118
118
|
|
|
119
|
-
const digest = totalEntries >
|
|
119
|
+
const digest = totalEntries > 25
|
|
120
120
|
? autoDigest(getContext({ project: proj, limit: 30 }), proj)
|
|
121
121
|
: null;
|
|
122
122
|
|
|
123
123
|
const graphStatus = graph
|
|
124
|
-
? { built: true, path: graph.path, nodes: graph.nodes, edges: graph.edges,
|
|
124
|
+
? { built: true, path: graph.path, nodes: graph.nodes, edges: graph.edges, builtAt: graph.builtAt }
|
|
125
125
|
: { built: false };
|
|
126
126
|
|
|
127
127
|
return {
|
package/src/tools/search.js
CHANGED
|
@@ -44,7 +44,7 @@ export async function handle(args, _state) {
|
|
|
44
44
|
id: e.id, project: e.project, title: e.title || '',
|
|
45
45
|
tags: e.tags, createdAt: e.createdAt,
|
|
46
46
|
similarity: e.similarity,
|
|
47
|
-
|
|
47
|
+
// ponytail: preview omitted — call context.get id:[...] for full content
|
|
48
48
|
}));
|
|
49
49
|
return {
|
|
50
50
|
matches, count: matches.length, mode,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* get_symbol_detail — return source code for a single function/class by name.
|
|
3
|
+
* Uses codegraph_query to locate the symbol, then reads only the relevant lines.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { handle as codegraphHandle } from './codegraph.js';
|
|
9
|
+
|
|
10
|
+
export const definition = {
|
|
11
|
+
name: 'get_symbol_detail',
|
|
12
|
+
description:
|
|
13
|
+
'Return the source code and location for a single function, class, or method by name. ' +
|
|
14
|
+
'Use instead of reading the whole file — much cheaper. Requires codegraph to be built. ' +
|
|
15
|
+
'Pass file to narrow when multiple symbols share the same name.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
required: ['name'],
|
|
19
|
+
properties: {
|
|
20
|
+
name: { type: 'string', description: 'Symbol name (function, class, method)' },
|
|
21
|
+
file: { type: 'string', description: 'Optional: narrow by file path (partial match ok)' },
|
|
22
|
+
path: { type: 'string', description: 'Project root path (required if codegraph was built for a specific path)' },
|
|
23
|
+
context_lines: { type: 'number', description: 'Extra lines to include above/below (default 3)' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function handle(args, state) {
|
|
29
|
+
const { name, file, context_lines = 3 } = args;
|
|
30
|
+
const rootPath = args.path || state.projectRootPath;
|
|
31
|
+
if (!rootPath) throw new Error('path or projectRootPath required');
|
|
32
|
+
|
|
33
|
+
// Ask codegraph for the node location
|
|
34
|
+
const queryResult = codegraphHandle('codegraph_query', { path: rootPath, node: name }, state);
|
|
35
|
+
|
|
36
|
+
// Find the node matching name (and optionally file)
|
|
37
|
+
const nodes = queryResult?.nodes || queryResult?.results || [];
|
|
38
|
+
let match = nodes.find(n =>
|
|
39
|
+
n.name === name && (!file || (n.file || '').includes(file))
|
|
40
|
+
);
|
|
41
|
+
if (!match && nodes.length === 1) match = nodes[0];
|
|
42
|
+
if (!match) {
|
|
43
|
+
return {
|
|
44
|
+
found: false,
|
|
45
|
+
message: `Symbol "${name}" not found in graph. Run codegraph_build first, or check spelling.`,
|
|
46
|
+
candidates: nodes.slice(0, 5).map(n => ({ name: n.name, file: n.file, type: n.type })),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Read the source file around the symbol's line
|
|
51
|
+
const absFile = join(rootPath, match.file);
|
|
52
|
+
let source;
|
|
53
|
+
try {
|
|
54
|
+
source = readFileSync(absFile, 'utf8');
|
|
55
|
+
} catch {
|
|
56
|
+
return { found: true, ...match, error: `Could not read file: ${absFile}` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const lines = source.split('\n');
|
|
60
|
+
const startLine = Math.max(0, (match.line || 1) - 1 - context_lines);
|
|
61
|
+
// Heuristic: read up to 80 lines or until we find the closing brace/dedent
|
|
62
|
+
const endLine = Math.min(lines.length, startLine + 80 + context_lines);
|
|
63
|
+
const snippet = lines.slice(startLine, endLine).join('\n');
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
found: true,
|
|
67
|
+
name: match.name,
|
|
68
|
+
type: match.type,
|
|
69
|
+
file: match.file,
|
|
70
|
+
line: match.line,
|
|
71
|
+
source: snippet,
|
|
72
|
+
hint: `Lines ${startLine + 1}–${endLine} of ${match.file}. Use read_file for the full file.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool_registry + safety_policy — static metadata about every MCP tool.
|
|
3
|
+
* No DB queries. Pure JSON describing side-effects and approval requirements.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const REGISTRY = [
|
|
7
|
+
// Core memory tools
|
|
8
|
+
{ name: 'context', side_effects: 'Writes to ~/.context-mcp on save/update/delete', requires_approval: false },
|
|
9
|
+
{ name: 'search', side_effects: 'none', requires_approval: false },
|
|
10
|
+
{ name: 'plan', side_effects: 'Writes plan files to planDir on save/update', requires_approval: false },
|
|
11
|
+
{ name: 'error_check', side_effects: 'Writes to ~/.context-mcp on save', requires_approval: false },
|
|
12
|
+
// Graph tools
|
|
13
|
+
{ name: 'codegraph_build', side_effects: 'Writes codegraph-cache/ in project root', requires_approval: false },
|
|
14
|
+
{ name: 'codegraph_query', side_effects: 'none', requires_approval: false },
|
|
15
|
+
{ name: 'codegraph_arch', side_effects: 'none', requires_approval: false },
|
|
16
|
+
{ name: 'codegraph_nodes', side_effects: 'none', requires_approval: false },
|
|
17
|
+
{ name: 'codegraph_report', side_effects: 'Writes CODEGRAPH_REPORT.md in project root', requires_approval: false },
|
|
18
|
+
{ name: 'codegraph_affected',side_effects: 'none', requires_approval: false },
|
|
19
|
+
{ name: 'codegraph_html', side_effects: 'Writes HTML/GraphML files to codegraph-cache/', requires_approval: false },
|
|
20
|
+
{ name: 'get_symbol_detail', side_effects: 'none', requires_approval: false },
|
|
21
|
+
// File tools (HTTP mode only)
|
|
22
|
+
{ name: 'read_file', side_effects: 'none', requires_approval: false },
|
|
23
|
+
{ name: 'list_dir', side_effects: 'none', requires_approval: false },
|
|
24
|
+
{ name: 'write_file', side_effects: 'Creates or overwrites a file', requires_approval: true },
|
|
25
|
+
{ name: 'patch_file', side_effects: 'Modifies an existing file', requires_approval: true },
|
|
26
|
+
{ name: 'create_dir', side_effects: 'Creates a directory', requires_approval: false },
|
|
27
|
+
{ name: 'delete_file', side_effects: 'Permanently deletes a file or directory', requires_approval: true },
|
|
28
|
+
// Git tools (ACCESS_GIT=true only)
|
|
29
|
+
{ name: 'git_status', side_effects: 'none', requires_approval: false },
|
|
30
|
+
{ name: 'git_diff', side_effects: 'none', requires_approval: false },
|
|
31
|
+
{ name: 'git_log', side_effects: 'none', requires_approval: false },
|
|
32
|
+
{ name: 'git_show', side_effects: 'none', requires_approval: false },
|
|
33
|
+
{ name: 'git_add', side_effects: 'Stages files for commit', requires_approval: false },
|
|
34
|
+
{ name: 'git_commit', side_effects: 'Creates a git commit', requires_approval: true },
|
|
35
|
+
{ name: 'git_push', side_effects: 'Pushes commits to remote — IRREVERSIBLE', requires_approval: true },
|
|
36
|
+
{ name: 'git_pull', side_effects: 'Modifies working tree', requires_approval: true },
|
|
37
|
+
{ name: 'git_branch', side_effects: 'May create or switch branches', requires_approval: false },
|
|
38
|
+
{ name: 'git_stash', side_effects: 'Modifies stash stack', requires_approval: false },
|
|
39
|
+
{ name: 'git_reset', side_effects: 'Modifies staging area or HEAD — use with care', requires_approval: true },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const SAFETY_POLICY = {
|
|
43
|
+
description: 'Actions that require explicit user confirmation before execution.',
|
|
44
|
+
requires_confirmation: REGISTRY.filter(t => t.requires_approval).map(t => t.name),
|
|
45
|
+
rules: [
|
|
46
|
+
'Always confirm before git_push — pushes affect the remote and are hard to undo.',
|
|
47
|
+
'Always confirm before delete_file — no recycle bin.',
|
|
48
|
+
'Always confirm before git_reset if mode is hard — discards uncommitted work.',
|
|
49
|
+
'Always confirm before write_file on files that look like credentials or config.',
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const definitions = [
|
|
54
|
+
{
|
|
55
|
+
name: 'tool_registry',
|
|
56
|
+
description:
|
|
57
|
+
'Lists every MCP tool with its side effects and whether it requires user approval. ' +
|
|
58
|
+
'Read this before calling any destructive tool to understand the risk.',
|
|
59
|
+
inputSchema: { type: 'object', properties: {} },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'safety_policy',
|
|
63
|
+
description:
|
|
64
|
+
'Lists which operations require explicit user confirmation before execution ' +
|
|
65
|
+
'(git_push, git_reset, delete_file, write_file, git_commit, patch_file). ' +
|
|
66
|
+
'Read before performing any irreversible action.',
|
|
67
|
+
inputSchema: { type: 'object', properties: {} },
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
export const TOOL_NAMES = new Set(definitions.map(d => d.name));
|
|
72
|
+
|
|
73
|
+
export function handle(name) {
|
|
74
|
+
if (name === 'tool_registry') return { tools: REGISTRY, count: REGISTRY.length };
|
|
75
|
+
if (name === 'safety_policy') return SAFETY_POLICY;
|
|
76
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
77
|
+
}
|
package/src/vector.js
CHANGED
|
@@ -64,15 +64,20 @@ function buildIDF(docs) {
|
|
|
64
64
|
return idf;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
let _idfFingerprint = null;
|
|
68
|
+
|
|
67
69
|
function getCachedIDF(corpus) {
|
|
68
70
|
const gen = getGeneration();
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
+
// Fingerprint first 20 IDs to detect content change at same size (e.g. delete+add)
|
|
72
|
+
const fingerprint = corpus.slice(0, 20).map(e => e.id).join(',');
|
|
73
|
+
if (_idfCache && _idfGeneration === gen && _idfCorpusLen === corpus.length
|
|
74
|
+
&& _idfFingerprint === fingerprint) {
|
|
71
75
|
return _idfCache;
|
|
72
76
|
}
|
|
73
77
|
_idfCache = buildIDF(corpus);
|
|
74
78
|
_idfGeneration = gen;
|
|
75
79
|
_idfCorpusLen = corpus.length;
|
|
80
|
+
_idfFingerprint = fingerprint;
|
|
76
81
|
return _idfCache;
|
|
77
82
|
}
|
|
78
83
|
|