context-mcp-server 1.0.8 → 1.1.1

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 (81) hide show
  1. package/README.md +29 -7
  2. package/codegraph/__pycache__/affected.cpython-313.pyc +0 -0
  3. package/codegraph/__pycache__/cache.cpython-313.pyc +0 -0
  4. package/codegraph/__pycache__/callflow_html.cpython-313.pyc +0 -0
  5. package/codegraph/__pycache__/export.cpython-313.pyc +0 -0
  6. package/codegraph/__pycache__/report.cpython-313.pyc +0 -0
  7. package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
  8. package/codegraph/__pycache__/tree_html.cpython-313.pyc +0 -0
  9. package/codegraph/affected.py +233 -0
  10. package/codegraph/cache.py +51 -2
  11. package/codegraph/callflow_html.py +273 -0
  12. package/codegraph/export.py +544 -0
  13. package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
  14. package/codegraph/extractors/ast_extractor.py +143 -16
  15. package/codegraph/graph/__pycache__/builder.cpython-313.pyc +0 -0
  16. package/codegraph/graph/__pycache__/clustering.cpython-313.pyc +0 -0
  17. package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
  18. package/codegraph/graph/__pycache__/symbol_resolution.cpython-313.pyc +0 -0
  19. package/codegraph/graph/builder.py +10 -0
  20. package/codegraph/graph/clustering.py +247 -10
  21. package/codegraph/graph/query.py +99 -0
  22. package/codegraph/graph/symbol_resolution.py +112 -0
  23. package/codegraph/report.py +53 -0
  24. package/codegraph/server.py +112 -20
  25. package/codegraph/tree_html.py +241 -0
  26. package/package.json +2 -2
  27. package/pyproject.toml +4 -1
  28. package/src/cli.js +329 -227
  29. package/src/db.js +79 -102
  30. package/src/search.js +73 -9
  31. package/src/server.js +7 -1
  32. package/src/templates/antigravity/GEMINI.md +96 -0
  33. package/src/templates/antigravity/hooks/context-mcp-post-tool-use.js +62 -0
  34. package/src/templates/antigravity/workflows/context-resume.md +20 -0
  35. package/src/templates/antigravity/workflows/graph-build.md +23 -0
  36. package/src/templates/antigravity/workflows/save-context.md +29 -0
  37. package/src/templates/claude/CLAUDE.md +140 -0
  38. package/src/templates/claude/commands/graph-build.md +9 -0
  39. package/src/templates/claude/commands/save-context.md +19 -0
  40. package/src/templates/claude/hooks/context-mcp-post-tool-use.js +59 -0
  41. package/src/templates/claude/hooks/context-mcp-pre-tool-use.js +26 -0
  42. package/src/templates/claude/skills/SKILL.md +144 -0
  43. package/src/templates/codex/AGENTS.md +107 -0
  44. package/src/templates/codex/hooks/context-mcp-post-tool-use.js +46 -0
  45. package/src/templates/codex/hooks/context-mcp-pre-tool-use.js +23 -0
  46. package/src/templates/codex/prompts/context-resume.md +15 -0
  47. package/src/templates/codex/prompts/graph-build.md +14 -0
  48. package/src/templates/codex/prompts/save-context.md +24 -0
  49. package/src/templates/cursor/commands/context-resume.md +7 -0
  50. package/src/templates/cursor/commands/graph-build.md +7 -0
  51. package/src/templates/cursor/commands/save-context.md +12 -0
  52. package/src/templates/{cursor-rules.mdc → cursor/cursor-rules.mdc} +13 -3
  53. package/src/templates/cursor/hooks/context-mcp-post-tool-use.js +55 -0
  54. package/src/templates/gemini/GEMINI.md +92 -0
  55. package/src/templates/gemini/commands/context-resume.toml +15 -0
  56. package/src/templates/gemini/commands/graph-build.toml +14 -0
  57. package/src/templates/gemini/commands/save-context.toml +24 -0
  58. package/src/templates/gemini/hooks/context-mcp-after-tool.js +59 -0
  59. package/src/templates/gemini/hooks/context-mcp-before-tool.js +26 -0
  60. package/src/templates/vscode/commands/context-resume.prompt.md +15 -0
  61. package/src/templates/vscode/commands/graph-build.prompt.md +10 -0
  62. package/src/templates/vscode/commands/save-context.prompt.md +16 -0
  63. package/src/templates/vscode/hooks/context-mcp-post-tool-use.js +58 -0
  64. package/src/templates/windsurf/hooks/context-mcp-post-run-command.js +57 -0
  65. package/src/templates/windsurf/windsurf-rules.md +86 -0
  66. package/src/templates/windsurf/workflows/context-resume.md +11 -0
  67. package/src/templates/windsurf/workflows/graph-build.md +11 -0
  68. package/src/templates/windsurf/workflows/save-context.md +18 -0
  69. package/src/tools/codegraph.js +83 -43
  70. package/src/tools/context.js +42 -24
  71. package/src/tools/plan.js +14 -11
  72. package/uv.lock +1101 -4
  73. package/src/migrator.js +0 -124
  74. package/src/templates/AGENTS.md +0 -80
  75. package/src/templates/CLAUDE.md +0 -103
  76. package/src/templates/GEMINI.md +0 -80
  77. package/src/templates/commands/graph-build.md +0 -5
  78. package/src/templates/commands/save-context.md +0 -9
  79. package/src/templates/skills/SKILL.md +0 -108
  80. package/src/templates/windsurf-rules.md +0 -35
  81. /package/src/templates/{commands → claude/commands}/context-resume.md +0 -0
