context-mcp-server 1.0.1 → 1.0.3

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.
@@ -0,0 +1,68 @@
1
+ """
2
+ build_extractor.py — extract key metadata from build/config files.
3
+ Returns a SINGLE node per file (not decomposed into functions/classes).
4
+ """
5
+
6
+ import json
7
+ import re
8
+ from pathlib import Path
9
+
10
+
11
+ def extract(abs_path: str, rel_path: str) -> list[dict]:
12
+ """Return a single node for the build file with key fields in description."""
13
+ p = Path(abs_path)
14
+ name = p.name
15
+ meta = _read_meta(p)
16
+
17
+ node = {
18
+ "id": f"{rel_path}::file::{name}",
19
+ "name": name,
20
+ "type": "file",
21
+ "file": rel_path,
22
+ "description": _format_meta(meta) if meta else None,
23
+ }
24
+ return [node]
25
+
26
+
27
+ def _read_meta(p: Path) -> dict:
28
+ try:
29
+ if p.name == "package.json":
30
+ data = json.loads(p.read_text(encoding="utf-8", errors="ignore"))
31
+ return {
32
+ "name": data.get("name"),
33
+ "version": data.get("version"),
34
+ "deps": list((data.get("dependencies") or {}).keys())[:10],
35
+ }
36
+ if p.suffix == ".toml":
37
+ text = p.read_text(encoding="utf-8", errors="ignore")
38
+ name = re.search(r'^name\s*=\s*"([^"]+)"', text, re.M)
39
+ version = re.search(r'^version\s*=\s*"([^"]+)"', text, re.M)
40
+ return {
41
+ "name": name.group(1) if name else None,
42
+ "version": version.group(1) if version else None,
43
+ }
44
+ if p.name in {"requirements.txt", "Pipfile"}:
45
+ lines = p.read_text(encoding="utf-8", errors="ignore").splitlines()
46
+ deps = [l.strip() for l in lines if l.strip() and not l.startswith("#")]
47
+ return {"deps": deps[:10]}
48
+ if p.name == "go.mod":
49
+ text = p.read_text(encoding="utf-8", errors="ignore")
50
+ module = re.search(r'^module\s+(\S+)', text, re.M)
51
+ go_ver = re.search(r'^go\s+(\S+)', text, re.M)
52
+ return {
53
+ "module": module.group(1) if module else None,
54
+ "go": go_ver.group(1) if go_ver else None,
55
+ }
56
+ except Exception:
57
+ pass
58
+ return {}
59
+
60
+
61
+ def _format_meta(meta: dict) -> str | None:
62
+ parts = []
63
+ if meta.get("name"): parts.append(f"name={meta['name']}")
64
+ if meta.get("version"): parts.append(f"version={meta['version']}")
65
+ if meta.get("module"): parts.append(f"module={meta['module']}")
66
+ if meta.get("go"): parts.append(f"go={meta['go']}")
67
+ if meta.get("deps"): parts.append(f"deps=[{', '.join(meta['deps'])}]")
68
+ return ", ".join(parts) if parts else None
@@ -7,12 +7,7 @@ from pathlib import Path
7
7
  from typing import Iterator
8
8
 
9
9
  from .cache import file_hash, get_cached_nodes, set_cached_nodes, remove_deleted, load_cache, save_cache
10
- from .config import (
11
- DEFAULT_IGNORE, MAX_FILE_BYTES,
12
- CODE_EXTENSIONS, SQL_EXTENSIONS, CONFIG_EXTENSIONS,
13
- DOC_EXTENSIONS, PDF_EXTENSIONS, IMAGE_EXTENSIONS,
14
- AUDIO_EXTENSIONS, VIDEO_EXTENSIONS,
15
- )
10
+ from .config import DEFAULT_IGNORE, MAX_FILE_BYTES, SKIP_FILENAMES, SKIP_EXTENSIONS, classify_file
16
11
 
17
12
 
18
13
  def _should_ignore(name: str, ignore: set) -> bool:
@@ -26,6 +21,9 @@ def walk_files(root: str, extra_ignore: set | None = None) -> Iterator[str]:
26
21
  # Prune ignored dirs in-place so os.walk doesn't descend
27
22
  dirnames[:] = [d for d in dirnames if d not in ignore and not d.startswith(".")]
