@contextstream/mcp-server 0.4.34 → 0.4.36

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 +330 -147
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -4297,7 +4297,7 @@ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504])
4297
4297
  var MAX_RETRIES = 3;
4298
4298
  var BASE_DELAY = 1e3;
4299
4299
  async function sleep(ms) {
4300
- return new Promise((resolve3) => setTimeout(resolve3, ms));
4300
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
4301
4301
  }
4302
4302
  async function request(config, path8, options = {}) {
4303
4303
  const { apiUrl, userAgent } = config;
@@ -4571,10 +4571,16 @@ var IGNORE_FILES = /* @__PURE__ */ new Set([
4571
4571
  "composer.lock"
4572
4572
  ]);
4573
4573
  var MAX_FILE_SIZE = 1024 * 1024;
4574
+ var MAX_BATCH_BYTES = 10 * 1024 * 1024;
4575
+ var LARGE_FILE_THRESHOLD = 2 * 1024 * 1024;
4576
+ var MAX_FILES_PER_BATCH = 200;
4574
4577
  async function* readAllFilesInBatches(rootPath, options = {}) {
4575
- const batchSize = options.batchSize ?? 50;
4578
+ const maxBatchBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
4579
+ const largeFileThreshold = options.largeFileThreshold ?? LARGE_FILE_THRESHOLD;
4580
+ const maxFilesPerBatch = options.maxFilesPerBatch ?? MAX_FILES_PER_BATCH;
4576
4581
  const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
4577
4582
  let batch = [];
4583
+ let currentBatchBytes = 0;
4578
4584
  async function* walkDir(dir, relativePath = "") {
4579
4585
  let entries;
4580
4586
  try {
@@ -4596,28 +4602,46 @@ async function* readAllFilesInBatches(rootPath, options = {}) {
4596
4602
  const stat2 = await fs.promises.stat(fullPath);
4597
4603
  if (stat2.size > maxFileSize) continue;
4598
4604
  const content = await fs.promises.readFile(fullPath, "utf-8");
4599
- yield { path: relPath, content };
4605
+ yield { path: relPath, content, sizeBytes: stat2.size };
4600
4606
  } catch {
4601
4607
  }
4602
4608
  }
4603
4609
  }
4604
4610
  }
4605
4611
  for await (const file of walkDir(rootPath)) {
4606
- batch.push(file);
4607
- if (batch.length >= batchSize) {
4608
- yield batch;
4609
- batch = [];
4612
+ if (file.sizeBytes > largeFileThreshold) {
4613
+ if (batch.length > 0) {
4614
+ yield batch.map(({ sizeBytes, ...rest }) => rest);
4615
+ batch = [];
4616
+ currentBatchBytes = 0;
4617
+ }
4618
+ yield [{ path: file.path, content: file.content }];
4619
+ continue;
4610
4620
  }
4621
+ const wouldExceedBytes = currentBatchBytes + file.sizeBytes > maxBatchBytes;
4622
+ const wouldExceedFiles = batch.length >= maxFilesPerBatch;
4623
+ if (wouldExceedBytes || wouldExceedFiles) {
4624
+ if (batch.length > 0) {
4625
+ yield batch.map(({ sizeBytes, ...rest }) => rest);
4626
+ batch = [];
4627
+ currentBatchBytes = 0;
4628
+ }
4629
+ }
4630
+ batch.push(file);
4631
+ currentBatchBytes += file.sizeBytes;
4611
4632
  }
4612
4633
  if (batch.length > 0) {
4613
- yield batch;
4634
+ yield batch.map(({ sizeBytes, ...rest }) => rest);
4614
4635
  }
4615
4636
  }
4616
4637
  async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}) {
4617
- const batchSize = options.batchSize ?? 50;
4638
+ const maxBatchBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
4639
+ const largeFileThreshold = options.largeFileThreshold ?? LARGE_FILE_THRESHOLD;
4640
+ const maxFilesPerBatch = options.maxFilesPerBatch ?? MAX_FILES_PER_BATCH;
4618
4641
  const maxFileSize = options.maxFileSize ?? MAX_FILE_SIZE;
4619
4642
  const sinceMs = sinceTimestamp.getTime();
4620
4643
  let batch = [];
4644
+ let currentBatchBytes = 0;
4621
4645
  let filesScanned = 0;
4622
4646
  let filesChanged = 0;
4623
4647
  async function* walkDir(dir, relativePath = "") {
@@ -4644,21 +4668,36 @@ async function* readChangedFilesInBatches(rootPath, sinceTimestamp, options = {}
4644
4668
  if (stat2.size > maxFileSize) continue;
4645
4669
  const content = await fs.promises.readFile(fullPath, "utf-8");
4646
4670
  filesChanged++;
4647
- yield { path: relPath, content };
4671
+ yield { path: relPath, content, sizeBytes: stat2.size };
4648
4672
  } catch {
4649
4673
  }
4650
4674
  }
4651
4675
  }
4652
4676
  }