@@ -6,6 +6,7 @@
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { dirname, join } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
+ import { saveGraph, saveContext, updateContext, getContext, flushToDisk } from '../db.js';
9
10
 
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
12
  const REPO_ROOT = join(__dirname, '..', '..');
@@ -82,16 +83,55 @@ export const definitions = [
82
83
  },
83
84
  },
84
85
  {
85
- name: 'codegraph_path',
86
- description: 'Find the shortest relationship path between two concepts in the graph.',
86
+ name: 'codegraph_arch',
87
+ description:
88
+ 'Return a module map of the project — every file with its exported functions/classes and its imports. ' +
89
+ 'Use this to understand project structure without reading any files. ' +
90
+ 'Call after codegraph_build. Much faster than reading each file individually.',
87
91
  inputSchema: {
88
92
  type: 'object',
89
93
  properties: {
90
- path: { type: 'string' },
91
- from: { type: 'string' },
92
- to: { type: 'string' },
94
+ path: { type: 'string', description: 'Project root' },
95
+ limit: { type: 'integer', description: 'Max files in output (default 100)' },
93
96
  },
94
- required: ['path', 'from', 'to'],
97
+ required: ['path'],
98
+ },
99
+ },
100
+ {
101
+ name: 'codegraph_affected',
102
+ description:
103
+ 'BFS traversal: given a node name, find every node that would be affected if you change it — ' +
104
+ 'callers, importers, inheritors, etc. Use before refactoring to understand blast radius. ' +
105
+ 'Returns affected nodes with file paths, relation types, and traversal depth.',
106
+ inputSchema: {
107
+ type: 'object',
108
+ properties: {
109
+ path: { type: 'string', description: 'Project root' },
110
+ node: { type: 'string', description: 'Node name, ID, or file path to start from' },
111
+ depth: { type: 'integer', description: 'BFS depth (default 2, max 5)' },
112
+ },
113
+ required: ['path', 'node'],
114
+ },
115
+ },
116
+ {
117
+ name: 'codegraph_html',
118
+ description:
119
+ 'Generate interactive visualizations from the knowledge graph. ' +
120
+ 'Outputs: graph.html (vis.js force graph, dark theme, search, community toggle), ' +
121
+ 'tree.html (D3 collapsible file tree), callflow.html (Mermaid architecture diagrams), ' +
122
+ 'graph.graphml (Gephi/yEd), obsidian/ vault (per-node .md with wikilinks). ' +
123
+ 'Run after codegraph_build. Pass formats array to select specific outputs.',
124
+ inputSchema: {
125
+ type: 'object',
126
+ properties: {
127
+ path: { type: 'string', description: 'Project root' },
128
+ formats: {
129
+ type: 'array',
130
+ items: { type: 'string', enum: ['html', 'tree', 'callflow', 'graphml', 'obsidian'] },
131
+ description: 'Formats to generate (default: all)',
132
+ },
133
+ },
134
+ required: ['path'],
95
135
  },
96
136
  },
