@contextstream/mcp-server 0.3.54 → 0.3.55

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 (3) hide show
  1. package/README.md +6 -0
  2. package/dist/index.js +201 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -28,9 +28,15 @@ No special syntax. No commands to learn. Just ask.
28
28
  - Memory capture and recall (decisions, preferences, tasks, bugs, lessons)
29
29
  - Code search (semantic, hybrid, keyword, pattern)
30
30
  - Knowledge graph and code analysis (dependencies, impact, call paths, circular deps, unused code)
31
+ - Graph ingestion for full graph builds (`graph_ingest`)
31
32
  - Local repo ingestion for indexing (`projects_ingest_local`)
32
33
  - Auto-context: on the first tool call in a new session, the server can auto-initialize context
33
34
 
35
+ ## Graph tiers
36
+
37
+ - **Pro (Graph-Lite):** module-level import graph, dependencies, and 1-hop impact
38
+ - **Elite/Team (Full Graph):** module + call + dataflow + type layers, plus `graph_ingest`
39
+
34
40
  ## Requirements
35
41
 
36
42
  - Node.js 18+
package/dist/index.js CHANGED
@@ -4854,6 +4854,19 @@ function normalizeNodeType(input) {
4854
4854
  );
4855
4855
  }
4856
4856
  }
4857
+ function pickString(value) {
4858
+ if (typeof value !== "string") return null;
4859
+ const trimmed = value.trim();
4860
+ return trimmed ? trimmed : null;
4861
+ }
4862
+ function normalizeGraphTier(value) {
4863
+ const normalized = value.trim().toLowerCase();
4864
+ if (!normalized) return null;
4865
+ if (normalized.includes("full") || normalized.includes("elite") || normalized.includes("team")) return "full";
4866
+ if (normalized.includes("lite") || normalized.includes("light") || normalized.includes("basic") || normalized.includes("module")) return "lite";
4867
+ if (normalized.includes("none") || normalized.includes("off") || normalized.includes("disabled") || normalized.includes("free")) return "none";
4868
+ return null;
4869
+ }
4857
4870
  var AI_PLAN_TIMEOUT_MS = 5e4;
4858
4871
  var AI_PLAN_RETRIES = 0;
4859
4872
  var ContextStreamClient = class {
@@ -4948,6 +4961,26 @@ var ContextStreamClient = class {
4948
4961
  return null;
4949
4962
  }
4950
4963
  }
4964
+ async getGraphTier() {
4965
+ try {
4966
+ const balance = await this.getCreditBalance();
4967
+ const plan = balance?.plan ?? {};
4968
+ const features = plan?.features ?? {};
4969
+ const tierCandidate = pickString(plan.graph_tier) || pickString(plan.graphTier) || pickString(features.graph_tier) || pickString(features.graphTier) || pickString(balance?.graph_tier) || pickString(balance?.graphTier);
4970
+ const normalizedTier = tierCandidate ? normalizeGraphTier(tierCandidate) : null;
4971
+ if (normalizedTier) return normalizedTier;
4972
+ const planName = pickString(plan.name)?.toLowerCase() ?? null;
4973
+ if (!planName) return "none";
4974
+ if (planName.includes("elite") || planName.includes("team") || planName.includes("enterprise") || planName.includes("business")) {
4975
+ return "full";
4976
+ }
4977
+ if (planName.includes("pro")) return "lite";
4978
+ if (planName.includes("free")) return "none";
4979
+ return "lite";
4980
+ } catch {
4981
+ return "none";
4982
+ }
4983
+ }
4951
4984
  // Workspaces & Projects
4952
4985
  listWorkspaces(params) {
4953
4986
  const query = new URLSearchParams();
@@ -7973,6 +8006,35 @@ function toStructured(data) {
7973
8006
  }
7974
8007
  return void 0;
7975
8008
  }