4653
4677
  for await (const file of walkDir(rootPath)) {
4654
- batch.push(file);
4655
- if (batch.length >= batchSize) {
4656
- yield batch;
4657
- batch = [];
4678
+ if (file.sizeBytes > largeFileThreshold) {
4679
+ if (batch.length > 0) {
4680
+ yield batch.map(({ sizeBytes, ...rest }) => rest);
4681
+ batch = [];
4682
+ currentBatchBytes = 0;
4683
+ }
4684
+ yield [{ path: file.path, content: file.content }];
4685
+ continue;
4686
+ }
4687
+ const wouldExceedBytes = currentBatchBytes + file.sizeBytes > maxBatchBytes;
4688
+ const wouldExceedFiles = batch.length >= maxFilesPerBatch;
4689
+ if (wouldExceedBytes || wouldExceedFiles) {
4690
+ if (batch.length > 0) {
4691
+ yield batch.map(({ sizeBytes, ...rest }) => rest);
4692
+ batch = [];
4693
+ currentBatchBytes = 0;
4694
+ }
4658
4695
  }
4696
+ batch.push(file);
4697
+ currentBatchBytes += file.sizeBytes;
4659
4698
  }
4660
4699
  if (batch.length > 0) {
4661
- yield batch;
4700
+ yield batch.map(({ sizeBytes, ...rest }) => rest);
4662
4701
  }
4663
4702
  console.error(
4664
4703
  `[ContextStream] Incremental scan: ${filesChanged} changed files out of ${filesScanned} scanned (since ${sinceTimestamp.toISOString()})`
@@ -5078,7 +5117,7 @@ var ContextStreamClient = class {
5078
5117
  return "full";
5079
5118
  }
5080
5119
  if (planName.includes("pro")) return "lite";
5081
- if (planName.includes("free")) return "none";
5120
+ if (planName.includes("free")) return "lite";
5082
5121
  return "lite";
5083
5122
  } catch {
5084
5123
  return "none";
@@ -6610,19 +6649,6 @@ var ContextStreamClient = class {
6610
6649
  used += next.length;
6611
6650
  }
6612
6651
  const summary = finalLines.join("\n");
6613
- this.trackTokenSavings({
6614
- tool: "session_summary",
6615
- workspace_id: withDefaults.workspace_id,
6616
- project_id: withDefaults.project_id,
6617
- candidate_chars: candidateSummary.length,
6618
- context_chars: summary.length,
6619
- max_tokens: maxTokens,
6620
- metadata: {
6621
- decision_count: decisionCount,
6622
- memory_count: memoryCount
6623
- }
6624
- }).catch(() => {
6625
- });
6626
6652
  return {
6627
6653
  summary,
6628
6654
  workspace_name: workspaceName,
@@ -6864,21 +6890,6 @@ var ContextStreamClient = class {
6864
6890
  const context = parts.join("");
6865
6891
  const candidateContext = candidateParts.join("");
6866
6892
  const tokenEstimate = Math.ceil(context.length / charsPerToken);
6867
- this.trackTokenSavings({
6868
- tool: "ai_context_budget",
6869
- workspace_id: withDefaults.workspace_id,
6870
- project_id: withDefaults.project_id,
6871
- candidate_chars: candidateContext.length,
6872
- context_chars: context.length,
6873
- max_tokens: maxTokens,
6874
- metadata: {
6875
- include_decisions: params.include_decisions !== false,
6876
- include_memory: params.include_memory !== false,
6877
- include_code: !!params.include_code,
6878
- sources: sources.length
6879
- }
6880
- }).catch(() => {
6881
- });
6882
6893
  return {
6883
6894
  context,
6884
6895
  token_estimate: tokenEstimate,
@@ -7222,21 +7233,6 @@ W:${wsHint}
7222
7233
  versionNotice = await getUpdateNotice();
7223
7234
  } catch {
7224
7235
  }
7225
- this.trackTokenSavings({
7226
- tool: "context_smart",
7227
- workspace_id: withDefaults.workspace_id,
7228
- project_id: withDefaults.project_id,
7229
- candidate_chars: candidateContext.length,
7230
- context_chars: context.length,
7231
- max_tokens: maxTokens,
7232
- metadata: {
7233
- format,
7234
- items: items.length,
7235
- keywords: keywords.slice(0, 10),
7236
- errors: errors.length
7237
- }
7238
- }).catch(() => {
7239
- });
7240
7236
  return {
7241
7237
  context,
7242
7238
  token_estimate: Math.ceil(context.length / 4),
@@ -7885,6 +7881,29 @@ W:${wsHint}
7885
7881
  { method: "GET" }
7886
7882
  );
7887
7883
  }
7884
+ /**
7885
+ * Create a new Notion database
7886
+ */
7887
+ async notionCreateDatabase(params) {
7888
+ const withDefaults = this.withDefaults(params || {});
7889
+ if (!withDefaults.workspace_id) {
7890
+ throw new Error("workspace_id is required for creating Notion database");
7891
+ }
7892
+ const query = new URLSearchParams();
7893
+ query.set("workspace_id", withDefaults.workspace_id);
7894
+ return request(
7895
+ this.config,
7896
+ `/integrations/notion/databases?${query.toString()}`,
7897
+ {
7898
+ method: "POST",
7899
+ body: {
7900
+ title: params.title,
7901
+ parent_page_id: params.parent_page_id,
7902
+ description: params.description
7903
+ }
7904
+ }
7905
+ );
7906
+ }
7888
7907
  /**
7889
7908
  * Search/list pages in Notion with smart type detection filtering
7890
7909
  */
@@ -8925,13 +8944,21 @@ var PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
8925
8944
  """
8926
8945
  ContextStream PreToolUse Hook for Claude Code
8927
8946
  Blocks Grep/Glob/Search/Task(Explore)/EnterPlanMode and redirects to ContextStream.
8947
+
8948
+ Only blocks if the current project is indexed in ContextStream.
8949
+ If not indexed, allows local tools through with a suggestion to index.
8928
8950
  """
8929
8951
 
8930
8952
  import json
8931
8953
  import sys
8932
8954
  import os
8955
+ from pathlib import Path
8956
+ from datetime import datetime, timedelta
8933
8957
 
8934
8958
  ENABLED = os.environ.get("CONTEXTSTREAM_HOOK_ENABLED", "true").lower() == "true"
8959
+ INDEX_STATUS_FILE = Path.home() / ".contextstream" / "indexed-projects.json"
8960
+ # Consider index stale after 7 days
8961
+ STALE_THRESHOLD_DAYS = 7
8935
8962
 
8936
8963
  DISCOVERY_PATTERNS = ["**/*", "**/", "src/**", "lib/**", "app/**", "components/**"]
8937
8964
 
@@ -8953,6 +8980,44 @@ def is_discovery_grep(file_path):
8953
8980
  return True
8954
8981
  return False
8955
8982
 
8983
+ def is_project_indexed(cwd: str) -> tuple[bool, bool]:
8984
+ """
8985
+ Check if the current directory is in an indexed project.
8986
+ Returns (is_indexed, is_stale).
8987
+ """
8988
+ if not INDEX_STATUS_FILE.exists():
8989
+ return False, False
8990
+
8991
+ try:
8992
+ with open(INDEX_STATUS_FILE, "r") as f:
8993
+ data = json.load(f)
8994
+ except:
8995
+ return False, False
8996
+
8997
+ projects = data.get("projects", {})
8998
+ cwd_path = Path(cwd).resolve()
8999
+
9000
+ # Check if cwd is within any indexed project
9001
+ for project_path, info in projects.items():
9002
+ try:
9003
+ indexed_path = Path(project_path).resolve()
9004
+ # Check if cwd is the project or a subdirectory
9005
+ if cwd_path == indexed_path or indexed_path in cwd_path.parents:
9006
+ # Check if stale
9007
+ indexed_at = info.get("indexed_at")
9008
+ if indexed_at:
9009
+ try:
9010
+ indexed_time = datetime.fromisoformat(indexed_at.replace("Z", "+00:00"))
9011
+ if datetime.now(indexed_time.tzinfo) - indexed_time > timedelta(days=STALE_THRESHOLD_DAYS):
9012
+ return True, True # Indexed but stale
9013
+ except:
9014
+ pass
9015
+ return True, False # Indexed and fresh
9016
+ except:
9017
+ continue
9018
+
9019
+ return False, False
9020
+
8956
9021
  def main():
8957
9022
  if not ENABLED:
8958
9023
  sys.exit(0)
@@ -8964,6 +9029,20 @@ def main():
8964
9029
 
8965
9030
  tool = data.get("tool_name", "")
8966
9031
  inp = data.get("tool_input", {})
9032
+ cwd = data.get("cwd", os.getcwd())
9033
+
9034
+ # Check if project is indexed
9035
+ is_indexed, is_stale = is_project_indexed(cwd)
9036
+
9037
+ if not is_indexed:
9038
+ # Project not indexed - allow local tools but suggest indexing
9039
+ # Don't block, just exit successfully
9040
+ sys.exit(0)
9041
+
9042
+ if is_stale:
9043
+ # Index is stale - allow with warning (printed but not blocking)
9044
+ # Still allow the tool but remind about re-indexing
9045
+ pass # Continue to blocking logic but could add warning
8967
9046
 
8968
9047
  if tool == "Glob":
8969
9048
  pattern = inp.get("pattern", "")
@@ -8971,18 +9050,16 @@ def main():
8971
9050
  print(f"STOP: Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of Glob.", file=sys.stderr)
8972
9051
  sys.exit(2)
8973
9052
 
8974
- elif tool == "Grep":
9053
+ elif tool == "Grep" or tool == "Search":
9054
+ # Block ALL Grep/Search operations - use ContextStream search or Read for specific files
8975
9055
  pattern = inp.get("pattern", "")
8976
9056
  path = inp.get("path", "")
8977
- if is_discovery_grep(path):
8978
- print(f"STOP: Use mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") instead of Grep.", file=sys.stderr)
8979
- sys.exit(2)
8980
-
8981
- elif tool == "Search":
8982
- # Block the Search tool which is Claude Code's default search
8983
- query = inp.get("pattern", "") or inp.get("query", "")
8984
- if query:
8985
- print(f"STOP: Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{query}\\") instead of Search.", file=sys.stderr)
9057
+ if pattern:
9058
+ if path and not is_discovery_grep(path):
9059
+ # Specific file - suggest Read instead
9060
+ print(f"STOP: Use Read(\\"{path}\\") to view file content, or mcp__contextstream__search(mode=\\"keyword\\", query=\\"{pattern}\\") for codebase search.", file=sys.stderr)
9061
+ else:
9062
+ print(f"STOP: Use mcp__contextstream__search(mode=\\"hybrid\\", query=\\"{pattern}\\") instead of {tool}.", file=sys.stderr)
8986
9063
  sys.exit(2)
8987
9064
 
8988
9065
  elif tool == "Task":
@@ -9148,6 +9225,97 @@ async function installClaudeCodeHooks(options) {
9148
9225
  }
9149
9226
  return result;
9150
9227
  }
9228
+ function getIndexStatusPath() {
9229
+ return path4.join(homedir2(), ".contextstream", "indexed-projects.json");
9230
+ }
9231
+ async function readIndexStatus() {
9232
+ const statusPath = getIndexStatusPath();
9233
+ try {
9234
+ const content = await fs3.readFile(statusPath, "utf-8");
9235
+ return JSON.parse(content);
9236
+ } catch {
9237
+ return { version: 1, projects: {} };
9238
+ }
9239
+ }
9240
+ async function writeIndexStatus(status) {
9241
+ const statusPath = getIndexStatusPath();
9242
+ const dir = path4.dirname(statusPath);
9243
+ await fs3.mkdir(dir, { recursive: true });
9244
+ await fs3.writeFile(statusPath, JSON.stringify(status, null, 2));
9245
+ }
9246
+ async function markProjectIndexed(projectPath, options) {
9247
+ const status = await readIndexStatus();
9248
+ const resolvedPath = path4.resolve(projectPath);
9249
+ status.projects[resolvedPath] = {
9250
+ indexed_at: (/* @__PURE__ */ new Date()).toISOString(),
9251
+ project_id: options?.project_id,
9252
+ project_name: options?.project_name
9253
+ };
9254
+ await writeIndexStatus(status);
9255
+ }
9256
+
9257
+ // src/token-savings.ts
9258
+ var TOKEN_SAVINGS_FORMULA_VERSION = 1;
9259
+ var MAX_CHARS_PER_EVENT = 2e7;
9260
+ var BASE_OVERHEAD_CHARS = 500;
9261
+ var CANDIDATE_MULTIPLIERS = {
9262
+ // context_smart: Replaces reading multiple files to gather context
9263
+ context_smart: 5,
9264
+ ai_context_budget: 5,
9265
+ // search: Semantic search replaces iterative Glob/Grep/Read cycles
9266
+ search_semantic: 4.5,
9267
+ search_hybrid: 4,
9268
+ search_keyword: 2.5,
9269
+ search_pattern: 3,
9270
+ search_exhaustive: 3.5,
9271
+ search_refactor: 3,
9272
+ // session: Recall/search replaces reading through history
9273
+ session_recall: 5,
9274
+ session_smart_search: 4,
9275
+ session_user_context: 3,
9276
+ session_summary: 4,
9277
+ // graph: Would require extensive file traversal
9278
+ graph_dependencies: 8,
9279
+ graph_impact: 10,
9280
+ graph_call_path: 8,
9281
+ graph_related: 6,
9282
+ // memory: Context retrieval
9283
+ memory_search: 3.5,
9284
+ memory_decisions: 3,
9285
+ memory_timeline: 3,
9286
+ memory_summary: 4
9287
+ };
9288
+ function clampCharCount(value) {
9289
+ if (!Number.isFinite(value) || value <= 0) return 0;
9290
+ return Math.min(MAX_CHARS_PER_EVENT, Math.floor(value));
9291
+ }
9292
+ function trackToolTokenSavings(client, tool, contextText, params, extraMetadata) {
9293
+ try {
9294
+ const contextChars = clampCharCount(contextText.length);
9295
+ const multiplier = CANDIDATE_MULTIPLIERS[tool] ?? 3;
9296
+ const baseOverhead = contextChars > 0 ? BASE_OVERHEAD_CHARS : 0;
9297
+ const estimatedCandidate = Math.round(contextChars * multiplier + baseOverhead);
9298
+ const candidateChars = Math.max(contextChars, clampCharCount(estimatedCandidate));
9299
+ client.trackTokenSavings({
9300
+ tool,
9301
+ workspace_id: params?.workspace_id,
9302
+ project_id: params?.project_id,
9303
+ candidate_chars: candidateChars,
9304
+ context_chars: contextChars,
9305
+ max_tokens: params?.max_tokens,
9306
+ metadata: {
9307
+ method: "multiplier_estimate",
9308
+ formula_version: TOKEN_SAVINGS_FORMULA_VERSION,
9309
+ source: "mcp-server",
9310
+ multiplier,
9311
+ base_overhead_chars: baseOverhead,
9312
+ ...extraMetadata ?? {}
9313
+ }
9314
+ }).catch(() => {
9315
+ });
9316
+ } catch {
9317
+ }
9318
+ }
9151
9319
 
9152
9320
  // src/tools.ts
9153
9321
  var LESSON_DEDUP_WINDOW_MS = 2 * 60 * 1e3;
@@ -10501,52 +10669,6 @@ function toStructured(data) {
10501
10669
  }
10502
10670
  return void 0;
10503
10671
  }
10504
- var CANDIDATE_MULTIPLIERS = {
10505
- // context_smart: Replaces reading multiple files to gather context
10506
- context_smart: 5,
10507
- // search: Semantic search replaces iterative Glob/Grep/Read cycles
10508
- search_semantic: 4.5,
10509
- search_hybrid: 4,
10510
- search_keyword: 2.5,
10511
- search_pattern: 3,
10512
- search_exhaustive: 3.5,
10513
- search_refactor: 3,
10514
- // session: Recall/search replaces reading through history
10515
- session_recall: 5,
10516
- session_smart_search: 4,
10517
- session_user_context: 3,
10518
- session_summary: 4,
10519
- // graph: Would require extensive file traversal
10520
- graph_dependencies: 8,
10521
- graph_impact: 10,
10522
- graph_call_path: 8,
10523
- graph_related: 6,
10524
- // memory: Context retrieval
10525
- memory_search: 3.5,
10526
- memory_decisions: 3,
10527
- memory_timeline: 3,
10528
- memory_summary: 4
10529
- };
10530
- function trackToolTokenSavings(client, tool, resultContent, params) {
10531
- try {
10532
- const contextStr = typeof resultContent === "string" ? resultContent : JSON.stringify(resultContent ?? {});
10533
- const contextChars = contextStr.length;
10534
- const multiplier = CANDIDATE_MULTIPLIERS[tool] ?? 3;
10535
- const baseOverhead = 500;
10536
- const candidateChars = Math.round(contextChars * multiplier + baseOverhead);
10537
- client.trackTokenSavings({
10538
- tool,
10539
- workspace_id: params?.workspace_id,
10540
- project_id: params?.project_id,
10541
- candidate_chars: candidateChars,
10542
- context_chars: contextChars,
10543
- max_tokens: params?.max_tokens,
10544
- metadata: { multiplier, source: "mcp-server" }
10545
- }).catch(() => {
10546
- });
10547
- } catch {
10548
- }
10549
- }
10550
10672
  function readStatNumber(payload, key) {
10551
10673
  if (!payload || typeof payload !== "object") return void 0;
10552
10674
  const direct = payload[key];
@@ -10790,7 +10912,8 @@ function registerTools(server, client, sessionManager) {
10790
10912
  ["graph_call_path", "full"],
10791
10913
  ["graph_circular_dependencies", "full"],
10792
10914
  ["graph_unused_code", "full"],
10793
- ["graph_ingest", "full"],
10915
+ ["graph_ingest", "lite"],
10916
+ // Pro can ingest (builds module-level graph), Elite gets full graph
10794
10917
  ["graph_contradictions", "full"]
10795
10918
  ]);
10796
10919
  const graphLiteMaxDepth = 1;
@@ -11262,6 +11385,12 @@ Hint: Run session_init(folder_path="<your_project_path>") first to establish a s
11262
11385
  console.error(
11263
11386
  `[ContextStream] Completed background ingestion: ${totalIndexed} files in ${batchCount} batches`
11264
11387
  );
11388
+ try {
11389
+ await markProjectIndexed(resolvedPath, { project_id: projectId });
11390
+ console.error(`[ContextStream] Marked project as indexed: ${resolvedPath}`);
11391
+ } catch (markError) {
11392
+ console.error(`[ContextStream] Failed to mark project as indexed:`, markError);
11393
+ }
11265
11394
  } catch (error) {
11266
11395
  console.error(`[ContextStream] Ingestion failed:`, error);
11267
11396
  }
