context-mcp-server 1.0.5 → 1.0.7

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.
@@ -4,8 +4,7 @@ codegraph/server.py — MCP server exposing codebase knowledge graph tools.
4
4
 
5
5
  Tools:
6
6
  codegraph_build — scan project, extract AST nodes, build graph (local only, no API)
7
- codegraph_query — fetch details about any part of the codebase via natural language
8
- codegraph_explain — look up a specific node: type, file, connections
7
+ codegraph_query — structural question OR single-node lookup (or both); replaces codegraph_explain
9
8
  codegraph_report — return full CODEGRAPH_REPORT.md
10
9
  codegraph_nodes — list nodes of a given type
11
10
  codegraph_path — shortest path between two concepts
@@ -56,36 +55,21 @@ TOOLS = [
56
55
  Tool(
57
56
  name="codegraph_query",
58
57
  description=(
59
- "Fetch details about any part of the codebase using a natural language question. "
60
- "Searches the knowledge graph instant, no API call. "
61
- "Use for: finding functions, classes, files, understanding what exists, "
62
- "what a module contains, what calls what, what imports what. "
63
- "NOT for: bug investigation or tracing unexpected behavior read the file for that."
58
+ "Ask a structural question about the codebase OR look up a specific node by name — or both in one call. "
59
+ "Pass `question` for natural-language traversal: what calls X, what does module Y depend on. "
60
+ "Pass `node` for fast single-node lookup: returns type, file, depends_on, used_by. "
61
+ "Pass both to get node detail + surrounding graph context together. "
62
+ "Returns structured text within token_budget. Use before reading any files."
64
63
  ),
65
64
  inputSchema={
66
65
  "type": "object",
67
66
  "properties": {
68
67
  "path": {"type": "string", "description": "Project root"},
69
- "question": {"type": "string", "description": "Natural language question"},
68
+ "question": {"type": "string", "description": "Natural language question about the codebase"},
69
+ "node": {"type": "string", "description": "Node name or partial name to look up (type, file, deps, callers)"},
70
70
  "token_budget": {"type": "integer", "description": "Max tokens in response (default 2000)"},
71
71
  },
72
- "required": ["path", "question"],
73
- },
74
- ),
75
- Tool(
76
- name="codegraph_explain",
77
- description=(
78
- "Look up a specific function, class, or module by name — returns its type, file location, "
79
- "and all direct connections (what it depends on, what uses it). "
80
- "Use when you already know the name and want its full context in the graph."
81
- ),
82
- inputSchema={
83
- "type": "object",
84
- "properties": {
85
- "path": {"type": "string", "description": "Project root"},
86
- "node": {"type": "string", "description": "Node name or partial name"},
87
- },
88
- "required": ["path", "node"],
72
+ "required": ["path"],
89
73
  },
90
74
  ),
91
75
  Tool(
@@ -143,7 +127,7 @@ async def call_tool(name: str, arguments: dict):
143
127
  async def _dispatch(name: str, args: dict):
144
128
  if name == "codegraph_build": return await _build(args)
145
129
  if name == "codegraph_query": return await _query(args)
146
- if name == "codegraph_explain": return await _explain(args)
130
+ if name == "codegraph_explain": return await _query(args)
147
131
  if name == "codegraph_report": return await _report(args)
148
132
  if name == "codegraph_nodes": return await _nodes(args)
149
133
  if name == "codegraph_path": return await _path(args)
@@ -223,19 +207,8 @@ async def _build(args: dict) -> dict:
223
207
 
224
208
  # ── Query / Report / Nodes / Path ─────────────────────────────────────────────
225
209
 
226
- async def _query(args: dict) -> dict:
227
- graph_dict = load_graph(args["path"])
228
- if not graph_dict:
229
- raise ValueError("No graph found. Run codegraph_build first.")
230
- return graph_answer(args["question"], graph_dict, token_budget=args.get("token_budget", 2000))
231
-
232
-
233
- async def _explain(args: dict) -> dict:
234
- graph_dict = load_graph(args["path"])
235
- if not graph_dict:
236
- raise ValueError("No graph found. Run codegraph_build first.")
237
-
238
- query = args["node"].lower()
210
+ def _explain_node(node_name: str, graph_dict: dict) -> dict:
211
+ query = node_name.lower()
239
212
  nodes = graph_dict.get("nodes", [])
240
213
  edges = graph_dict.get("edges", [])
241
214
 
@@ -244,8 +217,8 @@ async def _explain(args: dict) -> dict:
244
217
  match = next((n for n in nodes if query in n.get("name", "").lower()), None)
245
218
  if not match:
246
219
  candidates = [n["name"] for n in nodes if query in n.get("id", "").lower()]
247
- return {"found": False, "query": args["node"],
248
- "message": f"No node matching '{args['node']}'.",
220
+ return {"found": False, "query": node_name,
221
+ "message": f"No node matching '{node_name}'.",
249
222
  "suggestions": candidates[:10]}
250
223
 
251
224
  nid = match["id"]
@@ -254,13 +227,13 @@ async def _explain(args: dict) -> dict:
254
227
  if e.get("from") == nid:
255
228
  t = next((n for n in nodes if n.get("id") == e.get("to")), None)
256
229
  depends_on.append({"name": t["name"] if t else e["to"],
257
- "file": t.get("file","") if t else "",
258
- "relation": e.get("relation","→")})
230
+ "file": t.get("file", "") if t else "",
231
+ "relation": e.get("relation", "→")})
259
232
  elif e.get("to") == nid:
260
233
  s = next((n for n in nodes if n.get("id") == e.get("from")), None)
261
234
  used_by.append({"name": s["name"] if s else e["from"],
262
- "file": s.get("file","") if s else "",
263
- "relation": e.get("relation","→")})
235
+ "file": s.get("file", "") if s else "",
236
+ "relation": e.get("relation", "→")})
264
237
 