8009
+ function readStatNumber(payload, key) {
8010
+ if (!payload || typeof payload !== "object") return void 0;
8011
+ const direct = payload[key];
8012
+ if (typeof direct === "number") return direct;
8013
+ const nested = payload.data;
8014
+ if (nested && typeof nested === "object") {
8015
+ const nestedValue = nested[key];
8016
+ if (typeof nestedValue === "number") return nestedValue;
8017
+ }
8018
+ return void 0;
8019
+ }
8020
+ function estimateGraphIngestMinutes(stats) {
8021
+ const totalFiles = readStatNumber(stats, "total_files");
8022
+ const totalLines = readStatNumber(stats, "total_lines");
8023
+ if (!totalFiles && !totalLines) return null;
8024
+ const fileScore = totalFiles ? totalFiles / 1e3 : 0;
8025
+ const lineScore = totalLines ? totalLines / 5e4 : 0;
8026
+ const sizeScore = Math.max(fileScore, lineScore);
8027
+ const minMinutes = Math.min(45, Math.max(1, Math.round(1 + sizeScore * 1.5)));
8028
+ const maxMinutes = Math.min(60, Math.max(minMinutes + 1, Math.round(2 + sizeScore * 3)));
8029
+ const basisParts = [];
8030
+ if (totalFiles) basisParts.push(`${totalFiles.toLocaleString()} files`);
8031
+ if (totalLines) basisParts.push(`${totalLines.toLocaleString()} lines`);
8032
+ return {
8033
+ min: minMinutes,
8034
+ max: maxMinutes,
8035
+ basis: basisParts.length > 0 ? basisParts.join(" / ") : void 0
8036
+ };
8037
+ }
7976
8038
  function normalizeLessonField(value) {
7977
8039
  return value.trim().toLowerCase().replace(/\s+/g, " ");
7978
8040
  }
@@ -8047,6 +8109,9 @@ function registerTools(server, client, sessionManager) {
8047
8109
  return proTools.has(toolName) ? "pro" : "free";
8048
8110
  }
8049
8111
  function getToolAccessLabel(toolName) {
8112
+ const graphTier = graphToolTiers.get(toolName);
8113
+ if (graphTier === "lite") return "Pro (Graph-Lite)";
8114
+ if (graphTier === "full") return "Elite/Team (Full Graph)";
8050
8115
  return getToolAccessTier(toolName) === "pro" ? "PRO" : "Free";
8051
8116
  }
8052
8117
  async function gateIfProTool(toolName) {
@@ -8060,6 +8125,94 @@ function registerTools(server, client, sessionManager) {
8060
8125
  ].join("\n")
8061
8126
  );
8062
8127
  }