@@ -13007,6 +13136,12 @@ ${benefitsList}` : "",
13007
13136
  noticeLines.push(
13008
13137
  `[INGEST_STATUS] Background indexing started. Codebase will be searchable shortly.`
13009
13138
  );
13139
+ } else if (folderPathForRules && !ingestRec?.recommended) {
13140
+ const projectId = typeof result.project_id === "string" ? result.project_id : void 0;
13141
+ const projectName = typeof result.project_name === "string" ? result.project_name : void 0;
13142
+ markProjectIndexed(folderPathForRules, { project_id: projectId, project_name: projectName }).catch(
13143
+ (err) => console.error("[ContextStream] Failed to mark project as indexed:", err)
13144
+ );
13010
13145
  }
13011
13146
  if (noticeLines.length > 0) {
13012
13147
  text = `${text}
@@ -15274,12 +15409,13 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15274
15409
  default:
15275
15410
  toolType = "search_hybrid";
15276
15411
  }
15277
- trackToolTokenSavings(client, toolType, result, {
15412
+ const outputText = formatContent(result);
15413
+ trackToolTokenSavings(client, toolType, outputText, {
15278
15414
  workspace_id: params.workspace_id,
15279
15415
  project_id: params.project_id
15280
15416
  });
15281
15417
  return {
15282
- content: [{ type: "text", text: formatContent(result) }],
15418
+ content: [{ type: "text", text: outputText }],
15283
15419
  structuredContent: toStructured(result)
15284
15420
  };
15285
15421
  }
@@ -15490,12 +15626,13 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15490
15626
  include_related: input.include_related,
15491
15627
  include_decisions: input.include_decisions
15492
15628
  });
