@contextstream/mcp-server 0.4.23 → 0.4.25

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 (2) hide show
  1. package/dist/index.js +166 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4610,6 +4610,55 @@ async function* readAllFilesInBatches(rootPath, options = {}) {
4610
4610
  yield batch;
4611
4611
  }
4612
4612
  }
4613
+ async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}) {
4614
+ const batchSize = options.batchSize ?? 50;
4615
+ const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
4616
+ const sinceMs = sinceTimestamp.getTime();
4617
+ let batch = [];
4618
+ let filesScanned = 0;
4619
+ let filesChanged = 0;
4620
+ async function* walkDir(dir, relativePath = "") {
4621
+ let entries;
4622
+ try {
4623
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
4624
+ } catch {
4625
+ return;
4626
+ }
4627
+ for (const entry of entries) {
4628
+ const fullPath = path.join(dir, entry.name);
4629
+ const relPath = path.join(relativePath, entry.name);
4630
+ if (entry.isDirectory()) {
4631
+ if (IGNORE_DIRS.has(entry.name)) continue;
4632
+ yield* walkDir(fullPath, relPath);
4633
+ } else if (entry.isFile()) {
4634
+ if (IGNORE_FILES.has(entry.name)) continue;
4635
+ const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
4636
+ if (!CODE_EXTENSIONS.has(ext)) continue;
4637
+ try {
4638
+ const stat2 = await fs.promises.stat(fullPath);
4639
+ filesScanned++;
4640
+ if (stat2.mtimeMs <= sinceMs) continue;
4641
+ if (stat2.size > maxFileSize) continue;
4642
+ const content = await fs.promises.readFile(fullPath, "utf-8");
4643
+ filesChanged++;
4644
+ yield { path: relPath, content };
4645
+ } catch {
4646
+ }
4647
+ }
4648
+ }
4649
+ }
4650
+ for await (const file of walkDir(rootPath)) {
4651
+ batch.push(file);
4652
+ if (batch.length >= batchSize) {
4653
+ yield batch;
4654
+ batch = [];
4655
+ }
4656
+ }
4657
+ if (batch.length > 0) {
4658
+ yield batch;
4659
+ }
4660
+ console.error(`[ContextStream] Incremental scan: ${filesChanged} changed files out of ${filesScanned} scanned (since ${sinceTimestamp.toISOString()})`);
4661
+ }
4613
4662
  async function countIndexableFiles(rootPath, options = {}) {
4614
4663
  const maxFiles = options.maxFiles ?? 1;
4615
4664
  const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
@@ -4915,6 +4964,7 @@ var INGEST_BENEFITS = [
4915
4964
  var ContextStreamClient = class {
4916
4965
  constructor(config) {
4917
4966
  this.config = config;
4967
+ this.indexRefreshInProgress = false;
4918
4968
  }
4919
4969
  /**
4920
4970
  * Update the client's default workspace/project IDs at runtime.
@@ -5085,6 +5135,7 @@ var ContextStreamClient = class {
5085
5135
  return request(this.config, `/projects/${projectId}/index`, { body: {} });
5086
5136
  }
5087
5137
  // Search - each method adds required search_type and filters fields
5138
+ // Optional params: context_lines (like grep -C), exact_match_boost (boost for exact matches)
5088
5139
  searchSemantic(body) {
5089
5140
  return request(this.config, "/search/semantic", {
5090
5141
  body: {
@@ -5121,6 +5172,20 @@ var ContextStreamClient = class {
5121
5172
  }
5122
5173
  });
5123
5174
  }
5175
+ /**
5176
+ * Exhaustive search returns ALL matches from the index.
5177
+ * Use this when you need complete coverage like grep.
5178
+ * Includes index_freshness to indicate result trustworthiness.
5179
+ */
5180
+ searchExhaustive(body) {
5181
+ return request(this.config, "/search/exhaustive", {
5182
+ body: {
5183
+ ...this.withDefaults(body),
5184
+ search_type: "exhaustive",
5185
+ filters: body.workspace_id ? {} : { file_types: [], languages: [], file_paths: [], exclude_paths: [], content_types: [], tags: [] }
5186
+ }
5187
+ });
5188
+ }
5124
5189
  // Memory / Knowledge
5125
5190
  createMemoryEvent(body) {
5126
5191
  const withDefaults = this.withDefaults(body);
@@ -5682,6 +5747,7 @@ var ContextStreamClient = class {
5682
5747
  session_id: params.session_id || randomUUID(),
5683
5748
  initialized_at: (/* @__PURE__ */ new Date()).toISOString()
5684
5749
  };
5750
+ this.sessionStartTime = Date.now();
5685
5751
  const rootPath = ideRoots.length > 0 ? ideRoots[0] : void 0;
5686
5752
  if (!workspaceId && rootPath) {
5687
5753
  const resolved = resolveWorkspace(rootPath);
@@ -5906,6 +5972,8 @@ var ContextStreamClient = class {
5906
5972
  context.workspace_name = workspaceName;
5907
5973
  context.project_id = projectId;
5908
5974
  context.ide_roots = ideRoots;
5975
+ this.sessionProjectId = projectId;
5976
+ this.sessionRootPath = rootPath;
5909
5977
  if (!workspaceId) {
5910
5978
  context.workspace_warning = "No workspace was resolved for this session. Workspace-level tools (memory/search/graph) may not work until you associate this folder with a workspace.";
5911
5979
  }
@@ -5978,9 +6046,45 @@ var ContextStreamClient = class {
5978
6046
  if (projectId && !context.ingest_recommendation) {
5979
6047
  try {
5980
6048
  const recommendation = await this.checkIngestRecommendation(projectId, rootPath);
5981
- context.ingest_recommendation = recommendation;
5982
- if (recommendation.recommended) {
5983
- console.error(`[ContextStream] Ingest recommended for existing project ${projectId}: ${recommendation.status}`);
6049
+ const autoIndex = params.auto_index !== false;
6050
+ const needsRefresh = recommendation.status === "not_indexed" || recommendation.status === "stale" || recommendation.status === "indexed";
6051
+ if (autoIndex && rootPath && needsRefresh && recommendation.status !== "recently_indexed") {
6052
+ const useIncremental = recommendation.last_indexed && recommendation.status !== "not_indexed";
6053
+ console.error(`[ContextStream] Auto-refreshing stale index for project ${projectId}: ${recommendation.status} (${useIncremental ? "incremental" : "full"})`);
6054
+ context.indexing_status = "refreshing";
6055
+ context.ingest_recommendation = {
6056
+ recommended: false,
6057
+ status: "auto_refreshing",
6058
+ indexed_files: recommendation.indexed_files,
6059
+ last_indexed: recommendation.last_indexed,
6060
+ reason: useIncremental ? "Incremental index refresh started automatically (only changed files)." : "Background index refresh started automatically to capture recent changes."
6061
+ };
6062
+ const projectIdCopy = projectId;
6063
+ const rootPathCopy = rootPath;
6064
+ const lastIndexedCopy = recommendation.last_indexed;
6065
+ (async () => {
6066
+ try {
6067
+ if (useIncremental && lastIndexedCopy) {
6068
+ const sinceDate = new Date(lastIndexedCopy);
6069
+ for await (const batch of readChangedFilesInBatches(rootPathCopy, sinceDate, { batchSize: 50 })) {
6070
+ await this.ingestFiles(projectIdCopy, batch);
6071
+ }
6072
+ console.error(`[ContextStream] Incremental index refresh completed for ${rootPathCopy}`);
6073
+ } else {
6074
+ for await (const batch of readAllFilesInBatches(rootPathCopy, { batchSize: 50 })) {
6075
+ await this.ingestFiles(projectIdCopy, batch);
6076
+ }
6077
+ console.error(`[ContextStream] Full index refresh completed for ${rootPathCopy}`);
6078
+ }
6079
+ } catch (e) {
6080
+ console.error(`[ContextStream] Background index refresh failed:`, e);
6081
+ }
6082
+ })();
6083
+ } else {
6084
+ context.ingest_recommendation = recommendation;
6085
+ if (recommendation.recommended) {
6086
+ console.error(`[ContextStream] Ingest recommended for existing project ${projectId}: ${recommendation.status}`);
6087
+ }
5984
6088
  }
5985
6089
  } catch (e) {
5986
6090
  console.error(`[ContextStream] Failed to check ingest recommendation for existing project:`, e);
@@ -6692,6 +6796,46 @@ var ContextStreamClient = class {
6692
6796
  sources_used: 0
6693
6797
  };
6694
6798
  }
6799
+ const THIRTY_MINUTES_MS = 30 * 60 * 1e3;
6800
+ const TEN_MINUTES_MS = 10 * 60 * 1e3;
6801
+ const now = Date.now();
6802
+ const sessionAge = this.sessionStartTime ? now - this.sessionStartTime : 0;
6803
+ const timeSinceLastCheck = this.lastRefreshCheckTime ? now - this.lastRefreshCheckTime : Infinity;
6804
+ if (sessionAge > THIRTY_MINUTES_MS && timeSinceLastCheck > TEN_MINUTES_MS && this.sessionProjectId && this.sessionRootPath && !this.indexRefreshInProgress) {
6805
+ this.lastRefreshCheckTime = now;
6806
+ const projectIdCopy = this.sessionProjectId;
6807
+ const rootPathCopy = this.sessionRootPath;
6808
+ (async () => {
6809
+ try {
6810
+ const recommendation = await this.checkIngestRecommendation(projectIdCopy, rootPathCopy);
6811
+ const needsRefresh = recommendation.status === "not_indexed" || recommendation.status === "stale" || recommendation.status === "indexed";
6812
+ if (needsRefresh && recommendation.status !== "recently_indexed") {
6813
+ this.indexRefreshInProgress = true;
6814
+ const useIncremental = recommendation.last_indexed && recommendation.status !== "not_indexed";
6815
+ console.error(`[ContextStream] Long session re-index: refreshing stale index for project ${projectIdCopy} (session age: ${Math.round(sessionAge / 6e4)} mins, ${useIncremental ? "incremental" : "full"})`);
6816
+ try {
6817
+ if (useIncremental && recommendation.last_indexed) {
6818
+ const sinceDate = new Date(recommendation.last_indexed);
6819
+ for await (const batch of readChangedFilesInBatches(rootPathCopy, sinceDate, { batchSize: 50 })) {
6820
+ await this.ingestFiles(projectIdCopy, batch);
6821
+ }
6822
+ console.error(`[ContextStream] Long session incremental re-index completed for ${rootPathCopy}`);
6823
+ } else {
6824
+ for await (const batch of readAllFilesInBatches(rootPathCopy, { batchSize: 50 })) {
6825
+ await this.ingestFiles(projectIdCopy, batch);
6826
+ }
6827
+ console.error(`[ContextStream] Long session full re-index completed for ${rootPathCopy}`);
6828
+ }
6829
+ } finally {
6830
+ this.indexRefreshInProgress = false;
6831
+ }
6832
+ }
6833
+ } catch (e) {
6834
+ console.error(`[ContextStream] Long session re-index check failed:`, e);
6835
+ this.indexRefreshInProgress = false;
6836
+ }
6837
+ })();
6838
+ }
6695
6839
  try {
6696
6840
  const apiResult = await request(this.config, "/context/smart", {
6697
6841
  body: {
@@ -6721,7 +6865,8 @@ var ContextStreamClient = class {
6721
6865
  workspace_id: withDefaults.workspace_id,
6722
6866
  project_id: withDefaults.project_id,
6723
6867
  ...versionNotice2 ? { version_notice: versionNotice2 } : {},
6724
- ...Array.isArray(data?.errors) ? { errors: data.errors } : {}
6868
+ ...Array.isArray(data?.errors) ? { errors: data.errors } : {},
6869
+ ...this.indexRefreshInProgress ? { index_status: "refreshing" } : {}
6725
6870
  };
6726
6871
  } catch (err) {
6727
6872
  const message2 = err instanceof Error ? err.message : String(err);
@@ -6916,8 +7061,9 @@ W:${wsHint}
6916
7061
  workspace_id: withDefaults.workspace_id,
6917
7062
  project_id: withDefaults.project_id,
6918
7063
  ...versionNotice ? { version_notice: versionNotice } : {},
6919
- ...errors.length > 0 && { errors }
7064
+ ...errors.length > 0 && { errors },
6920
7065
  // Include errors for debugging
7066
+ ...this.indexRefreshInProgress ? { index_status: "refreshing" } : {}
6921
7067
  };
6922
7068
  }
6923
7069
  /**
@@ -7740,7 +7886,7 @@ Only after this preflight, proceed with search/analysis below.
7740
7886
 
7741
7887
  ### Search & Code Intelligence (ContextStream-first)
7742
7888
 
7743
- \u26A0\uFE0F **STOP: Before using Glob/Grep/Read/Explore** \u2192 Call \`search(mode="hybrid")\` FIRST. Use local tools ONLY if ContextStream returns 0 results.
7889
+ \u26A0\uFE0F **STOP: Before using Search/Glob/Grep/Read/Explore** \u2192 Call \`search(mode="hybrid")\` FIRST. Use local tools ONLY if ContextStream returns 0 results.
7744
7890
 
7745
7891
  **Search order:**
7746
7892
  1. \`session(action="smart_search", query="...")\` - context-enriched
@@ -7862,7 +8008,7 @@ Rules Version: ${RULES_VERSION}
7862
8008
 
7863
8009
  ### Behavior Rules
7864
8010
 
7865
- \u26A0\uFE0F **STOP: Before using Glob/Grep/Read/Explore** \u2192 Call \`search(mode="hybrid")\` FIRST. Use local tools ONLY if ContextStream returns 0 results.
8011
+ \u26A0\uFE0F **STOP: Before using Search/Glob/Grep/Read/Explore** \u2192 Call \`search(mode="hybrid")\` FIRST. Use local tools ONLY if ContextStream returns 0 results.
7866
8012
 
7867
8013
  - **First message**: Call \`session_init\` with context_hint, then call \`context_smart\` before any other tool or response
7868
8014
  - **On [INGEST_RECOMMENDED]**: Ask the user if they want to enable semantic code search. Explain: "Indexing your codebase enables AI-powered code search, dependency analysis, and better context. This takes a few minutes." If user agrees, run the provided \`project(action="ingest_local")\` command.
@@ -10448,13 +10594,17 @@ Access: Free`,
10448
10594
  const limit = typeof input.limit === "number" && input.limit > 0 ? Math.min(Math.floor(input.limit), 100) : DEFAULT_SEARCH_LIMIT;
10449
10595
  const offset = typeof input.offset === "number" && input.offset > 0 ? Math.floor(input.offset) : void 0;
10450
10596
  const contentMax = typeof input.content_max_chars === "number" && input.content_max_chars > 0 ? Math.max(50, Math.min(Math.floor(input.content_max_chars), 1e4)) : DEFAULT_SEARCH_CONTENT_MAX_CHARS;
10597
+ const contextLines = typeof input.context_lines === "number" && input.context_lines >= 0 ? Math.min(Math.floor(input.context_lines), 10) : void 0;
10598
+ const exactMatchBoost = typeof input.exact_match_boost === "number" && input.exact_match_boost >= 1 ? Math.min(input.exact_match_boost, 10) : void 0;
10451
10599
  return {
10452
10600
  query: input.query,
10453
10601
  workspace_id: resolveWorkspaceId(input.workspace_id),
10454
10602
  project_id: resolveProjectId(input.project_id),
10455
10603
  limit,
10456
10604
  offset,
10457
- content_max_chars: contentMax
10605
+ content_max_chars: contentMax,
10606
+ context_lines: contextLines,
10607
+ exact_match_boost: exactMatchBoost
10458
10608
  };
10459
10609
  }
10460
10610
  registerTool(
@@ -13282,15 +13432,17 @@ Use this to remove a reminder that is no longer relevant.`,
13282
13432
  "search",
13283
13433
  {
13284
13434
  title: "Search",
13285
- description: `Search workspace memory and knowledge. Modes: semantic (meaning-based), hybrid (semantic + keyword), keyword (exact match), pattern (regex).`,
13435
+ description: `Search workspace memory and knowledge. Modes: semantic (meaning-based), hybrid (semantic + keyword), keyword (exact match), pattern (regex), exhaustive (all matches like grep).`,
13286
13436
  inputSchema: external_exports.object({
13287
- mode: external_exports.enum(["semantic", "hybrid", "keyword", "pattern"]).describe("Search mode"),
13437
+ mode: external_exports.enum(["semantic", "hybrid", "keyword", "pattern", "exhaustive"]).describe("Search mode"),
13288
13438
  query: external_exports.string().describe("Search query"),
13289
13439
  workspace_id: external_exports.string().uuid().optional(),
13290
13440
  project_id: external_exports.string().uuid().optional(),
13291
13441
  limit: external_exports.number().optional().describe("Max results to return (default: 3)"),
13292
13442
  offset: external_exports.number().optional().describe("Offset for pagination"),
13293
- content_max_chars: external_exports.number().optional().describe("Max chars per result content (default: 400)")
13443
+ content_max_chars: external_exports.number().optional().describe("Max chars per result content (default: 400)"),
13444
+ context_lines: external_exports.number().min(0).max(10).optional().describe("Lines of context around matches (like grep -C)"),
13445
+ exact_match_boost: external_exports.number().min(1).max(10).optional().describe("Boost factor for exact matches (default: 2.0)")
13294
13446
  })
13295
13447
  },
13296
13448
  async (input) => {
@@ -13309,6 +13461,9 @@ Use this to remove a reminder that is no longer relevant.`,
13309
13461
  case "pattern":
13310
13462
  result = await client.searchPattern(params);
13311
13463
  break;
13464
+ case "exhaustive":
13465
+ result = await client.searchExhaustive(params);
13466
+ break;
13312
13467
  }
13313
13468
  return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
13314
13469
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@contextstream/mcp-server",
3
3
  "mcpName": "io.github.contextstreamio/mcp-server",
4
- "version": "0.4.23",
4
+ "version": "0.4.25",
5
5
  "description": "ContextStream MCP server - v0.4.x with consolidated domain tools (~11 tools, ~75% token reduction). Code context, memory, search, and AI tools.",
6
6
  "type": "module",
7
7
  "license": "MIT",