context-mcp-server 1.0.1 → 1.0.2

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,10 +1,11 @@
1
1
  {
2
2
  "name": "context-mcp-server",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
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,9 +1,19 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { guardPath } from '../guard.js';
1
3
  import {
2
4
  saveContext, updateContext, getContext, deleteContext,
3
5
  listProjects, findDuplicate, archiveExpired, linkContextToDiscussion,
4
6
  listDiscussions, listGraphs, countContext, shouldCompact, compactProject,
5
7
  ensureProject, getProjectRoot,
6
8
  } from '../db.js';
9
+
10
+ function detectGitRoot() {
11
+ try {
12
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
13
+ cwd: process.cwd(), encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
14
+ }).trim();
15
+ } catch { return null; }
16
+ }
7
17
  import { summarizeEntries } from '../summarizer.js';
8
18
  import { fireAutoLink } from '../hooks/autoLink.js';
9
19
 
@@ -71,10 +81,12 @@ export async function handle(args, state) {
71
81
  // Set project on state so autoLink works for subsequent saves
72
82
  if (proj) state.sessionProject = proj;
73
83
 
74
- // Store rootPath with project (first time only) and load it onto session state
75
- if (proj) ensureProject(proj, args.rootPath || undefined);
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.
76
86
  const storedRoot = proj ? getProjectRoot(proj) : null;
77
- state.projectRootPath = args.rootPath || storedRoot || null;
87
+ const resolvedRoot = args.rootPath || storedRoot || detectGitRoot() || null;
88
+ if (proj) ensureProject(proj, resolvedRoot || undefined);
89
+ state.projectRootPath = resolvedRoot;
78
90
 
79
91
  const entries = getContext({ project: proj, limit: 15, compact: true })
80
92
  .filter(e => e.status !== 'archived');
@@ -105,6 +117,9 @@ export async function handle(args, state) {
105
117
  stats: { totalEntries, projects: listProjects().length },
106
118
  message: `Loaded ${totalEntries} entries for project "${proj || 'global'}".${discussions.length === 1 ? ` Auto-linked to discussion "${discussions[0].name}".` : ''}`,
107
119
  rootPath: state.projectRootPath || undefined,
120
+ sandbox: state.projectRootPath
121
+ ? `All file and git operations are sandboxed to: ${state.projectRootPath} — do not use paths outside this root.`
122
+ : 'No project root configured — pass rootPath to restrict file/git access to a directory.',
108
123
  hint: graphStatus.built
109
124
  ? `Graph ready (${graphStatus.nodes} nodes). Use codegraph_query for structural questions.`
110
125
  : 'No graph built yet. Call codegraph_build on the project root to enable graph queries.',
@@ -114,6 +129,27 @@ export async function handle(args, state) {
114
129
  case 'save': {
115
130
  if (!args.content) throw new Error('content is required for save');
116
131
  if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
132
+ // Auto-detect and store project root if not yet configured
133
+ if (args.project) {
134
+ const existing = getProjectRoot(args.project);
135
+ if (!existing) {
136
+ const detected = state.projectRootPath || detectGitRoot();
137
+ if (detected) {
138
+ ensureProject(args.project, detected);
139
+ if (!state.projectRootPath) state.projectRootPath = detected;
140
+ }
141
+ }
142
+ }
143
+ // Validate file paths in files[] and codeRefs[] stay within project root
144
+ if (state.projectRootPath) {
145
+ if (Array.isArray(args.files)) {
146
+ args.files.forEach(f => { if (f.path) guardPath(f.path, state.projectRootPath); });
147
+ }
148
+ if (Array.isArray(args.codeRefs)) {
149
+ args.codeRefs.forEach(r => { if (r.file) guardPath(r.file, state.projectRootPath); });
150
+ }
151
+ }
152
+
117
153
  const dupe = findDuplicate(args.content, args.project);
118
154
  if (dupe) {
119
155
  const updated = updateContext({
@@ -20,28 +20,30 @@ function atomicWrite(filePath, data) {
20
20
  }
21
21
  }
22
22
 
23
+ const ROOT_NOTE = ' Sandboxed to project root — paths outside root are denied.';
24
+
23
25
  export const definitions = [
24
26
  {
25
27
  name: 'create_dir',
26
- description: 'Create a directory (and any missing parent directories). Safe to call if already exists.',
28
+ description: 'Create a directory (and any missing parent directories). Safe to call if already exists.' + ROOT_NOTE,
27
29
  inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
28
30
  outputSchema: { type: 'object', properties: { path: { type: 'string' }, existed: { type: 'boolean' }, message: { type: 'string' } } },
29
31
  },
30
32
  {
31
33
  name: 'list_dir',
32
- description: 'List the contents of a local directory.',
34
+ description: 'List the contents of a local directory.' + ROOT_NOTE,
33
35
  inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
34
36
  outputSchema: { type: 'object', properties: { directory: { type: 'string' }, items: { type: 'array' } } },
35
37
  },
36
38
  {
37
39
  name: 'read_file',
38
- description: 'Read the text contents of a local file.',
40
+ description: 'Read the text contents of a local file.' + ROOT_NOTE,
39
41
  inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
40
42
  outputSchema: { type: 'object', properties: { file: { type: 'string' }, content: { type: 'string' } } },
41
43
  },
42
44
  {
43
45
  name: 'write_file',
44
- description: 'Create a new file or overwrite an existing file.',
46
+ description: 'Create a new file or overwrite an existing file.' + ROOT_NOTE,
45
47
  inputSchema: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] },
46
48
  outputSchema: { type: 'object', properties: { file: { type: 'string' }, message: { type: 'string' } } },
47
49
  },
@@ -51,7 +53,7 @@ export const definitions = [
51
53
  `Apply targeted string replacement(s) to a file.\n` +
52
54
  `Single edit: pass old_str + new_str.\n` +
53
55
  `Multi edit: pass edits:[{old_str, new_str, description?}] — atomic.\n` +
54
- `Use dry_run:true to validate without writing.`,
56
+ `Use dry_run:true to validate without writing.` + ROOT_NOTE,
55
57
  inputSchema: {
56
58
  type: 'object',
57
59
  properties: {
@@ -68,7 +70,7 @@ export const definitions = [
68
70
  },
69
71
  {
70
72
  name: 'delete_file',
71
- description: 'Delete a local file. Pass recursive:true to delete a directory and its contents.',
73
+ description: 'Delete a local file. Pass recursive:true to delete a directory and its contents.' + ROOT_NOTE,
72
74
  inputSchema: { type: 'object', properties: { path: { type: 'string' }, recursive: { type: 'boolean', description: 'Required to delete a directory. Defaults to false.' } }, required: ['path'] },
73
75
  outputSchema: { type: 'object', properties: { path: { type: 'string' }, message: { type: 'string' } } },
74
76
  },
@@ -15,69 +15,85 @@ function runGit(argArr, cwd) {
15
15
  }
16
16
  }
17
17
 
18
+ function autoDetectRoot(fromDir) {
19
+ try {
20
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
21
+ cwd: fromDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
22
+ }).trim();
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
18
28
  function resolveCwd(args, state) {
19
- const raw = args.cwd ? pathResolve(args.cwd) : process.cwd();
20
- // If a project root is configured, validate cwd is within it
29
+ // Auto-detect project root on first use if not already configured.
30
+ if (!state.projectRootPath) {
31
+ const detected = autoDetectRoot(args.cwd ? pathResolve(args.cwd) : process.cwd());
32
+ if (detected) state.projectRootPath = detected;
33
+ }
34
+ const raw = args.cwd ? pathResolve(args.cwd) : (state.projectRootPath || process.cwd());
21
35
  if (state.projectRootPath) return guardPath(raw, state.projectRootPath);
22
36
  return raw;
23
37
  }
24
38
 
39
+ const ROOT_NOTE = ' All paths must be within the project root (sandboxed — access outside root is denied).';
40
+
25
41
  export const definitions = [
26
42
  {
27
43
  name: 'git_status',
28
- description: 'Show working tree status — current branch, staged, unstaged, and untracked files.',
44
+ description: 'Show working tree status — current branch, staged, unstaged, and untracked files.' + ROOT_NOTE,
29
45
  inputSchema: { type: 'object', properties: { cwd: { type: 'string' } } },
30
46
  outputSchema: { type: 'object', properties: { branch: { type: 'string' }, clean: { type: 'boolean' }, staged: { type: 'array' }, unstaged: { type: 'array' }, untracked: { type: 'array' } } },
31
47
  },
32
48
  {
33
49
  name: 'git_diff',
34
- description: 'Show file changes. Use staged:true for cached diff. Optionally scope to a path.',
50
+ description: 'Show file changes. Use staged:true for cached diff. Optionally scope to a path.' + ROOT_NOTE,
35
51
  inputSchema: { type: 'object', properties: { staged: { type: 'boolean' }, path: { type: 'string' }, cwd: { type: 'string' } } },
36
52
  outputSchema: { type: 'object', properties: { diff: { type: 'string' }, staged: { type: 'boolean' } } },
37
53
  },
38
54
  {
39
55
  name: 'git_log',
40
- description: 'Show recent commit history — hash, author, date, message.',
56
+ description: 'Show recent commit history — hash, author, date, message.' + ROOT_NOTE,
41
57
  inputSchema: { type: 'object', properties: { limit: { type: 'number' }, path: { type: 'string' }, cwd: { type: 'string' } } },
42
58
  outputSchema: { type: 'object', properties: { commits: { type: 'array' }, count: { type: 'number' } } },
43
59
  },
44
60
  {
45
61
  name: 'git_add',
46
- description: 'Stage files for commit. Pass paths:["."] to stage everything.',
62
+ description: 'Stage files for commit. Pass paths:["."] to stage everything.' + ROOT_NOTE,
47
63
  inputSchema: { type: 'object', properties: { paths: { type: 'array', items: { type: 'string' } }, cwd: { type: 'string' } }, required: ['paths'] },
48
64
  outputSchema: { type: 'object', properties: { success: { type: 'boolean' }, staged: { type: 'array' }, message: { type: 'string' } } },
49
65
  },
50
66
  {
51
67
  name: 'git_commit',
52
- description: 'Commit staged changes. Set all:true to auto-stage tracked modified files first. Auto-saves context entry.',
68
+ description: 'Commit staged changes. Set all:true to auto-stage tracked modified files first. Auto-saves context entry.' + ROOT_NOTE,
53
69
  inputSchema: { type: 'object', properties: { message: { type: 'string' }, all: { type: 'boolean' }, cwd: { type: 'string' } }, required: ['message'] },
54
70
  outputSchema: { type: 'object', properties: { success: { type: 'boolean' }, hash: { type: 'string' }, branch: { type: 'string' }, message: { type: 'string' }, files: { type: 'array' } } },
55
71
  },
56
72
  {
57
73
  name: 'git_push',
58
- description: 'Push current branch to remote.',
74
+ description: 'Push current branch to remote.' + ROOT_NOTE,
59
75
  inputSchema: { type: 'object', properties: { remote: { type: 'string' }, branch: { type: 'string' }, cwd: { type: 'string' } } },
60
76
  outputSchema: { type: 'object', properties: { success: { type: 'boolean' }, remote: { type: 'string' }, branch: { type: 'string' }, output: { type: 'string' } } },
61
77
  },
62
78
  {
63
79
  name: 'git_pull',
64
- description: 'Pull from remote and merge into current branch.',
80
+ description: 'Pull from remote and merge into current branch.' + ROOT_NOTE,
65
81
  inputSchema: { type: 'object', properties: { remote: { type: 'string' }, branch: { type: 'string' }, cwd: { type: 'string' } } },
66
82
  outputSchema: { type: 'object', properties: { success: { type: 'boolean' }, remote: { type: 'string' }, output: { type: 'string' } } },
67
83
  },
68
84
  {
69
85
  name: 'git_branch',
70
- description: 'List, create, or checkout branches.',
86
+ description: 'List, create, or checkout branches.' + ROOT_NOTE,
71
87
  inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'create', 'checkout'] }, name: { type: 'string' }, cwd: { type: 'string' } } },
72
88
  },
73
89
  {
74
90
  name: 'git_stash',
75
- description: 'Stash or restore work-in-progress changes.',
91
+ description: 'Stash or restore work-in-progress changes.' + ROOT_NOTE,
76
92
  inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['save', 'pop', 'list', 'drop'] }, message: { type: 'string' }, ref: { type: 'string' }, cwd: { type: 'string' } } },
77
93
  },
78
94
  {
79
95
  name: 'git_reset',
80
- description: 'Unstage files or reset HEAD. Use mode:file + path to restore a single file.',
96
+ description: 'Unstage files or reset HEAD. Use mode:file + path to restore a single file.' + ROOT_NOTE,
81
97
  inputSchema: { type: 'object', properties: { mode: { type: 'string', enum: ['soft', 'mixed', 'hard', 'file'] }, path: { type: 'string' }, ref: { type: 'string' }, cwd: { type: 'string' } } },
82
98
  },
83
99
  {