@contextstream/mcp-server 0.4.81 → 0.4.82

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/README.md CHANGED
@@ -43,7 +43,7 @@ npx --prefer-online -y @contextstream/mcp-server@latest setup --yes
43
43
  ```
44
44
 
45
45
  <p align="center">
46
- <img src="compare1.gif" alt="Side-by-side comparison: an AI coding assistant with ContextStream memory and semantic search vs. without" width="700" />
46
+ <img src="contextstream-mcp-comparison.gif" alt="Animated comparison of an AI coding assistant working without ContextStream versus with ContextStream project memory, search, and graph context" width="600" />
47
47
  </p>
48
48
 
49
49
  ---
@@ -72,6 +72,21 @@ It works with any MCP client: Claude Code, Cursor, VS Code + GitHub Copilot, Win
72
72
 
73
73
  ---
74
74
 
75
+ ## Which ContextStream runtime am I using?
76
+
77
+ ContextStream has independent release lines. A `0.5.x` hosted version and a `0.4.x` npm version are different runtimes, not evidence that either updater is broken.
78
+
79
+ | Runtime | How to identify it | Version line | Canonical release metadata and notes |
80
+ |---|---|---|---|
81
+ | **Hosted MCP** | Your editor config uses `https://mcp.contextstream.io/mcp`. `help(action="version")` reports `runtime_type: rust-mcp`. | Rust MCP `0.5.x` | `help(action="version")` returns release notes inline when published and links the [machine-readable R2 manifest](https://pub-68429b9f7857416c9484b75bf1887b96.r2.dev/mcp/latest/version.json). |
82
+ | **Installed Rust MCP** | Your editor launches a `contextstream-mcp` binary installed by `https://contextstream.io/scripts/mcp.sh`; run `contextstream-mcp --version`. | Rust MCP `0.5.x` | The same [machine-readable R2 manifest](https://pub-68429b9f7857416c9484b75bf1887b96.r2.dev/mcp/latest/version.json); `help(action="version")` maps this runtime to that manifest. |
83
+ | **Legacy npm MCP (this repository)** | Your editor launches `npx ... @contextstream/mcp-server` or a global npm install. `help(action="version")` reports `runtime_type: legacy-typescript-mcp`; `npm list -g @contextstream/mcp-server` shows the installed package. | TypeScript MCP `0.4.x` | [GitHub releases](https://github.com/contextstream/mcp-server/releases) and this repository's [CHANGELOG.md](CHANGELOG.md). |
84
+ | **ContextStream Desktop** | Check the app's About/update UI. Desktop may run the local sync bridge, but that does not change the MCP runtime configured in your editor. | Desktop `0.3.x` (independent) | The in-app updater and the public [Desktop version JSON](https://api.contextstream.io/api/v1/desktop/version), which includes `release_notes` and platform downloads. |
85
+
86
+ The `version` reported by `help(action="version")` is always the MCP process serving that tool call. Desktop's version is separate, even when Desktop added or indexed the local repository.
87
+
88
+ ---
89
+
75
90
  ## Why do AI coding assistants forget everything?
76
91
 
77
92
  Because every conversation starts from zero. Your AI re-reads the same files, re-derives the same architecture, repeats last week's mistake, and loses the thread the moment the context window compacts. ContextStream fixes the whole class of problem:
@@ -130,23 +145,33 @@ Anything that speaks the Model Context Protocol can connect — the table just s
130
145
 
131
146
  ---
132
147
 
133
- ## The tools your AI gets
148
+ ## Tools
134
149
 
135
- 36 tools in the default surface, organized as consolidated domains so they cost ~75% fewer tokens than individual registrations:
150
+ 36 tools in the default surface, organized as consolidated domains so they cost ~75% fewer tokens than individual registrations. The tools your AI gets:
136
151
 
137
- ```
138
- init / context → workspace state + the right context on every message
139
- search → semantic, hybrid, keyword, pattern, exhaustive, refactor modes
140
- memory → events, decisions, docs, runbooks, tasks, todos, diagrams, transcripts
141
- session → capture decisions & lessons, recall past sessions, plans, retroactive capture
142
- qa → grounded Q&A over your workspace knowledge base, with citations
143
- graph → dependencies, impact analysis, circular deps, unused code
144
- capsule → portable, shareable context snapshots for agent handoffs
145
- entity → tickets, incidents, releases, sprints, OKRs, risks
146
- project / workspace / skill / media / vcs / reminder / integration / help
147
- ```
152
+ - **init** / **context** — workspace state + the right context on every message
153
+ - **search** — semantic, hybrid, keyword, pattern, exhaustive, refactor modes
154
+ - **memory** — events, decisions, docs, runbooks, tasks, todos, diagrams, transcripts
155
+ - **session** — capture decisions & lessons, recall past sessions, plans, retroactive capture
156
+ - **qa** — grounded Q&A over your workspace knowledge base, with citations
157
+ - **graph** — dependencies, impact analysis, circular deps, unused code
158
+ - **capsule** — portable, shareable context snapshots for agent handoffs
159
+ - **entity** — tickets, incidents, releases, sprints, OKRs, risks
160
+ - **project** / **workspace** — project indexing, scope, and workspace management
161
+ - **skill** — reusable instruction + action bundles, portable across tools
162
+ - **media** — index and search images, video, audio, and documents
163
+ - **vcs** / **reminder** / **integration** / **help** — repo links, reminders, integrations, diagnostics
164
+
165
+ Plus focused write tools (`capture_plan`, `memory_create_doc`, `session_capture_lesson`, …) so agents that display tool names show *what* they're doing. Your AI uses all of this automatically — you just code.
166
+
167
+ ### Daily Recaps
168
+
169
+ Daily Recaps are generated around **23:00 in your configured timezone** when there is enough activity. They are not triggered by closing an editor, changing an MCP `session_id`, or starting a new chat, so long-lived VS Code/Copilot connections do not suppress the nightly job.
170
+
171
+ - `session(action="list_recaps", workspace_id="<uuid>", limit=30)` lists completed recaps newest-first with `recap_date` and `generated_at` timestamps.
172
+ - `session(action="trigger_recap", workspace_id="<uuid>")` queues an asynchronous manual recap. Call `list_recaps` afterward to verify completion.
148
173
 
149
- Plus focused write tools (`capture_plan`, `memory_create_doc`, …) so agents that display tool names show *what* they're doing. Your AI uses all of this automatically — you just code.
174
+ These actions use the same recap history and generation service as the dashboard.
150
175
 
151
176
  ---
152
177
 
@@ -5,11 +5,20 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
10
15
  };
11
16
  var __commonJS = (cb, mod) => function __require() {
12
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ try {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ } catch (e) {
20
+ throw mod = 0, e;
21
+ }
13
22
  };
14
23
  var __export = (target, all) => {
15
24
  for (var name in all)
@@ -492,39 +501,96 @@ var require_ignore = __commonJS({
492
501
  // src/ignore.ts
493
502
  import * as fs from "fs";
494
503
  import * as path from "path";
504
+ function isInvariantIgnoredPath(pathname) {
505
+ const normalized = pathname.replace(/\\/g, "/").replace(/^\.\//, "");
506
+ const segments = normalized.split("/").filter(Boolean);
507
+ if (segments.some((segment) => INVARIANT_IGNORED_DIRECTORIES.has(segment.toLowerCase()))) {
508
+ return true;
509
+ }
510
+ const basename = (segments.at(-1) || "").toLowerCase();
511
+ if (INVARIANT_SECRET_FILENAMES.has(basename)) return true;
512
+ if (INVARIANT_SECRET_SUFFIXES.some((suffix) => basename.endsWith(suffix))) return true;
513
+ return (basename === ".env" || basename.startsWith(".env.")) && !ENV_TEMPLATE_ALLOWLIST.has(basename);
514
+ }
495
515
  async function loadIgnorePatterns(projectRoot) {
496
516
  const ig = (0, import_ignore.default)();
497
517
  const patterns = [...DEFAULT_IGNORE_PATTERNS];
498
518
  ig.add(DEFAULT_IGNORE_PATTERNS);
499
- const ignoreFilePath = path.join(projectRoot, IGNORE_FILENAME);
500
519
  let hasUserPatterns = false;
501
- try {
502
- const content = await fs.promises.readFile(ignoreFilePath, "utf-8");
503
- const userPatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
504
- if (userPatterns.length > 0) {
505
- ig.add(userPatterns);
506
- patterns.push(...userPatterns);
507
- hasUserPatterns = true;
520
+ for (const filename of IGNORE_FILENAMES) {
521
+ try {
522
+ const content = await fs.promises.readFile(path.join(projectRoot, filename), "utf-8");
523
+ const userPatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
524
+ if (userPatterns.length > 0) {
525
+ ig.add(userPatterns);
526
+ patterns.push(...userPatterns);
527
+ hasUserPatterns = true;
528
+ }
529
+ } catch {
508
530
  }
509
- } catch {
510
531
  }
511
532
  return {
512
- ignores: (pathname) => ig.ignores(pathname),
533
+ ignores: (pathname) => isInvariantIgnoredPath(pathname) || ig.ignores(pathname),
513
534
  patterns,
514
535
  hasUserPatterns
515
536
  };
516
537
  }
517
- var import_ignore, IGNORE_FILENAME, DEFAULT_IGNORE_PATTERNS;
538
+ var import_ignore, IGNORE_FILENAMES, INVARIANT_IGNORED_DIRECTORIES, INVARIANT_SECRET_FILENAMES, INVARIANT_SECRET_SUFFIXES, ENV_TEMPLATE_ALLOWLIST, DEFAULT_IGNORE_PATTERNS;
518
539
  var init_ignore = __esm({
519
540
  "src/ignore.ts"() {
520
541
  "use strict";
521
542
  import_ignore = __toESM(require_ignore(), 1);
522
- IGNORE_FILENAME = ".contextstream/ignore";
543
+ IGNORE_FILENAMES = [".gitignore", ".contextignore", ".contextstream/ignore"];
544
+ INVARIANT_IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
545
+ ".contextstream",
546
+ ".claude",
547
+ ".codex",
548
+ ".cursor",
549
+ ".windsurf",
550
+ ".roo",
551
+ ".kilocode",
552
+ ".ssh",
553
+ ".aws",
554
+ ".gnupg",
555
+ ".kube",
556
+ ".docker"
557
+ ]);
558
+ INVARIANT_SECRET_FILENAMES = /* @__PURE__ */ new Set([
559
+ ".npmrc",
560
+ ".pypirc",
561
+ ".netrc",
562
+ ".pgpass",
563
+ ".git-credentials",
564
+ "id_rsa",
565
+ "id_ed25519",
566
+ "id_ecdsa",
567
+ "known_hosts",
568
+ "credentials.json",
569
+ "serviceaccountkey.json",
570
+ "auth.json"
571
+ ]);
572
+ INVARIANT_SECRET_SUFFIXES = [".pem", ".key", ".p12", ".pfx", ".jks"];
573
+ ENV_TEMPLATE_ALLOWLIST = /* @__PURE__ */ new Set([
574
+ ".env.example",
575
+ ".env.sample",
576
+ ".env.template",
577
+ ".env.dist",
578
+ ".env.defaults"
579
+ ]);
523
580
  DEFAULT_IGNORE_PATTERNS = [
524
581
  // Version control
525
582
  ".git/",
526
583
  ".svn/",
527
584
  ".hg/",
585
+ // ContextStream and AI editor/agent state. These directories can contain
586
+ // credentials, generated rules, caches, and nested worktrees.
587
+ ".contextstream/",
588
+ ".claude/",
589
+ ".codex/",
590
+ ".cursor/",
591
+ ".windsurf/",
592
+ ".roo/",
593
+ ".kilocode/",
528
594
  // Package managers / dependencies
529
595
  "node_modules/",
530
596
  "vendor/",
@@ -555,6 +621,12 @@ var init_ignore = __esm({
555
621
  ".idea/",
556
622
  ".vscode/",
557
623
  ".vs/",
624
+ // Credential stores
625
+ ".ssh/",
626
+ ".aws/",
627
+ ".gnupg/",
628
+ ".kube/",
629
+ ".docker/",
558
630
  // Coverage
559
631
  "coverage/",
560
632
  ".coverage/",
@@ -568,6 +640,22 @@ var init_ignore = __esm({
568
640
  "composer.lock",
569
641
  "*.min.js",
570
642
  "*.min.css",
643
+ "*.pem",
644
+ "*.key",
645
+ "*.p12",
646
+ "*.pfx",
647
+ "*.jks",
648
+ ".npmrc",
649
+ ".pypirc",
650
+ ".netrc",
651
+ ".pgpass",
652
+ ".git-credentials",
653
+ "id_rsa",
654
+ "id_ed25519",
655
+ "id_ecdsa",
656
+ "credentials.json",
657
+ "serviceAccountKey.json",
658
+ "auth.json",
571
659
  // OS files
572
660
  ".DS_Store",
573
661
  "Thumbs.db"
@@ -595,6 +683,15 @@ import { exec } from "child_process";
595
683
  import { promisify } from "util";
596
684
  import * as os from "os";
597
685
  import * as crypto from "crypto";
686
+ function shouldSkipSensitiveFile(name) {
687
+ const lower = name.toLowerCase();
688
+ if (SECRET_FILENAMES.has(lower)) return true;
689
+ if (SECRET_FILE_SUFFIXES.some((suffix) => lower.endsWith(suffix))) return true;
690
+ if ((lower === ".env" || lower.startsWith(".env.")) && !ENV_TEMPLATE_ALLOWLIST2.has(lower)) {
691
+ return true;
692
+ }
693
+ return false;
694
+ }
598
695
  function getMachineId() {
599
696
  const hostname2 = os.hostname();
600
697
  const hash = crypto.createHash("sha256").update(hostname2).digest("hex");
@@ -701,6 +798,7 @@ function clearGitContextCache() {
701
798
  gitContextCache.clear();
702
799
  }
703
800
  async function readFilesFromDirectory(rootPath, options = {}) {
801
+ if (isInvariantIgnoredPath(path2.resolve(rootPath))) return [];
704
802
  const maxFiles = options.maxFiles ?? MAX_FILES_PER_BATCH;
705
803
  const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
706
804
  const files = [];
@@ -725,6 +823,7 @@ async function readFilesFromDirectory(rootPath, options = {}) {
725
823
  await walkDir(fullPath, relPath);
726
824
  } else if (entry.isFile()) {
727
825
  if (IGNORE_FILES.has(entry.name)) continue;
826
+ if (shouldSkipSensitiveFile(entry.name)) continue;
728
827
  if (ig.ignores(relPath)) continue;
729
828
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
730
829
  if (!CODE_EXTENSIONS.has(ext)) continue;
@@ -760,6 +859,7 @@ async function readFilesFromDirectory(rootPath, options = {}) {
760
859
  return files;
761
860
  }
762
861
  async function* readAllFilesInBatches(rootPath, options = {}) {
862
+ if (isInvariantIgnoredPath(path2.resolve(rootPath))) return;
763
863
  const maxBatchBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
764
864
  const largeFileThreshold = options.largeFileThreshold ?? LARGE_FILE_THRESHOLD;
765
865
  const maxFilesPerBatch = options.maxFilesPerBatch ?? options.batchSize ?? MAX_FILES_PER_BATCH;
@@ -785,6 +885,7 @@ async function* readAllFilesInBatches(rootPath, options = {}) {
785
885
  yield* walkDir(fullPath, relPath);
786
886
  } else if (entry.isFile()) {
787
887
  if (IGNORE_FILES.has(entry.name)) continue;
888
+ if (shouldSkipSensitiveFile(entry.name)) continue;
788
889
  if (ig.ignores(relPath)) continue;
789
890
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
790
891
  if (!CODE_EXTENSIONS.has(ext)) continue;
@@ -845,6 +946,7 @@ async function* readAllFilesInBatches(rootPath, options = {}) {
845
946
  }
846
947
  }
847
948
  async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}) {
949
+ if (isInvariantIgnoredPath(path2.resolve(rootPath))) return;
848
950
  const maxBatchBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
849
951
  const largeFileThreshold = options.largeFileThreshold ?? LARGE_FILE_THRESHOLD;
850
952
  const maxFilesPerBatch = options.maxFilesPerBatch ?? options.batchSize ?? MAX_FILES_PER_BATCH;
@@ -873,6 +975,7 @@ async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}
873
975
  yield* walkDir(fullPath, relPath);
874
976
  } else if (entry.isFile()) {
875
977
  if (IGNORE_FILES.has(entry.name)) continue;
978
+ if (shouldSkipSensitiveFile(entry.name)) continue;
876
979
  if (ig.ignores(relPath)) continue;
877
980
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
878
981
  if (!CODE_EXTENSIONS.has(ext)) continue;
@@ -939,6 +1042,7 @@ async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}
939
1042
  );
940
1043
  }
941
1044
  async function countIndexableFiles(rootPath, options = {}) {
1045
+ if (isInvariantIgnoredPath(path2.resolve(rootPath))) return { count: 0, stopped: false };
942
1046
  const maxFiles = options.maxFiles ?? 1;
943
1047
  const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
944
1048
  const ig = options.ignoreInstance ?? await loadIgnorePatterns(rootPath);
@@ -969,6 +1073,7 @@ async function countIndexableFiles(rootPath, options = {}) {
969
1073
  await walkDir(fullPath, relPath);
970
1074
  } else if (entry.isFile()) {
971
1075
  if (IGNORE_FILES.has(entry.name)) continue;
1076
+ if (shouldSkipSensitiveFile(entry.name)) continue;
972
1077
  if (ig.ignores(relPath)) continue;
973
1078
  const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
974
1079
  if (!CODE_EXTENSIONS.has(ext)) continue;
@@ -1053,7 +1158,7 @@ function deleteHashManifest(projectId) {
1053
1158
  } catch {
1054
1159
  }
1055
1160
  }
1056
- var execAsync, CODE_EXTENSIONS, IGNORE_DIRS, IGNORE_FILES, MAX_FILE_SIZE, MAX_BATCH_BYTES, LARGE_FILE_THRESHOLD, MAX_FILES_PER_BATCH, gitContextCache;
1161
+ var execAsync, CODE_EXTENSIONS, IGNORE_DIRS, IGNORE_FILES, SECRET_FILENAMES, SECRET_FILE_SUFFIXES, ENV_TEMPLATE_ALLOWLIST2, MAX_FILE_SIZE, MAX_BATCH_BYTES, LARGE_FILE_THRESHOLD, MAX_FILES_PER_BATCH, gitContextCache;
1057
1162
  var init_files = __esm({
1058
1163
  "src/files.ts"() {
1059
1164
  "use strict";
@@ -1129,6 +1234,13 @@ var init_files = __esm({
1129
1234
  IGNORE_DIRS = /* @__PURE__ */ new Set([
1130
1235
  "node_modules",
1131
1236
  ".git",
1237
+ ".contextstream",
1238
+ ".claude",
1239
+ ".codex",
1240
+ ".cursor",
1241
+ ".windsurf",
1242
+ ".roo",
1243
+ ".kilocode",
1132
1244
  ".svn",
1133
1245
  ".hg",
1134
1246
  "target",
@@ -1156,7 +1268,18 @@ var init_files = __esm({
1156
1268
  "obj",
1157
1269
  ".idea",
1158
1270
  ".vscode",
1159
- ".vs"
1271
+ ".vs",
1272
+ ".npm",
1273
+ ".yarn",
1274
+ ".nyc_output",
1275
+ // Credential directories are invariant exclusions. Their contents often
1276
+ // have ordinary-looking names (config, credentials, known_hosts), so a
1277
+ // filename-only filter is not sufficient.
1278
+ ".ssh",
1279
+ ".aws",
1280
+ ".gnupg",
1281
+ ".kube",
1282
+ ".docker"
1160
1283
  ]);
1161
1284
  IGNORE_FILES = /* @__PURE__ */ new Set([
1162
1285
  ".DS_Store",
@@ -1171,6 +1294,28 @@ var init_files = __esm({
1171
1294
  "Gemfile.lock",
1172
1295
  "composer.lock"
1173
1296
  ]);
1297
+ SECRET_FILENAMES = /* @__PURE__ */ new Set([
1298
+ ".npmrc",
1299
+ ".pypirc",
1300
+ ".netrc",
1301
+ ".pgpass",
1302
+ ".git-credentials",
1303
+ "id_rsa",
1304
+ "id_ed25519",
1305
+ "id_ecdsa",
1306
+ "known_hosts",
1307
+ "credentials.json",
1308
+ "serviceaccountkey.json",
1309
+ "auth.json"
1310
+ ]);
1311
+ SECRET_FILE_SUFFIXES = [".pem", ".key", ".p12", ".pfx", ".jks"];
1312
+ ENV_TEMPLATE_ALLOWLIST2 = /* @__PURE__ */ new Set([
1313
+ ".env.example",
1314
+ ".env.sample",
1315
+ ".env.template",
1316
+ ".env.dist",
1317
+ ".env.defaults"
1318
+ ]);
1174
1319
  MAX_FILE_SIZE = 5 * 1024 * 1024;
1175
1320
  MAX_BATCH_BYTES = 1024 * 1024;
1176
1321
  LARGE_FILE_THRESHOLD = 2 * 1024 * 1024;