15493
- trackToolTokenSavings(client, "session_recall", result, {
15629
+ const outputText = formatContent(result);
15630
+ trackToolTokenSavings(client, "session_recall", outputText, {
15494
15631
  workspace_id: workspaceId,
15495
15632
  project_id: projectId
15496
15633
  });
15497
15634
  return {
15498
- content: [{ type: "text", text: formatContent(result) }],
15635
+ content: [{ type: "text", text: outputText }],
15499
15636
  structuredContent: toStructured(result)
15500
15637
  };
15501
15638
  }
@@ -15517,11 +15654,12 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15517
15654
  }
15518
15655
  case "user_context": {
15519
15656
  const result = await client.getUserContext({ workspace_id: workspaceId });
15520
- trackToolTokenSavings(client, "session_user_context", result, {
15657
+ const outputText = formatContent(result);
15658
+ trackToolTokenSavings(client, "session_user_context", outputText, {
15521
15659
  workspace_id: workspaceId
15522
15660
  });
15523
15661
  return {
15524
- content: [{ type: "text", text: formatContent(result) }],
15662
+ content: [{ type: "text", text: outputText }],
15525
15663
  structuredContent: toStructured(result)
15526
15664
  };
15527
15665
  }
@@ -15531,13 +15669,14 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15531
15669
  project_id: projectId,
15532
15670
  max_tokens: input.max_tokens
15533
15671
  });