28
23
  for fname in filenames:
24
+ ext = Path(fname).suffix.lower()
25
+ if fname in SKIP_FILENAMES or ext in SKIP_EXTENSIONS:
26
+ continue
29
27
  abs_path = os.path.join(dirpath, fname)
30
28
  try:
31
29
  if os.path.getsize(abs_path) > MAX_FILE_BYTES:
@@ -35,20 +33,6 @@ def walk_files(root: str, extra_ignore: set | None = None) -> Iterator[str]:
35
33
  yield abs_path
36
34
 
37
35
 
38
- def classify_file(path: str) -> str:
39
- """Return extraction category for a file."""
40
- ext = Path(path).suffix.lower()
41
- if ext in CODE_EXTENSIONS: return "code"
42
- if ext in SQL_EXTENSIONS: return "sql"
43
- if ext in CONFIG_EXTENSIONS: return "config"
44
- if ext in DOC_EXTENSIONS: return "doc"
45
- if ext in PDF_EXTENSIONS: return "pdf"
46
- if ext in IMAGE_EXTENSIONS: return "image"
47
- if ext in AUDIO_EXTENSIONS: return "audio"
48
- if ext in VIDEO_EXTENSIONS: return "video"
49
- return "unknown"
50
-
51
-
52
36
  def scan(project_root: str, extra_ignore: set | None = None) -> dict:
53
37
  """
54
38
  Walk project, diff against cache.
@@ -72,7 +56,7 @@ def scan(project_root: str, extra_ignore: set | None = None) -> dict:
72
56
  rel_path = os.path.relpath(abs_path, root).replace("\\", "/")
73
57
  existing_rel.add(rel_path)
74
58
  category = classify_file(abs_path)
75
- if category == "unknown":
59
+ if category in ("unknown", "skip"):
76
60
  continue
77
61
  h = file_hash(abs_path)
78
62
  nodes = get_cached_nodes(cache, rel_path, h)
@@ -3,13 +3,12 @@
3
3
  codegraph/server.py — MCP server exposing codebase knowledge graph tools.
4
4
 
5
5
  Tools:
6
- codegraph_build — scan project, extract AST nodes, build graph (local only, no API)
7
- codegraph_extract return raw doc content for the AI to read and extract concepts from
8
- codegraph_add_nodes AI pushes extracted concept nodes back into the graph
9
- codegraph_query natural language question → graph traversal answer
10
- codegraph_report return full CODEGRAPH_REPORT.md
11
- codegraph_nodes list nodes of a given type
12
- codegraph_path — shortest path between two concepts
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
9
+ codegraph_report return full CODEGRAPH_REPORT.md
10
+ codegraph_nodes list nodes of a given type
11
+ codegraph_path shortest path between two concepts
13
12
  """
14
13
 
15
14
  import asyncio
@@ -22,10 +21,10 @@ from mcp.server import Server
22
21
  from mcp.server.stdio import stdio_server
23
22
  from mcp.types import Tool, TextContent
24
23
 
25
- from .scanner import scan, classify_file
26
- from .cache import file_hash, set_cached_nodes, save_cache, save_semantic_cache
24
+ from .scanner import scan
25
+ from .config import classify_file
26
+ from .cache import file_hash, set_cached_nodes, save_cache
27
27
  from .extractors.ast_extractor import extract as ast_extract
28
- from .extractors.doc_extractor import extract_text
29
28
  from .graph.builder import build, to_json_dict, save_graph, load_graph
30
29
  from .graph.query import answer as graph_answer, find_path
31
30
  from .graph.clustering import detect_communities
