@databrainhq/mcp-server 0.2.16 → 0.2.17

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/dist/index.js CHANGED
@@ -187,6 +187,10 @@ var logger = {
187
187
  // src/version.ts
188
188
  var MCP_SERVER_VERSION = "0.2.10";
189
189
 
190
+ // src/utils/tool-context.ts
191
+ import { AsyncLocalStorage } from "async_hooks";
192
+ var toolContext = new AsyncLocalStorage();
193
+
190
194
  // src/client/databrain-client.ts
191
195
  var MCP_USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION}`;
192
196
  var MAX_RETRIES = 3;
@@ -282,6 +286,13 @@ var DatabrainClient = class {
282
286
  "User-Agent": MCP_USER_AGENT,
283
287
  "X-Databrain-Client": MCP_USER_AGENT
284
288
  };
289
+ const ctx = toolContext.getStore();
290
+ if (ctx) {
291
+ headers["X-Databrain-MCP-Tool"] = ctx.tool;
292
+ headers["X-Databrain-MCP-Tool-Category"] = ctx.category;
293
+ headers["X-Databrain-MCP-Workflow-Intent"] = ctx.workflowIntent;
294
+ headers["X-Databrain-MCP-Input-Mode"] = ctx.inputMode;
295
+ }
285
296
  if (token) {
286
297
  if (options.auth === "none") {
287
298
  headers["x-plugin-token"] = token;
@@ -356,6 +367,68 @@ var DatabrainClient = class {
356
367
  }
357
368
  };
358
369
 
370
+ // src/tools/tool-metadata.ts
371
+ var DEFAULT_TOOL_META = {
372
+ category: "discovery",
373
+ workflowIntent: "other"
374
+ };
375
+ var TOOL_METADATA = {
376
+ // Identity / discovery
377
+ whoami: { category: "discovery", workflowIntent: "discovery" },
378
+ // Infrastructure / data setup
379
+ list_datasources: { category: "infrastructure", workflowIntent: "setup_data" },
380
+ discover_datasource_schema: { category: "infrastructure", workflowIntent: "setup_data" },
381
+ list_datamarts: { category: "infrastructure", workflowIntent: "setup_data" },
382
+ list_workspaces: { category: "infrastructure", workflowIntent: "setup_data" },
383
+ sync_datasource: { category: "infrastructure", workflowIntent: "setup_data" },
384
+ create_datamart: { category: "infrastructure", workflowIntent: "setup_data" },
385
+ // Embedding — data apps & tokens
386
+ list_data_apps: { category: "embedding", workflowIntent: "embed_dashboard" },
387
+ list_tenants: { category: "embedding", workflowIntent: "embed_dashboard" },
388
+ create_data_app: { category: "embedding", workflowIntent: "embed_dashboard" },
389
+ list_api_tokens: { category: "embedding", workflowIntent: "embed_dashboard" },
390
+ create_api_token: { category: "embedding", workflowIntent: "embed_dashboard" },
391
+ rotate_api_token: { category: "embedding", workflowIntent: "embed_dashboard" },
392
+ // Embedding — embed management
393
+ create_embed: { category: "embedding", workflowIntent: "embed_dashboard" },
394
+ list_embeds: { category: "embedding", workflowIntent: "embed_dashboard" },
395
+ get_embed_details: { category: "embedding", workflowIntent: "embed_dashboard" },
396
+ update_embed: { category: "embedding", workflowIntent: "embed_dashboard" },
397
+ customize_embed_theme: { category: "embedding", workflowIntent: "embed_dashboard" },
398
+ get_demo_link: { category: "embedding", workflowIntent: "embed_dashboard" },
399
+ // Embedding — frontend
400
+ generate_guest_token: { category: "embedding", workflowIntent: "embed_dashboard" },
401
+ generate_embed_code: { category: "embedding", workflowIntent: "embed_dashboard" },
402
+ // Embedding — customer dashboards & widgets
403
+ list_dashboards: { category: "embedding", workflowIntent: "embedded_widget" },
404
+ list_widgets: { category: "embedding", workflowIntent: "embedded_widget" },
405
+ add_widget: { category: "embedding", workflowIntent: "embedded_widget" },
406
+ update_widget: { category: "embedding", workflowIntent: "embedded_widget" },
407
+ remove_widget: { category: "embedding", workflowIntent: "embedded_widget" },
408
+ generate_widget: { category: "embedding", workflowIntent: "embedded_widget" },
409
+ validate_metric_spec: { category: "embedding", workflowIntent: "sql_to_embedded_metric" },
410
+ create_or_update_metric_from_query: {
411
+ category: "embedding",
412
+ workflowIntent: "sql_to_embedded_metric"
413
+ },
414
+ // Internal analytics
415
+ create_workspace_metric: {
416
+ category: "internal_analytics",
417
+ workflowIntent: "sql_to_workspace_metric"
418
+ },
419
+ ask_question: { category: "internal_analytics", workflowIntent: "query_data" },
420
+ // Semantic layer
421
+ get_semantic_layer: { category: "semantic_layer", workflowIntent: "query_data" },
422
+ update_semantic_layer: { category: "semantic_layer", workflowIntent: "query_data" },
423
+ start_semantic_layer_generation: { category: "semantic_layer", workflowIntent: "query_data" },
424
+ get_semantic_layer_generation_status: {
425
+ category: "semantic_layer",
426
+ workflowIntent: "query_data"
427
+ },
428
+ // Reports
429
+ list_scheduled_reports: { category: "reports", workflowIntent: "reports" }
430
+ };
431
+
359
432
  // src/tools/data-app/list-data-apps.ts
360
433
  import { z as z2 } from "zod";
361
434
 
@@ -5868,7 +5941,7 @@ function registerCreateOrUpdateMetricFromQuery(server, client) {
5868
5941
  const services = createServiceBundle(client);
5869
5942
  server.tool(
5870
5943
  "create_or_update_metric_from_query",
5871
- "Create or update a production metric/widget from validated SQL. Defaults to dry-run; apply requires explicit confirmation.",
5944
+ "[EMBEDDING] Create or update a production metric/widget on a customer dashboard from validated SQL. Requires clientId (multi-tenant embed). Defaults to dry-run; apply requires explicit confirmation.",
5872
5945
  {
5873
5946
  mode: modeSchema.describe("dry_run returns the plan only; apply executes after confirmation"),
5874
5947
  confirm: z30.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
@@ -6051,21 +6124,25 @@ var modeSchema2 = z31.enum(["dry_run", "apply"]).default("dry_run");
6051
6124
  function registerCreateWorkspaceMetric(server, client) {
6052
6125
  server.tool(
6053
6126
  "create_workspace_metric",
6054
- "Create a workspace-level metric from validated SQL using the org service token. Scoped to the workspace, unpublished, and NOT attached to any dashboard. Defaults to dry-run; apply requires explicit confirmation.",
6127
+ "[INTERNAL ANALYTICS] Create or update a workspace-level metric from validated SQL (migrating your SQL into Databrain), not embedding. Pass an existing metricId to update in place \u2014 query and Dimensions/Measures re-hydrated, publish state preserved; omit to create. Workspace-scoped, unattached. Uses the org service token. Defaults to dry-run; apply needs confirmation.",
6055
6128
  {
6056
6129
  mode: modeSchema2.describe(
6057
- "dry_run validates the SQL only (no write); apply creates the metric after confirmation"
6130
+ "dry_run validates the SQL only (no write); apply creates or updates the metric after confirmation"
6058
6131
  ),
6059
6132
  confirm: z31.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
6060
6133
  workspaceName: z31.string().describe("Target workspace name"),
6061
6134
  name: z31.string().describe("Metric display name"),
6062
6135
  query: z31.string().describe("SQL query to validate and store"),
6063
- metricId: z31.string().optional().describe("Stable metric slug; generated from the name if omitted"),
6136
+ metricId: z31.string().optional().describe(
6137
+ "Stable metric slug. Pass an existing slug to UPDATE that metric in place. Omit to create \u2014 the slug is derived deterministically from the name (no random/uniqueness suffix), so re-running with the same name upserts the same metric rather than creating a duplicate."
6138
+ ),
6064
6139
  description: z31.string().optional().describe("Metric description"),
6065
6140
  datasourceId: z31.string().optional().describe("Datasource UUID override; defaults to the workspace datasource"),
6066
- isPublished: z31.boolean().optional().describe("Publish after apply. Defaults to false for safety")
6141
+ isPublished: z31.boolean().optional().describe(
6142
+ "Publish after apply. Defaults to false. Applies to creates; on updates the existing publish state is preserved."
6143
+ )
6067
6144
  },
6068
- WRITE_CREATE,
6145
+ WRITE_IDEMPOTENT,
6069
6146
  async (params) => {
6070
6147
  try {
6071
6148
  const mode = params.mode || "dry_run";
@@ -6102,7 +6179,7 @@ function registerCreateWorkspaceMetric(server, client) {
6102
6179
  mode,
6103
6180
  safety: productionSafetyNote(params.workspaceName),
6104
6181
  result,
6105
- nextStep: isApply ? "Metric created unpublished and unattached. Publish/attach it to a dashboard as a separate step when ready." : 'Dry-run only \u2014 no metric written. Re-run with mode "apply" and confirm to create it.'
6182
+ nextStep: isApply ? "Saved. If the metricId already existed it was UPDATED in place (query and Dimensions/Measures re-derived; publish state preserved); otherwise a new metric was CREATED, unpublished and unattached. Publish/attach it to a dashboard as a separate step when ready." : 'Dry-run only \u2014 no metric written; it validates the SQL and returns the output columns. Re-run with mode "apply" and confirm to create or update the metric.'
6106
6183
  },
6107
6184
  null,
6108
6185
  2
@@ -6341,7 +6418,30 @@ function registerWhoami(server, client) {
6341
6418
  }
6342
6419
 
6343
6420
  // src/tools/registry.ts
6421
+ function instrumentToolRegistration(server) {
6422
+ const original = server.tool.bind(server);
6423
+ server.tool = (...args) => {
6424
+ const name = args[0];
6425
+ const meta = TOOL_METADATA[name] ?? DEFAULT_TOOL_META;
6426
+ const cbIndex = args.length - 1;
6427
+ const cb = args[cbIndex];
6428
+ if (typeof cb === "function") {
6429
+ const handler = cb;
6430
+ args[cbIndex] = (...handlerArgs) => toolContext.run(
6431
+ {
6432
+ tool: name,
6433
+ category: meta.category,
6434
+ workflowIntent: meta.workflowIntent,
6435
+ inputMode: "tool_call"
6436
+ },
6437
+ () => handler(...handlerArgs)
6438
+ );
6439
+ }
6440
+ return original(...args);
6441
+ };
6442
+ }
6344
6443
  function registerAllTools(server, client) {
6444
+ instrumentToolRegistration(server);
6345
6445
  registerWhoami(server, client);
6346
6446
  registerListDatasources(server, client);
6347
6447
  registerDiscoverDatasourceSchema(server, client);
@@ -9565,6 +9665,67 @@ list_scheduled_reports({ clientId, dashboardName, workspaceName })
9565
9665
  \`\`\`
9566
9666
  Clarify that this lists configured schedules only. Delivery success/failure history is not yet persisted in the MCP/API surface.`;