265
238
  return {
266
239
  "found": True,
@@ -270,10 +243,28 @@ async def _explain(args: dict) -> dict:
270
243
  "description": match.get("description") or None,
271
244
  "depends_on": depends_on[:20],
272
245
  "used_by": used_by[:20],
273
- "hint": None,
274
246
  }
275
247
 
276
248
 
249
+ async def _query(args: dict) -> dict:
250
+ graph_dict = load_graph(args["path"])
251
+ if not graph_dict:
252
+ raise ValueError("No graph found. Run codegraph_build first.")
253
+
254
+ question = args.get("question")
255
+ node_name = args.get("node")
256
+
257
+ if not question and not node_name:
258
+ raise ValueError("Provide at least one of: question, node")
259
+
260
+ result = {}
261
+ if node_name:
262
+ result["node"] = _explain_node(node_name, graph_dict)
263
+ if question:
264
+ result["query"] = graph_answer(question, graph_dict, token_budget=args.get("token_budget", 2000))
265
+ return result
266
+
267
+
277
268
  async def _report(args: dict) -> dict:
278
269
  report_path = Path(args["path"]) / "codegraph-cache" / "CODEGRAPH_REPORT.md"
279
270
  if report_path.exists():
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "context-mcp-server",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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": {
7
7
  "context-mcp": "./src/index.js",
8
8
  "context-mcp-server": "./src/index.js",
9
9
  "context-mcp-http": "./src/http.js",
10
- "ctx": "./src/cli.js"
10
+ "ctx": "./src/cli.js",
11
+ "context": "./src/cli.js"
11
12
  },