15534
- trackToolTokenSavings(client, "session_summary", result, {
15672
+ const outputText = formatContent(result);
15673
+ trackToolTokenSavings(client, "session_summary", outputText, {
15535
15674
  workspace_id: workspaceId,
15536
15675
  project_id: projectId,
15537
15676
  max_tokens: input.max_tokens
15538
15677
  });
15539
15678
  return {
15540
- content: [{ type: "text", text: formatContent(result) }],
15679
+ content: [{ type: "text", text: outputText }],
15541
15680
  structuredContent: toStructured(result)
15542
15681
  };
15543
15682
  }
@@ -15581,12 +15720,13 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15581
15720
  include_decisions: input.include_decisions,
15582
15721
  include_related: input.include_related
15583
15722
  });
15584
- trackToolTokenSavings(client, "session_smart_search", result, {
15723
+ const outputText = formatContent(result);
15724
+ trackToolTokenSavings(client, "session_smart_search", outputText, {
15585
15725
  workspace_id: workspaceId,
15586
15726
  project_id: projectId
15587
15727
  });
15588
15728
  return {
15589
- content: [{ type: "text", text: formatContent(result) }],
15729
+ content: [{ type: "text", text: outputText }],
15590
15730
  structuredContent: toStructured(result)
15591
15731
  };
15592
15732
  }
@@ -15933,12 +16073,13 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15933
16073
  query: input.query,
15934
16074
  limit: input.limit
15935
16075
  });
