context-mcp-server 1.0.6 → 1.0.8

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,19 +1,20 @@
1
1
  {
2
2
  "name": "context-mcp-server",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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",
14
15
  "mcp-server": "node src/http.js",
15
16
  "cli": "node src/cli.js",
16
- "check": "node --check src/index.js && node --check src/server.js && node --check src/db.js && node --check src/vector.js && node --check src/summarizer.js && node --check src/search.js && node --check src/config.js && node --check src/cli.js && node --check src/http.js && node --check src/tools/context.js && node --check src/tools/search.js && node --check src/tools/discussion.js && node --check src/tools/errorCheck.js && node --check src/tools/fileTools.js && node --check src/tools/gitTools.js && node --check src/tools/codegraph.js && node --check src/hooks/autoLink.js && node --check src/hooks/autoContext.js",
17
+ "check": "node --check src/index.js && node --check src/server.js && node --check src/db.js && node --check src/vector.js && node --check src/summarizer.js && node --check src/search.js && node --check src/config.js && node --check src/cli.js && node --check src/http.js && node --check src/tools/context.js && node --check src/tools/search.js && node --check src/tools/plan.js &&node --check src/tools/errorCheck.js && node --check src/tools/fileTools.js && node --check src/tools/gitTools.js && node --check src/tools/codegraph.js && node --check src/hooks/autoLink.js && node --check src/hooks/autoContext.js",
17
18
  "check-mcp": "npm run check",
18
19
  "test": "node --test",
19
20
  "prepublishOnly": "npm run check"
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.6"
7
+ version = "1.0.8"
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');
@@ -163,17 +163,20 @@ function cmdList(args) {
163
163
 
164
164
  printSection('Context', filterProject ? `project: ${filterProject}` : 'all projects');
165
165
 
166
- // Build per-project map
166
+ // Build per-project map, split entries into their three trees
167
167
  const projects = {};
168
+ const ensureProj = p => {
169
+ if (!projects[p]) projects[p] = { context: [], summary: [], plans: [] };
170
+ return projects[p];
171
+ };
168
172
  for (const entry of entries) {
169
173
  const p = entry.project || 'global';
170
- if (!projects[p]) projects[p] = { contexts: [], discussions: [] };
171
- projects[p].contexts.push(entry);
174
+ const d = ensureProj(p);
175
+ if (entry.type === 'compaction') d.summary.push(entry);
176
+ else d.context.push(entry);
172
177
  }
173
178
  for (const disc of allDiscussions) {
174
- const p = disc.project || 'global';
175
- if (!projects[p]) projects[p] = { contexts: [], discussions: [] };
176
- projects[p].discussions.push(disc);
179
+ ensureProj(disc.project || 'global').plans.push(disc);
177
180
  }
178
181
 
179
182
  const projectNames = Object.keys(projects).sort();
@@ -185,73 +188,82 @@ function cmdList(args) {
185
188
  }
186
189
 
187
190
  for (const projectName of projectNames) {
188
- const pData = projects[projectName];
189
- const graph = _graphForProject(allGraphs, projectName);
190
- const activeD = pData.discussions.filter(d => d.status === 'active').length;
191
- const totalSecs = (pData.contexts.length > 0 ? 1 : 0) + (pData.discussions.length > 0 ? 1 : 0) + (graph ? 1 : 0);
192
- let secIdx = 0;
193
-
194
- const projReg = projectRegistry.get(projectName);
191
+ const pData = projects[projectName];
192
+ const graphBuild = _graphForProject(allGraphs, projectName);
193
+ const totalEntries = pData.context.length + pData.summary.length;
194
+ const activePlans = pData.plans.filter(p => p.status === 'active').length;
195
+ const sections = [
196
+ graphBuild && 'graph',
197
+ pData.context.length && 'context',
198
+ pData.summary.length && 'summary',
199
+ pData.plans.length && 'plans',
200
+ ].filter(Boolean);
201
+ let secIdx = 0;
202
+
203
+ const projReg = projectRegistry.get(projectName);
195
204
  const projIdStr = projReg?.id ? faint(' id:' + projReg.id.slice(0, 8)) : '';
196
- console.log(`\n ${color(C.dblue, '◆')} ${bold(lblue(projectName))}${projIdStr} ${faint(`${pData.contexts.length} entries · ${pData.discussions.length} discussions`)}${activeD ? ` ${warn('● ' + activeD + ' active')}` : ''}`);
205
+ console.log(`\n ${color(C.dblue, '◆')} ${bold(lblue(projectName))}${projIdStr} ${faint(`${totalEntries} entries · ${pData.plans.length} plans`)}${activePlans ? ` ${warn('● ' + activePlans + ' active')}` : ''}`);
197
206
  console.log(` ${color(C.darkgray, '│')}`);
198
207
 
199
- // ── Graph ────────────────────────────────────────────────────────────────
200
- if (graph) {
208
+ const renderEntries = (items, label, secIsLast) => {
209
+ console.log(` ${color(C.darkgray, secIsLast ? '└─' : '├─')} ${muted(label)} ${faint(items.length + ' entries')}`);
210
+ items.forEach((item, i) => {
211
+ const br = i === items.length - 1 ? '└─' : '├─';
212
+ const date = (item.createdAt || '').slice(0, 10);
213
+ const id = item.id.slice(0, 8);
214
+ const tags = safeTags(item.tags);
215
+ const pipe = secIsLast ? ' ' : '│';
216
+ console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${pill(item.type || 'note')} ${bold(item.title || '(no title)')} ${faint('id:' + id)} ${faint(date)}`);
217
+ if (tags.length) console.log(` ${color(C.darkgray, pipe)} ${faint(tags.map(t => '#' + t).join(' '))}`);
218
+ });
219
+ if (!secIsLast) console.log(` ${color(C.darkgray, '│')}`);
220
+ };
221
+
222
+ // ── Graph (build stats only) ──────────────────────────────────────────────
223
+ if (graphBuild) {
201
224
  secIdx++;
202
- const isLast = secIdx === totalSecs;
203
- const builtAt = (graph.builtAt || '').slice(0, 10);
204
- console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${accent('⬡')} ${muted('graph')} ${faint(`${graph.nodes}n · ${graph.edges}e · ${graph.communities} clusters · ${builtAt}`)}`);
225
+ const isLast = secIdx === sections.length;
226
+ const builtAt = (graphBuild.builtAt || '').slice(0, 10);
227
+ console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${accent('⬡')} ${muted('graph')} ${faint(`${graphBuild.nodes}n · ${graphBuild.edges}e · ${graphBuild.communities} clusters · ${builtAt}`)}`);
205
228
  if (!isLast) console.log(` ${color(C.darkgray, '│')}`);
206
229
  }
207
230
 
208
- // ── Context entries ───────────────────────────────────────────────────────
209
- if (pData.contexts.length) {
231
+ // ── Context ───────────────────────────────────────────────────────────────
232
+ if (pData.context.length) {
210
233
  secIdx++;
211
- const isLast = secIdx === totalSecs;
212
- console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('memory')} ${faint(pData.contexts.length + ' entries')}`);
213
- pData.contexts.forEach((item, i) => {
214
- const br = i === pData.contexts.length - 1 ? '└─' : '├─';
234
+ renderEntries(pData.context, 'context', secIdx === sections.length);
235
+ }
236
+
237
+ // ── Summary (compaction digests only no type labels) ───────────────────
238
+ if (pData.summary.length) {
239
+ secIdx++;
240
+ const isLast = secIdx === sections.length;
241
+ console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('summary')} ${faint(pData.summary.length + ' compactions')}`);
242
+ pData.summary.forEach((item, i) => {
243
+ const br = i === pData.summary.length - 1 ? '└─' : '├─';
215
244
  const date = (item.createdAt || '').slice(0, 10);
216
- const type = item.type || 'note';
217
- const id = item.id.slice(0, 8);
218
- const tags = safeTags(item.tags);
219
245
  const pipe = isLast ? ' ' : '│';
220
- console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${pill(type)} ${bold(item.title || '(no title)')} ${faint('id:' + id)} ${faint(date)}`);
221
- if (tags.length) console.log(` ${color(C.darkgray, pipe)} ${faint(tags.map(t => '#' + t).join(' '))}`);
246
+ console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${faint('◎')} ${bold(item.title || '(compaction)')} ${faint(date)}`);
222
247
  });
223
248
  if (!isLast) console.log(` ${color(C.darkgray, '│')}`);
224
249
  }
225
250
 
226
- // ── Discussions ───────────────────────────────────────────────────────────
227
- if (pData.discussions.length) {
251
+ // ── Plans ─────────────────────────────────────────────────────────────────
252
+ if (pData.plans.length) {
228
253
  secIdx++;
229
- const isLast = secIdx === totalSecs;
230
- console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('discussions')} ${faint(pData.discussions.length + ' total')}`);
231
- pData.discussions.forEach((disc, i) => {
232
- const br = i === pData.discussions.length - 1 ? '└─' : '├─';
233
- const sc = disc.status === 'done' ? 'green' : 'tcyan';
234
- const steps = disc.stepsSummary?.total ? faint(` ${disc.stepsSummary.done}/${disc.stepsSummary.total}`) : '';
235
- const pipe = isLast ? ' ' : '│';
236
- console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${warn(disc.status === 'active' ? '●' : '○')} ${bold(disc.name)} ${pill(disc.status, sc)} ${faint(disc.type || 'plan')}${steps}`);
237
- if (disc.description) console.log(` ${color(C.darkgray, pipe)} ${faint(disc.description)}`);
254
+ const isLast = secIdx === sections.length;
255
+ console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('plans')} ${faint(pData.plans.length + ' total')}`);
256
+ pData.plans.forEach((plan, i) => {
257
+ const br = i === pData.plans.length - 1 ? '└─' : '├─';
258
+ const pipe = isLast ? ' ' : '';
259
+ console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${warn(plan.status === 'active' ? '●' : '○')} ${bold(plan.name)} ${pill(plan.status, plan.status === 'done' ? 'green' : 'tcyan')} ${faint((plan.description || '').slice(0, 60))}`);
238
260
  });
239
261
  }
240
262
  }
241
263
 
242
- // Orphan graphs (no matching project)
243
- const orphanGraphs = allGraphs.filter(g => !projectNames.some(p => _graphForProject([g], p)));
244
- if (orphanGraphs.length) {
245
- console.log(`\n ${color(C.dblue, '◇')} ${muted('other graphs')}`);
246
- for (const g of orphanGraphs) {
247
- const pathShort = g.path.replace(/\\/g, '/').split('/').slice(-2).join('/');
248
- console.log(` ${accent('⬡')} ${bold(pathShort)} ${faint(`${g.nodes}n · ${g.edges}e · ${g.communities} clusters`)}`);
249
- }
250
- }
251
-
252
264
  console.log('');
253
265
  console.log(line());
254
- console.log(faint(` ${entries.length} entries · ${allDiscussions.length} discussions · ${allGraphs.length} graphs · ${projectNames.length} projects`));
266
+ console.log(faint(` ${entries.length} entries · ${allDiscussions.length} plans · ${allGraphs.length} graphs · ${projectNames.length} projects`));
255
267
  }
256
268
 
257
269
  // ── Search ────────────────────────────────────────────────────────────────────
@@ -429,7 +441,7 @@ function cmdBenchmark() {
429
441
  printSection('Benchmark', 'real token savings');
430
442
 
431
443
  const RESUME_LIMIT = 15;
432
- const COMPACT_AT = 50;
444
+ const COMPACT_AT = 20;
433
445
 
434
446
  // ── Measure entry sizes from actual stored data ──────────────────────────────
435
447
  const allEntries = getContext({ limit: 500, compact: false });