context-mcp-server 1.1.2 → 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.
Files changed (47) hide show
  1. package/README.md +2 -3
  2. package/codegraph/__pycache__/callflow_html.cpython-313.pyc +0 -0
  3. package/codegraph/__pycache__/export.cpython-313.pyc +0 -0
  4. package/codegraph/__pycache__/report.cpython-313.pyc +0 -0
  5. package/codegraph/__pycache__/scanner.cpython-313.pyc +0 -0
  6. package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
  7. package/codegraph/__pycache__/tree_html.cpython-313.pyc +0 -0
  8. package/codegraph/callflow_html.py +6 -4
  9. package/codegraph/export.py +25 -10
  10. package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
  11. package/codegraph/extractors/ast_extractor.py +37 -8
  12. package/codegraph/graph/__pycache__/builder.cpython-313.pyc +0 -0
  13. package/codegraph/graph/__pycache__/clustering.cpython-313.pyc +0 -0
  14. package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
  15. package/codegraph/graph/__pycache__/symbol_resolution.cpython-313.pyc +0 -0
  16. package/codegraph/graph/builder.py +27 -23
  17. package/codegraph/graph/clustering.py +5 -3
  18. package/codegraph/graph/query.py +5 -4
  19. package/codegraph/graph/symbol_resolution.py +14 -3
  20. package/codegraph/report.py +1 -1
  21. package/codegraph/scanner.py +1 -1
  22. package/codegraph/server.py +26 -5
  23. package/codegraph/tree_html.py +28 -30
  24. package/package.json +2 -2
  25. package/pyproject.toml +72 -72
  26. package/src/cli.js +12 -42
  27. package/src/db.js +26 -14
  28. package/src/http.js +1 -1
  29. package/src/search.js +4 -9
  30. package/src/server.js +16 -7
  31. package/src/templates/antigravity/GEMINI.md +18 -6
  32. package/src/templates/claude/CLAUDE.md +14 -1
  33. package/src/templates/claude/skills/SKILL.md +15 -3
  34. package/src/templates/codex/AGENTS.md +9 -2
  35. package/src/templates/cursor/cursor-rules.mdc +13 -4
  36. package/src/templates/gemini/GEMINI.md +14 -3
  37. package/src/templates/windsurf/windsurf-rules.md +14 -3
  38. package/src/tools/codegraph.js +4 -1
  39. package/src/tools/context.js +6 -6
  40. package/src/tools/errorCheck.js +3 -3
  41. package/src/tools/fileTools.js +2 -2
  42. package/src/tools/gitTools.js +1 -1
  43. package/src/tools/search.js +1 -1
  44. package/src/tools/symbolDetail.js +74 -0
  45. package/src/tools/toolRegistry.js +77 -0
  46. package/src/vector.js +7 -2
  47. package/uv.lock +3 -3
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.2"
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/cli.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import readline from 'node:readline';
8
- import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, chmodSync } from 'node:fs';
9
9
  import { dirname, join } from 'node:path';
10
10
  import { homedir } from 'node:os';
11
11
  import { fileURLToPath } from 'node:url';