15936
- trackToolTokenSavings(client, "memory_search", result, {
16076
+ const outputText = formatContent(result);
16077
+ trackToolTokenSavings(client, "memory_search", outputText, {
15937
16078
  workspace_id: workspaceId,
15938
16079
  project_id: projectId
15939
16080
  });
15940
16081
  return {
15941
- content: [{ type: "text", text: formatContent(result) }],
16082
+ content: [{ type: "text", text: outputText }],
15942
16083
  structuredContent: toStructured(result)
15943
16084
  };
15944
16085
  }
@@ -15949,12 +16090,13 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15949
16090
  category: input.category,
15950
16091
  limit: input.limit
15951
16092
  });
15952
- trackToolTokenSavings(client, "memory_decisions", result, {
16093
+ const outputText = formatContent(result);
16094
+ trackToolTokenSavings(client, "memory_decisions", outputText, {
15953
16095
  workspace_id: workspaceId,
15954
16096
  project_id: projectId
15955
16097
  });
15956
16098
  return {
15957
- content: [{ type: "text", text: formatContent(result) }],
16099
+ content: [{ type: "text", text: outputText }],
15958
16100
  structuredContent: toStructured(result)
15959
16101
  };
15960
16102
  }
@@ -15963,11 +16105,12 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15963
16105
  return errorResult("timeline requires workspace_id. Call session_init first.");
15964
16106
  }