97
137
  ];
@@ -103,46 +143,46 @@ export function handle(name, args, state) {
103
143
 
104
144
  // Persist graph metadata + save/update a context entry as a visible build record
105
145
  if (name === 'codegraph_build' && result.success) {
106
- import('../db.js').then(({ saveGraph, saveContext, updateContext, getContext }) => {
107
- saveGraph({
108
- path: args.path,
109
- nodes: result.nodes,
110
- edges: result.edges,
111
- communities: result.communities,
112
- cached: result.cached,
113
- changed: result.changed,
114
- time_ms: result.time_ms,
115
- summary: result.summary || '',
116
- });
146
+ saveGraph({
147
+ path: args.path,
148
+ nodes: result.nodes,
149
+ edges: result.edges,
150
+ communities: result.communities,
151
+ cached: result.cached,
152
+ changed: result.changed,
153
+ time_ms: result.time_ms,
154
+ summary: result.summary || '',
155
+ });
156
+ flushToDisk(); // write graph.json to disk immediately so ctx list sees it
117
157
 
118
- const inferredProject = args.path
119
- ? args.path.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop()
120
- : null;
121
- const project = state?.sessionProject || inferredProject || null;
122
- const title = `CodeGraph — ${args.path}`;
123
- const content = [
124
- `nodes: ${result.nodes} | edges: ${result.edges} | communities: ${result.communities}`,
125
- `cached: ${result.cached} | changed: ${result.changed} | time: ${result.time_ms}ms`,
126
- result.summary || '',
127
- ].filter(Boolean).join('\n');
158
+ const inferredProject = args.path
159
+ ? args.path.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop()
160
+ : null;
161
+ const project = state?.sessionProject || inferredProject || null;
162
+ const title = `ContextGraph built — ${args.path}`;
163
+ const content = [
164
+ `nodes: ${result.nodes} | edges: ${result.edges} | communities: ${result.communities}`,
165
+ `cached: ${result.cached} | changed: ${result.changed} | time: ${result.time_ms}ms`,
166
+ result.summary || '',
167
+ ].filter(Boolean).join('\n');
128
168
 
129
- const existing = getContext({ project, tags: ['codegraph'], limit: 100 })
130
- .find(e => e.title === title);
169
+ // Search all projects same path always produces same title regardless of session
170
+ const existing = getContext({ tags: ['codegraph'], limit: 100 })
171
+ .find(e => e.title === title);
131
172
 
132
- if (existing) {
133
- updateContext({ id: existing.id, content, status: 'active' });
134
- } else {
135
- saveContext({
136
- project,
137
- sessionId: state?.sessionId || null,
138
- title,
139
- content,
140
- type: 'architecture',
141
- source: 'auto',
142
- tags: ['codegraph', 'graph-build'],
143
- });
144
- }
145
- }).catch(() => {});
173
+ if (existing) {
174
+ updateContext({ id: existing.id, content, status: 'active' });
175
+ } else {
176
+ saveContext({
177
+ project,
178
+ sessionId: state?.sessionId || null,
179
+ title,
180
+ content,
181
+ type: 'note',
182
+ source: 'auto',
183
+ tags: ['codegraph', 'graph-build'],
184
+ });
185
+ }
146
186
  }
147
187
 
148
188
  return result;
@@ -25,22 +25,24 @@ function autoDigest(entries, project) {
25
25
  export const definition = {
26
26
  name: 'context',
27
27
  description:
28
- `Factual memory — record what happened, what was decided, what broke, what was built.\n` +
29
- `• "resume" — START HERE every conversation. Loads recent context, active discussions, and graph status for a project.\n` +
30
- `• "save" — Store a note, decision, bug, or code snippet. Auto-deduplicates.\n` +
31
- `• "get" — Load entries. Pass id/ids to fetch specific ones, or project/tags/limit for recent.\n` +
32
- `• "update" — Edit an existing entry by id (any field).\n` +
33
- `• "delete" — Remove one entry (id) or multiple at once (ids: [...]).\n` +
34
- `• "list_projects"— Show all projects and entry counts.`,
28
+ `Factual memory — decisions, bugs, notes, discoveries.\n` +
29
+ `• "resume" — call first every session. Returns recent entries, active plans, graph status.\n` +
30
+ `• "save" — store an entry. Auto-deduplicates by content similarity.\n` +
31
+ `• "get" — fetch by id/ids, or filter by project/tags/limit.\n` +
32
+ `• "update" — edit an entry by id.\n` +
33
+ `• "delete" — remove one entry (id) or several (ids: [...]).\n` +
34
+ `• "list_projects"— list all projects and entry counts.`,
35
35
  inputSchema: {
36
36
  type: 'object',
37
37
  properties: {
38
38
  action: { type: 'string', enum: ['resume', 'save', 'get', 'update', 'delete', 'list_projects'] },
39
39
  content: { type: 'string' },
40
- title: { type: 'string' },
40
+ title: { type: 'string', description: 'Up to 120 chars' },
41
+ why: { type: 'string', description: 'Why it mattered' },
42
+ outcome: { type: 'string', description: 'What the result was' },
41
43
  project: { type: 'string' },
42
44
  rootPath: { type: 'string', description: 'Absolute path to the project root directory. Stored on first call and used to sandbox file/git tool access.' },
43
- type: { type: 'string', enum: ['decision', 'bug', 'note', 'config'] },
45
+ type: { type: 'string', enum: ['note', 'compaction'] },
44
46
  status: { type: 'string', enum: ['active', 'archived'] },
45
47
  tags: { type: 'array', items: { type: 'string' } },
46
48
  source: { type: 'string', enum: ['user', 'ai-summary', 'file', 'web', 'cli', 'auto'] },
@@ -49,8 +51,8 @@ export const definition = {
49
51
  expiresAt: { type: 'string' },
50
52
  limit: { type: 'number' },
51
53
  includeArchived: { type: 'boolean' },
52
- id: { type: 'string', description: 'Single entry ID (get/update/delete)' },
53
- ids: { type: 'array', items: { type: 'string' }, description: 'Multiple entry IDs — fetch or delete several at once' },
54
+ id: { type: 'string', description: 'Single entry ID' },
55
+ ids: { type: 'array', items: { type: 'string' }, description: 'Multiple entry IDs' },
54
56
  },
55
57
  required: ['action'],
56
58
  },
@@ -78,18 +80,27 @@ export async function handle(args, state) {
78
80
  const proj = args.project || null;
79
81
  archiveExpired(proj);
80
82
 
81
- // Set project on state so autoLink works for subsequent saves
82
83
  if (proj) state.sessionProject = proj;
83
84
 
84
- // Store rootPath with project (first time only) and load it onto session state.
85
- // Auto-detect from git if neither provided nor previously stored.
86
85
  const storedRoot = proj ? getProjectRoot(proj) : null;
87
86
  const resolvedRoot = args.rootPath || storedRoot || detectGitRoot() || null;
88
87
  if (proj) ensureProject(proj, resolvedRoot || undefined);
89
88
  state.projectRootPath = resolvedRoot;
90
89
 
91
- const entries = getContext({ project: proj, limit: 15, compact: true })
90
+ const rawEntries = getContext({ project: proj, limit: 15, compact: false })
92
91
  .filter(e => e.status !== 'archived');
92
+ // Newest 5 get full content; older entries get a lightweight preview
93
+ const entries = rawEntries.map((e, i) => {
94
+ if (i < 5) return e;
95
+ return {
96
+ id: e.id, project: e.project, title: e.title, type: e.type,
97
+ status: e.status, tags: e.tags, source: e.source,
98
+ createdAt: e.createdAt, updatedAt: e.updatedAt,
99
+ ...(e.why ? { why: e.why } : {}),
100
+ ...(e.outcome ? { outcome: e.outcome } : {}),
101
+ preview: (e.content || '').slice(0, 200),
102
+ };
103
+ });
93
104
  const discussions = listDiscussions({ project: proj, status: 'active' });
94
105
  const allGraphs = listGraphs();
95
106
  const np = p => (p || '').toLowerCase().replace(/\\/g, '/');
@@ -103,7 +114,6 @@ export async function handle(args, state) {
103
114
  : null;
104
115
  const totalEntries = countContext(proj);
105
116
 
106
- // Auto-restore single active discussion
107
117
  if (discussions.length === 1) state.discussionId = discussions[0].id;
108
118
 
109
119
  const digest = totalEntries > 10
@@ -127,7 +137,7 @@ export async function handle(args, state) {
127
137
  ? `All file and git operations are sandboxed to: ${state.projectRootPath} — do not use paths outside this root.`
128
138
  : 'No project root configured — pass rootPath to restrict file/git access to a directory.',
129
139
  hint: graphStatus.built
130
- ? `Graph ready (${graphStatus.nodes} nodes). Use codegraph_query for structural questions.`
140
+ ? `Graph ready (${graphStatus.nodes} nodes). Use codegraph_arch for module map, codegraph_query for specific symbol lookups.`
131
141
  : 'No graph built yet. Call codegraph_build on the project root to enable graph queries.',
132
142
  };
133
143
  }
@@ -135,7 +145,6 @@ export async function handle(args, state) {
135
145
  case 'save': {
136
146
  if (!args.content) throw new Error('content is required for save');
137
147
  if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
138
- // Auto-detect and store project root if not yet configured
139
148
  if (args.project) {
140
149
  const existing = getProjectRoot(args.project);
141
150
  if (!existing) {
@@ -146,7 +155,6 @@ export async function handle(args, state) {
146
155
  }
147
156
  }
148
157
  }
149
- // Validate file paths in files[] and codeRefs[] stay within project root
150
158
  if (state.projectRootPath) {
151
159
  if (Array.isArray(args.files)) {
152
160
  args.files.forEach(f => { if (f.path) guardPath(f.path, state.projectRootPath); });
@@ -162,6 +170,8 @@ export async function handle(args, state) {
162
170
  id: dupe.id, content: args.content,
163
171
  title: args.title || dupe.title, tags: args.tags || dupe.tags,
164
172
  type: args.type || dupe.type, status: args.status || dupe.status,
173
+ why: args.why !== undefined ? args.why : dupe.why,
174
+ outcome: args.outcome !== undefined ? args.outcome : dupe.outcome,
165
175
  expiresAt: args.expiresAt !== undefined ? args.expiresAt : dupe.expiresAt,
166
176
  files: args.files || dupe.files, codeRefs: args.codeRefs || dupe.codeRefs,
167
177
  });
@@ -172,13 +182,21 @@ export async function handle(args, state) {
172
182
  const entry = saveContext({ ...args, rootPath: state.projectRootPath || undefined });
173
183
  fireAutoLink(entry.id, state);
174
184
 
175
- // Auto-compact when too many entries accumulate
185
+ // Auto-compact when too many entries accumulate.
186
+ // If the AI just saved a compaction entry, use that content as the summary
187
+ // instead of running TF-IDF on top of it.
176
188
  let compaction = null;
177
189
  if (shouldCompact(entry.project)) {
178
- const old = getContext({ project: entry.project, limit: 500 });
179
- const { summarizeEntries: summarize } = await import('../summarizer.js');
180
- const cs = summarize(old.slice(old.length - 30), { project: entry.project || 'global', sessionLabel: 'auto-compaction', topN: 5 });
181
- compaction = compactProject(entry.project, cs);
190
+ if (entry.type === 'compaction') {
191
+ // AI wrote a proper summary compact old entries without creating a duplicate summary
192
+ compaction = compactProject(entry.project, entry.content, { skipSummaryEntry: true });
193
+ } else {
194
+ // AI didn't write a summary — fall back to TF-IDF extractive summarization
195
+ const old = getContext({ project: entry.project, limit: 500 });
196
+ const { summarizeEntries: summarize } = await import('../summarizer.js');
197
+ const summaryContent = summarize(old.slice(old.length - 30), { project: entry.project || 'global', sessionLabel: 'auto-compaction', topN: 5 });
198
+ compaction = compactProject(entry.project, summaryContent);
199
+ }
182
200
  }
183
201
 
184
202
  return { success: true, id: entry.id, deduplicated: false,
package/src/tools/plan.js CHANGED
@@ -19,13 +19,12 @@ function writePlanFile(planDir, name, content, title) {
19
19
  export const definition = {
20
20
  name: 'plan',
21
21
  description:
22
- `AI plan storage — auto-call this tool whenever you create any kind of plan.\n` +
23
- `Saves a summary of the plan to the project store and writes a .md file to planDir.\n` +
24
- `• "save" Store a new plan or overwrite an existing one by name. Pass planDir to write a file.\n` +
25
- `• "update" Patch title/content/status on an existing plan.\n` +
26
- `• "get" Retrieve a plan by name or id (full content).\n` +
27
- `• "list" List all plans for the project.\n` +
28
- `• "delete" — Remove a plan by name or id.`,
22
+ `Plan storage — saves to project store, optionally writes a .md file to planDir.\n` +
23
+ `• "save" — store a new plan or overwrite by name.\n` +
24
+ `• "update" patch title/content/status.\n` +
25
+ `• "get" retrieve a plan by name or id.\n` +
26
+ `• "list" list all plans for the project.\n` +
27
+ `• "delete" remove a plan by name or id.`,
29
28
  inputSchema: {
30
29
  type: 'object',
31
30
  properties: {
@@ -33,7 +32,7 @@ export const definition = {
33
32
  name: { type: 'string', description: 'Short slug-style identifier for the plan, e.g. "auth-refactor"' },
34
33
  id: { type: 'string' },
35
34
  project: { type: 'string' },
36
- title: { type: 'string', description: 'Human-readable plan title' },
35
+ title: { type: 'string', description: 'Plan title' },
37
36
  content: { type: 'string', description: 'Full plan summary in markdown' },
38
37
  status: { type: 'string', enum: ['active', 'done'] },
39
38
  tags: { type: 'array', items: { type: 'string' } },
@@ -83,6 +82,11 @@ export async function handle(args, state) {
83
82
 
84
83
  case 'update': {
85
84
  if (!args.name && !args.id) throw new Error('name or id is required for update');
85
+ if (args.status === 'done') {
86
+ const result = deleteDiscussion({ name: args.name, id: args.id });
87
+ state.discussionId = null;
88
+ return { success: true, message: `Plan "${args.name || args.id}" completed and removed.` };
89
+ }
86
90
  const updated = updateDiscussion({
87
91
  name: args.name,
88
92
  id: args.id,
@@ -92,13 +96,12 @@ export async function handle(args, state) {
92
96
  tags: args.tags,
93
97
  });
94
98
  if (!updated) throw new Error(`No plan found for "${args.name || args.id}".`);
95
- if (updated.status !== 'active' && state.discussionId === updated.id) state.discussionId = null;
96
99
  if (updated.status === 'active') state.discussionId = updated.id;
97
100
  const filePath = writePlanFile(args.planDir, updated.name, updated.content, updated.title);
98
101
  return {
99
- success: true, id: updated.id, name: updated.name, status: updated.status,
102
+ success: true, id: updated.id, name: updated.name,
100
103
  filePath: filePath || undefined,
101
- message: `Plan "${updated.name}" updated (${updated.status}).`,
104
+ message: `Plan "${updated.name}" updated.`,
102
105
  };
103
106
  }
104
107