context-mcp-server 1.0.5 → 1.0.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mcp-server",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
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": {
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codegraph-mcp"
7
- version = "1.0.5"
7
+ version = "1.0.6"
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"
package/src/cli.js CHANGED
@@ -186,7 +186,7 @@ function cmdList(args) {
186
186
 
187
187
  for (const projectName of projectNames) {
188
188
  const pData = projects[projectName];
189
- const graph = allGraphs.find(g => g.path?.toLowerCase().includes(projectName.toLowerCase()));
189
+ const graph = _graphForProject(allGraphs, projectName);
190
190
  const activeD = pData.discussions.filter(d => d.status === 'active').length;
191
191
  const totalSecs = (pData.contexts.length > 0 ? 1 : 0) + (pData.discussions.length > 0 ? 1 : 0) + (graph ? 1 : 0);
192
192
  let secIdx = 0;
@@ -240,7 +240,7 @@ function cmdList(args) {
240
240
  }
241
241
 
242
242
  // Orphan graphs (no matching project)
243
- const orphanGraphs = allGraphs.filter(g => !projectNames.some(p => g.path?.toLowerCase().includes(p.toLowerCase())));
243
+ const orphanGraphs = allGraphs.filter(g => !projectNames.some(p => _graphForProject([g], p)));
244
244
  if (orphanGraphs.length) {
245
245
  console.log(`\n ${color(C.dblue, '◇')} ${muted('other graphs')}`);
246
246
  for (const g of orphanGraphs) {
@@ -292,7 +292,7 @@ function cmdProjects() {
292
292
  const entries = getContext({ project: project.name, limit: 3, compact: true }).filter(e => e.status !== 'archived');
293
293
  const discs = allDiscs.filter(d => (d.project || 'global') === project.name);
294
294
  const activeD = discs.filter(d => d.status === 'active');
295
- const graph = graphs.find(g => g.path?.toLowerCase().includes(project.name.toLowerCase()));
295
+ const graph = _graphForProject(graphs, project.name);
296
296
 
297
297
  const barLen = Math.min(Math.ceil(project.count / 2), 24);
298
298
  const bar = color(C.dblue, '█'.repeat(barLen)) + color(C.darkgray, '░'.repeat(24 - barLen));
@@ -567,6 +567,30 @@ const _GLOBAL_GITIGNORE_ENTRIES = [
567
567
  '.mcp.json',
568
568
  ];
569
569
 
570
+ // Match a graph to a project by exact last-path-component comparison (not substring)
571
+ function _graphForProject(graphs, projectName) {
572
+ const norm = p => (p || '').toLowerCase().replace(/\\/g, '/').replace(/\/$/, '');
573
+ const name = projectName.toLowerCase();
574
+ return graphs.find(g => norm(g.path).split('/').pop() === name) || null;
575
+ }
576
+
577
+ const _PROJECT_GITIGNORE_ENTRIES = [
578
+ '.claude/', '.cursor/', '.vscode/', '.gemini/', '.codex/',
579
+ 'codegraph-cache/', '.mcp.json', 'CLAUDE.md', 'GEMINI.md', 'AGENTS.md',
580
+ ];
581
+
582
+ function _updateProjectGitignore(projectDir) {
583
+ const giPath = join(projectDir, '.gitignore');
584
+ const existing = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
585
+ const lines = existing.split(/\r?\n/);
586
+ const missing = _PROJECT_GITIGNORE_ENTRIES.filter(e => !lines.includes(e));
587
+ if (!missing.length) return;
588
+ const block = '\n# context-mcp — written by ctx install\n' + missing.join('\n') + '\n';
589
+ writeFileSync(giPath, (existing ? existing.trimEnd() : '') + block, 'utf8');
590
+ console.log(` ${ok('✓')} ${'project .gitignore'.padEnd(28)} ${faint(giPath.replace(/\\/g, '/'))}`);
591
+ for (const e of missing) console.log(` ${faint('+ ' + e)}`);
592
+ }
593
+
570
594
  function _updateGlobalGitignore() {
571
595
  // Resolve global gitignore path: git config > ~/.gitignore_global > ~/.gitignore
572
596
  let giPath = null;
@@ -829,6 +853,9 @@ async function cmdInstall(args) {
829
853
  console.log(faint(` ${keys.length} platform(s) installed · scope: ${scope} · ${destLabel}`));
830
854
  console.log('');
831
855
 
856
+ // ── Project .gitignore — add context-mcp entries for this project ──────────
857
+ _updateProjectGitignore(process.cwd());
858
+
832
859
  // ── Global gitignore — add context-mcp runtime files if global gitignore exists ──
833
860
  _updateGlobalGitignore();
834
861
  console.log('');
package/src/db.js CHANGED
@@ -23,6 +23,11 @@ const PROJECTS_PATH = join(DATA_DIR, 'projects.json');
23
23
 
24
24
  const MAX_CONTENT_LENGTH = 5000;
25
25
  const PREVIEW_LENGTH = 200;
26
+
27
+ // Normalize file paths for cross-platform comparison (Windows case + slash variants)
28
+ function normPath(p) {
29
+ return p ? p.toLowerCase().replace(/\\/g, '/').replace(/\/$/, '') : '';
30
+ }
26
31
  const WRITE_DEBOUNCE_MS = 500;
27
32
  const LOCK_WAIT_TIMEOUT_MS = 2000;
28
33
 
@@ -126,9 +131,9 @@ function mergeStore(latest, local) {
126
131
  if (_changedDiscussionNames.has(disc.name)) discussionsByName.set(disc.name, disc);
127
132
  }
128
133
 
129
- const graphsByPath = new Map((latest.graphs || []).map(g => [g.path, g]));
134
+ const graphsByPath = new Map((latest.graphs || []).map(g => [normPath(g.path), g]));
130
135
  for (const graph of (local.graphs || [])) {
131
- if (_changedGraphPaths.has(graph.path)) graphsByPath.set(graph.path, graph);
136
+ if (_changedGraphPaths.has(graph.path)) graphsByPath.set(normPath(graph.path), graph);
132
137
  }
133
138
 
134
139
  const projectsById = new Map((latest.projects || []).map(p => [p.id, p]));
@@ -713,7 +718,7 @@ export function flushStore() { flushToDisk(); }
713
718
 
714
719
  // ── Auto-compaction ───────────────────────────────────────────────────────────
715
720
 
716
- const COMPACTION_THRESHOLD = 50;
721
+ const COMPACTION_THRESHOLD = 20;
717
722
  const COMPACTION_TARGET = 30;
718
723
 
719
724
  export function shouldCompact(project) {
@@ -753,7 +758,14 @@ export function compactProject(project, summaryContent) {
753
758
  export function saveGraph({ path, nodes, edges, communities, cached, changed, time_ms, summary }) {
754
759
  refreshFromDisk();
755
760
  const store = load();
756
- const existing = store.graphs.find(g => g.path === path);
761
+ // Deduplicate: collapse any case/slash variants of same path, keep newest
762
+ const dupes = store.graphs.filter(g => normPath(g.path) === normPath(path));
763
+ if (dupes.length > 1) {
764
+ const keep = dupes.reduce((a, b) => (a.builtAt >= b.builtAt ? a : b));
765
+ store.graphs = store.graphs.filter(g => normPath(g.path) !== normPath(path));
766
+ store.graphs.push(keep);
767
+ }
768
+ const existing = store.graphs.find(g => normPath(g.path) === normPath(path));
757
769
  const record = {
758
770
  path,
759
771
  nodes: nodes ?? existing?.nodes ?? 0,
@@ -777,7 +789,7 @@ export function saveGraph({ path, nodes, edges, communities, cached, changed, ti
777
789
 
778
790
  export function getGraph(path) {
779
791
  const store = load();
780
- if (path) return store.graphs.find(g => g.path === path) || null;
792
+ if (path) return store.graphs.find(g => normPath(g.path) === normPath(path)) || null;
781
793
  return store.graphs;
782
794
  }
783
795
 
@@ -3,7 +3,7 @@ import { fireAutoLink } from './autoLink.js';
3
3
 
4
4
  export function saveAutoContext({ title, content, type, files, state, tags = [] }) {
5
5
  const entry = saveContext({
6
- project: state.sessionProject || 'global',
6
+ project: state.sessionProject || null,
7
7
  sessionId: state.sessionId || null,
8
8
  title,
9
9
  content,
@@ -15,7 +15,12 @@ Every conversation starts with `context.resume`. Every codebase question uses `c
15
15
 
16
16
  ## 1. Start of Every Conversation (MANDATORY)
17
17
 
18
- Call `context` tool, `action: "resume"`, `project: "<project-name>"` **before anything else**.
18
+ Call `context` tool **before anything else** with:
19
+ - `action: "resume"`
20
+ - `project: "<basename of git repo root dir>"` — infer from cwd if not stated
21
+ - `rootPath: "<absolute path to git repo root>"` — required for sandbox + graph lookup
22
+
23
+ Both fields are required: `project` names the memory bucket, `rootPath` enables exact graph matching and file sandboxing.
19
24
 
20
25
  Returns:
21
26
  - `recentEntries` — decisions, bugs, notes from previous conversations
@@ -34,7 +39,7 @@ Then:
34
39
 
35
40
  **After graph build or rebuild** — every time `codegraph_build` completes:
36
41
  ```
37
- context.save type: "architecture" title: "ContextGraph built — <project>"
42
+ context.save project: "<project>" type: "architecture" title: "ContextGraph built — <project>"
38
43
  content: "nodes: X | edges: Y | communities: Z"
39
44
  ```
40
45
 
@@ -59,7 +64,7 @@ Do NOT save: routine reads, search results, temporary debugging dead-ends.
59
64
 
60
65
  Feature spans multiple sessions → `discussion.create` or `discussion.update`.
61
66
  Need past info → `search` before asking user.
62
- Always pass `project`. Auto-compact fires at >50 entries.
67
+ Always pass `project`. Auto-compact fires at >20 entries.
63
68
 
64
69
  ---
65
70
 
@@ -1,4 +1,6 @@
1
- Call the `context` MCP tool with `action: "resume"` and `project: "$ARGUMENTS"` (if no argument given, infer the project name from the current working directory name).
1
+ Call the `context` MCP tool with `action: "resume"`, `project: "$ARGUMENTS"` (if no argument given, infer the project name from the current working directory name), and `rootPath: "<absolute path to the project root / git repo root>"`.
2
+
3
+ Both `project` and `rootPath` are required: `project` names the memory bucket, `rootPath` enables exact graph lookup and file sandboxing.
2
4
 
3
5
  This loads:
4
6
  - Recent decisions, bugs, and notes from past sessions
@@ -18,9 +18,12 @@ Persistent memory + codebase knowledge graph across every conversation.
18
18
 
19
19
  ## MANDATORY: Start of Every Conversation
20
20
 
21
- Call `context` tool, `action: "resume"`, `project: "<project-name>"` **before any tool or response**.
21
+ Call `context` tool **before any tool or response** with:
22
+ - `action: "resume"`
23
+ - `project: "<basename of git repo root dir>"` — infer from `cwd` if not stated
24
+ - `rootPath: "<absolute path to git repo root>"` — required for sandbox + graph lookup
22
25
 
23
- Infer `project` from the working directory name if not stated.
26
+ Both fields are required: `project` names the memory bucket, `rootPath` enables exact graph matching and file sandboxing.
24
27
 
25
28
  Returns:
26
29
  - `recentEntries` — decisions, bugs, notes from past sessions
@@ -40,7 +43,7 @@ Then:
40
43
  **1. After graph build or rebuild**
41
44
  Every time `codegraph_build` completes successfully, immediately call:
42
45
  ```
43
- context.save type: "architecture" title: "ContextGraph built — <project>"
46
+ context.save project: "<project>" type: "architecture" title: "ContextGraph built — <project>"
44
47
  content: "nodes: X | edges: Y | communities: Z | built: <timestamp>"
45
48
  ```
46
49
 
@@ -70,7 +73,7 @@ Do NOT save for: routine file reads, search results, explanations of existing co
70
73
  | Deploy / release step discovered | `note` |
71
74
  | Milestone / feature / task completed | `note` |
72
75
 
73
- Always pass `project`. Feature spans multiple sessions → `discussion.create` or `discussion.update`. Need past info → `search` before asking user. Auto-compact fires at >50 entries.
76
+ Always pass `project`. Feature spans multiple sessions → `discussion.create` or `discussion.update`. Need past info → `search` before asking user. Auto-compact fires at >20 entries.
74
77
 
75
78
  ---
76
79
 
@@ -184,7 +184,10 @@ export function handle(name, args, state) {
184
184
  summary: result.summary || '',
185
185
  });
186
186
 
187
- const project = state?.sessionProject || 'global';
187
+ const inferredProject = args.path
188
+ ? args.path.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop()
189
+ : null;
190
+ const project = state?.sessionProject || inferredProject || null;
188
191
  const title = `CodeGraph — ${args.path}`;
189
192
  const content = [
190
193
  `nodes: ${result.nodes} | edges: ${result.edges} | communities: ${result.communities}`,
@@ -92,9 +92,15 @@ export async function handle(args, state) {
92
92
  .filter(e => e.status !== 'archived');
93
93
  const discussions = listDiscussions({ project: proj, status: 'active' });
94
94
  const allGraphs = listGraphs();
95
- const graph = proj
96
- ? allGraphs.find(g => g.path?.toLowerCase().includes(proj.toLowerCase())) || allGraphs[0] || null
97
- : allGraphs[0] || null;
95
+ const np = p => (p || '').toLowerCase().replace(/\\/g, '/');
96
+ const graph = resolvedRoot
97
+ ? allGraphs.find(g => np(g.path) === np(resolvedRoot)) || null
98
+ : proj
99
+ ? allGraphs.find(g => {
100
+ const parts = np(g.path).split('/');
101
+ return parts[parts.length - 1] === proj.toLowerCase();
102
+ }) || null
103
+ : null;
98
104
  const totalEntries = countContext(proj);
99
105
 
100
106
  // Auto-restore single active discussion
@@ -29,11 +29,10 @@ function resolveCwd(args, state) {
29
29
  // Auto-detect project root on first use if not already configured.
30
30
  if (!state.projectRootPath) {
31
31
  const detected = autoDetectRoot(args.cwd ? pathResolve(args.cwd) : process.cwd());
32
- if (detected) state.projectRootPath = detected;
32
+ state.projectRootPath = detected || process.cwd();
33
33
  }
34
- const raw = args.cwd ? pathResolve(args.cwd) : (state.projectRootPath || process.cwd());
35
- if (state.projectRootPath) return guardPath(raw, state.projectRootPath);
36
- return raw;
34
+ const raw = args.cwd ? pathResolve(args.cwd) : state.projectRootPath;
35
+ return guardPath(raw, state.projectRootPath);
37
36
  }
38
37
 
39
38
  const ROOT_NOTE = ' All paths must be within the project root (sandboxed — access outside root is denied).';
package/uv.lock CHANGED
@@ -168,7 +168,7 @@ wheels = [
168
168
 
169
169
  [[package]]
170
170
  name = "codegraph-mcp"
171
- version = "1.0.4"
171
+ version = "1.0.5"
172
172
  source = { editable = "." }
173
173
  dependencies = [
174
174
  { name = "mcp" },