15965
16107
  const result = await client.memoryTimeline(workspaceId);
15966
- trackToolTokenSavings(client, "memory_timeline", result, {
16108
+ const outputText = formatContent(result);
16109
+ trackToolTokenSavings(client, "memory_timeline", outputText, {
15967
16110
  workspace_id: workspaceId
15968
16111
  });
15969
16112
  return {
15970
- content: [{ type: "text", text: formatContent(result) }],
16113
+ content: [{ type: "text", text: outputText }],
15971
16114
  structuredContent: toStructured(result)
15972
16115
  };
15973
16116
  }
@@ -15976,11 +16119,12 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
15976
16119
  return errorResult("summary requires workspace_id. Call session_init first.");
15977
16120
  }
15978
16121
  const result = await client.memorySummary(workspaceId);
15979
- trackToolTokenSavings(client, "memory_summary", result, {
16122
+ const outputText = formatContent(result);
16123
+ trackToolTokenSavings(client, "memory_summary", outputText, {
15980
16124
  workspace_id: workspaceId
15981
16125
  });
15982
16126
  return {
15983
- content: [{ type: "text", text: formatContent(result) }],
16127
+ content: [{ type: "text", text: outputText }],
15984
16128
  structuredContent: toStructured(result)
15985
16129
  };
15986
16130
  }
@@ -16165,12 +16309,13 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16165
16309
  max_depth: input.max_depth,
16166
16310
  include_transitive: input.include_transitive
16167
16311
  });
16168
- trackToolTokenSavings(client, "graph_dependencies", result, {
16312
+ const outputText = formatContent(result);
16313
+ trackToolTokenSavings(client, "graph_dependencies", outputText, {
16169
16314
  workspace_id: workspaceId,
16170
16315
  project_id: projectId
16171
16316
  });
16172
16317
  return {
16173
- content: [{ type: "text", text: formatContent(result) }],
16318
+ content: [{ type: "text", text: outputText }],
16174
16319
  structuredContent: toStructured(result)
16175
16320
  };
16176
16321
  }
@@ -16182,12 +16327,13 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16182
16327
  target: input.target,
16183
16328
  max_depth: input.max_depth
16184
16329
  });
16185
- trackToolTokenSavings(client, "graph_impact", result, {
16330
+ const outputText = formatContent(result);
16331
+ trackToolTokenSavings(client, "graph_impact", outputText, {
16186
16332
  workspace_id: workspaceId,
16187
16333
  project_id: projectId
16188
16334
  });
16189
16335
  return {
16190
- content: [{ type: "text", text: formatContent(result) }],
16336
+ content: [{ type: "text", text: outputText }],
16191
16337
  structuredContent: toStructured(result)
16192
16338
  };
16193
16339
  }
@@ -16200,12 +16346,13 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16200
16346
  target: input.target,
16201
16347
  max_depth: input.max_depth
16202
16348
  });
16203
- trackToolTokenSavings(client, "graph_call_path", result, {
16349
+ const outputText = formatContent(result);
16350
+ trackToolTokenSavings(client, "graph_call_path", outputText, {
16204
16351
  workspace_id: workspaceId,
16205
16352
  project_id: projectId
16206
16353
  });
16207
16354
  return {
16208
- content: [{ type: "text", text: formatContent(result) }],
16355
+ content: [{ type: "text", text: outputText }],
16209
16356
  structuredContent: toStructured(result)
16210
16357
  };
16211
16358
  }
@@ -16219,12 +16366,13 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16219
16366
  project_id: projectId,
16220
16367
  limit: input.limit
16221
16368
  });