12
13
  "scripts": {
13
14
  "mcp": "node src/index.js",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codegraph-mcp"
7
- version = "1.0.5"
7
+ version = "1.0.7"
8
8
  description = "Codebase knowledge graph MCP server — AST extraction, graph queries, community detection"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
Binary file
package/src/cli.js CHANGED
@@ -107,8 +107,8 @@ function printSection(title, meta = '') {
107
107
  function printUsage() {
108
108
  printBanner();
109
109
 
110
- // Terminal commands (ctx ...)
111
- printSection('Terminal commands', 'run from your shell');
110
+ // Terminal commands (ctx / context ...)
111
+ printSection('Terminal commands', 'run from your shell (ctx … or context …)');
112
112
  const cmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
113
113
  cmd('ctx', 'open interactive mode');
114
114
  cmd('ctx list [project]', 'list entries + discussions + graphs');
@@ -131,8 +131,8 @@ function printUsage() {
131
131
  cmd('ctx help', 'show this screen');
132
132
  console.log('');
133
133
 
134
- // Interactive mode commands (no ctx prefix)
135
- printSection('Interactive mode', 'type these inside ctx (no "ctx" prefix)');
134
+ // Interactive mode commands (no prefix needed)
135
+ printSection('Interactive mode', 'type these inside the UI — no "ctx" prefix needed');
136
136
  const icmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
137
137
  icmd('list [project]', 'list entries');
138
138
  icmd('search <query>', 'search context');
@@ -186,7 +186,7 @@ function cmdList(args) {
186
186
 
187
187
  for (const projectName of projectNames) {
188
188
  const pData = projects[projectName];
189
- const graph = allGraphs.find(g => g.path?.toLowerCase().includes(projectName.toLowerCase()));
189
+ const graph = _graphForProject(allGraphs, projectName);
190
190
  const activeD = pData.discussions.filter(d => d.status === 'active').length;
191
191
  const totalSecs = (pData.contexts.length > 0 ? 1 : 0) + (pData.discussions.length > 0 ? 1 : 0) + (graph ? 1 : 0);
192
192
  let secIdx = 0;
@@ -240,7 +240,7 @@ function cmdList(args) {
240
240
  }
241
241
 
242
242
  // Orphan graphs (no matching project)
243
- const orphanGraphs = allGraphs.filter(g => !projectNames.some(p => g.path?.toLowerCase().includes(p.toLowerCase())));
243
+ const orphanGraphs = allGraphs.filter(g => !projectNames.some(p => _graphForProject([g], p)));
244
244
  if (orphanGraphs.length) {
245
245
  console.log(`\n ${color(C.dblue, '◇')} ${muted('other graphs')}`);
246
246
  for (const g of orphanGraphs) {
@@ -292,7 +292,7 @@ function cmdProjects() {
292
292
  const entries = getContext({ project: project.name, limit: 3, compact: true }).filter(e => e.status !== 'archived');
293
293
  const discs = allDiscs.filter(d => (d.project || 'global') === project.name);
294
294
  const activeD = discs.filter(d => d.status === 'active');
295
- const graph = graphs.find(g => g.path?.toLowerCase().includes(project.name.toLowerCase()));
295
+ const graph = _graphForProject(graphs, project.name);
296
296
 
297
297
  const barLen = Math.min(Math.ceil(project.count / 2), 24);
298
298
  const bar = color(C.dblue, '█'.repeat(barLen)) + color(C.darkgray, '░'.repeat(24 - barLen));
@@ -429,7 +429,7 @@ function cmdBenchmark() {
429
429
  printSection('Benchmark', 'real token savings');
430
430
 
431
431
  const RESUME_LIMIT = 15;
432
- const COMPACT_AT = 50;
432
+ const COMPACT_AT = 20;
433
433
 
434
434
  // ── Measure entry sizes from actual stored data ──────────────────────────────
435
435
  const allEntries = getContext({ limit: 500, compact: false });
@@ -567,6 +567,30 @@ const _GLOBAL_GITIGNORE_ENTRIES = [
567
567
  '.mcp.json',
568
568
  ];
569
569
 
570
+ // Match a graph to a project by exact last-path-component comparison (not substring)
571
+ function _graphForProject(graphs, projectName) {
572
+ const norm = p => (p || '').toLowerCase().replace(/\\/g, '/').replace(/\/$/, '');
573
+ const name = projectName.toLowerCase();
574
+ return graphs.find(g => norm(g.path).split('/').pop() === name) || null;
575
+ }
576
+
577
+ const _PROJECT_GITIGNORE_ENTRIES = [
578
+ '.claude/', '.cursor/', '.vscode/', '.gemini/', '.codex/',
579
+ 'codegraph-cache/', '.mcp.json', 'CLAUDE.md', 'GEMINI.md', 'AGENTS.md',
580
+ ];
581
+
582
+ function _updateProjectGitignore(projectDir) {
583
+ const giPath = join(projectDir, '.gitignore');
584
+ const existing = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
585
+ const lines = existing.split(/\r?\n/);
586
+ const missing = _PROJECT_GITIGNORE_ENTRIES.filter(e => !lines.includes(e));
587
+ if (!missing.length) return;
588
+ const block = '\n# context-mcp — written by ctx install\n' + missing.join('\n') + '\n';
589
+ writeFileSync(giPath, (existing ? existing.trimEnd() : '') + block, 'utf8');
590
+ console.log(` ${ok('✓')} ${'project .gitignore'.padEnd(28)} ${faint(giPath.replace(/\\/g, '/'))}`);
591
+ for (const e of missing) console.log(` ${faint('+ ' + e)}`);
592
+ }
593
+
570
594
  function _updateGlobalGitignore() {
571
595
  // Resolve global gitignore path: git config > ~/.gitignore_global > ~/.gitignore
572
596
  let giPath = null;
@@ -829,6 +853,9 @@ async function cmdInstall(args) {
829
853
  console.log(faint(` ${keys.length} platform(s) installed · scope: ${scope} · ${destLabel}`));
830
854
  console.log('');
831
855
 
856
+ // ── Project .gitignore — add context-mcp entries for this project ──────────
857
+ _updateProjectGitignore(process.cwd());
858
+
832
859
  // ── Global gitignore — add context-mcp runtime files if global gitignore exists ──
833
860
  _updateGlobalGitignore();
834
861
  console.log('');
package/src/db.js CHANGED
@@ -23,6 +23,11 @@ const PROJECTS_PATH = join(DATA_DIR, 'projects.json');
23
23
 
24
24
  const MAX_CONTENT_LENGTH = 5000;
25
25
  const PREVIEW_LENGTH = 200;
26
+
27
+ // Normalize file paths for cross-platform comparison (Windows case + slash variants)
28
+ function normPath(p) {
29
+ return p ? p.toLowerCase().replace(/\\/g, '/').replace(/\/$/, '') : '';
30
+ }
26
31
  const WRITE_DEBOUNCE_MS = 500;
27
32
  const LOCK_WAIT_TIMEOUT_MS = 2000;
28
33
 
@@ -126,9 +131,9 @@ function mergeStore(latest, local) {
126
131
  if (_changedDiscussionNames.has(disc.name)) discussionsByName.set(disc.name, disc);
127
132
  }
128
133
 
129
- const graphsByPath = new Map((latest.graphs || []).map(g => [g.path, g]));
134
+ const graphsByPath = new Map((latest.graphs || []).map(g => [normPath(g.path), g]));
130
135
  for (const graph of (local.graphs || [])) {
131
- if (_changedGraphPaths.has(graph.path)) graphsByPath.set(graph.path, graph);
136
+ if (_changedGraphPaths.has(graph.path)) graphsByPath.set(normPath(graph.path), graph);
132
137
  }
133
138
 
134
139
  const projectsById = new Map((latest.projects || []).map(p => [p.id, p]));
@@ -307,10 +312,15 @@ export function updateContext({ id, content, title, tags, type, status, files, c
307
312
  * @param {Object} opts
308
313
  * @param {boolean} opts.compact - If true, returns previews instead of full content (saves tokens)
309
314
  */
310
- export function getContext({ project, tags, limit = 20, compact = false } = {}) {
315
+ export function getContext({ project, tags, limit = 20, compact = false, ids } = {}) {
311
316
  refreshFromDisk();
312
317
  const store = load();
313
318
  let results = store.contexts;
319
+ if (ids && ids.length) {
320
+ const idSet = new Set(ids);
321
+ results = results.filter(c => idSet.has(c.id));
322
+ return compact ? results.map(compactEntry) : results;
323
+ }
314
324
  if (project) results = results.filter(c => c.project === project || c.project === 'global');
315
325
  if (tags && tags.length) {
316
326
  const tagList = Array.isArray(tags) ? tags : tags.split(',').map(t => t.trim());
@@ -343,12 +353,14 @@ export function searchContext({ query, project, limit = 10, compact = false }) {
343
353
  return compact ? sliced.map(compactEntry) : sliced;
344
354
  }
345
355
 
346
- export function deleteContext({ id }) {
356
+ export function deleteContext({ id, ids }) {
347
357
  refreshFromDisk();
348
358
  const store = load();
349
359
  const before = store.contexts.length;
350
- const removed = store.contexts.filter(c => c.id === id);
351
- store.contexts = store.contexts.filter(c => c.id !== id);
360
+ const idSet = new Set(ids && ids.length ? ids : (id ? [id] : []));
361
+ if (!idSet.size) return { deleted: 0 };
362
+ const removed = store.contexts.filter(c => idSet.has(c.id));
363
+ store.contexts = store.contexts.filter(c => !idSet.has(c.id));
352
364
  if (store.contexts.length < before) {
353
365
  for (const entry of removed) {
354
366
  _deletedContextIds.add(entry.id);
@@ -713,7 +725,7 @@ export function flushStore() { flushToDisk(); }
713
725
 
714
726
  // ── Auto-compaction ───────────────────────────────────────────────────────────
715
727
 
716
- const COMPACTION_THRESHOLD = 50;
728
+ const COMPACTION_THRESHOLD = 20;
717
729
  const COMPACTION_TARGET = 30;
718
730
 
719
731
  export function shouldCompact(project) {
@@ -753,7 +765,14 @@ export function compactProject(project, summaryContent) {
753
765
  export function saveGraph({ path, nodes, edges, communities, cached, changed, time_ms, summary }) {
754
766
  refreshFromDisk();
755
767
  const store = load();
756
- const existing = store.graphs.find(g => g.path === path);
768
+ // Deduplicate: collapse any case/slash variants of same path, keep newest
769
+ const dupes = store.graphs.filter(g => normPath(g.path) === normPath(path));
770
+ if (dupes.length > 1) {
771
+ const keep = dupes.reduce((a, b) => (a.builtAt >= b.builtAt ? a : b));
772
+ store.graphs = store.graphs.filter(g => normPath(g.path) !== normPath(path));
773
+ store.graphs.push(keep);
774
+ }
775
+ const existing = store.graphs.find(g => normPath(g.path) === normPath(path));
757
776
  const record = {
758
777
  path,
759
778
  nodes: nodes ?? existing?.nodes ?? 0,
@@ -777,7 +796,7 @@ export function saveGraph({ path, nodes, edges, communities, cached, changed, ti
777
796
 
778
797
  export function getGraph(path) {
779
798
  const store = load();
780
- if (path) return store.graphs.find(g => g.path === path) || null;
799
+ if (path) return store.graphs.find(g => normPath(g.path) === normPath(path)) || null;
781
800
  return store.graphs;
782
801
  }
783
802
 
@@ -3,7 +3,7 @@ import { fireAutoLink } from './autoLink.js';
3
3
 
4
4
  export function saveAutoContext({ title, content, type, files, state, tags = [] }) {
5
5
  const entry = saveContext({
6
- project: state.sessionProject || 'global',
6
+ project: state.sessionProject || null,
7
7
  sessionId: state.sessionId || null,
8
8
  title,
9
9
  content,
@@ -15,7 +15,12 @@ Every conversation starts with `context.resume`. Every codebase question uses `c
15
15
 
16
16
  ## 1. Start of Every Conversation (MANDATORY)
17
17
 
18
- Call `context` tool, `action: "resume"`, `project: "<project-name>"` **before anything else**.
18
+ Call `context` tool **before anything else** with:
19
+ - `action: "resume"`
20
+ - `project: "<basename of git repo root dir>"` — infer from cwd if not stated
21
+ - `rootPath: "<absolute path to git repo root>"` — required for sandbox + graph lookup
22
+
23
+ Both fields are required: `project` names the memory bucket, `rootPath` enables exact graph matching and file sandboxing.
19
24
 
20
25
  Returns:
21
26
  - `recentEntries` — decisions, bugs, notes from previous conversations
@@ -34,7 +39,7 @@ Then:
34
39
 
35
40
  **After graph build or rebuild** — every time `codegraph_build` completes:
36
41
  ```
37
- context.save type: "architecture" title: "ContextGraph built — <project>"
42
+ context.save project: "<project>" type: "architecture" title: "ContextGraph built — <project>"
38
43
  content: "nodes: X | edges: Y | communities: Z"
39
44
  ```
40
45
 
@@ -59,7 +64,7 @@ Do NOT save: routine reads, search results, temporary debugging dead-ends.
59
64
 
60
65
  Feature spans multiple sessions → `discussion.create` or `discussion.update`.
61
66
  Need past info → `search` before asking user.
62
- Always pass `project`. Auto-compact fires at >50 entries.
67
+ Always pass `project`. Auto-compact fires at >20 entries.
63
68
 
64
69
  ---
65
70
 
@@ -1,4 +1,6 @@
1
- Call the `context` MCP tool with `action: "resume"` and `project: "$ARGUMENTS"` (if no argument given, infer the project name from the current working directory name).
1
+ Call the `context` MCP tool with `action: "resume"`, `project: "$ARGUMENTS"` (if no argument given, infer the project name from the current working directory name), and `rootPath: "<absolute path to the project root / git repo root>"`.
2
+
3
+ Both `project` and `rootPath` are required: `project` names the memory bucket, `rootPath` enables exact graph lookup and file sandboxing.
2
4
 
3
5
  This loads:
4
6
  - Recent decisions, bugs, and notes from past sessions
@@ -18,9 +18,12 @@ Persistent memory + codebase knowledge graph across every conversation.
18
18
 
19
19
  ## MANDATORY: Start of Every Conversation
20
20
 
21
- Call `context` tool, `action: "resume"`, `project: "<project-name>"` **before any tool or response**.
21
+ Call `context` tool **before any tool or response** with:
22
+ - `action: "resume"`
23
+ - `project: "<basename of git repo root dir>"` — infer from `cwd` if not stated
24
+ - `rootPath: "<absolute path to git repo root>"` — required for sandbox + graph lookup
22
25
 
23
- Infer `project` from the working directory name if not stated.
26
+ Both fields are required: `project` names the memory bucket, `rootPath` enables exact graph matching and file sandboxing.
24
27
 
25
28
  Returns:
26
29
  - `recentEntries` — decisions, bugs, notes from past sessions
@@ -40,7 +43,7 @@ Then:
40
43
  **1. After graph build or rebuild**
41
44
  Every time `codegraph_build` completes successfully, immediately call:
42
45
  ```
43
- context.save type: "architecture" title: "ContextGraph built — <project>"
46
+ context.save project: "<project>" type: "architecture" title: "ContextGraph built — <project>"
44
47
  content: "nodes: X | edges: Y | communities: Z | built: <timestamp>"
45
48
  ```
46
49
 
@@ -70,7 +73,7 @@ Do NOT save for: routine file reads, search results, explanations of existing co
70
73
  | Deploy / release step discovered | `note` |
71
74
  | Milestone / feature / task completed | `note` |
72
75
 
73
- Always pass `project`. Feature spans multiple sessions → `discussion.create` or `discussion.update`. Need past info → `search` before asking user. Auto-compact fires at >50 entries.
76
+ Always pass `project`. Feature spans multiple sessions → `discussion.create` or `discussion.update`. Need past info → `search` before asking user. Auto-compact fires at >20 entries.
74
77
 
75
78
  ---
76
79
 
@@ -86,9 +89,8 @@ Parses codebase into AST graph via tree-sitter. Extracts functions, classes, imp
86
89
 
87
90
  ### Query (free, instant, forever)
88
91
  ```
89
- codegraph_query(path, question) find functions, classes, files, dependencies, callers
90
- codegraph_explain(path, node) one node: type, file, depends_on, used_by
91
- codegraph_path(path, from, to) → shortest path between two concepts
92
+ codegraph_query(path, question?, node?) structural question OR single-node lookup (or both in one call)
93
+ codegraph_path(path, from, to) shortest path between two concepts
92
94
  codegraph_nodes(path, type) → list all nodes of a type
93
95
  codegraph_report(path) → god nodes, clusters, surprises
94
96
  ```
@@ -40,92 +40,23 @@ export const definitions = [
40
40
  required: ['path'],
41
41
  },
42
42
  },
43
- {
44
- name: 'codegraph_extract',
45
- description:
46
- 'Return raw content of changed code and doc/PDF files so the AI can write descriptions. ' +
47
- 'Code files: lists existing AST nodes — AI writes a description for each. ' +
48
- 'Doc files: AI extracts new concept nodes. ' +
49
- 'Call after codegraph_build, then call codegraph_add_nodes with results. ' +
50
- 'Pass force:true to re-enrich all files (not just changed ones).',
51
- inputSchema: {
52
- type: 'object',
53
- properties: {
54
- path: { type: 'string', description: 'Project root (same as codegraph_build)' },
55
- limit: { type: 'integer', description: 'Max files to return per call (default 10)' },
56
- force: { type: 'boolean', description: 'Return all files, not just changed (for re-enrichment)' },
57
- },
58
- required: ['path'],
59
- },
60
- },
61
- {
62
- name: 'codegraph_add_nodes',
63
- description:
64
- 'Add concept nodes extracted by the AI into the graph. ' +
65
- 'Call after reading codegraph_extract output. ' +
66
- 'Each node: name, type, file, and optionally description and relations.',
67
- inputSchema: {
68
- type: 'object',
69
- properties: {
70
- path: { type: 'string', description: 'Project root' },
71
- nodes: {
72
- type: 'array',
73
- description: 'Concept nodes to add',
74
- items: {
75
- type: 'object',
76
- properties: {
77
- name: { type: 'string' },
78
- type: { type: 'string', description: 'class|function|concept|service|decision|requirement' },
79
- file: { type: 'string', description: 'Relative file path this concept came from' },
80
- description: { type: 'string' },
81
- relations: {
82
- type: 'array',
83
- items: {
84
- type: 'object',
85
- properties: {
86
- name: { type: 'string' },
87
- relation: { type: 'string', description: 'depends-on|uses|implements|defines|documents' },
88
- },
89
- },
90
- },
91
- },
92
- required: ['name', 'type', 'file'],
93
- },
94
- },
95
- },
96
- required: ['path', 'nodes'],
97
- },
98
- },
99
43
  {
100
44
  name: 'codegraph_query',
101
45
  description:
102
- 'Ask a structural/dependency question about the codebase. ' +
103
- 'Pure graph traversal returns NODE/EDGE structured text truncated to token_budget. ' +
104
- 'Good for: "what does module X depend on?", "what calls function Y?", "what is the path from A to B?". ' +
105
- 'NOT for: bug investigation, logic errors, or understanding what code actually does — read the file directly for those.',
46
+ 'Ask a structural question about the codebase OR look up a specific node by name — or both in one call. ' +
47
+ 'Pass `question` for natural-language traversal: "what does module X depend on?", "what calls function Y?". ' +
48
+ 'Pass `node` for fast single-node lookup: returns type, file, depends_on, used_by. ' +
49
+ 'Pass both to get node detail + surrounding graph context together. ' +
50
+ 'Returns structured text within token_budget. Use before reading any files.',
106
51
  inputSchema: {
107
52
  type: 'object',
108
53
  properties: {
109
54
  path: { type: 'string', description: 'Project root' },
110
- question: { type: 'string', description: 'Natural language question' },
55
+ question: { type: 'string', description: 'Natural language question about the codebase' },
56
+ node: { type: 'string', description: 'Node name or partial name to look up (type, file, deps, callers)' },
111
57
  token_budget: { type: 'integer', description: 'Max tokens in response (default 2000)' },
112
58
  },
113
- required: ['path', 'question'],
114
- },
115
- },
116
- {
117
- name: 'codegraph_explain',
118
- description:
119
- 'Look up a node by name — returns description, type, file, and direct neighbors (depends_on + used_by). ' +
120
- 'Use to understand what a specific function/class/module does and how it connects. ' +
121
- 'Descriptions are AI-written via codegraph_add_nodes.',
122
- inputSchema: {
123
- type: 'object',
124
- properties: {
125
- path: { type: 'string', description: 'Project root' },
126
- node: { type: 'string', description: 'Node name or partial name' },
127
- },
128
- required: ['path', 'node'],
59
+ required: ['path'],
129
60
  },
130
61
  },
131
62
  {
@@ -184,7 +115,10 @@ export function handle(name, args, state) {
184
115
  summary: result.summary || '',
185
116
  });
186
117
 
187
- const project = state?.sessionProject || 'global';
118
+ const inferredProject = args.path
119
+ ? args.path.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop()
120
+ : null;
121
+ const project = state?.sessionProject || inferredProject || null;
188
122
  const title = `CodeGraph — ${args.path}`;
189
123
  const content = [
190
124
  `nodes: ${result.nodes} | edges: ${result.edges} | communities: ${result.communities}`,
@@ -28,9 +28,9 @@ export const definition = {
28
28
  `Factual memory — record what happened, what was decided, what broke, what was built.\n` +
29
29
  `• "resume" — START HERE every conversation. Loads recent context, active discussions, and graph status for a project.\n` +
30
30
  `• "save" — Store a note, decision, bug, or code snippet. Auto-deduplicates.\n` +
31
- `• "get" — Load recent entries (compact previews). Auto-digests when large.\n` +
31
+ `• "get" — Load entries. Pass id/ids to fetch specific ones, or project/tags/limit for recent.\n` +
32
32
  `• "update" — Edit an existing entry by id (any field).\n` +
33
- `• "delete" — Remove an entry by id.\n` +
33
+ `• "delete" — Remove one entry (id) or multiple at once (ids: [...]).\n` +
34
34
  `• "list_projects"— Show all projects and entry counts.`,
35
35
  inputSchema: {
36
36
  type: 'object',
@@ -50,7 +50,8 @@ export const definition = {
50
50
  expiresAt: { type: 'string' },
51
51
  limit: { type: 'number' },
52
52
  includeArchived: { type: 'boolean' },
53
- id: { type: 'string' },
53
+ id: { type: 'string', description: 'Single entry ID (get/update/delete)' },
54
+ ids: { type: 'array', items: { type: 'string' }, description: 'Multiple entry IDs — fetch or delete several at once' },
54
55
  },
55
56
  required: ['action'],
56
57
  },
@@ -92,9 +93,15 @@ export async function handle(args, state) {
92
93
  .filter(e => e.status !== 'archived');
93
94
  const discussions = listDiscussions({ project: proj, status: 'active' });
94
95
  const allGraphs = listGraphs();
95
- const graph = proj
96
- ? allGraphs.find(g => g.path?.toLowerCase().includes(proj.toLowerCase())) || allGraphs[0] || null
97
- : allGraphs[0] || null;
96
+ const np = p => (p || '').toLowerCase().replace(/\\/g, '/');
97
+ const graph = resolvedRoot
98
+ ? allGraphs.find(g => np(g.path) === np(resolvedRoot)) || null
99
+ : proj
100
+ ? allGraphs.find(g => {
101
+ const parts = np(g.path).split('/');
102
+ return parts[parts.length - 1] === proj.toLowerCase();
103
+ }) || null
104
+ : null;
98
105
  const totalEntries = countContext(proj);
99
106
 
100
107
  // Auto-restore single active discussion
@@ -183,8 +190,20 @@ export async function handle(args, state) {
183
190
 
184
191
  case 'get': {
185
192
  if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
186
- archiveExpired(args.project);
187
193
  const includeArchived = args.includeArchived === true;
194
+
195
+ // Fetch by specific ID(s) — bypass project/tag/limit filters
196
+ const ids = args.ids || (args.id ? [args.id] : null);
197
+ if (ids) {
198
+ const entries = getContext({ ids, compact: false })
199
+ .filter(e => includeArchived || e.status !== 'archived');
200
+ return {
201
+ entries, count: entries.length,
202
+ message: entries.length ? `Found ${entries.length} entries.` : 'No entries found for given IDs.',
203
+ };
204
+ }
205
+
206
+ archiveExpired(args.project);
188
207
  let entries = getContext({ project: args.project, tags: args.tags, limit: args.limit, compact: true });
189
208
  if (!includeArchived) entries = entries.filter(e => e.status !== 'archived');
190
209
  const fullEntries = entries.length > 10
@@ -210,8 +229,9 @@ export async function handle(args, state) {
210
229
  }
211
230
 
212
231
  case 'delete': {
213
- if (!args.id) throw new Error('id is required for delete');
214
- return deleteContext(args);
232
+ if (!args.id && !args.ids) throw new Error('id or ids is required for delete');
233
+ const result = deleteContext(args);
234
+ return { ...result, message: `Deleted ${result.deleted} entr${result.deleted === 1 ? 'y' : 'ies'}.` };
215
235
  }
216
236
 
217
237
  case 'list_projects': {