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 +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.js +30 -3
- package/src/db.js +17 -5
- package/src/hooks/autoContext.js +1 -1
- package/src/templates/CLAUDE.md +8 -3
- package/src/templates/commands/context-resume.md +3 -1
- package/src/templates/skills/SKILL.md +7 -4
- package/src/tools/codegraph.js +4 -1
- package/src/tools/context.js +9 -3
- package/src/tools/gitTools.js +3 -4
- package/uv.lock +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
package/src/hooks/autoContext.js
CHANGED
|
@@ -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 ||
|
|
6
|
+
project: state.sessionProject || null,
|
|
7
7
|
sessionId: state.sessionId || null,
|
|
8
8
|
title,
|
|
9
9
|
content,
|
package/src/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|
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 >
|
|
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"
|
|
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
|
|
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
|
-
|
|
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 >
|
|
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
|
|
package/src/tools/codegraph.js
CHANGED
|
@@ -184,7 +184,10 @@ export function handle(name, args, state) {
|
|
|
184
184
|
summary: result.summary || '',
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
const
|
|
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}`,
|
package/src/tools/context.js
CHANGED
|
@@ -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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
package/src/tools/gitTools.js
CHANGED
|
@@ -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
|
-
|
|
32
|
+
state.projectRootPath = detected || process.cwd();
|
|
33
33
|
}
|
|
34
|
-
const raw = args.cwd ? pathResolve(args.cwd) :
|
|
35
|
-
|
|
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).';
|