@@ -414,7 +414,6 @@ const _GLOBAL_GITIGNORE_ENTRIES = [
414
414
  '.gemini/',
415
415
  '.codex/',
416
416
  '.windsurf/',
417
- '.agents/',
418
417
  // Build outputs and session artifacts
419
418
  'codegraph-cache/',
420
419
  '.mcp.json',
@@ -429,7 +428,6 @@ function _graphForProject(graphs, projectName) {
429
428
 
430
429
  const _PROJECT_GITIGNORE_ENTRIES = [
431
430
  '.claude/', '.cursor/', '.vscode/', '.gemini/', '.codex/',
432
- '.agents/',
433
431
  'codegraph-cache/', '.mcp.json', 'CLAUDE.md', 'GEMINI.md', 'AGENTS.md',
434
432
  ];
435
433
 
@@ -498,7 +496,12 @@ function _copyHooks(platform, dotDir, dir, hookFiles) {
498
496
  for (const file of hookFiles) {
499
497
  const src = join(hooksSrc, file);
500
498
  if (existsSync(src)) {
501
- _writeFile(join(hooksDest, file), readFileSync(src, 'utf8'), `${dotDir}/hooks/${file}`);
499
+ const dest = join(hooksDest, file);
500
+ _writeFile(dest, readFileSync(src, 'utf8'), `${dotDir}/hooks/${file}`);
501
+ // Make executable on Unix so shells can run it without explicit `node` prefix
502
+ if (process.platform !== 'win32') {
503
+ try { chmodSync(dest, 0o755); } catch {}
504
+ }
502
505
  }
503
506
  }
504
507
  }
@@ -618,11 +621,11 @@ const PLATFORMS = {
618
621
  _mergeHooksIntoSettings(settingsPath, {
619
622
  PreToolUse: [{
620
623
  matcher: 'Bash',
621
- hooks: [{ type: 'command', command: preHook, timeout: 30, statusMessage: 'Checking shell command' }],
624
+ hooks: [{ type: 'command', command: `node "${preHook}"`, timeout: 30, statusMessage: 'Checking shell command' }],
622
625
  }],
623
626
  PostToolUse: [{
624
627
  matcher: 'Bash',
625
- hooks: [{ type: 'command', command: postHook, timeout: 30, statusMessage: 'Saving failed shell context' }],
628
+ hooks: [{ type: 'command', command: `node "${postHook}"`, timeout: 30, statusMessage: 'Saving failed shell context' }],
626
629
  }],
627
630
  }, scope === 'project' ? '.claude/settings.json' : '~/.claude/settings.json');
628
631
  // Register MCP server via claude CLI
@@ -732,11 +735,11 @@ const PLATFORMS = {
732
735
  );
733
736
  settings.hooks.BeforeTool = stripOld(settings.hooks.BeforeTool).concat([{
734
737
  matcher: 'run_shell_command',
735
- hooks: [{ type: 'command', command: `node ${beforeHook}`, timeout: 30 }],
738
+ hooks: [{ type: 'command', command: `node "${beforeHook}"`, timeout: 30 }],
736
739
  }]);
737
740
  settings.hooks.AfterTool = stripOld(settings.hooks.AfterTool).concat([{
738
741
  matcher: 'run_shell_command',
739
- hooks: [{ type: 'command', command: `node ${afterHook}`, timeout: 30 }],
742
+ hooks: [{ type: 'command', command: `node "${afterHook}"`, timeout: 30 }],
740
743
  }]);
741
744
  _writeFile(settingsPath,
742
745
  JSON.stringify(settings, null, 2),
@@ -829,39 +832,6 @@ const PLATFORMS = {
829
832
  }
830
833
  },
831
834
  },
832
- antigravity: {
833
- label: 'Antigravity IDE',
834
- restartNote: 'Restart your Antigravity session to pick up hooks and rules.',
835
- install(dir, scope) {
836
- // Antigravity uses stdio-incompatible MCP transport — integrate via ctx CLI + GEMINI.md instead.
837
- if (scope === 'project') {
838
- // Post-tool hook — saves failed tool calls to context-mcp via ctx CLI
839
- _copyHooks('antigravity', '.agents', dir, ['context-mcp-post-tool-use.js']);
840
- const hookPath = join(dir, '.agents', 'hooks', 'context-mcp-post-tool-use.js');
841
- _mergeJsonFile(join(dir, '.agents', 'hooks.json'), '.agents/hooks.json', obj => {
842
- const strip = arr => (arr || []).filter(h => !String(h.command || '').includes('context-mcp-'));
843
- obj.hooks = strip(obj.hooks).concat([{
844
- event: 'PostToolUse',
845
- command: `node "${hookPath}"`,
846
- }]);
847
- });
848
- // Workflows (slash commands) — .agents/workflows/
849
- const wfSrc = join(TPLS, 'antigravity', 'workflows');
850
- for (const file of ['context-resume.md', 'graph-build.md', 'save-context.md']) {
851
- const src = join(wfSrc, file);
852
- if (existsSync(src)) _writeFile(join(dir, '.agents', 'workflows', file), readFileSync(src, 'utf8'), `.agents/workflows/${file}`);
853
- }
854
- }
855
- // Rules file — project root for project scope, ~/.config/antigravity/GEMINI.md for global
856
- const agMd = _tpl('antigravity/GEMINI.md');
857
- if (agMd) {
858
- const agMdPath = scope === 'project'
859
- ? join(dir, 'GEMINI.md')
860
- : join(homedir(), '.config', 'antigravity', 'GEMINI.md');
861
- _writeFile(agMdPath, agMd, scope === 'project' ? 'GEMINI.md' : '~/.config/antigravity/GEMINI.md');
862
- }
863
- },
864
- },
865
835
  };
866
836
 
867
837
  async function cmdInstall(args) {
@@ -923,7 +893,7 @@ async function cmdInstall(args) {
923
893
 
924
894
  if (!keys.length) {
925
895
  printSection('Install');
926
- console.log(` ${muted('Usage:')} ctx install ${faint('[--initial] [--claude] [--cursor] [--vscode] [--gemini] [--codex] [--windsurf] [--antigravity] [--all]')}`);
896
+ console.log(` ${muted('Usage:')} ctx install ${faint('[--initial] [--claude] [--cursor] [--vscode] [--gemini] [--codex] [--windsurf] [--all]')}`);
927
897
  console.log('');
928
898
  console.log(` ${accent('--initial ')} ${faint('Install / update Node.js + Python (codegraph) deps')}`);
929
899
  console.log('');
package/src/db.js CHANGED
@@ -178,6 +178,11 @@ function findEntryById(id, projectHint) {
178
178
  const e = search(data);
179
179
  if (e) return { entry: e, projectName: name };
180
180
  }
181
+ // Always check 'global' — it is never in the projects index
182
+ if (!_projectData.has('global') && 'global' !== projectHint) {
183
+ const e = search(loadProjectData('global'));
184
+ if (e) return { entry: e, projectName: 'global' };
185
+ }
181
186
  const idx = loadProjectsIndex();
182
187
  for (const proj of idx) {
183
188
  if (_projectData.has(proj.name) || proj.name === projectHint) continue;
@@ -384,10 +389,12 @@ export function getContext({ project, tags, limit = 20, compact = false, ids } =
384
389
 
385
390
  if (ids && ids.length) {
386
391
  const idSet = new Set(ids);
387
- // Load all projects to find entries
392
+ // Load all projects to find entries — always include 'global' since it is
393
+ // never registered in the projects index (ensureProject skips it)
388
394
  const idx = loadProjectsIndex();
389
395
  const all = [];
390
396
  const loaded = new Set(_projectData.keys());
397
+ loaded.add('global'); // ensure global is always searched
391
398
  for (const proj of idx) loaded.add(proj.name);
392
399
  for (const name of loaded) {
393
400
  for (const e of getAllEntries(name)) {
@@ -403,10 +410,11 @@ export function getContext({ project, tags, limit = 20, compact = false, ids } =
403
410
  const globalEntries = project !== 'global' ? getAllEntries('global') : [];
404
411
  results = [...entries, ...globalEntries];
405
412
  } else {
406
- // No project filter: load all
413
+ // No project filter: load all — always include 'global' since it is never in the index
407
414
  const idx = loadProjectsIndex();
408
415
  const all = [];
409
416
  const seen = new Set(_projectData.keys());
417
+ seen.add('global');
410
418
  for (const proj of idx) seen.add(proj.name);
411
419
  for (const name of seen) {
412
420
  all.push(...getAllEntries(name));
@@ -434,7 +442,7 @@ export function getContextSince(since, project) {
434
442
  } else {
435
443
  const idx = loadProjectsIndex();
436
444
  results = [];
437
- const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
445
+ const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
438
446
  for (const name of seen) results.push(...getAllEntries(name));
439
447
  }
440
448
  return results.filter(c => c.createdAt >= since);
@@ -450,12 +458,15 @@ export function searchContext({ query, project, limit = 10, compact = false }) {
450
458
  } else {
451
459
  const idx = loadProjectsIndex();
452
460
  results = [];
453
- const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
461
+ const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
454
462
  for (const name of seen) results.push(...getAllEntries(name));
455
463
  }
456
464
  const scored = results.map(c => {
457
465
  const haystack = `${c.title || ''} ${c.content || ''} ${(Array.isArray(c.tags) ? c.tags : []).join(' ')}`.toLowerCase();
458
- 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);
459
470
  return { ...c, score };
460
471
  }).filter(c => c.score > 0).sort((a, b) => b.score - a.score);
461
472
  const sliced = scored.slice(0, limit).map(({ score, ...c }) => c);
@@ -467,8 +478,9 @@ export function deleteContext({ id, ids }) {
467
478
  const idSet = new Set(ids && ids.length ? ids : (id ? [id] : []));
468
479
  if (!idSet.size) return { deleted: 0 };
469
480
  let deleted = 0;
470
- // Scan all loaded projects
481
+ // Scan all loaded projects — always include 'global' since it is never in the index
471
482
  const seen = new Set(_projectData.keys());
483
+ seen.add('global');
472
484
  loadProjectsIndex().forEach(p => seen.add(p.name));
473
485
  for (const name of seen) {
474
486
  const data = loadProjectData(name);
@@ -524,7 +536,7 @@ export function countContext(project) {
524
536
  if (!project) {
525
537
  const idx = loadProjectsIndex();
526
538
  let total = 0;
527
- const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
539
+ const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
528
540
  for (const name of seen) total += getAllEntries(name).length;
529
541
  return total;
530
542
  }
@@ -658,7 +670,7 @@ export function updateDiscussion({ id, name, title, description, content, status
658
670
  let disc = null;
659
671
  let projName = null;
660
672
  const idx = loadProjectsIndex();
661
- const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
673
+ const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
662
674
  for (const pName of seen) {
663
675
  const d = loadProjectData(pName);
664
676
  const found = id ? d.discussions.find(x => x.id === id) : d.discussions.find(x => x.name === name);
@@ -690,9 +702,9 @@ export function getDiscussion({ project, name, id } = {}) {
690
702
  if (name) return list.find(d => d.name === name) || null;
691
703
  return null;
692
704
  }
693
- // Search all
705
+ // Search all — always include 'global' since it is never in the projects index
694
706
  const idx = loadProjectsIndex();
695
- const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
707
+ const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
696
708
  for (const pName of seen) {
697
709
  const d = loadProjectData(pName);
698
710
  const found = id ? d.discussions.find(x => x.id === id) : d.discussions.find(x => x.name === name);
@@ -709,7 +721,7 @@ export function listDiscussions({ project, status, type } = {}) {
709
721
  if (project !== 'global') list = [...list, ...loadProjectData('global').discussions];
710
722
  } else {
711
723
  const idx = loadProjectsIndex();
712
- const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
724
+ const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
713
725
  for (const pName of seen) list.push(...loadProjectData(pName).discussions);
714
726
  }
715
727
  if (status) list = list.filter(d => d.status === status);
@@ -730,7 +742,7 @@ export function linkContextToDiscussion({ discussionId, discussionName, contextI
730
742
  let disc = null;
731
743
  let discProject = null;
732
744
  const idx = loadProjectsIndex();
733
- const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
745
+ const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
734
746
  for (const pName of seen) {
735
747
  const d = loadProjectData(pName);
736
748
  const found = discussionId
@@ -764,7 +776,7 @@ export function linkContextToDiscussion({ discussionId, discussionName, contextI
764
776
  export function deleteDiscussion({ name, id }) {
765
777
  init();
766
778
  const idx = loadProjectsIndex();
767
- const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
779
+ const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
768
780
  for (const pName of seen) {
769
781
  const data = loadProjectData(pName);
770
782
  const before = data.discussions.length;
@@ -803,7 +815,7 @@ export function archiveExpired(project) {
803
815
  processEntries(getAllEntries(project), project);
804
816
  } else {
805
817
  const idx = loadProjectsIndex();
806
- const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
818
+ const seen = new Set([..._projectData.keys(), 'global', ...idx.map(p => p.name)]);
807
819
  for (const name of seen) processEntries(getAllEntries(name).slice(), name);
808
820
  }
809
821
  if (count > 0) markDirty();
package/src/http.js CHANGED
@@ -622,7 +622,7 @@ async function handleRequest(req, res) {
622
622
  <div class="step">
623
623
  <span class="step-num">2</span>
624
624
  <span class="step-title">Server URL</span>
625
- <span class="step-content">Enter: <code>${req.headers['x-forwarded-proto'] || req.socket?.encrypted ? 'https' : 'http'}://${req.headers.host}</code></span>
625
+ <span class="step-content">Enter: <code>${req.headers['x-forwarded-proto'] === 'https' || req.socket?.encrypted ? 'https' : 'http'}://${req.headers.host}</code></span>
626
626
  </div>
627
627
 
628
628
  <div class="step">
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