9567
9667
 
9668
+ // src/prompts/sql-to-workspace-metric.ts
9669
+ var sqlToWorkspaceMetricPrompt = `You are helping turn validated warehouse SQL into an INTERNAL Databrain metric \u2014 a workspace-level metric for the team's own analytics. This is NOT embedding: there is no customer, no clientId, and the metric is not attached to any dashboard.
9670
+
9671
+ Use this flow when the user says things like "I have this SQL, make it a metric in Databrain", "migrate my query", "add an unpublished metric", or "update/fix the SQL on an existing workspace metric" \u2014 and they are not setting up a customer-facing embed. If the goal is a widget on a customer's embedded dashboard, use the sql-to-metric-migration prompt instead (that one requires a clientId).
9672
+
9673
+ **Why this path**
9674
+ - \`create_workspace_metric\` is workspace-scoped, unpublished, and uses the org service token. No clientId.
9675
+ - It upserts on \`metricId\`: pass an existing metric's \`metricId\` to UPDATE it in place (the query and its Dimensions/Measures are re-derived; archived/draft and publish state are preserved); omit \`metricId\` to CREATE a new metric.
9676
+ - The metric is intentionally NOT attached to a dashboard \u2014 attaching/publishing is a separate step the user does later.
9677
+
9678
+ **Critical safety rules**
9679
+ - Default to dry-run. Never apply on the first pass.
9680
+ - Applying requires the exact confirmation string: \`APPLY_TO_PRODUCTION\`.
9681
+ - Service-token mode means writes are attributed as system until OAuth ships. State that clearly before apply.
9682
+ - Keep \`isPublished: false\` unless the user explicitly asks to publish.
9683
+
9684
+ **Minimum inputs**
9685
+ - \`workspaceName\`
9686
+ - \`name\`
9687
+ - \`query\`
9688
+ - Optional: \`description\`, \`datasourceId\` (defaults to the workspace datasource), \`metricId\` (a stable slug). **Omit \`metricId\` to create** (it's generated from the name). **To UPDATE an existing metric, pass its exact \`metricId\`** \u2014 carry that same value through BOTH the dry-run and apply calls, or the update won't target the existing metric. If the user is editing a metric and you don't know its \`metricId\`, ask them for the exact slug it was created with (there is no workspace-metric list tool).
9689
+
9690
+ **Step 1 \u2014 Confirm the workspace and datasource**
9691
+ Call:
9692
+ \`\`\`
9693
+ list_workspaces
9694
+ discover_datasource_schema({ datasourceName })
9695
+ \`\`\`
9696
+ Confirm the target workspace and that the columns referenced by the SQL exist. This avoids the most common validation failure.
9697
+
9698
+ **Step 2 \u2014 Dry-run (validate the SQL, no write)**
9699
+ Call:
9700
+ \`\`\`
9701
+ create_workspace_metric({
9702
+ mode: "dry_run",
9703
+ workspaceName,
9704
+ name,
9705
+ query,
9706
+ description,
9707
+ metricId, // include the existing metricId when UPDATING; omit to create
9708
+ isPublished: false
9709
+ })
9710
+ \`\`\`
9711
+ The dry-run validates the SQL against the workspace datasource and returns the output columns \u2014 it does NOT report whether apply will create or update. Show the result and the output columns. Stop and ask the user to review.
9712
+
9713
+ **Step 3 \u2014 Apply only after explicit confirmation**
9714
+ Only after the user approves, call:
9715
+ \`\`\`
9716
+ create_workspace_metric({
9717
+ mode: "apply",
9718
+ confirm: "APPLY_TO_PRODUCTION",
9719
+ workspaceName,
9720
+ name,
9721
+ query,
9722
+ description,
9723
+ metricId, // pass the SAME metricId used in the dry-run when UPDATING
9724
+ isPublished: false
9725
+ })
9726
+ \`\`\`
9727
+ Never invent the confirmation. Pass the same \`metricId\` you used in the dry-run so an update targets the intended metric. On create, the metric is unpublished and unattached; on update, the existing metric is rewritten in place. Either way its Dimensions/Measures are (re-)hydrated from the query, and archived/draft + publish state are preserved on update; tell the user they can publish/attach it to a dashboard as a separate step when ready.`;
9728
+
9568
9729
  // src/prompts/registry.ts