@@ -41,10 +40,9 @@ TOOLS = [
41
40
  name="codegraph_build",
42
41
  description=(
43
42
  "Scan a project directory and build the knowledge graph from code files. "
44
- "Uses AST extraction (with regex fallback) for all code files. "
43
+ "Uses tree-sitter AST (with regex fallback) for all code files. "
45
44
  "Fast, local, no API key needed. "
46
- "For docs and images, call codegraph_extract afterward — the AI reads and extracts concepts, "
47
- "then calls codegraph_add_nodes to push them into the graph."
45
+ "Run once per project; rebuild whenever code changes."
48
46
  ),
49
47
  inputSchema={
50
48
  "type": "object",
@@ -55,69 +53,14 @@ TOOLS = [
55
53
  "required": ["path"],
56
54
  },
57
55
  ),
58
- Tool(
59
- name="codegraph_extract",
60
- description=(
61
- "Return raw content of changed doc files so the AI can read them and extract concepts. "
62
- "Call this after codegraph_build. Read the returned files, extract key concepts and "
63
- "relationships, then call codegraph_add_nodes with your findings. "
64
- "Works with any AI — no API key required."
65
- ),
66
- inputSchema={
67
- "type": "object",
68
- "properties": {
69
- "path": {"type": "string", "description": "Project root (same as codegraph_build)"},
70
- "limit": {"type": "integer", "description": "Max files to return per call (default 10)"},
71
- },
72
- "required": ["path"],
73
- },
74
- ),
75
- Tool(
76
- name="codegraph_add_nodes",
77
- description=(
78
- "Add concept nodes extracted by the AI into the graph. "
79
- "Call this after reading the output of codegraph_extract. "
80
- "Each node should have: name, type, file, and optionally description and relations."
81
- ),
82
- inputSchema={
83
- "type": "object",
84
- "properties": {
85
- "path": {"type": "string", "description": "Project root"},
86
- "nodes": {
87
- "type": "array",
88
- "description": "Concept nodes to add",
89
- "items": {
90
- "type": "object",
91
- "properties": {
92
- "name": {"type": "string"},
93
- "type": {"type": "string", "description": "class|function|concept|service|decision|requirement"},
94
- "file": {"type": "string", "description": "Relative file path this concept came from"},
95
- "description": {"type": "string"},
96
- "relations": {
97
- "type": "array",
98
- "items": {
99
- "type": "object",
100
- "properties": {
101
- "name": {"type": "string"},
102
- "relation": {"type": "string", "description": "depends-on|uses|implements|defines|documents"},
103
- },
104
- },
105
- },
106
- },
107
- "required": ["name", "type", "file"],
108
- },
109
- },
110
- },
111
- "required": ["path", "nodes"],
112
- },
113
- ),
114
56
  Tool(
115
57
  name="codegraph_query",
116
58
  description=(
117
- "Ask a structural question about the codebase. Pure graph traversal instant, no API call. "
118
- "Returns structured NODE/EDGE text truncated to token_budget. "
119
- "Good for: dependencies, callers, module relationships. "
120
- "NOT for: bug investigation or understanding code logic read the file for that."
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."
121
64
  ),
122
65
  inputSchema={
123
66
  "type": "object",
@@ -132,9 +75,9 @@ TOOLS = [
132
75
  Tool(
133
76
  name="codegraph_explain",
134
77
  description=(
135
- "Look up a node by name — returns description, type, file, and direct neighbors. "
136
- "Use to understand what a specific function/class/module does and how it connects. "
137
- "Descriptions are AI-written via codegraph_add_nodes."
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."
138
81
  ),
139
82
  inputSchema={
140
83
  "type": "object",
@@ -198,14 +141,12 @@ async def call_tool(name: str, arguments: dict):
198
141
 
199
142
 
200
143
  async def _dispatch(name: str, args: dict):
201
- if name == "codegraph_build": return await _build(args)
202
- if name == "codegraph_extract": return await _extract(args)
203
- if name == "codegraph_add_nodes": return await _add_nodes(args)
204
- if name == "codegraph_query": return await _query(args)
205
- if name == "codegraph_explain": return await _explain(args)
206
- if name == "codegraph_report": return await _report(args)
207
- if name == "codegraph_nodes": return await _nodes(args)
208
- if name == "codegraph_path": return await _path(args)
144
+ if name == "codegraph_build": return await _build(args)
145
+ if name == "codegraph_query": return await _query(args)
146
+ if name == "codegraph_explain": return await _explain(args)
147
+ if name == "codegraph_report": return await _report(args)
148
+ if name == "codegraph_nodes": return await _nodes(args)
149
+ if name == "codegraph_path": return await _path(args)
209
150
  raise ValueError(f"Unknown tool: {name}")
210
151
 
211
152
 
@@ -222,9 +163,7 @@ async def _build(args: dict) -> dict:
222
163
  changed = scan_result["changed"]
223
164
  deleted = scan_result["deleted"]
224
165
 
225
- # Local AST extraction — code/sql/config files only
226
166
  all_nodes: list[dict] = []
227
- pending_docs: list[str] = [] # rel_paths of changed docs/images for codegraph_extract
228
167
 
229
168
  for nodes in cached.values():
230
169
  all_nodes.extend(nodes)
@@ -241,9 +180,12 @@ async def _build(args: dict) -> dict:
241
180
  "name": Path(rel_path).name, "type": "file", "file": rel_path}
242
181
  set_cached_nodes(cache, rel_path, file_hash(abs_path), [node])
243
182
  all_nodes.append(node)
244
- elif cat in ("doc", "pdf"):
245
- pending_docs.append(rel_path)
246
- elif cat in ("image", "audio", "video"):
183
+ elif cat == "build":
184
+ from codegraph.extractors.build_extractor import extract as build_extract
185
+ nodes = build_extract(abs_path, rel_path)
186
+ set_cached_nodes(cache, rel_path, file_hash(abs_path), nodes)
187
+ all_nodes.extend(nodes)
188
+ elif cat in ("image", "audio", "video", "doc", "pdf"):
247
189
  # Label-only — node in graph so AI can reference the file, no content extraction
248
190
  node = {"id": f"{rel_path}::file::{Path(rel_path).name}",
249
191
  "name": Path(rel_path).name, "type": "file", "file": rel_path}
@@ -276,147 +218,9 @@ async def _build(args: dict) -> dict:
276
218
  "summary": f"Built graph: {len(graph_dict.get('nodes', []))} nodes from code files.",
277
219
  }
278
220
 
279
- if pending_docs:
280
- result["pending_docs"] = len(pending_docs)
281
- result["hint"] = (
282
- f"{len(pending_docs)} doc/image file(s) need concept extraction. "
283
- "Call codegraph_extract to get their content, then codegraph_add_nodes with your findings."
284
- )
285
-
286
221
  return result
287
222
 
288
223
 
289
- # ── Extract (return raw content for AI to read) ───────────────────────────────
290
-
291
- async def _extract(args: dict) -> dict:
292
- root = args["path"]
293
- limit = args.get("limit", 10)
294
- force = args.get("force", False)
295
-
296
- scan_result = scan(root)
297
- cache = scan_result["cache"]
298
- scan_root = scan_result["root"]
299
- # force=True: return all files; otherwise only changed
300
- if force:
301
- from codegraph.scanner import walk_files, classify_file
302
- candidates = {
303
- os.path.relpath(p, scan_root).replace("\\", "/"): p
304
- for p in walk_files(scan_root)
305
- if classify_file(p) in ("doc", "code")
306
- }
307
- else:
308
- candidates = scan_result["changed"]
309
-
310
- files = []
311
- for rel_path, abs_path in list(candidates.items()):
312
- if len(files) >= limit:
313
- break
314
- cat = classify_file(abs_path)
315
- if cat in ("doc", "code"):
316
- text = extract_text(abs_path)
317
- if not text:
318
- continue
319
- entry: dict = {"rel_path": rel_path, "type": cat, "content": text}
320
- # For code files, include existing AST nodes so AI knows what to describe
321
- if cat == "code":
322
- cached_entry = cache.get(rel_path, {})
323
- existing_nodes = cached_entry.get("nodes", [])
324
- entry["existing_nodes"] = [
325
- {"name": n.get("name"), "type": n.get("type")}
326
- for n in existing_nodes
327
- ]
328
- files.append(entry)
329
-
330
- remaining = max(0, len(candidates) - limit)
331
- return {
332
- "files": files,
333
- "returned": len(files),
334
- "remaining": remaining,
335
- "instruction": (
336
- "For each file: read the content. "
337
- "For code files, write a description for each existing_node (name + type listed). "
338
- "For doc files, extract key concepts, decisions, and relationships as new nodes. "
339
- "Then call codegraph_add_nodes with all nodes (include description field)."
340
- ),
341
- }
342
-
343
-
344
- # ── Add nodes (AI pushes extracted concepts in) ───────────────────────────────
345
-
346
- async def _add_nodes(args: dict) -> dict:
347
- root = args["path"]
348
- nodes = args.get("nodes", [])
349
-
350
- if not nodes:
351
- return {"success": False, "message": "No nodes provided."}
352
-
353
- graph_dict = load_graph(root) or {"nodes": [], "edges": [], "communities": [], "god_nodes": []}
354
-
355
- # Assign IDs and merge; update description on existing nodes
356
- existing_map = {n["id"]: n for n in graph_dict["nodes"]}
357
- added = 0
358
- updated = 0
359
- for node in nodes:
360
- nid = f"{node['file']}::concept::{node['name']}"
361
- desc = node.get("description", "")
362
- if nid in existing_map:
363
- if desc and desc != existing_map[nid].get("description", ""):
364
- existing_map[nid]["description"] = desc
365
- updated += 1
366
- # Still add edges below
367
- else:
368
- new_node = {
369
- "id": nid,
370
- "name": node["name"],
371
- "type": node.get("type", "concept"),
372
- "file": node["file"],
373
- "description": desc,
374
- }
375
- graph_dict["nodes"].append(new_node)
376
- existing_map[nid] = new_node
377
- added += 1
378
-
379
- # Also try to enrich AST nodes (different ID pattern: file::type::name)
380
- for id_pattern in [
381
- f"{node['file']}::{node.get('type','function')}::{node['name']}",
382
- f"{node['file']}::function::{node['name']}",
383
- f"{node['file']}::class::{node['name']}",
384
- f"{node['file']}::module::{node['name']}",
385
- ]:
386
- if id_pattern in existing_map and desc:
387
- if not existing_map[id_pattern].get("description"):
388
- existing_map[id_pattern]["description"] = desc
389
- updated += 1
390
-
391
- # Add relation edges
392
- for rel in node.get("relations", []):
393
- graph_dict["edges"].append({
394
- "from": nid,
395
- "to": rel["name"],
396
- "relation": rel.get("relation", "relates-to"),
397
- "confidence": "EXTRACTED",
398
- })
399
-
400
- # Persist descriptions to semantic cache (never overwritten by rebuild)
401
- by_file: dict = {}
402
- for node in nodes:
403
- if node.get("description"):
404
- by_file.setdefault(node["file"], []).append(node)
405
- if by_file:
406
- save_semantic_cache(root, by_file)
407
-
408
- save_graph(root, graph_dict)
409
- generate_report(graph_dict, root)
410
-
411
- return {
412
- "success": True,
413
- "nodes_added": added,
414
- "nodes_updated": updated,
415
- "total_nodes": len(graph_dict["nodes"]),
416
- "message": f"Added {added}, updated {updated} node description(s).",
417
- }
418
-
419
-
420
224
  # ── Query / Report / Nodes / Path ─────────────────────────────────────────────
421
225
 
422
226
  async def _query(args: dict) -> dict:
@@ -466,8 +270,7 @@ async def _explain(args: dict) -> dict:
466
270
  "description": match.get("description") or None,
467
271
  "depends_on": depends_on[:20],
468
272
  "used_by": used_by[:20],
469
- "hint": None if match.get("description") else
470
- "No description yet. Call codegraph_extract → codegraph_add_nodes to enrich.",
273
+ "hint": None,
471
274
  }
472
275
 
473
276
 
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "context-mcp-server",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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
+ "context-mcp-server": "./src/index.js",
8
9
  "context-mcp-http": "./src/http.js",
9
10
  "ctx": "./src/cli.js"
10
11
  },
package/src/cli.js CHANGED
@@ -119,9 +119,11 @@ function printUsage() {
119
119
  cmd('ctx discuss [project]', 'show discussions');
120
120
  cmd('ctx benchmark', 'token savings report (memory + graph)');
121
121
  console.log('');
122
+ cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps');
122
123
  cmd('ctx install --<platform>', 'write MCP config + instruction file for an AI platform');
123
124
  cmd('ctx install --all', 'install for all platforms at once');
124
125
  cmd('ctx online [--port N]', 'start HTTP server + show credentials for Claude.ai / ChatGPT');
126
+ cmd('ctx online --close', 'stop the running HTTP server');
125
127
  cmd('ctx settings', 'view and edit config (port, host, client id/secret)');
126
128
  console.log('');
127
129
  cmd('ctx help', 'show this screen');
@@ -586,10 +588,8 @@ const PLATFORMS = {
586
588
  windsurf: {
587
589
  label: 'Windsurf',
588
590
  install(cwd) {
589
- // Local rule file
590
591
  const rules = _tpl('windsurf-rules.md');
591
592
  if (rules) _writeFile(join(cwd, '.windsurf', 'rules', 'context-mcp.md'), rules, '.windsurf/rules/context-mcp.md');
592
- // Global Windsurf config
593
593
  const globalCfgPath = join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
594
594
  let existing = {};
595
595
  try { existing = JSON.parse(readFileSync(globalCfgPath, 'utf8')); } catch {}
@@ -602,12 +602,57 @@ const PLATFORMS = {
602
602
 
603
603
  function cmdInstall(args) {
604
604
  const flags = new Set(args.map(a => a.replace(/^--/, '')));
605
- const all = flags.has('all');
605
+ const all = flags.has('all');
606
+ const initial = flags.has('initial');
606
607
  const keys = all ? Object.keys(PLATFORMS) : Object.keys(PLATFORMS).filter(k => flags.has(k));
607
608
 
609
+ if (initial) {
610
+ printSection('Install', 'install / update');
611
+ console.log('');
612
+
613
+ const __dirname_init = dirname(fileURLToPath(import.meta.url));
614
+ const pkgRootInit = join(__dirname_init, '..');
615
+
616
+ // Node.js packages
617
+ console.log(` ${bold(lblue('Node.js packages'))}`);
618
+ const npmInstall = spawnSync('npm', ['install', '--omit=dev'], {
619
+ cwd: pkgRootInit, encoding: 'utf8', shell: true,
620
+ });
621
+ if (npmInstall.status !== 0) {
622
+ console.log(` ${bad('✗')} npm install failed:\n${faint((npmInstall.stderr || npmInstall.stdout || '').trim())}`);
623
+ } else {
624
+ console.log(` ${ok('✓')} Node.js dependencies installed`);
625
+ }
626
+ console.log('');
627
+
628
+ // Python / uv (codegraph)
629
+ console.log(` ${bold(lblue('Python Codegraph'))}`);
630
+ const uvCheck2 = spawnSync('uv', ['--version'], { encoding: 'utf8', shell: true });
631
+ if (uvCheck2.error || uvCheck2.status !== 0) {
632
+ console.log(` ${bad('✗')} uv not found — install from ${accent('https://docs.astral.sh/uv/')} to enable codegraph`);
633
+ } else {
634
+ console.log(` ${ok('✓')} uv found: ${faint(uvCheck2.stdout.trim())}`);
635
+ // On Windows, an existing .venv contains a lib64 junction that uv can't remove — wipe it first
636
+ if (process.platform === 'win32') {
637
+ const venvPath = join(pkgRootInit, '.venv');
638
+ spawnSync('cmd', ['/c', 'rmdir', '/s', '/q', venvPath], { encoding: 'utf8' });
639
+ }
640
+ const sync2 = spawnSync('uv', ['sync', '--no-dev'], { cwd: pkgRootInit, encoding: 'utf8', shell: true });
641
+ if (sync2.status !== 0) {
642
+ console.log(` ${bad('✗')} uv sync failed:\n${faint((sync2.stderr || sync2.stdout || '').trim())}`);
643
+ } else {
644
+ console.log(` ${ok('✓')} Python environment ready — codegraph enabled`);
645
+ }
646
+ }
647
+ console.log('');
648
+ return;
649
+ }
650
+
608
651
  if (!keys.length) {
609
652
  printSection('Install');
610
- console.log(` ${muted('Usage:')} ctx install ${faint('[--claude] [--cursor] [--vscode] [--gemini] [--codex] [--windsurf] [--all]')}`);
653
+ console.log(` ${muted('Usage:')} ctx install ${faint('[--initial] [--claude] [--cursor] [--vscode] [--gemini] [--codex] [--windsurf] [--all]')}`);
654
+ console.log('');
655
+ console.log(` ${accent('--initial ')} ${faint('Install / update Node.js + Python (codegraph) deps')}`);
611
656
  console.log('');
612
657
  console.log(` Writes MCP config file + AI instruction file for each selected platform.`);
613
658
  console.log(` Files are written into the ${accent('current directory')} (your project root).`);
@@ -700,6 +745,7 @@ function cmdOnline(args) {
700
745
  const host = hostIdx !== -1 && args[hostIdx + 1] ? args[hostIdx + 1] : null;
701
746
  const git = args.includes('--access-git');
702
747
  const restart = args.includes('--restart');
748
+ const close = args.includes('--close');
703
749
 
704
750
  let cfg;
705
751
  try { cfg = getConfig(); } catch { cfg = { client_id: 'context-mcp', client_secret: '(unavailable)', port: 3100, host: 'localhost' }; }
@@ -710,6 +756,21 @@ function cmdOnline(args) {
710
756
  printSection('Online', `HTTP MCP server → Claude.ai / ChatGPT`);
711
757
  console.log('');
712
758
 
759
+ if (close) {
760
+ const existing2 = _checkExistingHttpServer(resolvedPort);
761
+ if (existing2.status === 'running') {
762
+ if (existing2.pid) {
763
+ try { process.kill(existing2.pid); } catch {}
764
+ }
765
+ try { unlinkSync(_httpPidFile(resolvedPort)); } catch {}
766
+ const pidStr = existing2.pid ? `pid ${existing2.pid} · ` : '';
767
+ console.log(` ${ok('✓')} ${bold('server stopped')} ${faint(pidStr + 'port ' + resolvedPort)}\n`);
768
+ } else {
769
+ console.log(` ${warn('–')} no server running on port ${resolvedPort}\n`);
770
+ }
771
+ return;
772
+ }
773
+
713
774
  // Check if a server is already running on this port
714
775
  const existing = _checkExistingHttpServer(resolvedPort);
715
776
  if (existing.status === 'running') {
@@ -1,7 +1,7 @@
1
1
  # Context-MCP — Codex CLI Usage Guide
2
2
 
3
3
  Persistent memory + codebase knowledge graph.
4
- Every conversation starts with `context.resume`. Every structural question uses `codegraph_query`. Files only read for bugs/logic.
4
+ Every conversation starts with `context.resume`. Every codebase question uses `codegraph_query`. Files only read for bugs/logic.
5
5
 
6
6
  ---
7
7
 
@@ -37,21 +37,15 @@ Always pass `project`. Auto-compact fires at >50 entries.
37
37
 
38
38
  ## 3. CodeGraph Pipeline
39
39
 
40
- ### Step 1 — Build (once, free)
40
+ ### Step 1 — Build (once, fast, local)
41
41
  ```
42
42
  codegraph_build(path) → AST graph: functions, classes, imports, edges
43
43
  ```
44
44
 
45
- ### Step 2 — Enrich (one-time per file)
45
+ ### Step 2 — Query (free, instant)
46
46
  ```
47
- codegraph_extract(path) file content + node list
48
- codegraph_add_nodes(path, nodes) semantic descriptions (permanent cache)
49
- ```
50
-
51
- ### Step 3 — Query (free, instant)
52
- ```
53
- codegraph_query(path, question) → NODE/EDGE subgraph (token_budget default 2000)
54
- codegraph_explain(path, node) → single node + neighbors
47
+ codegraph_query(path, question) fetch any details about the codebase
48
+ codegraph_explain(path, node) single node: type, file, connections
55
49
  codegraph_path(path, from, to) → shortest path
56
50
  codegraph_nodes(path, type) → list nodes by type
57
51
  codegraph_report(path) → full graph analysis
@@ -61,8 +55,8 @@ codegraph_report(path) → full graph analysis
61
55
 
62
56
  ## 4. Graph vs File
63
57
 
64
- **Graph** — structural questions: dependencies, callers, imports.
65
- **File** — bugs, logic, tracing behavior.
58
+ **Graph** — use for any question about what exists: finding functions, classes, files, dependencies, callers, imports, paths between concepts.
59
+ **File** — bugs, logic, tracing unexpected behavior.
66
60
 
67
61
  ---
68
62
 
@@ -71,6 +65,5 @@ codegraph_report(path) → full graph analysis
71
65
  1. **`context.resume` first** — before any tool or response
72
66
  2. **Always pass `project`**
73
67
  3. **`search` before asking** — if user references past work
74
- 4. **`codegraph_query` before reading files**
68
+ 4. **`codegraph_query` before reading files** — graph is faster and cheaper
75
69
  5. **Read files for bugs/logic only**
76
- 6. **Enrich once** — descriptions persist forever