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.
@@ -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 string for use as a Mermaid node ID."""
56
- return re.sub(r"[^a-zA-Z0-9_]", "_", s)[:40] or "node"
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:
@@ -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: #555; }
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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">Type: ${{esc(n._file_type || 'unknown')}}</div>
248
- <div class="field">Community: ${{esc(n._community_name || '-')}}</div>
249
- <div class="field">Source: ${{esc(n._source_file || '-')}}</div>
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
  }}
@@ -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
- m = re.match(r'(?:import|from)\s+([\w./"\']+)', text)
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).strip("\"'").split(".")[0].split("/")[-1]
332
- if raw and raw not in import_names:
333
- import_names.append(raw)
334
- if import_names:
335
- for entry in nodes:
336
- entry["imports"] = import_names[:]
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
@@ -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
- # Only emit when unambiguous
96
- if len(target_ids) != 1:
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:
@@ -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, str(Path(root) / "codegraph-cache"))
253
- except Exception:
254
- pass
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
@@ -112,24 +112,27 @@ _HTML_TEMPLATE = r"""<!DOCTYPE html>
112
112
  <meta charset="UTF-8">
113
113
  <title>{title}</title>
114
114
  <style>
115
- body {{ font-family: 'Segoe UI', sans-serif; margin: 0; padding: 0; background: #f9f9f9; color: #333; }}
116
- h1 {{ margin: 20px 0 0 24px; font-size: 2.2rem; font-weight: bold; color: #1e3a56; }}
117
- .controls {{ margin: 20px 0 15px 24px; }}
118
- button {{ margin-right: 10px; padding: 8px 18px; background: #007bff; color: #fff; border: none; border-radius: 5px; font-size: 0.95rem; cursor: pointer; }}
119
- button:hover {{ background: #0056b3; }}
120
- #tree-container {{ width: calc(100vw - 48px); height: 85vh; overflow: auto; border-radius: 8px; background: #fff; margin-left: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); border: 1px solid #ddd; }}
121
- svg {{ background: #fff; border-radius: 8px; display: block; }}
122
- .node circle {{ stroke-width: 2.5px; }}
123
- .node text {{ font: 13px 'Segoe UI', sans-serif; paint-order: stroke fill; stroke: #fff; stroke-width: 3px; stroke-linejoin: round; stroke-opacity: 0.85; }}
124
- .link {{ fill: none; stroke-opacity: 0.7; stroke-width: 2px; }}
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 View</button>
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
- let dn = node.name;
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
- let rn = d.name;
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
- ["#3498DB","#2980B9","#AED6F1"],["#2ECC71","#27AE60","#A9DFBF"],
157
- ["#E74C3C","#C0392B","#F5B7B1"],["#9B59B6","#8E44AD","#D7BDE2"],
158
- ["#F39C12","#D68910","#FAD7A0"],["#1ABC9C","#117864","#A2D9CE"],
159
- ["#34495E","#1B2631","#ABB2B9"],["#E67E22","#BA4A00","#F5CBA7"],
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:"#4A4A4A",stroke:"#333333",collapsedFill:"#6C757D" }}, "Default": {{ fill:"#BDC3C7",stroke:"#95A5A6",collapsedFill:"#ECF0F1" }} }};
162
- (initialJsonData.children||[]).forEach((c,i) => {{ const pal=PALETTE[i%PALETTE.length]; phaseColors[c.name]={{ fill:pal[0],stroke:pal[1],collapsedFill:pal[2] }}; }});
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:"#4A4A4A",stroke:"#333333",collapsedFill:"#6C757D"}},
165
- 2:{{fill:"#6ab04c",stroke:"#508a38",collapsedFill:"#a3d391"}},
166
- 3:{{fill:"#f0932b",stroke:"#d0730f",collapsedFill:"#f6c07e"}},
167
- 4:{{fill:"#be2edd",stroke:"#a01cb3",collapsedFill:"#e08bf2"}},
168
- default:{{fill:"#747d8c",stroke:"#57606f",collapsedFill:"#a4b0be"}}
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",
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.3"
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) => s + (haystack.split(t).length - 1), 0);
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
- const explicitIds = new Set([
102
- ...(target.relations || []).map(r => r.id),
103
- ...(target.relatedBy || []).map(r => r.id),
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 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';
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> → 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
81
- ctx graph report <path> → god nodes, clusters, surprising connections
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
- Use `ctx graph arch` first. Never read files for structure questions.
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 5 have full content, rest have 200-char preview
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
- Use `codegraph_arch` first. Read files only when you need exact bug or
94
- implementation details.
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
- Use `codegraph_arch` for structural questions. Read files for bugs/logic.
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. `codegraph_arch` before reading files
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
- Use `codegraph_arch` first. Never read files for structure questions.
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
- Use `codegraph_arch` first. Never read files for structure questions.
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
 
@@ -71,7 +71,10 @@ export const definitions = [
71
71
  },
72
72
  {
73
73
  name: 'codegraph_nodes',
74
- description: 'List all nodes of a given type in the graph.',
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: {
@@ -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
- // Newest 5 get full content; older entries get a lightweight preview
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
- if (i < 5) return e;
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 > 10
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, communities: graph.communities, builtAt: graph.builtAt }
124
+ ? { built: true, path: graph.path, nodes: graph.nodes, edges: graph.edges, builtAt: graph.builtAt }
125
125
  : { built: false };
126
126
 
127
127
  return {
@@ -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
- preview: (e.content || '').slice(0, 200),
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
- // Cache hit: same generation (no mutations) and same corpus size
70
- if (_idfCache && _idfGeneration === gen && _idfCorpusLen === corpus.length) {
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
 
package/uv.lock CHANGED
@@ -201,7 +201,7 @@ wheels = [
201
201
 
202
202
  [[package]]
203
203
  name = "codegraph-mcp"
204
- version = "1.1.2"
204
+ version = "1.1.3"
205
205
  source = { editable = "." }
206
206
  dependencies = [
207
207
  { name = "mcp" },