9569
9730
  var prompts = [
9570
9731
  {
@@ -9601,6 +9762,11 @@ var prompts = [
9601
9762
  name: "sql-to-metric-migration",
9602
9763
  description: "Production-safe SQL-to-metric migration workflow: validate, dry-run, then apply with explicit confirmation.",
9603
9764
  content: sqlToMetricMigrationPrompt
9765
+ },
9766
+ {
9767
+ name: "sql-to-workspace-metric",
9768
+ description: "Internal analytics: turn validated SQL into an unpublished workspace metric (no clientId, not embedding). Validate, dry-run, then apply with confirmation.",
9769
+ content: sqlToWorkspaceMetricPrompt
9604
9770
  }
9605
9771
  ];
9606
9772
  function registerAllPrompts(server) {
@@ -9637,18 +9803,23 @@ function createServer(config) {
9637
9803
  {
9638
9804
  name: "databrain",
9639
9805
  version: MCP_SERVER_VERSION,
9640
- description: "Databrain MCP Server \u2014 embed analytics dashboards and charts into your app. Supports embed setup, semantic-layer maintenance, production dry-run/apply metric creation, theming, filters, permissions, and guest token generation."
9806
+ description: "Databrain MCP Server \u2014 build customer-facing embedded analytics AND run internal analytics on your own data. Supports embed setup, semantic-layer maintenance, natural-language querying, SQL\u2192metric creation (embedded production metrics and unpublished internal workspace metrics), theming, filters, permissions, and guest token generation."
9641
9807
  },
9642
9808
  {
9643
9809
  instructions: operatorBlurb + `Databrain uses two token types \u2014 never confuse them:
9644
9810
  \u2022 SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, sync_datasource, list_datamarts, create_datamart, list_workspaces, list_data_apps, list_tenants, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status. ${serviceTokenStatus}
9645
9811
  \u2022 API TOKEN (per data app): scoped to one data app, used for embedding operations (list_embeds, get_embed_details, create_embed, update_embed, generate_guest_token), data-app scoped dashboard listing, widget CRUD/metric creation through guest-token APIs, and list_scheduled_reports. ${apiTokenStatus}
9646
9812
 
9647
- 4 core use cases \u2014 match the user's scenario to the right prompt:
9813
+ FIRST classify the request before calling tools \u2014 this avoids wasted turns:
9814
+ \u2022 (A) EMBEDDING: putting Databrain dashboards/charts in front of YOUR customers (data apps, embeds, guest tokens, per-customer dashboard widgets). Needs an API token and usually a clientId.
9815
+ \u2022 (B) INTERNAL ANALYTICS: exploring your own data or migrating SQL into your own Databrain metrics for internal use. Workspace-scoped, uses the service token, NO clientId. Creating an unpublished workspace metric (create_workspace_metric) lives here \u2014 it is NOT embedding.
9816
+
9817
+ 5 core use cases \u2014 match the user's scenario to the right prompt:
9648
9818
  1. Embed existing dashboard \u2192 prompt: embed-existing-dashboard (list_data_apps \u2192 create_api_token \u2192 list_dashboards \u2192 create_embed \u2192 generate_guest_token \u2192 generate_embed_code)
9649
9819
  2. Blank dashboard per client \u2192 prompt: embed-blank-dashboard (list_data_apps \u2192 list_datamarts \u2192 create_embed with clientId per client \u2192 generate_guest_token \u2192 generate_embed_code)
9650
9820
  3. Explore & query data \u2192 prompt: query-data (list_datamarts \u2192 get_semantic_layer \u2192 if empty, auto-generate descriptions with update_semantic_layer or use populate-semantic-layer prompt \u2192 ask_question)
9651
- 4. SQL-to-metric migration \u2192 prompt: sql-to-metric-migration (validate_metric_spec \u2192 create_or_update_metric_from_query dry_run \u2192 apply only with explicit production confirmation)
9821
+ 4. SQL \u2192 embedded production metric on a customer dashboard (EMBEDDING, requires clientId) \u2192 prompt: sql-to-metric-migration (validate_metric_spec \u2192 create_or_update_metric_from_query dry_run \u2192 apply only with explicit production confirmation)
9822
+ 5. SQL \u2192 unpublished workspace metric (INTERNAL ANALYTICS, no clientId, not attached to any dashboard) \u2192 prompt: sql-to-workspace-metric (discover_datasource_schema \u2192 create_workspace_metric dry_run \u2192 apply with explicit confirmation)
9652
9823
 
9653
9824
  Production safety: OAuth may not be available yet. In service-token mode, writes are actor=system. For create_datamart and create_or_update_metric_from_query, dry_run is the default. Apply requires confirm="APPLY_TO_PRODUCTION" after the user reviews the plan. Widget validation and mutation tools must verify the resolved embed is tenant-scoped for the requested clientId; if not, they must refuse to touch it as a likely production/data-app dashboard. Do not apply from a vague request.
9654
9825