8128
+ const graphToolTiers = /* @__PURE__ */ new Map([
8129
+ ["graph_dependencies", "lite"],
8130
+ ["graph_impact", "lite"],
8131
+ ["graph_related", "full"],
8132
+ ["graph_decisions", "full"],
8133
+ ["graph_path", "full"],
8134
+ ["graph_call_path", "full"],
8135
+ ["graph_circular_dependencies", "full"],
8136
+ ["graph_unused_code", "full"],
8137
+ ["graph_ingest", "full"],
8138
+ ["graph_contradictions", "full"]
8139
+ ]);
8140
+ const graphLiteMaxDepth = 1;
8141
+ function normalizeGraphTargetType(value) {
8142
+ return String(value ?? "").trim().toLowerCase();
8143
+ }
8144
+ function isModuleTargetType(value) {
8145
+ return value === "module" || value === "file" || value === "path";
8146
+ }
8147
+ function graphLiteConstraintError(toolName, detail) {
8148
+ return errorResult(
8149
+ [
8150
+ `Access denied: \`${toolName}\` is limited to Graph-Lite (module-level, 1-hop queries).`,
8151
+ detail,
8152
+ `Upgrade to Elite or Team for full graph access: ${upgradeUrl}`
8153
+ ].join("\n")
8154
+ );
8155
+ }
8156
+ async function gateIfGraphTool(toolName, input) {
8157
+ const requiredTier = graphToolTiers.get(toolName);
8158
+ if (!requiredTier) return null;
8159
+ const graphTier = await client.getGraphTier();
8160
+ if (graphTier === "full") return null;
8161
+ if (graphTier === "lite") {
8162
+ if (requiredTier === "full") {
8163
+ return errorResult(
8164
+ [
8165
+ `Access denied: \`${toolName}\` requires Elite or Team (Full Graph).`,
8166
+ "Pro includes Graph-Lite (module-level dependencies and 1-hop impact only).",
8167
+ `Upgrade: ${upgradeUrl}`
8168
+ ].join("\n")
8169
+ );
8170
+ }
8171
+ if (toolName === "graph_dependencies") {
8172
+ const targetType = normalizeGraphTargetType(input?.target?.type);
8173
+ if (!isModuleTargetType(targetType)) {
8174
+ return graphLiteConstraintError(
8175
+ toolName,
8176
+ "Set target.type to module, file, or path."
8177
+ );
8178
+ }
8179
+ if (typeof input?.max_depth === "number" && input.max_depth > graphLiteMaxDepth) {
8180
+ return graphLiteConstraintError(
8181
+ toolName,
8182
+ `Set max_depth to ${graphLiteMaxDepth} or lower.`
8183
+ );
8184
+ }
8185
+ if (input?.include_transitive === true) {
8186
+ return graphLiteConstraintError(
8187
+ toolName,
8188
+ "Set include_transitive to false."
8189
+ );
8190
+ }
8191
+ }
8192
+ if (toolName === "graph_impact") {
8193
+ const targetType = normalizeGraphTargetType(input?.target?.type);
8194
+ if (!isModuleTargetType(targetType)) {
8195
+ return graphLiteConstraintError(
8196
+ toolName,
8197
+ "Set target.type to module, file, or path."
8198
+ );
8199
+ }
8200
+ if (typeof input?.max_depth === "number" && input.max_depth > graphLiteMaxDepth) {
8201
+ return graphLiteConstraintError(
8202
+ toolName,
8203
+ `Set max_depth to ${graphLiteMaxDepth} or lower.`
8204
+ );
8205
+ }
8206
+ }
8207
+ return null;
8208
+ }
8209
+ return errorResult(
8210
+ [
8211
+ `Access denied: \`${toolName}\` requires ContextStream Pro (Graph-Lite) or Elite/Team (Full Graph).`,
8212
+ `Upgrade: ${upgradeUrl}`
8213
+ ].join("\n")
8214
+ );
8215
+ }
8063
8216
  function wrapWithAutoContext(toolName, handler) {
8064
8217
  if (!sessionManager) {
8065
8218
  return handler;
@@ -8092,12 +8245,13 @@ function registerTools(server, client, sessionManager) {
8092
8245
  return;
8093
8246
  }
8094
8247
  const accessLabel = getToolAccessLabel(name);
8248
+ const showUpgrade = accessLabel !== "Free";
8095
8249
  const labeledConfig = {
8096
8250
  ...config,
8097
8251
  title: `${config.title} (${accessLabel})`,
8098
8252
  description: `${config.description}
8099
8253
 
8100
- Access: ${accessLabel}${accessLabel === "PRO" ? ` (upgrade: ${upgradeUrl})` : ""}`
8254
+ Access: ${accessLabel}${showUpgrade ? ` (upgrade: ${upgradeUrl})` : ""}`
8101
8255
  };
8102
8256
  const safeHandler = async (input) => {
8103
8257
  try {
@@ -8579,6 +8733,8 @@ Access: Free`,
8579
8733
  })
8580
8734
  },
8581
8735
  async (input) => {
8736
+ const gate = await gateIfGraphTool("graph_related", input);
8737
+ if (gate) return gate;
8582
8738
  const result = await client.graphRelated(input);
8583
8739
  return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8584
8740
  }
@@ -8596,6 +8752,8 @@ Access: Free`,
8596
8752
  })
8597
8753
  },
8598
8754
  async (input) => {
8755
+ const gate = await gateIfGraphTool("graph_path", input);
8756
+ if (gate) return gate;
8599
8757
  const result = await client.graphPath(input);
8600
8758
  return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8601
8759
  }
@@ -8612,6 +8770,8 @@ Access: Free`,
8612
8770
  })