16222
- trackToolTokenSavings(client, "graph_related", result, {
16369
+ const outputText = formatContent(result);
16370
+ trackToolTokenSavings(client, "graph_related", outputText, {
16223
16371
  workspace_id: workspaceId,
16224
16372
  project_id: projectId
16225
16373
  });
16226
16374
  return {
16227
- content: [{ type: "text", text: formatContent(result) }],
16375
+ content: [{ type: "text", text: outputText }],
16228
16376
  structuredContent: toStructured(result)
16229
16377
  };
16230
16378
  }
@@ -16688,7 +16836,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16688
16836
  "integration",
16689
16837
  {
16690
16838
  title: "Integration",
16691
- description: `Integration operations for Slack, GitHub, and Notion. Provider: slack, github, notion, all. Actions: status, search, stats, activity, contributors, knowledge, summary, channels (slack), discussions (slack), repos (github), issues (github), create_page (notion), list_databases (notion), search_pages (notion with smart type detection - filter by event_type, status, priority, has_due_date, tags), get_page (notion), query_database (notion), update_page (notion).`,
16839
+ description: `Integration operations for Slack, GitHub, and Notion. Provider: slack, github, notion, all. Actions: status, search, stats, activity, contributors, knowledge, summary, channels (slack), discussions (slack), repos (github), issues (github), create_page (notion), create_database (notion), list_databases (notion), search_pages (notion with smart type detection - filter by event_type, status, priority, has_due_date, tags), get_page (notion), query_database (notion), update_page (notion).`,
16692
16840
  inputSchema: external_exports.object({
16693
16841
  provider: external_exports.enum(["slack", "github", "notion", "all"]).describe("Integration provider"),
16694
16842
  action: external_exports.enum([
@@ -16706,6 +16854,7 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16706
16854
  "issues",
16707
16855
  // Notion-specific actions
16708
16856
  "create_page",
16857
+ "create_database",
16709
16858
  "list_databases",
16710
16859
  "search_pages",
16711
16860
  "get_page",
@@ -16719,10 +16868,11 @@ Output formats: full (default, includes content), paths (file paths only - 80% t
16719
16868
  since: external_exports.string().optional(),
16720
16869
  until: external_exports.string().optional(),
16721
16870
  // Notion-specific parameters
16722
- title: external_exports.string().optional().describe("Page title (for Notion create_page/update_page)"),
16871
+ title: external_exports.string().optional().describe("Page/database title (for Notion create_page/update_page/create_database)"),
16723
16872
  content: external_exports.string().optional().describe("Page content in Markdown (for Notion create_page/update_page)"),
16873
+ description: external_exports.string().optional().describe("Database description (for Notion create_database)"),
16724
16874
  parent_database_id: external_exports.string().optional().describe("Parent database ID (for Notion create_page)"),
16725
- parent_page_id: external_exports.string().optional().describe("Parent page ID (for Notion create_page)"),
16875
+ parent_page_id: external_exports.string().optional().describe("Parent page ID (for Notion create_page/create_database)"),
16726
16876
  page_id: external_exports.string().optional().describe("Page ID (for Notion get_page/update_page)"),
16727
16877
  database_id: external_exports.string().optional().describe("Database ID (for Notion query_database/search_pages/activity)"),
16728
16878
  days: external_exports.number().optional().describe("Number of days for stats/summary (default: 7)"),
@@ -17014,6 +17164,39 @@ Created: ${result.created_time}`
17014
17164
  structuredContent: toStructured(result)
17015
17165
  };
17016
17166
  }
17167
+ case "create_database": {
17168
+ if (input.provider !== "notion") {
17169
+ return errorResult("create_database is only available for notion provider");
17170
+ }
17171
+ if (!input.title) {
17172
+ return errorResult("title is required for create_database action");
17173
+ }
17174
+ if (!input.parent_page_id) {
17175
+ return errorResult("parent_page_id is required for create_database action");
17176
+ }
17177
+ if (!workspaceId) {
17178
+ return errorResult(
17179
+ "Error: workspace_id is required. Please call session_init first or provide workspace_id explicitly."
17180
+ );
17181
+ }
17182
+ const newDatabase = await client.notionCreateDatabase({
17183
+ workspace_id: workspaceId,
17184
+ title: input.title,
17185
+ parent_page_id: input.parent_page_id,
17186
+ description: input.description
17187
+ });
17188
+ return {
17189
+ content: [
17190
+ {
17191
+ type: "text",
17192
+ text: `Created database "${newDatabase.title}"
17193
+ ID: ${newDatabase.id}
17194
+ URL: ${newDatabase.url}`
17195
+ }
17196
+ ],
17197
+ structuredContent: toStructured(newDatabase)
17198
+ };
17199
+ }
17017
17200
  case "list_databases": {
17018
17201
  if (input.provider !== "notion") {
17019
17202
  return errorResult("list_databases is only available for notion provider");
@@ -19569,10 +19752,10 @@ Code: ${device.user_code}`);
19569
19752
  if (poll && poll.status === "pending") {
19570
19753
  const intervalSeconds = typeof poll.interval === "number" ? poll.interval : 5;
19571
19754
  const waitMs = Math.max(1, intervalSeconds) * 1e3;
19572
- await new Promise((resolve3) => setTimeout(resolve3, waitMs));
19755
+ await new Promise((resolve4) => setTimeout(resolve4, waitMs));
19573
19756
  continue;
19574
19757
  }
19575
- await new Promise((resolve3) => setTimeout(resolve3, 1e3));
19758
+ await new Promise((resolve4) => setTimeout(resolve4, 1e3));
19576
19759
  }
19577
19760
  if (!accessToken) {
19578
19761
  throw new Error(
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.34",
4
+ "version": "0.4.36",
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",
@@ -20,8 +20,8 @@
20
20
  "test-server": "tsx src/test-server.ts",
21
21
  "test-server:start": "node dist/test-server.js",
22
22
  "typecheck": "tsc --noEmit",
23
- "test": "vitest run",
24
- "test:watch": "vitest",
23
+ "test": "vitest run --config vitest.config.cjs",
24
+ "test:watch": "vitest --config vitest.config.cjs",
25
25
  "lint": "eslint src/",
26
26
  "lint:fix": "eslint src/ --fix",
27
27
  "format": "prettier --write src/",