@databrainhq/mcp-server 0.2.15 → 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;
@@ -326,7 +337,7 @@ var DatabrainClient = class {
326
337
  }
327
338
  const text = await response.text();
328
339
  let data = text ? JSON.parse(text) : {};
329
- if (data && typeof data === "object" && !Array.isArray(data) && "data" in data) {
340
+ if (!options.skipDataUnwrap && data && typeof data === "object" && !Array.isArray(data) && "data" in data) {
330
341
  const inner = data.data;
331
342
  if (inner !== null && inner !== void 0) {
332
343
  data = inner;
@@ -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
 
@@ -735,9 +808,10 @@ var TenantsApi = class {
735
808
  }
736
809
  return this.client.request({
737
810
  method: "POST",
738
- path: "/hasuraActions/getClientData",
811
+ path: "/api/mcp/getClientData",
739
812
  auth: "service",
740
813
  body: { input: { input } },
814
+ skipDataUnwrap: true,
741
815
  cacheKey: `list_tenants:${JSON.stringify(input)}`,
742
816
  cacheTtlMs: 5 * 60 * 1e3
743
817
  });
@@ -5867,7 +5941,7 @@ function registerCreateOrUpdateMetricFromQuery(server, client) {
5867
5941
  const services = createServiceBundle(client);
5868
5942
  server.tool(
5869
5943
  "create_or_update_metric_from_query",
5870
- "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.",
5871
5945
  {
5872
5946
  mode: modeSchema.describe("dry_run returns the plan only; apply executes after confirmation"),
5873
5947
  confirm: z30.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
@@ -6044,26 +6118,101 @@ function registerCreateOrUpdateMetricFromQuery(server, client) {
6044
6118
  );
6045
6119
  }
6046
6120
 
6047
- // src/tools/widgets/validate-metric-spec.ts
6121
+ // src/tools/widgets/create-workspace-metric.ts
6048
6122
  import { z as z31 } from "zod";
6123
+ var modeSchema2 = z31.enum(["dry_run", "apply"]).default("dry_run");
6124
+ function registerCreateWorkspaceMetric(server, client) {
6125
+ server.tool(
6126
+ "create_workspace_metric",
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.",
6128
+ {
6129
+ mode: modeSchema2.describe(
6130
+ "dry_run validates the SQL only (no write); apply creates or updates the metric after confirmation"
6131
+ ),
6132
+ confirm: z31.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
6133
+ workspaceName: z31.string().describe("Target workspace name"),
6134
+ name: z31.string().describe("Metric display name"),
6135
+ query: z31.string().describe("SQL query to validate and store"),
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
+ ),
6139
+ description: z31.string().optional().describe("Metric description"),
6140
+ datasourceId: z31.string().optional().describe("Datasource UUID override; defaults to the workspace datasource"),
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
+ )
6144
+ },
6145
+ WRITE_IDEMPOTENT,
6146
+ async (params) => {
6147
+ try {
6148
+ const mode = params.mode || "dry_run";
6149
+ if (!params.name.trim()) return toolError("`name` cannot be empty.");
6150
+ if (!params.query.trim()) return toolError("`query` cannot be empty.");
6151
+ const isApply = mode === "apply";
6152
+ if (isApply) {
6153
+ const guardError = validateProductionApply({
6154
+ workspaceName: params.workspaceName,
6155
+ confirm: params.confirm
6156
+ });
6157
+ if (guardError) return toolError(guardError);
6158
+ }
6159
+ const body = {
6160
+ workspaceName: params.workspaceName,
6161
+ name: params.name,
6162
+ query: params.query,
6163
+ metricId: params.metricId,
6164
+ description: params.description,
6165
+ datasourceId: params.datasourceId,
6166
+ isPublished: params.isPublished ?? false,
6167
+ validateOnly: !isApply
6168
+ };
6169
+ const result = await client.request({
6170
+ method: "POST",
6171
+ // MCP-only control-plane route (not the public /api/v2 surface).
6172
+ path: "/api/mcp/metrics/create",
6173
+ auth: "service",
6174
+ body
6175
+ });
6176
+ return toolSuccess(
6177
+ JSON.stringify(
6178
+ {
6179
+ mode,
6180
+ safety: productionSafetyNote(params.workspaceName),
6181
+ result,
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.'
6183
+ },
6184
+ null,
6185
+ 2
6186
+ )
6187
+ );
6188
+ } catch (err) {
6189
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
6190
+ return toolError(`Unexpected error: ${err.message}`);
6191
+ }
6192
+ }
6193
+ );
6194
+ }
6195
+
6196
+ // src/tools/widgets/validate-metric-spec.ts
6197
+ import { z as z32 } from "zod";
6049
6198
  function registerValidateMetricSpec(server, client) {
6050
6199
  const services = createServiceBundle(client);
6051
6200
  server.tool(
6052
6201
  "validate_metric_spec",
6053
6202
  "Validate a production metric spec from SQL without saving it. Resolves dashboard, data app, datasource, conflicts, and required fields.",
6054
6203
  {
6055
- clientId: z31.string().describe("Client/tenant identifier used to validate dashboard widget scope"),
6056
- workspaceName: z31.string().describe("Target production-backed workspace name"),
6057
- dashboardName: z31.string().optional().describe("Dashboard name to attach to"),
6058
- embedId: z31.string().optional().describe("Technical embed id alternative to dashboardName"),
6059
- dataAppName: z31.string().optional().describe("Optional data app override"),
6060
- name: z31.string().describe("Metric/widget display name"),
6061
- metricId: z31.string().describe("Stable metric slug to create or update"),
6062
- query: z31.string().describe("SQL query already validated against the warehouse"),
6063
- description: z31.string().optional().describe("Metric description"),
6064
- chartOptions: z31.record(z31.unknown()).optional().describe("Chart configuration JSON"),
6065
- companyIntegrationId: z31.string().optional().describe("Datasource UUID override"),
6066
- integrationName: z31.string().optional().describe("Datasource display name override")
6204
+ clientId: z32.string().describe("Client/tenant identifier used to validate dashboard widget scope"),
6205
+ workspaceName: z32.string().describe("Target production-backed workspace name"),
6206
+ dashboardName: z32.string().optional().describe("Dashboard name to attach to"),
6207
+ embedId: z32.string().optional().describe("Technical embed id alternative to dashboardName"),
6208
+ dataAppName: z32.string().optional().describe("Optional data app override"),
6209
+ name: z32.string().describe("Metric/widget display name"),
6210
+ metricId: z32.string().describe("Stable metric slug to create or update"),
6211
+ query: z32.string().describe("SQL query already validated against the warehouse"),
6212
+ description: z32.string().optional().describe("Metric description"),
6213
+ chartOptions: z32.record(z32.unknown()).optional().describe("Chart configuration JSON"),
6214
+ companyIntegrationId: z32.string().optional().describe("Datasource UUID override"),
6215
+ integrationName: z32.string().optional().describe("Datasource display name override")
6067
6216
  },
6068
6217
  READ_ONLY,
6069
6218
  async (params) => {
@@ -6154,20 +6303,20 @@ function registerValidateMetricSpec(server, client) {
6154
6303
  }
6155
6304
 
6156
6305
  // src/tools/reports/list-scheduled-reports.ts
6157
- import { z as z32 } from "zod";
6306
+ import { z as z33 } from "zod";
6158
6307
  function registerListScheduledReports(server, client) {
6159
6308
  const services = createServiceBundle(client);
6160
6309
  server.tool(
6161
6310
  "list_scheduled_reports",
6162
6311
  "List configured scheduled reports for a specific client/dashboard embed. Shows schedule metadata, not delivery success history.",
6163
6312
  {
6164
- clientId: z32.string().describe("Client/tenant identifier. Required by the current backend API for non-empty results"),
6165
- dashboardName: z32.string().optional().describe("Dashboard name to inspect"),
6166
- workspaceName: z32.string().optional().describe("Workspace name if dashboardName is ambiguous"),
6167
- embedId: z32.string().optional().describe("Technical embed id alternative to dashboardName"),
6168
- userIdentifier: z32.string().optional().describe("Optional creator/user filter"),
6169
- pageNumber: z32.number().int().positive().optional().describe("Page number when pagination is enabled"),
6170
- isPagination: z32.boolean().optional().describe("Enable backend pagination")
6313
+ clientId: z33.string().describe("Client/tenant identifier. Required by the current backend API for non-empty results"),
6314
+ dashboardName: z33.string().optional().describe("Dashboard name to inspect"),
6315
+ workspaceName: z33.string().optional().describe("Workspace name if dashboardName is ambiguous"),
6316
+ embedId: z33.string().optional().describe("Technical embed id alternative to dashboardName"),
6317
+ userIdentifier: z33.string().optional().describe("Optional creator/user filter"),
6318
+ pageNumber: z33.number().int().positive().optional().describe("Page number when pagination is enabled"),
6319
+ isPagination: z33.boolean().optional().describe("Enable backend pagination")
6171
6320
  },
6172
6321
  READ_ONLY,
6173
6322
  async (params) => {
@@ -6269,7 +6418,30 @@ function registerWhoami(server, client) {
6269
6418
  }
6270
6419
 
6271
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
+ }
6272
6443
  function registerAllTools(server, client) {
6444
+ instrumentToolRegistration(server);
6273
6445
  registerWhoami(server, client);
6274
6446
  registerListDatasources(server, client);
6275
6447
  registerDiscoverDatasourceSchema(server, client);
@@ -6300,6 +6472,7 @@ function registerAllTools(server, client) {
6300
6472
  registerListWidgets(server, client);
6301
6473
  registerValidateMetricSpec(server, client);
6302
6474
  registerCreateOrUpdateMetricFromQuery(server, client);
6475
+ registerCreateWorkspaceMetric(server, client);
6303
6476
  registerAddWidget(server, client);
6304
6477
  registerUpdateWidget(server, client);
6305
6478
  registerRemoveWidget(server, client);
@@ -9492,6 +9665,67 @@ list_scheduled_reports({ clientId, dashboardName, workspaceName })
9492
9665
  \`\`\`
9493
9666
  Clarify that this lists configured schedules only. Delivery success/failure history is not yet persisted in the MCP/API surface.`;
9494
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
+
9495
9729
  // src/prompts/registry.ts
9496
9730
  var prompts = [
9497
9731
  {
@@ -9528,6 +9762,11 @@ var prompts = [
9528
9762
  name: "sql-to-metric-migration",
9529
9763
  description: "Production-safe SQL-to-metric migration workflow: validate, dry-run, then apply with explicit confirmation.",
9530
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
9531
9770
  }
9532
9771
  ];
9533
9772
  function registerAllPrompts(server) {
@@ -9564,18 +9803,23 @@ function createServer(config) {
9564
9803
  {
9565
9804
  name: "databrain",
9566
9805
  version: MCP_SERVER_VERSION,
9567
- 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."
9568
9807
  },
9569
9808
  {
9570
9809
  instructions: operatorBlurb + `Databrain uses two token types \u2014 never confuse them:
9571
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}
9572
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}
9573
9812
 
9574
- 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:
9575
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)
9576
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)
9577
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)
9578
- 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)
9579
9823
 
9580
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.
9581
9825