8613
8771
  },
8614
8772
  async (input) => {
8773
+ const gate = await gateIfGraphTool("graph_decisions", input);
8774
+ if (gate) return gate;
8615
8775
  const result = await client.graphDecisions(input);
8616
8776
  return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8617
8777
  }
@@ -8620,9 +8780,7 @@ Access: Free`,
8620
8780
  "graph_dependencies",
8621
8781
  {
8622
8782
  title: "Code dependencies",
8623
- description: `Dependency graph query
8624
-
8625
- Access: Free`,
8783
+ description: "Dependency graph query",
8626
8784
  inputSchema: external_exports.object({
8627
8785
  target: external_exports.object({
8628
8786
  type: external_exports.string().describe("Code element type. Accepted values: module (aliases: file, path), function (alias: method), type (aliases: struct, enum, trait, class), variable (aliases: data, const, constant). For knowledge/memory nodes, use graph_path with UUID ids instead."),
@@ -8633,6 +8791,8 @@ Access: Free`,
8633
8791
  })
8634
8792
  },
8635
8793
  async (input) => {
8794
+ const gate = await gateIfGraphTool("graph_dependencies", input);
8795
+ if (gate) return gate;
8636
8796
  const result = await client.graphDependencies(input);
8637
8797
  return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8638
8798
  }
@@ -8641,9 +8801,7 @@ Access: Free`,
8641
8801
  "graph_call_path",
8642
8802
  {
8643
8803
  title: "Call path",
8644
- description: `Find call path between two targets
8645
-
8646
- Access: Free`,
8804
+ description: "Find call path between two targets",
8647
8805
  inputSchema: external_exports.object({
8648
8806
  source: external_exports.object({
8649
8807
  type: external_exports.string().describe('Must be "function" (alias: method). Only function types are supported for call path analysis. For knowledge/memory nodes, use graph_path with UUID ids instead.'),
@@ -8657,6 +8815,8 @@ Access: Free`,
8657
8815
  })
8658
8816
  },
8659
8817
  async (input) => {
8818
+ const gate = await gateIfGraphTool("graph_call_path", input);
8819
+ if (gate) return gate;
8660
8820
  const result = await client.graphCallPath(input);
8661
8821
  return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8662
8822
  }
@@ -8665,9 +8825,7 @@ Access: Free`,
8665
8825
  "graph_impact",
8666
8826
  {
8667
8827
  title: "Impact analysis",
8668
- description: `Analyze impact of a target node
8669
-
8670
- Access: Free`,
8828
+ description: "Analyze impact of a target node",
8671
8829
  inputSchema: external_exports.object({
8672
8830
  target: external_exports.object({
8673
8831
  type: external_exports.string().describe("Code element type. Accepted values: module (aliases: file, path), function (alias: method), type (aliases: struct, enum, trait, class), variable (aliases: data, const, constant). For knowledge/memory nodes, use graph_path with UUID ids instead."),
@@ -8677,6 +8835,8 @@ Access: Free`,
8677
8835
  })
8678
8836
  },
8679
8837
  async (input) => {
8838
+ const gate = await gateIfGraphTool("graph_impact", input);
8839
+ if (gate) return gate;
8680
8840
  const result = await client.graphImpact(input);
8681
8841
  return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8682
8842
  }
@@ -8685,19 +8845,40 @@ Access: Free`,
8685
8845
  "graph_ingest",
8686
8846
  {
8687
8847
  title: "Ingest code graph",
8688
- description: "Build and persist the dependency graph for a project",
8848
+ description: "Build and persist the dependency graph for a project. Runs async by default (wait=false) and can take a few minutes for larger repos.",
8689
8849
  inputSchema: external_exports.object({
8690
8850
  project_id: external_exports.string().uuid().optional(),
8691
- wait: external_exports.boolean().optional().describe("If true, wait for ingestion to finish before returning.")
8851
+ wait: external_exports.boolean().optional().describe("If true, wait for ingestion to finish before returning. Defaults to false (async).")
8692
8852
  })
8693
8853
  },
8694
8854
  async (input) => {
8855
+ const gate = await gateIfGraphTool("graph_ingest", input);
8856
+ if (gate) return gate;
8695
8857
  const projectId = resolveProjectId(input.project_id);
8696
8858
  if (!projectId) {
8697
8859
  return errorResult("Error: project_id is required. Please call session_init first or provide project_id explicitly.");
8698
8860
  }
8699
- const result = await client.graphIngest({ project_id: projectId, wait: input.wait });
8700
- return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
8861
+ const wait = input.wait ?? false;
8862
+ let estimate = null;
8863
+ try {
8864
+ const stats = await client.projectStatistics(projectId);
8865
+ estimate = estimateGraphIngestMinutes(stats);
8866
+ } catch (error) {
8867
+ console.error("[ContextStream] Failed to fetch project statistics for graph ingest estimate:", error);
8868
+ }
8869
+ const result = await client.graphIngest({ project_id: projectId, wait });
8870
+ const estimateText = estimate ? `Estimated time: ${estimate.min}-${estimate.max} min${estimate.basis ? ` (based on ${estimate.basis})` : ""}.` : "Estimated time varies with repo size.";
8871
+ const note = `Graph ingestion is running ${wait ? "synchronously" : "asynchronously"} and can take a few minutes. ${estimateText}`;
8872
+ const structured = toStructured(result);
8873
+ const structuredContent = structured && typeof structured === "object" ? { ...structured, wait, note, ...estimate ? { estimate_minutes: { min: estimate.min, max: estimate.max }, estimate_basis: estimate.basis } : {} } : { wait, note, ...estimate ? { estimate_minutes: { min: estimate.min, max: estimate.max }, estimate_basis: estimate.basis } : {} };
8874
+ return {
8875
+ content: [{
8876
+ type: "text",
8877
+ text: `${note}
8878
+ ${formatContent(result)}`
8879
+ }],
8880
+ structuredContent
8881
+ };
8701
8882
  }
8702
8883
  );
8703
8884
  registerTool(
@@ -9148,6 +9329,8 @@ Automatically detects code files and skips ignored directories like node_modules
9148
9329
  inputSchema: external_exports.object({ project_id: external_exports.string().uuid().optional() })
9149
9330
  },
9150
9331
  async (input) => {
9332
+ const gate = await gateIfGraphTool("graph_circular_dependencies", input);
9333
+ if (gate) return gate;
9151
9334
  const projectId = resolveProjectId(input.project_id);
9152
9335
  if (!projectId) {
9153
9336
  return errorResult("Error: project_id is required. Please call session_init first or provide project_id explicitly.");
@@ -9164,6 +9347,8 @@ Automatically detects code files and skips ignored directories like node_modules
9164
9347
  inputSchema: external_exports.object({ project_id: external_exports.string().uuid().optional() })
9165
9348
  },
9166
9349
  async (input) => {
9350
+ const gate = await gateIfGraphTool("graph_unused_code", input);
9351
+ if (gate) return gate;
9167
9352
  const projectId = resolveProjectId(input.project_id);
9168
9353
  if (!projectId) {
9169
9354
  return errorResult("Error: project_id is required. Please call session_init first or provide project_id explicitly.");
@@ -9180,6 +9365,8 @@ Automatically detects code files and skips ignored directories like node_modules
9180
9365
  inputSchema: external_exports.object({ node_id: external_exports.string().uuid() })
9181
9366
  },
9182
9367
  async (input) => {
9368
+ const gate = await gateIfGraphTool("graph_contradictions", input);
9369
+ if (gate) return gate;
9183
9370
  const result = await client.findContradictions(input.node_id);
9184
9371
  return { content: [{ type: "text", text: formatContent(result) }], structuredContent: toStructured(result) };
9185
9372
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextstream/mcp-server",
3
- "version": "0.3.54",
3
+ "version": "0.3.55",
4
4
  "description": "MCP server exposing ContextStream public API - code context, memory, search, and AI tools for developers",
5
5
  "type": "module",
6
6
  "license": "MIT",