@databrainhq/mcp-server 0.2.10 → 0.2.12

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
@@ -8,7 +8,8 @@ import { z, ZodError } from "zod";
8
8
  var configSchema = z.object({
9
9
  serviceToken: z.string().min(1).optional(),
10
10
  apiToken: z.string().min(1).optional(),
11
- apiUrl: z.string().url().default("https://api.usedatabrain.com")
11
+ apiUrl: z.string().url().default("https://api.usedatabrain.com"),
12
+ demoDomain: z.string().url().default("https://demo.usedatabrain.com")
12
13
  }).refine((c) => c.serviceToken || c.apiToken, {
13
14
  message: "At least one of DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN must be set"
14
15
  });
@@ -19,8 +20,10 @@ function formatConfigError(err) {
19
20
  lines.push(` - ${issue}`);
20
21
  }
21
22
  lines.push("");
22
- lines.push(" Get a service token at Settings > Service Tokens in your Databrain dashboard.");
23
+ lines.push(" Set DATABRAIN_SERVICE_TOKEN (Settings > Service Tokens) \u2014 org-level token for setup and management.");
23
24
  lines.push(' Then add to your MCP config: { "env": { "DATABRAIN_SERVICE_TOKEN": "..." } }');
25
+ lines.push("");
26
+ lines.push(" Self-hosted? Also set DATABRAIN_API_URL to your instance's API origin (e.g. https://api.your-databrain.example.com). Tokens minted on cloud do not work against a self-hosted instance and vice versa.");
24
27
  return lines.join("\n");
25
28
  }
26
29
  function loadConfig() {
@@ -28,7 +31,8 @@ function loadConfig() {
28
31
  return configSchema.parse({
29
32
  serviceToken: process.env.DATABRAIN_SERVICE_TOKEN || void 0,
30
33
  apiToken: process.env.DATABRAIN_API_TOKEN || void 0,
31
- apiUrl: process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com"
34
+ apiUrl: process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com",
35
+ demoDomain: process.env.DATABRAIN_DEMO_DOMAIN || "https://demo.usedatabrain.com"
32
36
  });
33
37
  } catch (err) {
34
38
  if (err instanceof ZodError) {
@@ -78,13 +82,39 @@ var DatabrainApiError = class extends Error {
78
82
  this.name = "DatabrainApiError";
79
83
  }
80
84
  toToolResponse() {
85
+ const parts = [`Error: ${this.message}`];
86
+ const detailDump = this.formatDetails();
87
+ if (detailDump) parts.push(detailDump);
88
+ const hint = this.hint();
89
+ if (hint) parts.push(hint);
81
90
  return {
82
- content: [{ type: "text", text: `Error: ${this.message}
83
-
84
- ${this.hint()}` }],
91
+ content: [{ type: "text", text: parts.join("\n\n") }],
85
92
  isError: true
86
93
  };
87
94
  }
95
+ /**
96
+ * Surface API-returned details for 4xx errors so the LLM can recover or
97
+ * report the real reason (validation messages, field names, etc.). Skips
98
+ * fields already collapsed into `message`/`code`, and skips 5xx where the
99
+ * payload usually isn't actionable.
100
+ */
101
+ formatDetails() {
102
+ if (this.status < 400 || this.status >= 500) return "";
103
+ if (!this.details || typeof this.details !== "object") return "";
104
+ const skipKeys = /* @__PURE__ */ new Set(["message", "detail", "error", "code"]);
105
+ const extra = {};
106
+ for (const [k, v] of Object.entries(this.details)) {
107
+ if (skipKeys.has(k)) continue;
108
+ if (v === null || v === void 0) continue;
109
+ extra[k] = v;
110
+ }
111
+ if (Object.keys(extra).length === 0) return "";
112
+ try {
113
+ return `API details: ${JSON.stringify(extra)}`;
114
+ } catch {
115
+ return "";
116
+ }
117
+ }
88
118
  hint() {
89
119
  if (this.status === 401 || this.status === 403) {
90
120
  if (this.code === "INVALID_SERVICE_TOKEN" || this.code === "MISSING_SERVICE_TOKEN" || this.message.toLowerCase().includes("service token")) {
@@ -154,8 +184,10 @@ var logger = {
154
184
  error: (msg, data) => log("error", msg, data)
155
185
  };
156
186
 
187
+ // src/version.ts
188
+ var MCP_SERVER_VERSION = "0.2.10";
189
+
157
190
  // src/client/databrain-client.ts
158
- var MCP_SERVER_VERSION = "0.2.0";
159
191
  var MCP_USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION}`;
160
192
  var MAX_RETRIES = 3;
161
193
  var REQUEST_TIMEOUT_MS = 3e4;
@@ -188,12 +220,27 @@ var DatabrainClient = class {
188
220
  getApiUrl() {
189
221
  return this.config.apiUrl;
190
222
  }
223
+ getDemoDomain() {
224
+ return this.config.demoDomain;
225
+ }
226
+ /**
227
+ * Describes which credential is currently used as the bearer for outgoing
228
+ * API calls. Used by `whoami` so the LLM can give the user a truthful answer.
229
+ */
230
+ getActiveAuth() {
231
+ if (this.config.serviceToken) return { source: "service-token" };
232
+ if (this.runtimeApiToken || this.config.apiToken) return { source: "api-token" };
233
+ return { source: "none" };
234
+ }
191
235
  getToken(auth) {
192
236
  if (auth === "none") return void 0;
193
237
  if (auth === "service") return this.config.serviceToken;
194
238
  return this.runtimeApiToken || this.config.apiToken;
195
239
  }
196
240
  async request(options) {
241
+ return this.requestInternal(options);
242
+ }
243
+ async requestInternal(options) {
197
244
  if (options.invalidateCache) {
198
245
  for (const prefix of options.invalidateCache) {
199
246
  this.cache.invalidateByPrefix(prefix);
@@ -596,6 +643,69 @@ var QueryApi = class {
596
643
  throw err;
597
644
  }
598
645
  }
646
+ async startSemanticLayerGeneration(datamartId) {
647
+ return this.client.request({
648
+ method: "POST",
649
+ path: `/api/schema/datamart/${encodeURIComponent(datamartId)}/semantic-layer/auto-generate`,
650
+ auth: "service",
651
+ invalidateCache: ["semantic_layer_"]
652
+ });
653
+ }
654
+ async getSemanticLayerGenerationStatus(datamartId) {
655
+ return this.client.request({
656
+ method: "GET",
657
+ path: `/api/schema/datamart/${encodeURIComponent(datamartId)}/semantic-layer/generation-status`,
658
+ auth: "service"
659
+ });
660
+ }
661
+ };
662
+
663
+ // src/api/scheduled-reports-api.ts
664
+ function asRecord(value) {
665
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
666
+ return value;
667
+ }
668
+ function normalizeRecords(value) {
669
+ if (Array.isArray(value)) {
670
+ return value.filter((item) => asRecord(item) !== null);
671
+ }
672
+ const record = asRecord(value);
673
+ if (!record) return [];
674
+ const candidates = [
675
+ record.scheduleEmailReports,
676
+ record.reports,
677
+ record.items,
678
+ record.data
679
+ ];
680
+ for (const candidate of candidates) {
681
+ if (Array.isArray(candidate)) {
682
+ return candidate.filter((item) => asRecord(item) !== null);
683
+ }
684
+ }
685
+ return [];
686
+ }
687
+ var ScheduledReportsApi = class {
688
+ constructor(client) {
689
+ this.client = client;
690
+ }
691
+ async listByEmbed(params) {
692
+ const query = {
693
+ embedId: params.embedId,
694
+ clientId: params.clientId
695
+ };
696
+ if (params.userIdentifier) query.userIdentifier = params.userIdentifier;
697
+ if (params.pageNumber !== void 0) query.pageNumber = String(params.pageNumber);
698
+ if (params.isPagination !== void 0) {
699
+ query.isPagination = params.isPagination ? "true" : "false";
700
+ }
701
+ const response = await this.client.request({
702
+ method: "GET",
703
+ path: "/api/v2/data-app/embeds/reports",
704
+ auth: "api",
705
+ query
706
+ });
707
+ return normalizeRecords(response);
708
+ }
599
709
  };
600
710
 
601
711
  // src/api/widgets-api.ts
@@ -735,19 +845,19 @@ var DEFAULT_ACCESS_SETTINGS = {
735
845
  function isNonEmptyString2(value) {
736
846
  return typeof value === "string" && value.trim().length > 0;
737
847
  }
738
- function asRecord(value) {
848
+ function asRecord2(value) {
739
849
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
740
850
  return value;
741
851
  }
742
852
  function extractAccessSettings(embed) {
743
- const metadata = asRecord(embed.embedMetadata);
744
- const metadataConfig = asRecord(metadata?.embedConfig);
745
- const options = asRecord(embed.options);
853
+ const metadata = asRecord2(embed.embedMetadata);
854
+ const metadataConfig = asRecord2(metadata?.embedConfig);
855
+ const options = asRecord2(embed.options);
746
856
  const candidates = [
747
- asRecord(embed.accessSettings),
748
- asRecord(metadata?.accessSettings),
749
- asRecord(metadataConfig?.accessSettings),
750
- asRecord(options?.accessSettings)
857
+ asRecord2(embed.accessSettings),
858
+ asRecord2(metadata?.accessSettings),
859
+ asRecord2(metadataConfig?.accessSettings),
860
+ asRecord2(options?.accessSettings)
751
861
  ].filter((c) => !!c);
752
862
  for (const candidate of candidates) {
753
863
  if (Object.keys(candidate).length > 0) return candidate;
@@ -755,8 +865,8 @@ function extractAccessSettings(embed) {
755
865
  return null;
756
866
  }
757
867
  function extractDatamartName(embed, existingAccess) {
758
- const metadata = asRecord(embed.embedMetadata);
759
- const metadataConfig = asRecord(metadata?.embedConfig);
868
+ const metadata = asRecord2(embed.embedMetadata);
869
+ const metadataConfig = asRecord2(metadata?.embedConfig);
760
870
  const values = [
761
871
  existingAccess?.datamartName,
762
872
  existingAccess?.dataMartName,
@@ -793,7 +903,7 @@ var EmbedService = class {
793
903
  return this.embedsApi.listEmbeds(dataAppName);
794
904
  }
795
905
  async createEmbedStrict(body) {
796
- const accessSettings = asRecord(body.accessSettings);
906
+ const accessSettings = asRecord2(body.accessSettings);
797
907
  if (!accessSettings || Object.keys(accessSettings).length === 0) {
798
908
  throw new Error(
799
909
  "Embed creation requires `accessSettings` for future update compatibility."
@@ -881,8 +991,8 @@ Run \`update_embed\` with explicit \`accessSettings.datamartName\`, or recreate
881
991
  }
882
992
  async updateEmbed(embedId, updatePatch) {
883
993
  const body = await this.buildUpdateBase(embedId);
884
- const baseAccessSettings = asRecord(body.accessSettings);
885
- const patchAccessSettings = asRecord(updatePatch.accessSettings);
994
+ const baseAccessSettings = asRecord2(body.accessSettings);
995
+ const patchAccessSettings = asRecord2(updatePatch.accessSettings);
886
996
  for (const [key, value] of Object.entries(updatePatch)) {
887
997
  if (key === "accessSettings") continue;
888
998
  if (value !== void 0) body[key] = value;
@@ -942,7 +1052,7 @@ var GuestTokenService = class {
942
1052
 
943
1053
  // src/services/widget-service.ts
944
1054
  var isNonEmptyString3 = (v) => typeof v === "string" && v.trim().length > 0;
945
- var asRecord2 = (v) => {
1055
+ var asRecord3 = (v) => {
946
1056
  if (!v || typeof v !== "object" || Array.isArray(v)) return null;
947
1057
  return v;
948
1058
  };
@@ -1263,8 +1373,8 @@ Do not guess between the two \u2014 wait for the user to choose.`
1263
1373
  if (!exact) {
1264
1374
  throw new Error(`Embed "${embedId}" not found. Run \`list_embeds\` to verify.`);
1265
1375
  }
1266
- const metadata = asRecord2(exact.embedMetadata);
1267
- const externalDashboard = asRecord2(exact.externalDashboard);
1376
+ const metadata = asRecord3(exact.embedMetadata);
1377
+ const externalDashboard = asRecord3(exact.externalDashboard);
1268
1378
  const dataAppId = isNonEmptyString3(exact.dataAppId) ? exact.dataAppId : null;
1269
1379
  const externalDashboardId = isNonEmptyString3(exact.externalDashboardId) && exact.externalDashboardId || (externalDashboard && isNonEmptyString3(externalDashboard.externalDashboardId) ? externalDashboard.externalDashboardId : null) || (metadata && isNonEmptyString3(metadata.externalDashboardId) ? metadata.externalDashboardId : null);
1270
1380
  const dashboardId = isNonEmptyString3(exact.dashboardId) && exact.dashboardId || (metadata && isNonEmptyString3(metadata.dashboardId) ? metadata.dashboardId : null);
@@ -1314,7 +1424,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
1314
1424
  guestToken,
1315
1425
  templateMetricId
1316
1426
  );
1317
- const metric = asRecord2(response.externalMetric);
1427
+ const metric = asRecord3(response.externalMetric);
1318
1428
  if (!metric) {
1319
1429
  throw new Error(
1320
1430
  `Template metric "${templateMetricId}" not found or could not be loaded. Response keys: ${Object.keys(
@@ -1330,7 +1440,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
1330
1440
  query: metric.query || void 0,
1331
1441
  metricQuery: metric.metricQuery || void 0,
1332
1442
  description: metric.description || "",
1333
- chartOptions: asRecord2(metric.chartOptions) || {},
1443
+ chartOptions: asRecord3(metric.chartOptions) || {},
1334
1444
  companyId: metric.companyId || void 0,
1335
1445
  companyIntegrationId: metric.companyIntegrationId || void 0,
1336
1446
  integrationName: metric.integrationName || void 0,
@@ -1371,6 +1481,7 @@ function createServiceBundle(client) {
1371
1481
  const datasourcesApi = new DatasourcesApi(client);
1372
1482
  const guestTokenApi = new GuestTokenApi(client);
1373
1483
  const queryApi = new QueryApi(client);
1484
+ const scheduledReportsApi = new ScheduledReportsApi(client);
1374
1485
  const widgetsApi = new WidgetsApi(client);
1375
1486
  const workspacesApi = new WorkspacesApi(client);
1376
1487
  const discoveryService = new DiscoveryService();
@@ -1383,6 +1494,7 @@ function createServiceBundle(client) {
1383
1494
  datasourcesApi,
1384
1495
  workspacesApi,
1385
1496
  queryApi,
1497
+ scheduledReportsApi,
1386
1498
  widgetsApi,
1387
1499
  embedService: new EmbedService(embedsApi, datamartsApi),
1388
1500
  discoveryService,
@@ -1990,68 +2102,223 @@ function registerListWorkspaces(server, client) {
1990
2102
  );
1991
2103
  }
1992
2104
 
1993
- // src/tools/infrastructure/link-datamart-to-workspace.ts
2105
+ // src/tools/infrastructure/create-datamart.ts
1994
2106
  import { z as z9 } from "zod";
1995
- function registerLinkDatamartToWorkspace(server, client) {
2107
+
2108
+ // src/tools/widgets/production-guards.ts
2109
+ var APPLY_TO_PRODUCTION_CONFIRMATION = "APPLY_TO_PRODUCTION";
2110
+ function validateProductionApply(params) {
2111
+ if (params.confirm !== APPLY_TO_PRODUCTION_CONFIRMATION) {
2112
+ return `Applying to production requires confirm: "${APPLY_TO_PRODUCTION_CONFIRMATION}". Run dry-run first, review the plan, then retry with that confirmation string.`;
2113
+ }
2114
+ return null;
2115
+ }
2116
+ function productionSafetyNote(workspaceName) {
2117
+ return `This workspace is production-backed. Dry-run is the default; apply requires explicit production confirmation. Resolved workspace: "${workspaceName.trim()}".`;
2118
+ }
2119
+ function genericProductionSafetyNote() {
2120
+ return "This operation targets production-backed Databrain resources. Dry-run is the default; apply requires explicit production confirmation.";
2121
+ }
2122
+
2123
+ // src/tools/infrastructure/create-datamart.ts
2124
+ var columnSchema = z9.object({
2125
+ name: z9.string(),
2126
+ alias: z9.string().optional(),
2127
+ label: z9.string().optional(),
2128
+ isHide: z9.boolean().optional(),
2129
+ isCustomColumn: z9.boolean().optional(),
2130
+ sql: z9.string().optional()
2131
+ });
2132
+ var tableSchema = z9.object({
2133
+ name: z9.string(),
2134
+ schemaName: z9.string().optional(),
2135
+ clientColumn: z9.string().optional(),
2136
+ label: z9.string().optional(),
2137
+ columns: z9.array(columnSchema)
2138
+ });
2139
+ var relationshipSchema = z9.object({
2140
+ parentTableName: z9.string(),
2141
+ parentColumnName: z9.string(),
2142
+ childTableName: z9.string(),
2143
+ childColumnName: z9.string(),
2144
+ cardinality: z9.enum(["OneToOne", "OneToMany", "ManyToOne", "ManyToMany"]),
2145
+ join: z9.enum(["INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN"]),
2146
+ relationshipName: z9.string()
2147
+ });
2148
+ function datamartSuccessMessage(name, result) {
2149
+ return `Datamart "${name}" created successfully (ID: ${result.id || result._id || "UNKNOWN"}).
2150
+
2151
+ **To query data**, complete the semantic layer first:
2152
+ 1. \`get_semantic_layer\` with \`datamartName: "${name}"\` \u2014 check if table descriptions exist
2153
+ 2. \`update_semantic_layer\` \u2014 add table/column descriptions and synonyms so the AI can generate accurate SQL
2154
+ 3. \`ask_question\` with \`datamartName: "${name}"\` \u2014 ask natural language questions
2155
+
2156
+ **To embed dashboards** (optional):
2157
+ 1. Connect this datamart to a workspace outside MCP
2158
+ 2. Create a dashboard in the Databrain UI
2159
+ 3. Use \`create_embed\` to embed the dashboard`;
2160
+ }
2161
+ function registerCreateDatamart(server, client) {
2162
+ const services = createServiceBundle(client);
1996
2163
  server.tool(
1997
- "link_datamart_to_workspace",
1998
- "Link a datamart to an existing workspace. ALWAYS ask the user which workspace to use \u2014 do not auto-select.",
2164
+ "create_datamart",
2165
+ "Create a production datamart from selected tables and relationships. Defaults to dry-run; apply requires explicit confirmation.",
1999
2166
  {
2000
- workspaceName: z9.string().optional().describe(
2001
- "Name of the workspace to update. Omit to list available workspaces and let the user choose."
2002
- ),
2003
- datamartName: z9.string().describe(
2004
- "Name of the datamart to link. Use `list_datamarts` to find it."
2005
- )
2167
+ mode: z9.enum(["dry_run", "apply"]).default("dry_run").describe("dry_run returns the plan only; apply creates the datamart"),
2168
+ confirm: z9.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
2169
+ name: z9.string().describe("Unique datamart name"),
2170
+ datasourceName: z9.string().optional().describe("Name of the datasource to use. Omit to list available datasources."),
2171
+ tableList: z9.array(tableSchema).optional().describe("Array of tables with their columns"),
2172
+ relationships: z9.array(relationshipSchema).optional().describe("Join definitions between tables"),
2173
+ tenancySettings: z9.object({
2174
+ tenancyLevel: z9.enum(["TABLE", "DATABASE"]).optional(),
2175
+ clientColumnType: z9.string().optional(),
2176
+ schemaName: z9.string().optional(),
2177
+ tableName: z9.string().optional(),
2178
+ tableClientNameColumn: z9.string().optional(),
2179
+ tablePrimaryKeyColumn: z9.string().optional()
2180
+ }).optional().describe("Multi-tenant configuration"),
2181
+ body: z9.record(z9.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/datamarts")
2006
2182
  },
2007
- WRITE_IDEMPOTENT,
2008
- async ({ workspaceName, datamartName }) => {
2183
+ WRITE_CREATE,
2184
+ async (params) => {
2009
2185
  try {
2010
- if (!workspaceName) {
2011
- const workspaces = await client.request({
2012
- method: "GET",
2013
- path: "/api/v2/workspace",
2014
- auth: "service",
2015
- cacheKey: "list_workspaces",
2016
- cacheTtlMs: 5 * 60 * 1e3
2186
+ const mode = params.mode || "dry_run";
2187
+ if (!params.body) {
2188
+ const datasources = await services.datasourcesApi.listDatasources();
2189
+ const resolution = resolveResource({
2190
+ items: datasources,
2191
+ name: params.datasourceName,
2192
+ getName: (ds) => ds.name,
2193
+ formatItem: formatDatasource,
2194
+ resourceType: "datasource",
2195
+ toolName: "create_datamart"
2017
2196
  });
2018
- if (!Array.isArray(workspaces) || workspaces.length === 0) {
2197
+ if (resolution.kind !== "resolved") return resolution.response;
2198
+ if (!params.tableList || params.tableList.length === 0) {
2019
2199
  return toolError(
2020
- "No workspaces found. Create a workspace in the **Databrain UI** first, then come back here to link the datamart."
2200
+ "tableList is required. Use `discover_datasource_schema` to see available tables first."
2201
+ );
2202
+ }
2203
+ const body = {
2204
+ name: params.name,
2205
+ datasourceName: resolution.name,
2206
+ tableList: params.tableList,
2207
+ relationships: params.relationships,
2208
+ tenancySettings: params.tenancySettings
2209
+ };
2210
+ if (mode === "dry_run") {
2211
+ return toolSuccess(
2212
+ JSON.stringify(
2213
+ {
2214
+ status: "dry_run_only",
2215
+ safety: genericProductionSafetyNote(),
2216
+ requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
2217
+ body,
2218
+ nextStep: `Review this plan. To execute, call again with mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}".`
2219
+ },
2220
+ null,
2221
+ 2
2222
+ )
2021
2223
  );
2022
2224
  }
2023
- const list = workspaces.map((ws) => `- **${ws.name}** (ID: ${ws._id || ws.id})`).join("\n");
2225
+ const guardError2 = validateProductionApply({ confirm: params.confirm });
2226
+ if (guardError2) return toolError(guardError2);
2227
+ const result2 = await client.request({
2228
+ method: "POST",
2229
+ path: "/api/v2/data-app/datamarts",
2230
+ body,
2231
+ auth: "service",
2232
+ invalidateCache: ["list_datamarts"]
2233
+ });
2234
+ if (result2.error) {
2235
+ return toolError(`Datamart creation failed: ${stringify(result2.error)}`);
2236
+ }
2237
+ return toolSuccess(datamartSuccessMessage(params.name, result2));
2238
+ }
2239
+ if (mode === "dry_run") {
2024
2240
  return toolSuccess(
2025
- `Found ${workspaces.length} workspace(s):
2026
-
2027
- ${list}
2028
-
2029
- **Ask the user** which workspace they want to link to datamart **${datamartName}**, then call \`link_datamart_to_workspace\` with both \`workspaceName\` and \`datamartName\`.`
2241
+ JSON.stringify(
2242
+ {
2243
+ status: "dry_run_only",
2244
+ safety: genericProductionSafetyNote(),
2245
+ requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
2246
+ body: params.body,
2247
+ nextStep: `Review this raw datamart body. To execute, call again with mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}".`
2248
+ },
2249
+ null,
2250
+ 2
2251
+ )
2030
2252
  );
2031
2253
  }
2254
+ const guardError = validateProductionApply({ confirm: params.confirm });
2255
+ if (guardError) return toolError(guardError);
2032
2256
  const result = await client.request({
2033
- method: "PUT",
2034
- path: "/api/v2/workspace",
2035
- body: {
2036
- name: workspaceName,
2037
- connectionType: "DATAMART",
2038
- datamartName
2039
- },
2257
+ method: "POST",
2258
+ path: "/api/v2/data-app/datamarts",
2259
+ body: params.body,
2040
2260
  auth: "service",
2041
- invalidateCache: ["list_workspaces"]
2261
+ invalidateCache: ["list_datamarts"]
2042
2262
  });
2043
2263
  if (result.error) {
2044
- return toolError(
2045
- `Failed to link datamart: ${typeof result.error === "object" ? JSON.stringify(result.error) : result.error}`
2046
- );
2264
+ return toolError(`Datamart creation failed: ${stringify(result.error)}`);
2265
+ }
2266
+ return toolSuccess(datamartSuccessMessage(params.name, result));
2267
+ } catch (err) {
2268
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
2269
+ return toolError(`Unexpected error: ${err.message}`);
2270
+ }
2271
+ }
2272
+ );
2273
+ }
2274
+
2275
+ // src/tools/infrastructure/sync-datasource.ts
2276
+ import { z as z10 } from "zod";
2277
+ function registerSyncDatasource(server, client) {
2278
+ const datasourcesApi = new DatasourcesApi(client);
2279
+ server.tool(
2280
+ "sync_datasource",
2281
+ "Trigger a datasource schema sync to refresh table and column metadata. If datasourceName is omitted and there are multiple datasources, returns the list so the user can choose.",
2282
+ {
2283
+ datasourceName: z10.string().optional().describe("Name of the datasource to sync. Omit to list available datasources.")
2284
+ },
2285
+ WRITE_IDEMPOTENT,
2286
+ async (params) => {
2287
+ try {
2288
+ const datasources = await client.request({
2289
+ method: "GET",
2290
+ path: "/api/v2/datasource/",
2291
+ auth: "service",
2292
+ cacheKey: "list_datasources",
2293
+ cacheTtlMs: 10 * 60 * 1e3
2294
+ });
2295
+ const resolution = resolveResource({
2296
+ items: datasources,
2297
+ name: params.datasourceName,
2298
+ getName: (ds) => ds.name,
2299
+ formatItem: formatDatasource,
2300
+ resourceType: "datasource",
2301
+ toolName: "sync_datasource"
2302
+ });
2303
+ if (resolution.kind !== "resolved") return resolution.response;
2304
+ const { selected } = resolution;
2305
+ const datasourceId = selected._id || selected.id || (selected.name ? await datasourcesApi.resolveIdByName(selected.name) : null);
2306
+ if (!datasourceId) {
2307
+ return toolError(`Datasource "${selected.name}" found but has no ID. This is unexpected.`);
2047
2308
  }
2309
+ const result = await client.request({
2310
+ method: "POST",
2311
+ path: "/api/v2/datasource/sync",
2312
+ body: { input: { datasourceId } },
2313
+ auth: "service",
2314
+ invalidateCache: [`datasource_schema_${selected.name}`]
2315
+ });
2316
+ const message = result?.data?.message || "Sync completed";
2048
2317
  return toolSuccess(
2049
- `Workspace **${workspaceName}** is now linked to datamart **${datamartName}**.
2318
+ `Datasource "${selected.name}" sync triggered (ID: ${datasourceId}).
2319
+ Status: ${message}
2050
2320
 
2051
- Next steps:
2052
- - Build dashboards in the Databrain UI using this workspace
2053
- - Use \`list_dashboards\` to find dashboards in this workspace
2054
- - Use \`create_embed\` to embed dashboards into your app`
2321
+ The schema metadata will be refreshed. You can now use \`discover_datasource_schema\` or \`create_datamart\` to select tables.`
2055
2322
  );
2056
2323
  } catch (err) {
2057
2324
  if (err instanceof DatabrainApiError) return err.toToolResponse();
@@ -2062,7 +2329,7 @@ Next steps:
2062
2329
  }
2063
2330
 
2064
2331
  // src/tools/embed/create-embed.ts
2065
- import { z as z10 } from "zod";
2332
+ import { z as z11 } from "zod";
2066
2333
  var DEFAULT_ACCESS_SETTINGS2 = {
2067
2334
  isAllowMetricCreation: false,
2068
2335
  isAllowMetricDeletion: false,
@@ -2075,18 +2342,18 @@ var DEFAULT_ACCESS_SETTINGS2 = {
2075
2342
  isAllowCreateDashboardView: false,
2076
2343
  metricCreationMode: "DRAG_DROP"
2077
2344
  };
2078
- var accessSettingsSchema = z10.object({
2079
- datamartName: z10.string().describe("Name of the datamart (required)"),
2080
- isAllowMetricCreation: z10.boolean().optional().describe("Allow end-users to create metrics"),
2081
- metricCreationMode: z10.enum(["DRAG_DROP", "CHAT"]).optional().describe("Metric creation mode"),
2082
- isAllowMetricDeletion: z10.boolean().optional().describe("Allow metric deletion"),
2083
- isAllowMetricUpdate: z10.boolean().optional().describe("Allow metric editing"),
2084
- isAllowMetricLayoutChange: z10.boolean().optional().describe("Allow layout changes"),
2085
- isAllowManageMetrics: z10.boolean().optional().describe("Show manage metrics UI"),
2086
- isAllowUnderlyingData: z10.boolean().optional().describe("Allow viewing underlying data"),
2087
- isAllowAiPilot: z10.boolean().optional().describe("Enable AI pilot features"),
2088
- isAllowEmailReports: z10.boolean().optional().describe("Allow email report scheduling"),
2089
- isAllowCreateDashboardView: z10.boolean().optional().describe("Allow creating dashboard views")
2345
+ var accessSettingsSchema = z11.object({
2346
+ datamartName: z11.string().describe("Name of the datamart (required)"),
2347
+ isAllowMetricCreation: z11.boolean().optional().describe("Allow end-users to create metrics"),
2348
+ metricCreationMode: z11.enum(["DRAG_DROP", "CHAT"]).optional().describe("Metric creation mode"),
2349
+ isAllowMetricDeletion: z11.boolean().optional().describe("Allow metric deletion"),
2350
+ isAllowMetricUpdate: z11.boolean().optional().describe("Allow metric editing"),
2351
+ isAllowMetricLayoutChange: z11.boolean().optional().describe("Allow layout changes"),
2352
+ isAllowManageMetrics: z11.boolean().optional().describe("Show manage metrics UI"),
2353
+ isAllowUnderlyingData: z11.boolean().optional().describe("Allow viewing underlying data"),
2354
+ isAllowAiPilot: z11.boolean().optional().describe("Enable AI pilot features"),
2355
+ isAllowEmailReports: z11.boolean().optional().describe("Allow email report scheduling"),
2356
+ isAllowCreateDashboardView: z11.boolean().optional().describe("Allow creating dashboard views")
2090
2357
  }).describe("Embed access settings. Only datamartName is required \u2014 all flags default to false.");
2091
2358
  function registerCreateEmbed(server, client) {
2092
2359
  const services = createServiceBundle(client);
@@ -2094,23 +2361,23 @@ function registerCreateEmbed(server, client) {
2094
2361
  "create_embed",
2095
2362
  "Create an embed. Provide dashboardId, workspaceName, accessSettings.datamartName. Add clientId for multi-tenant per-client dashboards. All isAllow* flags default to false.",
2096
2363
  {
2097
- body: z10.record(z10.unknown()).optional().describe(
2364
+ body: z11.record(z11.unknown()).optional().describe(
2098
2365
  "Raw API body passthrough. If provided, all other fields are ignored."
2099
2366
  ),
2100
- dashboardId: z10.string().optional().describe("Dashboard identifier for embed creation"),
2101
- workspaceName: z10.string().optional().describe("Workspace name. Omit to auto-resolve if only one exists."),
2102
- embedType: z10.enum(["dashboard", "metric"]).optional().describe("Embed type"),
2103
- metricId: z10.string().optional().describe("Required for metric embed type"),
2104
- name: z10.string().optional().describe("Human-readable embed config name"),
2367
+ dashboardId: z11.string().optional().describe("Dashboard identifier for embed creation"),
2368
+ workspaceName: z11.string().optional().describe("Workspace name. Omit to auto-resolve if only one exists."),
2369
+ embedType: z11.enum(["dashboard", "metric"]).optional().describe("Embed type"),
2370
+ metricId: z11.string().optional().describe("Required for metric embed type"),
2371
+ name: z11.string().optional().describe("Human-readable embed config name"),
2105
2372
  accessSettings: accessSettingsSchema.optional(),
2106
2373
  // Multi-tenant (dashboard embed) params
2107
- clientId: z10.string().optional().describe(
2374
+ clientId: z11.string().optional().describe(
2108
2375
  "Client identifier for multi-tenant isolation. When provided, creates a dashboard embed (each client gets their own dashboard copy)."
2109
2376
  ),
2110
- templateDashboardId: z10.string().optional().describe("Template dashboard ID to clone metrics from"),
2111
- isImportMetrics: z10.boolean().optional().describe("Import metrics from template dashboard"),
2112
- metricImportMode: z10.enum(["COPY", "LINK"]).optional().describe("COPY duplicates metrics, LINK shares them"),
2113
- isRenameDashboard: z10.boolean().optional().describe("Rename the dashboard for this client")
2377
+ templateDashboardId: z11.string().optional().describe("Template dashboard ID to clone metrics from"),
2378
+ isImportMetrics: z11.boolean().optional().describe("Import metrics from template dashboard"),
2379
+ metricImportMode: z11.enum(["COPY", "LINK"]).optional().describe("COPY duplicates metrics, LINK shares them"),
2380
+ isRenameDashboard: z11.boolean().optional().describe("Rename the dashboard for this client")
2114
2381
  },
2115
2382
  WRITE_CREATE,
2116
2383
  async (params) => {
@@ -2212,14 +2479,14 @@ Next: run \`get_embed_details\` to verify the embed configuration.`
2212
2479
  }
2213
2480
 
2214
2481
  // src/tools/embed/list-embeds.ts
2215
- import { z as z11 } from "zod";
2482
+ import { z as z12 } from "zod";
2216
2483
  function registerListEmbeds(server, client) {
2217
2484
  const services = createServiceBundle(client);
2218
2485
  server.tool(
2219
2486
  "list_embeds",
2220
- "List embed configurations. Requires an API token (use create_api_token first).",
2487
+ "List embed configurations. Requires an API token (use create_api_token first). Response includes `distinctClientIds` \u2014 a deduped list of end-customer client IDs that have multi-tenant dashboard embeds, useful for discovering which customers exist when only the workspace operator persona knows them.",
2221
2488
  {
2222
- dataAppName: z11.string().optional().describe("Filter by data app name")
2489
+ dataAppName: z12.string().optional().describe("Filter by data app name")
2223
2490
  },
2224
2491
  READ_ONLY,
2225
2492
  async (params) => {
@@ -2230,19 +2497,28 @@ function registerListEmbeds(server, client) {
2230
2497
  "No embeds found. Create an embed in the Databrain UI, then run `list_embeds` again."
2231
2498
  );
2232
2499
  }
2500
+ const embeds = result.map((e) => {
2501
+ const n = services.discoveryService.normalizeEmbed(e);
2502
+ const clientId = typeof e.clientId === "string" && e.clientId.trim().length > 0 ? e.clientId : null;
2503
+ return {
2504
+ embedId: n.embedId || null,
2505
+ internalId: !n.embedId && n.internalId ? n.internalId : void 0,
2506
+ name: n.name,
2507
+ type: n.embedType || "dashboard",
2508
+ dashboardId: n.dashboardId || null,
2509
+ dataApp: n.dataAppName || null,
2510
+ clientId
2511
+ };
2512
+ });
2513
+ const distinctClientIds = Array.from(
2514
+ new Set(
2515
+ embeds.map((e) => e.clientId).filter((id) => typeof id === "string" && id.length > 0)
2516
+ )
2517
+ ).sort();
2233
2518
  return toolSuccess(JSON.stringify({
2234
- count: result.length,
2235
- embeds: result.map((e) => {
2236
- const n = services.discoveryService.normalizeEmbed(e);
2237
- return {
2238
- embedId: n.embedId || null,
2239
- internalId: !n.embedId && n.internalId ? n.internalId : void 0,
2240
- name: n.name,
2241
- type: n.embedType || "dashboard",
2242
- dashboardId: n.dashboardId || null,
2243
- dataApp: n.dataAppName || null
2244
- };
2245
- })
2519
+ count: embeds.length,
2520
+ distinctClientIds,
2521
+ embeds
2246
2522
  }));
2247
2523
  } catch (err) {
2248
2524
  if (err instanceof DatabrainApiError) return err.toToolResponse();
@@ -2253,14 +2529,14 @@ function registerListEmbeds(server, client) {
2253
2529
  }
2254
2530
 
2255
2531
  // src/tools/embed/get-embed-details.ts
2256
- import { z as z12 } from "zod";
2532
+ import { z as z13 } from "zod";
2257
2533
  function registerGetEmbedDetails(server, client) {
2258
2534
  const services = createServiceBundle(client);
2259
2535
  server.tool(
2260
2536
  "get_embed_details",
2261
2537
  "Get full embed configuration including access settings and theme.",
2262
2538
  {
2263
- embedId: z12.string().describe("ID of the embed")
2539
+ embedId: z13.string().describe("ID of the embed")
2264
2540
  },
2265
2541
  READ_ONLY,
2266
2542
  async (params) => {
@@ -2276,7 +2552,7 @@ function registerGetEmbedDetails(server, client) {
2276
2552
  }
2277
2553
 
2278
2554
  // src/tools/embed/update-embed.ts
2279
- import { z as z13 } from "zod";
2555
+ import { z as z14 } from "zod";
2280
2556
 
2281
2557
  // src/tools/embed/helpers.ts
2282
2558
  var GUEST_TOKEN_REMINDER = "\n\n\u26A0\uFE0F **Guest tokens must be regenerated** for these changes to take effect. Existing guest tokens still carry the old settings. Call `generate_guest_token` to create a new token with the updated configuration.";
@@ -2314,15 +2590,15 @@ function registerUpdateEmbed(server, client) {
2314
2590
  "update_embed",
2315
2591
  "Update embed access settings, permissions, theme, or name.",
2316
2592
  {
2317
- embedId: z13.string().describe("ID of the embed to update"),
2318
- body: z13.record(z13.unknown()).optional().describe(
2593
+ embedId: z14.string().describe("ID of the embed to update"),
2594
+ body: z14.record(z14.unknown()).optional().describe(
2319
2595
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, it is sent directly (passthrough mode)."
2320
2596
  ),
2321
- name: z13.string().optional().describe("New name for the embed"),
2322
- accessSettings: z13.record(z13.unknown()).optional().describe(
2597
+ name: z14.string().optional().describe("New name for the embed"),
2598
+ accessSettings: z14.record(z14.unknown()).optional().describe(
2323
2599
  "Partial update of access settings. See `get_embed_details` for current settings."
2324
2600
  ),
2325
- theme: z13.record(z13.unknown()).optional().describe(
2601
+ theme: z14.record(z14.unknown()).optional().describe(
2326
2602
  "Theme configuration object. See databrain://theme-reference for the full schema."
2327
2603
  )
2328
2604
  },
@@ -2368,7 +2644,7 @@ function registerUpdateEmbed(server, client) {
2368
2644
  }
2369
2645
 
2370
2646
  // src/tools/embed/customize-theme.ts
2371
- import { z as z14 } from "zod";
2647
+ import { z as z15 } from "zod";
2372
2648
  var THEME_PRESETS = {
2373
2649
  light: {
2374
2650
  general: {
@@ -2502,7 +2778,7 @@ var ACCESS_PRESETS = {
2502
2778
  metricCreationMode: "CHAT"
2503
2779
  }
2504
2780
  };
2505
- var fontFamilyEnum = z14.enum([
2781
+ var fontFamilyEnum = z15.enum([
2506
2782
  "Inter",
2507
2783
  "Roboto",
2508
2784
  "Open Sans",
@@ -2514,17 +2790,17 @@ var fontFamilyEnum = z14.enum([
2514
2790
  "Nunito",
2515
2791
  "system-ui"
2516
2792
  ]).describe("Font family");
2517
- var tooltipFontSchema = z14.object({
2518
- fontFamily: z14.string().optional(),
2519
- fontSize: z14.number().optional(),
2520
- fontWeight: z14.number().optional(),
2521
- fontColor: z14.string().optional()
2793
+ var tooltipFontSchema = z15.object({
2794
+ fontFamily: z15.string().optional(),
2795
+ fontSize: z15.number().optional(),
2796
+ fontWeight: z15.number().optional(),
2797
+ fontColor: z15.string().optional()
2522
2798
  }).optional();
2523
- var fontConfigInputSchema = z14.object({
2524
- fontSize: z14.number().optional(),
2525
- fontFamily: z14.string().optional(),
2526
- fontWeight: z14.number().optional(),
2527
- color: z14.string().optional()
2799
+ var fontConfigInputSchema = z15.object({
2800
+ fontSize: z15.number().optional(),
2801
+ fontFamily: z15.string().optional(),
2802
+ fontWeight: z15.number().optional(),
2803
+ color: z15.string().optional()
2528
2804
  }).optional();
2529
2805
  function mapToFrontendFontConfig(input) {
2530
2806
  if (!input) return void 0;
@@ -2535,23 +2811,23 @@ function mapToFrontendFontConfig(input) {
2535
2811
  ...input.color && { color: input.color }
2536
2812
  };
2537
2813
  }
2538
- var axisSchema = z14.object({
2539
- hideAxisLines: z14.boolean().optional().describe("Hide axis lines"),
2540
- hideSplitLines: z14.boolean().optional().describe("Hide grid/split lines"),
2541
- hideAxisLabels: z14.boolean().optional().describe("Hide axis labels"),
2542
- hideAxisTicks: z14.boolean().optional().describe("Hide axis tick marks"),
2543
- axisName: z14.string().optional().describe("Axis title text"),
2544
- axisNameOffset: z14.number().optional().describe("Axis title offset from axis"),
2545
- axisLabelMargin: z14.number().optional().describe("Margin between axis labels and chart area in pixels"),
2546
- fontSize: z14.number().optional().describe("Axis label font size"),
2547
- fontFamily: z14.string().optional().describe("Axis label font family"),
2548
- fontWeight: z14.number().optional().describe("Axis label font weight"),
2549
- color: z14.string().optional().describe("Axis label color (hex)"),
2550
- axisColor: z14.string().optional().describe("Axis line color (hex)"),
2551
- axisNameFontConfig: z14.object({
2552
- fontFamily: z14.string().optional(),
2553
- fontSize: z14.number().optional(),
2554
- fontWeight: z14.number().optional()
2814
+ var axisSchema = z15.object({
2815
+ hideAxisLines: z15.boolean().optional().describe("Hide axis lines"),
2816
+ hideSplitLines: z15.boolean().optional().describe("Hide grid/split lines"),
2817
+ hideAxisLabels: z15.boolean().optional().describe("Hide axis labels"),
2818
+ hideAxisTicks: z15.boolean().optional().describe("Hide axis tick marks"),
2819
+ axisName: z15.string().optional().describe("Axis title text"),
2820
+ axisNameOffset: z15.number().optional().describe("Axis title offset from axis"),
2821
+ axisLabelMargin: z15.number().optional().describe("Margin between axis labels and chart area in pixels"),
2822
+ fontSize: z15.number().optional().describe("Axis label font size"),
2823
+ fontFamily: z15.string().optional().describe("Axis label font family"),
2824
+ fontWeight: z15.number().optional().describe("Axis label font weight"),
2825
+ color: z15.string().optional().describe("Axis label color (hex)"),
2826
+ axisColor: z15.string().optional().describe("Axis line color (hex)"),
2827
+ axisNameFontConfig: z15.object({
2828
+ fontFamily: z15.string().optional(),
2829
+ fontSize: z15.number().optional(),
2830
+ fontWeight: z15.number().optional()
2555
2831
  }).optional().describe("Axis title font configuration")
2556
2832
  }).optional();
2557
2833
  function buildAdminTheme(p, presetBase) {
@@ -2610,37 +2886,37 @@ function buildAdminTheme(p, presetBase) {
2610
2886
  function buildComponentTheme(p) {
2611
2887
  const componentTheme = {};
2612
2888
  if (p.buttonPrimaryColor || p.buttonPrimaryTextColor || p.buttonSecondaryColor || p.buttonSecondaryTextColor) {
2613
- componentTheme.button = {
2614
- ...p.buttonPrimaryColor && { primary: p.buttonPrimaryColor },
2615
- ...p.buttonPrimaryTextColor && { primaryText: p.buttonPrimaryTextColor },
2616
- ...p.buttonSecondaryColor && { secondary: p.buttonSecondaryColor },
2617
- ...p.buttonSecondaryTextColor && { secondaryText: p.buttonSecondaryTextColor }
2618
- };
2889
+ const button = {};
2890
+ if (p.buttonPrimaryColor) button.primary = p.buttonPrimaryColor;
2891
+ if (p.buttonPrimaryTextColor) button.primaryText = p.buttonPrimaryTextColor;
2892
+ if (p.buttonSecondaryColor) button.secondary = p.buttonSecondaryColor;
2893
+ if (p.buttonSecondaryTextColor) button.secondaryText = p.buttonSecondaryTextColor;
2894
+ componentTheme.button = button;
2619
2895
  }
2620
2896
  if (p.checkboxCheckedColor || p.checkboxUncheckedColor) {
2621
- componentTheme.checkbox = {
2622
- ...p.checkboxCheckedColor && { checked: p.checkboxCheckedColor },
2623
- ...p.checkboxUncheckedColor && { unChecked: p.checkboxUncheckedColor }
2624
- };
2897
+ const checkbox = {};
2898
+ if (p.checkboxCheckedColor) checkbox.checked = p.checkboxCheckedColor;
2899
+ if (p.checkboxUncheckedColor) checkbox.unChecked = p.checkboxUncheckedColor;
2900
+ componentTheme.checkbox = checkbox;
2625
2901
  }
2626
2902
  if (p.switchEnabledColor || p.switchDisabledColor) {
2627
- componentTheme.switch = {
2628
- ...p.switchEnabledColor && { enabled: p.switchEnabledColor },
2629
- ...p.switchDisabledColor && { disabled: p.switchDisabledColor }
2630
- };
2903
+ const switchTheme = {};
2904
+ if (p.switchEnabledColor) switchTheme.enabled = p.switchEnabledColor;
2905
+ if (p.switchDisabledColor) switchTheme.disabled = p.switchDisabledColor;
2906
+ componentTheme.switch = switchTheme;
2631
2907
  }
2632
2908
  if (p.breadcrumbFontFamily || p.breadcrumbFontColor || p.breadcrumbActiveColor) {
2633
- componentTheme.drillBreadCrumbs = {
2634
- ...p.breadcrumbFontFamily && { fontFamily: p.breadcrumbFontFamily },
2635
- ...p.breadcrumbFontColor && { fontColor: p.breadcrumbFontColor },
2636
- ...p.breadcrumbActiveColor && { activeColor: p.breadcrumbActiveColor }
2637
- };
2909
+ const drillBreadCrumbs = {};
2910
+ if (p.breadcrumbFontFamily) drillBreadCrumbs.fontFamily = p.breadcrumbFontFamily;
2911
+ if (p.breadcrumbFontColor) drillBreadCrumbs.fontColor = p.breadcrumbFontColor;
2912
+ if (p.breadcrumbActiveColor) drillBreadCrumbs.activeColor = p.breadcrumbActiveColor;
2913
+ componentTheme.drillBreadCrumbs = drillBreadCrumbs;
2638
2914
  }
2639
2915
  if (p.filterBadgeColor || p.filterBadgeTextColor) {
2640
- componentTheme.multiSelectFilterDropdown = {
2641
- ...p.filterBadgeColor && { badgeColor: p.filterBadgeColor },
2642
- ...p.filterBadgeTextColor && { badgeTextColor: p.filterBadgeTextColor }
2643
- };
2916
+ const multiSelectFilterDropdown = {};
2917
+ if (p.filterBadgeColor) multiSelectFilterDropdown.badgeColor = p.filterBadgeColor;
2918
+ if (p.filterBadgeTextColor) multiSelectFilterDropdown.badgeTextColor = p.filterBadgeTextColor;
2919
+ componentTheme.multiSelectFilterDropdown = multiSelectFilterDropdown;
2644
2920
  }
2645
2921
  if (p.datePickerColor) componentTheme.datePickerColor = p.datePickerColor;
2646
2922
  const breakpoint = {};
@@ -2707,94 +2983,94 @@ function registerCustomizeTheme(server, client) {
2707
2983
  "customize_embed_theme",
2708
2984
  `Theme an embed: presets (${Object.keys(THEME_PRESETS).join("/")}), colors, fonts, cards, chart appearance, responsive layout, or access presets (${Object.keys(ACCESS_PRESETS).join("/")}).`,
2709
2985
  {
2710
- embedId: z14.string().describe("ID of the embed to theme"),
2711
- body: z14.record(z14.unknown()).optional().describe(
2986
+ embedId: z15.string().describe("ID of the embed to theme"),
2987
+ body: z15.record(z15.unknown()).optional().describe(
2712
2988
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, sent directly and all convenience fields are ignored."
2713
2989
  ),
2714
2990
  // --- Presets ---
2715
- themePreset: z14.enum(["light", "dark", "corporate", "minimal"]).optional().describe("Apply a theme preset as the base. Granular params below override preset values."),
2716
- accessPreset: z14.enum(["view-only", "power-user", "export-only", "ai-enabled"]).optional().describe("Apply an access settings preset"),
2991
+ themePreset: z15.enum(["light", "dark", "corporate", "minimal"]).optional().describe("Apply a theme preset as the base. Granular params below override preset values."),
2992
+ accessPreset: z15.enum(["view-only", "power-user", "export-only", "ai-enabled"]).optional().describe("Apply an access settings preset"),
2717
2993
  // --- General ---
2718
2994
  fontFamily: fontFamilyEnum.optional().describe("Global font family"),
2719
- datePickerFormat: z14.string().optional().describe("Date picker display format"),
2995
+ datePickerFormat: z15.string().optional().describe("Date picker display format"),
2720
2996
  // --- Colors ---
2721
- primaryColor: z14.string().optional().describe("Primary accent color (hex)"),
2722
- backgroundColor: z14.string().optional().describe("Overall background color (hex)"),
2723
- textColor: z14.string().optional().describe("Default text color (hex)"),
2724
- ctaColor: z14.string().optional().describe("Call-to-action button color (hex)"),
2725
- ctaTextColor: z14.string().optional().describe("CTA button text color (hex)"),
2997
+ primaryColor: z15.string().optional().describe("Primary accent color (hex)"),
2998
+ backgroundColor: z15.string().optional().describe("Overall background color (hex)"),
2999
+ textColor: z15.string().optional().describe("Default text color (hex)"),
3000
+ ctaColor: z15.string().optional().describe("Call-to-action button color (hex)"),
3001
+ ctaTextColor: z15.string().optional().describe("CTA button text color (hex)"),
2726
3002
  // --- Dashboard ---
2727
- dashboardBackgroundColor: z14.string().optional().describe("Dashboard container background (hex)"),
2728
- selectBoxSize: z14.enum(["small", "medium", "large"]).optional().describe("Filter select box size"),
2729
- selectBoxVariant: z14.enum(["floating", "outlined", "standard"]).optional().describe("Filter select box style"),
2730
- selectBoxBorderRadius: z14.string().optional().describe('Select box border radius (e.g. "20px")'),
2731
- selectBoxTextColor: z14.string().optional().describe("Select box text color (hex)"),
2732
- metricCardColor: z14.string().optional().describe("Metric card background color (hex)"),
3003
+ dashboardBackgroundColor: z15.string().optional().describe("Dashboard container background (hex)"),
3004
+ selectBoxSize: z15.enum(["small", "medium", "large"]).optional().describe("Filter select box size"),
3005
+ selectBoxVariant: z15.enum(["floating", "outlined", "standard"]).optional().describe("Filter select box style"),
3006
+ selectBoxBorderRadius: z15.string().optional().describe('Select box border radius (e.g. "20px")'),
3007
+ selectBoxTextColor: z15.string().optional().describe("Select box text color (hex)"),
3008
+ metricCardColor: z15.string().optional().describe("Metric card background color (hex)"),
2733
3009
  // --- Card Title ---
2734
- cardTitleFontSize: z14.string().optional().describe('Card title font size (e.g. "16px")'),
2735
- cardTitleFontWeight: z14.string().optional().describe('Card title font weight (e.g. "600")'),
2736
- cardTitleColor: z14.string().optional().describe("Card title text color (hex)"),
3010
+ cardTitleFontSize: z15.string().optional().describe('Card title font size (e.g. "16px")'),
3011
+ cardTitleFontWeight: z15.string().optional().describe('Card title font weight (e.g. "600")'),
3012
+ cardTitleColor: z15.string().optional().describe("Card title text color (hex)"),
2737
3013
  // --- Card Description ---
2738
- cardDescriptionFontSize: z14.string().optional().describe("Card description font size"),
2739
- cardDescriptionFontWeight: z14.string().optional().describe("Card description font weight"),
2740
- cardDescriptionColor: z14.string().optional().describe("Card description text color (hex)"),
3014
+ cardDescriptionFontSize: z15.string().optional().describe("Card description font size"),
3015
+ cardDescriptionFontWeight: z15.string().optional().describe("Card description font weight"),
3016
+ cardDescriptionColor: z15.string().optional().describe("Card description text color (hex)"),
2741
3017
  // --- Card Customization ---
2742
- cardPadding: z14.string().optional().describe("Card internal padding"),
2743
- cardBorderRadius: z14.string().optional().describe("Card border radius"),
2744
- cardShadow: z14.string().optional().describe("Card box-shadow CSS value"),
2745
- disableShadowOnHover: z14.boolean().optional().describe("Disable shadow on card hover"),
2746
- disableCardStroke: z14.boolean().optional().describe("Remove card border stroke"),
2747
- cardStrokeColor: z14.string().optional().describe("Card border stroke color (hex)"),
3018
+ cardPadding: z15.string().optional().describe("Card internal padding"),
3019
+ cardBorderRadius: z15.string().optional().describe("Card border radius"),
3020
+ cardShadow: z15.string().optional().describe("Card box-shadow CSS value"),
3021
+ disableShadowOnHover: z15.boolean().optional().describe("Disable shadow on card hover"),
3022
+ disableCardStroke: z15.boolean().optional().describe("Remove card border stroke"),
3023
+ cardStrokeColor: z15.string().optional().describe("Card border stroke color (hex)"),
2748
3024
  // --- Chart Colors (admin theme level) ---
2749
- chartFontFamily: z14.string().optional().describe("Font family for chart text"),
2750
- chartFontColor: z14.string().optional().describe("Font color for chart text (hex)"),
2751
- chartColors: z14.array(z14.string()).optional().describe("Array of chart series colors (hex)"),
2752
- chartPaletteName: z14.string().optional().describe("Name for a custom palette"),
2753
- chartPaletteColors: z14.array(z14.string()).optional().describe("9 colors for a custom palette"),
3025
+ chartFontFamily: z15.string().optional().describe("Font family for chart text"),
3026
+ chartFontColor: z15.string().optional().describe("Font color for chart text (hex)"),
3027
+ chartColors: z15.array(z15.string()).optional().describe("Array of chart series colors (hex)"),
3028
+ chartPaletteName: z15.string().optional().describe("Name for a custom palette"),
3029
+ chartPaletteColors: z15.array(z15.string()).optional().describe("9 colors for a custom palette"),
2754
3030
  // --- Chart Tooltip (admin theme level) ---
2755
3031
  tooltipHeader: tooltipFontSchema.describe("Tooltip header font config"),
2756
3032
  tooltipLabel: tooltipFontSchema.describe("Tooltip label font config"),
2757
3033
  tooltipValue: tooltipFontSchema.describe("Tooltip value font config"),
2758
3034
  // --- Component Theme ---
2759
- buttonPrimaryColor: z14.string().optional().describe("Primary button background (hex)"),
2760
- buttonPrimaryTextColor: z14.string().optional().describe("Primary button text (hex)"),
2761
- buttonSecondaryColor: z14.string().optional().describe("Secondary button background (hex)"),
2762
- buttonSecondaryTextColor: z14.string().optional().describe("Secondary button text (hex)"),
2763
- checkboxCheckedColor: z14.string().optional().describe("Checked checkbox color (hex)"),
2764
- checkboxUncheckedColor: z14.string().optional().describe("Unchecked checkbox color (hex)"),
2765
- switchEnabledColor: z14.string().optional().describe("Enabled switch color (hex)"),
2766
- switchDisabledColor: z14.string().optional().describe("Disabled switch color (hex)"),
2767
- breadcrumbFontFamily: z14.string().optional().describe("Drill breadcrumb font family"),
2768
- breadcrumbFontColor: z14.string().optional().describe("Drill breadcrumb font color (hex)"),
2769
- breadcrumbActiveColor: z14.string().optional().describe("Active breadcrumb color (hex)"),
2770
- filterBadgeColor: z14.string().optional().describe("Multi-select filter badge color (hex)"),
2771
- filterBadgeTextColor: z14.string().optional().describe("Filter badge text color (hex)"),
2772
- datePickerColor: z14.string().optional().describe("Date picker accent color (hex)"),
3035
+ buttonPrimaryColor: z15.string().optional().describe("Primary button background (hex)"),
3036
+ buttonPrimaryTextColor: z15.string().optional().describe("Primary button text (hex)"),
3037
+ buttonSecondaryColor: z15.string().optional().describe("Secondary button background (hex)"),
3038
+ buttonSecondaryTextColor: z15.string().optional().describe("Secondary button text (hex)"),
3039
+ checkboxCheckedColor: z15.string().optional().describe("Checked checkbox color (hex)"),
3040
+ checkboxUncheckedColor: z15.string().optional().describe("Unchecked checkbox color (hex)"),
3041
+ switchEnabledColor: z15.string().optional().describe("Enabled switch color (hex)"),
3042
+ switchDisabledColor: z15.string().optional().describe("Disabled switch color (hex)"),
3043
+ breadcrumbFontFamily: z15.string().optional().describe("Drill breadcrumb font family"),
3044
+ breadcrumbFontColor: z15.string().optional().describe("Drill breadcrumb font color (hex)"),
3045
+ breadcrumbActiveColor: z15.string().optional().describe("Active breadcrumb color (hex)"),
3046
+ filterBadgeColor: z15.string().optional().describe("Multi-select filter badge color (hex)"),
3047
+ filterBadgeTextColor: z15.string().optional().describe("Filter badge text color (hex)"),
3048
+ datePickerColor: z15.string().optional().describe("Date picker accent color (hex)"),
2773
3049
  // --- Responsive Breakpoints ---
2774
- breakpointXl: z14.number().optional().describe("XL breakpoint width in px"),
2775
- breakpointLg: z14.number().optional().describe("LG breakpoint width in px"),
2776
- breakpointMd: z14.number().optional().describe("MD breakpoint width in px"),
2777
- breakpointSm: z14.number().optional().describe("SM breakpoint width in px"),
2778
- breakpointXs: z14.number().optional().describe("XS breakpoint width in px"),
3050
+ breakpointXl: z15.number().optional().describe("XL breakpoint width in px"),
3051
+ breakpointLg: z15.number().optional().describe("LG breakpoint width in px"),
3052
+ breakpointMd: z15.number().optional().describe("MD breakpoint width in px"),
3053
+ breakpointSm: z15.number().optional().describe("SM breakpoint width in px"),
3054
+ breakpointXs: z15.number().optional().describe("XS breakpoint width in px"),
2779
3055
  // --- Metric Layout Columns ---
2780
- layoutColsXl: z14.number().optional().describe("Grid columns at XL breakpoint"),
2781
- layoutColsLg: z14.number().optional().describe("Grid columns at LG breakpoint"),
2782
- layoutColsMd: z14.number().optional().describe("Grid columns at MD breakpoint"),
2783
- layoutColsSm: z14.number().optional().describe("Grid columns at SM breakpoint"),
2784
- layoutColsXs: z14.number().optional().describe("Grid columns at XS breakpoint"),
2785
- layoutColsXxs: z14.number().optional().describe("Grid columns at XXS breakpoint"),
3056
+ layoutColsXl: z15.number().optional().describe("Grid columns at XL breakpoint"),
3057
+ layoutColsLg: z15.number().optional().describe("Grid columns at LG breakpoint"),
3058
+ layoutColsMd: z15.number().optional().describe("Grid columns at MD breakpoint"),
3059
+ layoutColsSm: z15.number().optional().describe("Grid columns at SM breakpoint"),
3060
+ layoutColsXs: z15.number().optional().describe("Grid columns at XS breakpoint"),
3061
+ layoutColsXxs: z15.number().optional().describe("Grid columns at XXS breakpoint"),
2786
3062
  // --- Chart Appearance (detailed) ---
2787
3063
  tooltipHeaderStyle: fontConfigInputSchema.describe("Tooltip header: fontSize, fontFamily, fontWeight, color"),
2788
3064
  tooltipLabelStyle: fontConfigInputSchema.describe("Tooltip label: fontSize, fontFamily, fontWeight, color"),
2789
3065
  tooltipValueStyle: fontConfigInputSchema.describe("Tooltip value: fontSize, fontFamily, fontWeight, color"),
2790
- labelPosition: z14.enum(["hidden", "top", "left", "right", "bottom", "inside"]).optional().describe("Data label position on charts"),
2791
- radialLabelPosition: z14.enum(["outside", "inside"]).optional().describe("Label position for radial/pie charts"),
2792
- marginTop: z14.number().optional().describe("Chart top margin in pixels"),
2793
- marginLeft: z14.number().optional().describe("Chart left margin in pixels"),
2794
- marginRight: z14.number().optional().describe("Chart right margin in pixels"),
2795
- marginBottom: z14.number().optional().describe("Chart bottom margin in pixels"),
2796
- legendShow: z14.boolean().optional().describe("Show or hide chart legend"),
2797
- legendPosition: z14.enum([
3066
+ labelPosition: z15.enum(["hidden", "top", "left", "right", "bottom", "inside"]).optional().describe("Data label position on charts"),
3067
+ radialLabelPosition: z15.enum(["outside", "inside"]).optional().describe("Label position for radial/pie charts"),
3068
+ marginTop: z15.number().optional().describe("Chart top margin in pixels"),
3069
+ marginLeft: z15.number().optional().describe("Chart left margin in pixels"),
3070
+ marginRight: z15.number().optional().describe("Chart right margin in pixels"),
3071
+ marginBottom: z15.number().optional().describe("Chart bottom margin in pixels"),
3072
+ legendShow: z15.boolean().optional().describe("Show or hide chart legend"),
3073
+ legendPosition: z15.enum([
2798
3074
  "top-left",
2799
3075
  "top-center",
2800
3076
  "top-right",
@@ -2804,17 +3080,17 @@ function registerCustomizeTheme(server, client) {
2804
3080
  "bottom-center",
2805
3081
  "bottom-right"
2806
3082
  ]).optional().describe("Legend position"),
2807
- legendLayout: z14.enum(["horizontal", "vertical"]).optional().describe("Legend layout direction"),
2808
- legendShape: z14.enum(["circle", "rect", "roundRect", "triangle", "diamond", "arrow", "none"]).optional().describe("Legend icon shape"),
2809
- legendFontSize: z14.number().optional().describe("Legend font size"),
2810
- legendFontWeight: z14.number().optional().describe("Legend font weight"),
2811
- legendFontFamily: z14.string().optional().describe("Legend font family"),
2812
- legendColor: z14.string().optional().describe("Legend text color (hex)"),
2813
- legendTruncateLength: z14.number().optional().describe("Max characters before truncating legend"),
2814
- disableLegendScrolling: z14.boolean().optional().describe("Disable scrolling in legend"),
2815
- enableVariableLegendPosition: z14.boolean().optional().describe("Allow user to drag legend"),
2816
- legendTop: z14.number().optional().describe("Custom legend top offset"),
2817
- legendLeft: z14.number().optional().describe("Custom legend left offset"),
3083
+ legendLayout: z15.enum(["horizontal", "vertical"]).optional().describe("Legend layout direction"),
3084
+ legendShape: z15.enum(["circle", "rect", "roundRect", "triangle", "diamond", "arrow", "none"]).optional().describe("Legend icon shape"),
3085
+ legendFontSize: z15.number().optional().describe("Legend font size"),
3086
+ legendFontWeight: z15.number().optional().describe("Legend font weight"),
3087
+ legendFontFamily: z15.string().optional().describe("Legend font family"),
3088
+ legendColor: z15.string().optional().describe("Legend text color (hex)"),
3089
+ legendTruncateLength: z15.number().optional().describe("Max characters before truncating legend"),
3090
+ disableLegendScrolling: z15.boolean().optional().describe("Disable scrolling in legend"),
3091
+ enableVariableLegendPosition: z15.boolean().optional().describe("Allow user to drag legend"),
3092
+ legendTop: z15.number().optional().describe("Custom legend top offset"),
3093
+ legendLeft: z15.number().optional().describe("Custom legend left offset"),
2818
3094
  verticalAxis: axisSchema.describe("Vertical (Y) axis configuration"),
2819
3095
  horizontalAxis: axisSchema.describe("Horizontal (X) axis configuration")
2820
3096
  },
@@ -2888,71 +3164,141 @@ Use \`get_embed_details\` to view the full current theme configuration.` + GUEST
2888
3164
  );
2889
3165
  }
2890
3166
 
3167
+ // src/tools/embed/get-demo-link.ts
3168
+ import { z as z16 } from "zod";
3169
+ var HOUR_MS = 60 * 60 * 1e3;
3170
+ var DEFAULT_EXPIRY_HOURS = 24;
3171
+ var MAX_EXPIRY_HOURS = 24 * 30;
3172
+ function registerGetDemoLink(server, client) {
3173
+ const services = createServiceBundle(client);
3174
+ server.tool(
3175
+ "get_demo_link",
3176
+ `Get a shareable demo URL for a dashboard embed scoped to a specific clientId \u2014 same link the UI's "Share Demo Link" generates. Loads the dashboard as that end-customer sees it. Use to verify ops-built widgets without a test app.`,
3177
+ {
3178
+ clientId: z16.string().describe("End-customer identifier to scope the preview to."),
3179
+ dashboardName: z16.string().optional().describe("Dashboard name (non-technical). Preferred \u2014 pass with optional `workspaceName`."),
3180
+ workspaceName: z16.string().optional().describe("Workspace name. Only needed if `dashboardName` is ambiguous."),
3181
+ embedId: z16.string().optional().describe("Technical alternative to `dashboardName`."),
3182
+ expiryHours: z16.number().min(1).max(MAX_EXPIRY_HOURS).optional().describe(`Token TTL in hours. Defaults to ${DEFAULT_EXPIRY_HOURS}. Max ${MAX_EXPIRY_HOURS} (30 days).`)
3183
+ },
3184
+ WRITE_CREATE,
3185
+ async (params) => {
3186
+ try {
3187
+ const embed = await services.widgetService.resolveEmbedId({
3188
+ embedId: params.embedId,
3189
+ dashboardName: params.dashboardName,
3190
+ workspaceName: params.workspaceName,
3191
+ required: true
3192
+ });
3193
+ if (!embed.ok) return toolError(embed.error);
3194
+ const embedId = embed.value.embedId;
3195
+ const dashboardName = embed.value.resolvedFrom?.dashboardName || null;
3196
+ const ctx = await services.widgetService.resolveEmbedContext(embedId);
3197
+ if (!ctx.dataAppName) {
3198
+ return toolError(
3199
+ `Could not resolve the data app for embed "${embedId}". The demo link needs a data app to mint a guest token against. Run \`list_data_apps\` to confirm one exists, or pass \`embedId\` for an embed with metadata.dataAppName populated.`
3200
+ );
3201
+ }
3202
+ const dashboardSlug = ctx.externalDashboardId || ctx.dashboardId;
3203
+ if (!dashboardSlug) {
3204
+ return toolError(
3205
+ `Embed "${embedId}" has no dashboardId/externalDashboardId \u2014 the demo URL requires one to load the dashboard. This is unexpected; verify the embed in the Databrain UI.`
3206
+ );
3207
+ }
3208
+ const expiryHours = params.expiryHours ?? DEFAULT_EXPIRY_HOURS;
3209
+ const { token } = await services.guestTokenService.generateToken({
3210
+ clientId: params.clientId,
3211
+ dataAppName: ctx.dataAppName,
3212
+ allowedEmbeds: [embedId],
3213
+ expiryTime: expiryHours * HOUR_MS
3214
+ });
3215
+ const baseUrl = client.getDemoDomain().replace(/\/+$/, "");
3216
+ const demoUrl = `${baseUrl}/?token=${encodeURIComponent(token)}&dashboardId=${encodeURIComponent(dashboardSlug)}`;
3217
+ return toolSuccess(
3218
+ JSON.stringify({
3219
+ demoUrl,
3220
+ clientId: params.clientId,
3221
+ dashboardName,
3222
+ embedId,
3223
+ dashboardId: dashboardSlug,
3224
+ dataAppName: ctx.dataAppName,
3225
+ expiresInHours: expiryHours,
3226
+ note: `Open this URL in a browser to preview the dashboard exactly as the end-customer sees it. This matches the UI's "Share Demo Link" flow. Override the host with DATABRAIN_DEMO_DOMAIN for self-hosted / custom preview domains.`
3227
+ })
3228
+ );
3229
+ } catch (err) {
3230
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
3231
+ return toolError(`Unexpected error: ${err.message}`);
3232
+ }
3233
+ }
3234
+ );
3235
+ }
3236
+
2891
3237
  // src/tools/frontend/generate-guest-token.ts
2892
- import { z as z15 } from "zod";
3238
+ import { z as z17 } from "zod";
2893
3239
  function registerGenerateGuestToken(server, client) {
2894
3240
  const services = createServiceBundle(client);
2895
3241
  server.tool(
2896
3242
  "generate_guest_token",
2897
3243
  "Generate a guest token for frontend embedding. Requires clientId. Omit dataAppName to auto-resolve.",
2898
3244
  {
2899
- clientId: z15.string().describe('Unique end-user identifier for multi-tenant filtering. Use "None" if no tenancy.'),
2900
- dataAppName: z15.string().optional().describe("Name of the data app. Omit to list available data apps."),
2901
- body: z15.record(z15.unknown()).optional().describe(
3245
+ clientId: z17.string().describe('Unique end-user identifier for multi-tenant filtering. Use "None" if no tenancy.'),
3246
+ dataAppName: z17.string().optional().describe("Name of the data app. Omit to list available data apps."),
3247
+ body: z17.record(z17.unknown()).optional().describe(
2902
3248
  "Raw API body for POST /api/v2/guest-token/create. If provided, sent directly and convenience fields are ignored."
2903
3249
  ),
2904
- params: z15.object({
2905
- allowedEmbeds: z15.array(z15.string()).optional().describe("Restrict token to specific embed IDs"),
2906
- rlsSettings: z15.array(z15.object({
2907
- metricId: z15.string().describe("Metric ID"),
2908
- values: z15.record(z15.unknown()).describe("RLS values by column name")
3250
+ params: z17.object({
3251
+ allowedEmbeds: z17.array(z17.string()).optional().describe("Restrict token to specific embed IDs"),
3252
+ rlsSettings: z17.array(z17.object({
3253
+ metricId: z17.string().describe("Metric ID"),
3254
+ values: z17.record(z17.unknown()).describe("RLS values by column name")
2909
3255
  })).optional().describe("Legacy alias used in docs/examples; forwarded as params.rlsSettings"),
2910
- appFilters: z15.array(z15.object({
2911
- metricId: z15.string().describe("Metric ID"),
2912
- values: z15.record(z15.unknown()).describe("Filter values by column name")
3256
+ appFilters: z17.array(z17.object({
3257
+ metricId: z17.string().describe("Metric ID"),
3258
+ values: z17.record(z17.unknown()).describe("Filter values by column name")
2913
3259
  })).optional().describe("Metric-level app filters"),
2914
- dashboardAppFilters: z15.array(z15.object({
2915
- dashboardId: z15.string().describe("Dashboard ID"),
2916
- values: z15.record(z15.unknown()).describe("Filter values by dashboard filter name"),
2917
- isShowOnUrl: z15.boolean().optional().describe("Whether filters are shown in URL params")
3260
+ dashboardAppFilters: z17.array(z17.object({
3261
+ dashboardId: z17.string().describe("Dashboard ID"),
3262
+ values: z17.record(z17.unknown()).describe("Filter values by dashboard filter name"),
3263
+ isShowOnUrl: z17.boolean().optional().describe("Whether filters are shown in URL params")
2918
3264
  })).optional().describe("Dashboard-level app filters"),
2919
- hideDashboardFilters: z15.array(z15.string()).optional().describe("Dashboard filter names to hide"),
2920
- userIdentifier: z15.string().optional().describe("End-user identifier for private metrics support"),
2921
- timezone: z15.string().optional().describe('IANA timezone, e.g. "America/New_York"')
3265
+ hideDashboardFilters: z17.array(z17.string()).optional().describe("Dashboard filter names to hide"),
3266
+ userIdentifier: z17.string().optional().describe("End-user identifier for private metrics support"),
3267
+ timezone: z17.string().optional().describe('IANA timezone, e.g. "America/New_York"')
2922
3268
  }).optional().describe("Advanced token params"),
2923
3269
  // Legacy convenience fields (auto-mapped into params for backward compatibility)
2924
- timezone: z15.string().optional().describe("Legacy: moved under params.timezone"),
2925
- allowedEmbeds: z15.array(z15.string()).optional().describe("Legacy: moved under params.allowedEmbeds"),
2926
- rlsSettings: z15.array(z15.object({
2927
- metricId: z15.string().describe("Metric ID"),
2928
- values: z15.record(z15.unknown()).describe("RLS values by column name")
3270
+ timezone: z17.string().optional().describe("Legacy: moved under params.timezone"),
3271
+ allowedEmbeds: z17.array(z17.string()).optional().describe("Legacy: moved under params.allowedEmbeds"),
3272
+ rlsSettings: z17.array(z17.object({
3273
+ metricId: z17.string().describe("Metric ID"),
3274
+ values: z17.record(z17.unknown()).describe("RLS values by column name")
2929
3275
  })).optional().describe("Legacy: moved under params.rlsSettings"),
2930
- appFilters: z15.array(z15.object({
2931
- metricId: z15.string().describe("Metric ID"),
2932
- values: z15.record(z15.unknown()).describe("Filter values by column name")
3276
+ appFilters: z17.array(z17.object({
3277
+ metricId: z17.string().describe("Metric ID"),
3278
+ values: z17.record(z17.unknown()).describe("Filter values by column name")
2933
3279
  })).optional().describe("Legacy: moved under params.appFilters"),
2934
- dashboardAppFilters: z15.array(z15.object({
2935
- dashboardId: z15.string().describe("Dashboard ID to apply filters to"),
2936
- values: z15.record(z15.unknown()).describe("Filter values for this dashboard"),
2937
- isShowOnUrl: z15.boolean().optional().describe("Whether filters are shown in URL params")
3280
+ dashboardAppFilters: z17.array(z17.object({
3281
+ dashboardId: z17.string().describe("Dashboard ID to apply filters to"),
3282
+ values: z17.record(z17.unknown()).describe("Filter values for this dashboard"),
3283
+ isShowOnUrl: z17.boolean().optional().describe("Whether filters are shown in URL params")
2938
3284
  })).optional().describe("Legacy: moved under params.dashboardAppFilters"),
2939
- permissions: z15.object({
2940
- isEnableArchiveMetrics: z15.boolean().optional(),
2941
- isEnableManageMetrics: z15.boolean().optional(),
2942
- isEnableCreateDashboardView: z15.boolean().optional(),
2943
- isEnableMetricUpdation: z15.boolean().optional(),
2944
- isEnableCustomizeLayout: z15.boolean().optional(),
2945
- isEnableUnderlyingData: z15.boolean().optional(),
2946
- isEnableDownloadMetrics: z15.boolean().optional(),
2947
- isShowSideBar: z15.boolean().optional(),
2948
- isShowDashboardName: z15.boolean().optional(),
2949
- isDisableMetricCreation: z15.boolean().optional()
3285
+ permissions: z17.object({
3286
+ isEnableArchiveMetrics: z17.boolean().optional(),
3287
+ isEnableManageMetrics: z17.boolean().optional(),
3288
+ isEnableCreateDashboardView: z17.boolean().optional(),
3289
+ isEnableMetricUpdation: z17.boolean().optional(),
3290
+ isEnableCustomizeLayout: z17.boolean().optional(),
3291
+ isEnableUnderlyingData: z17.boolean().optional(),
3292
+ isEnableDownloadMetrics: z17.boolean().optional(),
3293
+ isShowSideBar: z17.boolean().optional(),
3294
+ isShowDashboardName: z17.boolean().optional(),
3295
+ isDisableMetricCreation: z17.boolean().optional()
2950
3296
  }).optional().describe("Runtime permission overrides (merge with and override embed-level settings)"),
2951
- theme: z15.record(z15.unknown()).optional().describe("Runtime theme overrides (overrides embed-level theme)"),
2952
- hideDashboardFilters: z15.array(z15.string()).optional().describe("Legacy: moved under params.hideDashboardFilters"),
2953
- userIdentifier: z15.string().optional().describe("Legacy: moved under params.userIdentifier"),
2954
- expiryTime: z15.number().optional().describe("Token TTL in milliseconds"),
2955
- datasourceName: z15.string().optional().describe("Datasource name for multi-datasource routing")
3297
+ theme: z17.record(z17.unknown()).optional().describe("Runtime theme overrides (overrides embed-level theme)"),
3298
+ hideDashboardFilters: z17.array(z17.string()).optional().describe("Legacy: moved under params.hideDashboardFilters"),
3299
+ userIdentifier: z17.string().optional().describe("Legacy: moved under params.userIdentifier"),
3300
+ expiryTime: z17.number().optional().describe("Token TTL in milliseconds"),
3301
+ datasourceName: z17.string().optional().describe("Datasource name for multi-datasource routing")
2956
3302
  },
2957
3303
  WRITE_CREATE,
2958
3304
  async (params) => {
@@ -3005,7 +3351,7 @@ Next step: Use \`generate_embed_code\` to get copy-paste-ready frontend code.`
3005
3351
  }
3006
3352
 
3007
3353
  // src/tools/frontend/generate-embed-code.ts
3008
- import { z as z16 } from "zod";
3354
+ import { z as z18 } from "zod";
3009
3355
 
3010
3356
  // src/templates/react.ts
3011
3357
  function kebabCase(str) {
@@ -3114,7 +3460,7 @@ export default function DatabrainEmbed() {
3114
3460
  ### Environment Variables
3115
3461
  Set the following environment variables on your backend server:
3116
3462
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3117
- - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://app.usedatabrain.com)
3463
+ - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
3118
3464
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3119
3465
 
3120
3466
  ### Security
@@ -3247,7 +3593,7 @@ export default function DatabrainEmbed() {
3247
3593
  ### Environment Variables
3248
3594
  Add the following to your \`.env.local\` file:
3249
3595
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3250
- - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://app.usedatabrain.com)
3596
+ - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
3251
3597
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3252
3598
 
3253
3599
  ### Security
@@ -3379,7 +3725,7 @@ onMounted(async () => {
3379
3725
  ### Environment Variables
3380
3726
  Set the following environment variables on your backend server:
3381
3727
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3382
- - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://app.usedatabrain.com)
3728
+ - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
3383
3729
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3384
3730
 
3385
3731
  ### Security
@@ -3529,7 +3875,7 @@ export class DatabrainEmbedComponent implements OnInit {
3529
3875
  ### Environment Variables
3530
3876
  Set the following environment variables on your backend server:
3531
3877
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3532
- - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://app.usedatabrain.com)
3878
+ - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
3533
3879
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3534
3880
 
3535
3881
  ### Security
@@ -3671,7 +4017,7 @@ app.listen(3001, () => {
3671
4017
  ### Environment Variables
3672
4018
  Set the following environment variables on your backend server:
3673
4019
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3674
- - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://app.usedatabrain.com)
4020
+ - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
3675
4021
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3676
4022
 
3677
4023
  ### Security
@@ -3803,7 +4149,7 @@ export default function DatabrainEmbed() {
3803
4149
  ### Environment Variables
3804
4150
  Set the following environment variables on your backend server:
3805
4151
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3806
- - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://app.usedatabrain.com)
4152
+ - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
3807
4153
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3808
4154
 
3809
4155
  ### Security
@@ -3950,7 +4296,7 @@ app.listen(3001, () => {
3950
4296
  ### Environment Variables
3951
4297
  Set the following environment variables on your backend server:
3952
4298
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3953
- - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://app.usedatabrain.com)
4299
+ - \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
3954
4300
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3955
4301
 
3956
4302
  ### Security
@@ -4001,22 +4347,22 @@ function registerGenerateEmbedCode(server, _client) {
4001
4347
  "generate_embed_code",
4002
4348
  "Generate framework-specific embed code (React, Next.js, Vue, Angular, Svelte, SolidJS, vanilla HTML).",
4003
4349
  {
4004
- embedId: z16.string().describe("The embed ID to generate code for"),
4005
- framework: z16.enum(FRAMEWORKS).describe("Frontend framework"),
4006
- embedType: z16.enum(["dashboard", "metric"]).optional().default("dashboard"),
4007
- options: z16.object({
4008
- enableDownloadCsv: z16.boolean().optional(),
4009
- enableEmailCsv: z16.boolean().optional(),
4010
- enableDownloadAllMetrics: z16.boolean().optional(),
4011
- enableDownloadAllPdf: z16.boolean().optional(),
4012
- disableFullscreen: z16.boolean().optional(),
4013
- isHideTablePreview: z16.boolean().optional(),
4014
- isHideChartSettings: z16.boolean().optional(),
4015
- isStickyDashboardFilters: z16.boolean().optional(),
4016
- enableMultiMetricFilters: z16.boolean().optional(),
4017
- themeName: z16.string().optional(),
4018
- customTheme: z16.record(z16.unknown()).optional(),
4019
- optionsIcon: z16.enum(["kebab-menu-vertical", "download"]).optional()
4350
+ embedId: z18.string().describe("The embed ID to generate code for"),
4351
+ framework: z18.enum(FRAMEWORKS).describe("Frontend framework"),
4352
+ embedType: z18.enum(["dashboard", "metric"]).optional().default("dashboard"),
4353
+ options: z18.object({
4354
+ enableDownloadCsv: z18.boolean().optional(),
4355
+ enableEmailCsv: z18.boolean().optional(),
4356
+ enableDownloadAllMetrics: z18.boolean().optional(),
4357
+ enableDownloadAllPdf: z18.boolean().optional(),
4358
+ disableFullscreen: z18.boolean().optional(),
4359
+ isHideTablePreview: z18.boolean().optional(),
4360
+ isHideChartSettings: z18.boolean().optional(),
4361
+ isStickyDashboardFilters: z18.boolean().optional(),
4362
+ enableMultiMetricFilters: z18.boolean().optional(),
4363
+ themeName: z18.string().optional(),
4364
+ customTheme: z18.record(z18.unknown()).optional(),
4365
+ optionsIcon: z18.enum(["kebab-menu-vertical", "download"]).optional()
4020
4366
  }).optional()
4021
4367
  },
4022
4368
  READ_ONLY,
@@ -4056,17 +4402,17 @@ ${generated.instructions}`;
4056
4402
  }
4057
4403
 
4058
4404
  // src/tools/analytics/list-dashboards.ts
4059
- import { z as z17 } from "zod";
4405
+ import { z as z19 } from "zod";
4060
4406
  function registerListDashboards(server, client) {
4061
4407
  const services = createServiceBundle(client);
4062
4408
  server.tool(
4063
4409
  "list_dashboards",
4064
4410
  "List dashboards globally or for a specific data app.",
4065
4411
  {
4066
- scope: z17.enum(["auto", "global", "data_app"]).optional().describe(
4412
+ scope: z19.enum(["auto", "global", "data_app"]).optional().describe(
4067
4413
  "Dashboard listing scope. auto: use data_app when dataAppName + API token are available, otherwise global."
4068
4414
  ),
4069
- dataAppName: z17.string().optional().describe("Data app name for data_app scope. Ignored for global scope.")
4415
+ dataAppName: z19.string().optional().describe("Data app name for data_app scope. Ignored for global scope.")
4070
4416
  },
4071
4417
  READ_ONLY,
4072
4418
  async (params) => {
@@ -4101,7 +4447,7 @@ function registerListDashboards(server, client) {
4101
4447
  }
4102
4448
 
4103
4449
  // src/tools/query/ask-question.ts
4104
- import { z as z18 } from "zod";
4450
+ import { z as z20 } from "zod";
4105
4451
  var MAX_DISPLAY_ROWS = 50;
4106
4452
  function formatDataTable(data, columns) {
4107
4453
  if (!Array.isArray(data) || data.length === 0) return "_No data returned._";
@@ -4131,16 +4477,16 @@ function registerAskQuestion(server, client) {
4131
4477
  "ask_question",
4132
4478
  "Ask a natural language question \u2014 converts to SQL, returns data + chart suggestion. Requires datamartName with a populated semantic layer.",
4133
4479
  {
4134
- question: z18.string().describe(
4480
+ question: z20.string().describe(
4135
4481
  'The natural language question to ask (e.g. "What was total revenue last month?")'
4136
4482
  ),
4137
- datamartName: z18.string().optional().describe(
4483
+ datamartName: z20.string().optional().describe(
4138
4484
  "Name of the datamart to query. Use `list_datamarts` to find available names. REQUIRED unless exactly one datamart exists (auto-resolved). Do NOT pass a workspace name here."
4139
4485
  ),
4140
- sessionId: z18.string().optional().describe(
4486
+ sessionId: z20.string().optional().describe(
4141
4487
  "Session ID from a previous ask_question response, for follow-up questions."
4142
4488
  ),
4143
- timezone: z18.string().optional().describe('IANA timezone (e.g. "America/New_York"). Defaults to UTC.')
4489
+ timezone: z20.string().optional().describe('IANA timezone (e.g. "America/New_York"). Defaults to UTC.')
4144
4490
  },
4145
4491
  WRITE_CREATE,
4146
4492
  async (params) => {
@@ -4154,7 +4500,13 @@ function registerAskQuestion(server, client) {
4154
4500
  );
4155
4501
  }
4156
4502
  if (datamarts.length === 1) {
4157
- datamartName = datamarts[0].name || datamarts[0].datamartName;
4503
+ const resolvedName = datamarts[0].name || datamarts[0].datamartName;
4504
+ if (!resolvedName) {
4505
+ return toolError(
4506
+ "Found one datamart, but the API did not return a usable name. Use `list_datamarts` to inspect the response and pass `datamartName` explicitly."
4507
+ );
4508
+ }
4509
+ datamartName = resolvedName;
4158
4510
  } else {
4159
4511
  const list = datamarts.map(formatDatamart).join("\n");
4160
4512
  return toolSuccess(
@@ -4166,6 +4518,11 @@ Call \`ask_question\` again with the \`datamartName\` parameter.`
4166
4518
  );
4167
4519
  }
4168
4520
  }
4521
+ if (!datamartName) {
4522
+ return toolError(
4523
+ "Could not resolve a datamart name. Use `list_datamarts`, then call `ask_question` again with `datamartName`."
4524
+ );
4525
+ }
4169
4526
  try {
4170
4527
  const semanticLayer = await services.queryApi.getSemanticLayer(
4171
4528
  datamartName
@@ -4305,14 +4662,14 @@ To fix this:
4305
4662
  }
4306
4663
 
4307
4664
  // src/tools/query/get-semantic-layer.ts
4308
- import { z as z19 } from "zod";
4665
+ import { z as z21 } from "zod";
4309
4666
  function registerGetSemanticLayer(server, client) {
4310
4667
  const services = createServiceBundle(client);
4311
4668
  server.tool(
4312
4669
  "get_semantic_layer",
4313
4670
  "Get semantic layer metadata for a datamart: table/column descriptions, synonyms, completion score. Omit datamartName to auto-resolve.",
4314
4671
  {
4315
- datamartName: z19.string().optional().describe(
4672
+ datamartName: z21.string().optional().describe(
4316
4673
  "Name of the datamart to inspect. Omit to list available datamarts."
4317
4674
  )
4318
4675
  },
@@ -4422,15 +4779,15 @@ ${result.feedback}`);
4422
4779
  }
4423
4780
 
4424
4781
  // src/tools/query/update-semantic-layer.ts
4425
- import { z as z20 } from "zod";
4426
- var columnSchema = z20.object({
4427
- name: z20.string().describe("Column name (must exist in the datamart)"),
4428
- description: z20.string().optional().describe("Business-friendly description of what this column represents"),
4429
- synonyms: z20.array(z20.string()).optional().describe(
4782
+ import { z as z22 } from "zod";
4783
+ var columnSchema2 = z22.object({
4784
+ name: z22.string().describe("Column name (must exist in the datamart)"),
4785
+ description: z22.string().optional().describe("Business-friendly description of what this column represents"),
4786
+ synonyms: z22.array(z22.string()).optional().describe(
4430
4787
  'Alternative names users might use for this column (e.g. ["revenue", "sales amount"])'
4431
4788
  ),
4432
- miscellaneousInfo: z20.string().optional().describe("Additional context such as valid values, units, or caveats"),
4433
- columnType: z20.enum([
4789
+ miscellaneousInfo: z22.string().optional().describe("Additional context such as valid values, units, or caveats"),
4790
+ columnType: z22.enum([
4434
4791
  "String",
4435
4792
  "Long String",
4436
4793
  "String (Custom)",
@@ -4443,30 +4800,58 @@ var columnSchema = z20.object({
4443
4800
  "JSON"
4444
4801
  ]).nullable().optional().describe('Semantic column type. Use "Identifier" for IDs/foreign keys, "Number" for numeric values, "String" for text. Omit for date columns.')
4445
4802
  });
4446
- var tableSchema = z20.object({
4447
- name: z20.string().describe("Table name (must exist in the datamart)"),
4448
- description: z20.string().optional().describe(
4803
+ var tableSchema2 = z22.object({
4804
+ name: z22.string().describe("Table name (must exist in the datamart)"),
4805
+ description: z22.string().optional().describe(
4449
4806
  "Business-friendly description of what this table represents"
4450
4807
  ),
4451
- synonyms: z20.array(z20.string()).optional().describe("Alternative names for this table"),
4452
- miscellaneousInfo: z20.string().optional().describe(
4808
+ synonyms: z22.array(z22.string()).optional().describe("Alternative names for this table"),
4809
+ miscellaneousInfo: z22.string().optional().describe(
4453
4810
  "Additional context such as how the table relates to other tables"
4454
4811
  ),
4455
- columns: z20.array(columnSchema).optional().describe("Column-level semantic definitions to update")
4812
+ columns: z22.array(columnSchema2).optional().describe("Column-level semantic definitions to update")
4456
4813
  });
4814
+ function normalizeTablesForApi(tables) {
4815
+ if (!tables) return void 0;
4816
+ return tables.map((table) => {
4817
+ const normalized = { name: table.name };
4818
+ if (table.description !== void 0) normalized.description = table.description;
4819
+ if (table.synonyms !== void 0) normalized.synonyms = table.synonyms;
4820
+ if (table.miscellaneousInfo !== void 0) {
4821
+ normalized.miscellaneousInfo = table.miscellaneousInfo;
4822
+ }
4823
+ if (table.columns !== void 0) {
4824
+ normalized.columns = table.columns.map((column) => {
4825
+ const normalizedColumn = { name: column.name };
4826
+ if (column.description !== void 0) {
4827
+ normalizedColumn.description = column.description;
4828
+ }
4829
+ if (column.synonyms !== void 0) normalizedColumn.synonyms = column.synonyms;
4830
+ if (column.miscellaneousInfo !== void 0) {
4831
+ normalizedColumn.miscellaneousInfo = column.miscellaneousInfo;
4832
+ }
4833
+ if (column.columnType !== void 0 && column.columnType !== null) {
4834
+ normalizedColumn.columnType = column.columnType;
4835
+ }
4836
+ return normalizedColumn;
4837
+ });
4838
+ }
4839
+ return normalized;
4840
+ });
4841
+ }
4457
4842
  function registerUpdateSemanticLayer(server, client) {
4458
4843
  const services = createServiceBundle(client);
4459
4844
  server.tool(
4460
4845
  "update_semantic_layer",
4461
4846
  "Populate or update the semantic layer for a datamart. Add table/column descriptions, synonyms, context. Required before ask_question works. Auto-generate descriptions from column names when empty.",
4462
4847
  {
4463
- datamartName: z20.string().optional().describe(
4848
+ datamartName: z22.string().optional().describe(
4464
4849
  "Name of the datamart to update. Omit to list available datamarts."
4465
4850
  ),
4466
- tables: z20.array(tableSchema).optional().describe(
4851
+ tables: z22.array(tableSchema2).optional().describe(
4467
4852
  "Table and column definitions to update. Only include tables/columns you want to change."
4468
4853
  ),
4469
- feedback: z20.string().optional().describe(
4854
+ feedback: z22.string().optional().describe(
4470
4855
  'General feedback or context about the datamart (e.g. "This datamart contains e-commerce data for the US market").'
4471
4856
  )
4472
4857
  },
@@ -4489,9 +4874,10 @@ function registerUpdateSemanticLayer(server, client) {
4489
4874
  });
4490
4875
  if (resolution.kind !== "resolved") return resolution.response;
4491
4876
  const datamartName = resolution.name;
4877
+ const tables = normalizeTablesForApi(params.tables);
4492
4878
  await services.queryApi.updateSemanticLayer({
4493
4879
  datamartName,
4494
- tables: params.tables,
4880
+ tables,
4495
4881
  feedback: params.feedback
4496
4882
  });
4497
4883
  const updatedParts = [];
@@ -4523,18 +4909,128 @@ You can now:
4523
4909
  );
4524
4910
  }
4525
4911
 
4912
+ // src/tools/query/semantic-generation.ts
4913
+ import { z as z23 } from "zod";
4914
+ function datamartIdFrom(datamart) {
4915
+ return typeof datamart.id === "string" && datamart.id || typeof datamart._id === "string" && datamart._id || typeof datamart.datamartId === "string" && datamart.datamartId || null;
4916
+ }
4917
+ async function resolveDatamart(params) {
4918
+ const services = createServiceBundle(params.client);
4919
+ const datamarts = await services.datamartsApi.listDatamarts();
4920
+ const resolution = resolveResource({
4921
+ items: datamarts,
4922
+ name: params.datamartName,
4923
+ getName: (dm) => dm.name || dm.datamartName,
4924
+ formatItem: formatDatamart,
4925
+ resourceType: "datamart",
4926
+ toolName: params.toolName
4927
+ });
4928
+ if (resolution.kind !== "resolved") return { ok: false, response: resolution.response };
4929
+ const datamartId = datamartIdFrom(resolution.selected);
4930
+ if (!datamartId) {
4931
+ return {
4932
+ ok: false,
4933
+ response: toolError(
4934
+ `Datamart "${resolution.name}" resolved by name but no id/_id/datamartId was returned. Use list_datamarts to verify the API response includes IDs.`
4935
+ )
4936
+ };
4937
+ }
4938
+ return {
4939
+ ok: true,
4940
+ datamartName: resolution.name,
4941
+ datamartId
4942
+ };
4943
+ }
4944
+ function registerStartSemanticLayerGeneration(server, client) {
4945
+ const services = createServiceBundle(client);
4946
+ server.tool(
4947
+ "start_semantic_layer_generation",
4948
+ "Start backend semantic-layer auto-generation for a datamart. Returns an async job id to poll.",
4949
+ {
4950
+ datamartName: z23.string().optional().describe("Datamart name. Omit to list available datamarts")
4951
+ },
4952
+ WRITE_CREATE,
4953
+ async (params) => {
4954
+ try {
4955
+ const resolved = await resolveDatamart({
4956
+ client,
4957
+ datamartName: params.datamartName,
4958
+ toolName: "start_semantic_layer_generation"
4959
+ });
4960
+ if (!resolved.ok) return resolved.response;
4961
+ const result = await services.queryApi.startSemanticLayerGeneration(
4962
+ resolved.datamartId
4963
+ );
4964
+ return toolSuccess(
4965
+ JSON.stringify(
4966
+ {
4967
+ datamartName: resolved.datamartName,
4968
+ datamartId: resolved.datamartId,
4969
+ ...result,
4970
+ nextStep: "Call get_semantic_layer_generation_status with the same datamartName until status is complete or failed."
4971
+ },
4972
+ null,
4973
+ 2
4974
+ )
4975
+ );
4976
+ } catch (err) {
4977
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
4978
+ return toolError(`Unexpected error: ${err.message}`);
4979
+ }
4980
+ }
4981
+ );
4982
+ }
4983
+ function registerGetSemanticLayerGenerationStatus(server, client) {
4984
+ const services = createServiceBundle(client);
4985
+ server.tool(
4986
+ "get_semantic_layer_generation_status",
4987
+ "Get semantic-layer auto-generation job status for a datamart.",
4988
+ {
4989
+ datamartName: z23.string().optional().describe("Datamart name. Omit to list available datamarts")
4990
+ },
4991
+ READ_ONLY,
4992
+ async (params) => {
4993
+ try {
4994
+ const resolved = await resolveDatamart({
4995
+ client,
4996
+ datamartName: params.datamartName,
4997
+ toolName: "get_semantic_layer_generation_status"
4998
+ });
4999
+ if (!resolved.ok) return resolved.response;
5000
+ const result = await services.queryApi.getSemanticLayerGenerationStatus(
5001
+ resolved.datamartId
5002
+ );
5003
+ return toolSuccess(
5004
+ JSON.stringify(
5005
+ {
5006
+ datamartName: resolved.datamartName,
5007
+ datamartId: resolved.datamartId,
5008
+ ...result
5009
+ },
5010
+ null,
5011
+ 2
5012
+ )
5013
+ );
5014
+ } catch (err) {
5015
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
5016
+ return toolError(`Unexpected error: ${err.message}`);
5017
+ }
5018
+ }
5019
+ );
5020
+ }
5021
+
4526
5022
  // src/tools/widgets/list-widgets.ts
4527
- import { z as z21 } from "zod";
5023
+ import { z as z24 } from "zod";
4528
5024
  function registerListWidgets(server, client) {
4529
5025
  const services = createServiceBundle(client);
4530
5026
  server.tool(
4531
5027
  "list_widgets",
4532
5028
  "List widgets on a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous). Returns each widget's externalMetricId (uuid, used for update/remove), metricId (slug), name, published state, and creator identifier.",
4533
5029
  {
4534
- clientId: z21.string().describe("Client/tenant identifier for this dashboard"),
4535
- dashboardName: z21.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
4536
- workspaceName: z21.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
4537
- embedId: z21.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier.")
5030
+ clientId: z24.string().describe("Client/tenant identifier for this dashboard"),
5031
+ dashboardName: z24.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
5032
+ workspaceName: z24.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
5033
+ embedId: z24.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier.")
4538
5034
  },
4539
5035
  READ_ONLY,
4540
5036
  async (params) => {
@@ -4580,28 +5076,28 @@ function registerListWidgets(server, client) {
4580
5076
  }
4581
5077
 
4582
5078
  // src/tools/widgets/add-widget.ts
4583
- import { z as z22 } from "zod";
5079
+ import { z as z25 } from "zod";
4584
5080
  function registerAddWidget(server, client) {
4585
5081
  const services = createServiceBundle(client);
4586
5082
  server.tool(
4587
5083
  "add_widget",
4588
5084
  "Add a widget to a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous). Cloning via `templateMetricId` is the recommended path \u2014 query, chart config, and datasource are all inherited. Data app and datasource auto-resolve; never ask the user for them.",
4589
5085
  {
4590
- clientId: z22.string().describe("Client/tenant identifier (widget is created in their scope)"),
4591
- dashboardName: z22.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
4592
- workspaceName: z22.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
4593
- embedId: z22.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
4594
- dataAppName: z22.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
4595
- templateMetricId: z22.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
4596
- name: z22.string().optional().describe("Widget name (overrides template)"),
4597
- metricId: z22.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
4598
- query: z22.string().optional().describe("SQL query body (overrides template)"),
4599
- chartOptions: z22.record(z22.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
4600
- description: z22.string().optional(),
4601
- companyIntegrationId: z22.string().optional().describe("Datasource UUID override. Auto-resolved from `embedId` or sole active datasource; leave blank unless the user explicitly picked one by ID."),
4602
- integrationName: z22.string().optional().describe('Datasource display name (e.g. "Snowflake"). Use this when the user picks a datasource by name \u2014 the id is looked up automatically.'),
4603
- isPublished: z22.boolean().optional().describe("Publish immediately (default true)"),
4604
- body: z22.record(z22.unknown()).optional().describe("Raw createMetric body passthrough. If provided, used as-is (after defaults). Overrides all other params.")
5086
+ clientId: z25.string().describe("Client/tenant identifier (widget is created in their scope)"),
5087
+ dashboardName: z25.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
5088
+ workspaceName: z25.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
5089
+ embedId: z25.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
5090
+ dataAppName: z25.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
5091
+ templateMetricId: z25.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
5092
+ name: z25.string().optional().describe("Widget name (overrides template)"),
5093
+ metricId: z25.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
5094
+ query: z25.string().optional().describe("SQL query body (overrides template)"),
5095
+ chartOptions: z25.record(z25.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
5096
+ description: z25.string().optional(),
5097
+ companyIntegrationId: z25.string().optional().describe("Datasource UUID override. Auto-resolved from `embedId` or sole active datasource; leave blank unless the user explicitly picked one by ID."),
5098
+ integrationName: z25.string().optional().describe('Datasource display name (e.g. "Snowflake"). Use this when the user picks a datasource by name \u2014 the id is looked up automatically.'),
5099
+ isPublished: z25.boolean().optional().describe("Publish immediately (default true)"),
5100
+ body: z25.record(z25.unknown()).optional().describe("Raw createMetric body passthrough. If provided, used as-is (after defaults). Overrides all other params.")
4605
5101
  },
4606
5102
  WRITE_CREATE,
4607
5103
  async (params) => {
@@ -4711,20 +5207,20 @@ Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust
4711
5207
  }
4712
5208
 
4713
5209
  // src/tools/widgets/update-widget.ts
4714
- import { z as z23 } from "zod";
5210
+ import { z as z26 } from "zod";
4715
5211
  function registerUpdateWidget(server, client) {
4716
5212
  const services = createServiceBundle(client);
4717
5213
  server.tool(
4718
5214
  "update_widget",
4719
5215
  "Update a widget on a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous) + `externalMetricId` + `set`. Data app auto-resolves from the dashboard.",
4720
5216
  {
4721
- externalMetricId: z23.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
4722
- clientId: z23.string().describe("Client/tenant identifier"),
4723
- dashboardName: z23.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
4724
- workspaceName: z23.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
4725
- embedId: z23.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
4726
- dataAppName: z23.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank."),
4727
- set: z23.record(z23.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
5217
+ externalMetricId: z26.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
5218
+ clientId: z26.string().describe("Client/tenant identifier"),
5219
+ dashboardName: z26.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
5220
+ workspaceName: z26.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
5221
+ embedId: z26.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
5222
+ dataAppName: z26.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank."),
5223
+ set: z26.record(z26.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
4728
5224
  },
4729
5225
  WRITE_IDEMPOTENT,
4730
5226
  async (params) => {
@@ -4779,19 +5275,19 @@ Use \`list_widgets\` to verify.`
4779
5275
  }
4780
5276
 
4781
5277
  // src/tools/widgets/remove-widget.ts
4782
- import { z as z24 } from "zod";
5278
+ import { z as z27 } from "zod";
4783
5279
  function registerRemoveWidget(server, client) {
4784
5280
  const services = createServiceBundle(client);
4785
5281
  server.tool(
4786
5282
  "remove_widget",
4787
5283
  "Remove a widget from a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous) + `externalMetricId`. Per-client soft delete \u2014 the widget stays on the template dashboard and on other customers' dashboards.",
4788
5284
  {
4789
- externalMetricId: z24.string().describe("Internal uuid of the widget (externalMetrics.id)"),
4790
- clientId: z24.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
4791
- dashboardName: z24.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
4792
- workspaceName: z24.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
4793
- embedId: z24.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
4794
- dataAppName: z24.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank.")
5285
+ externalMetricId: z27.string().describe("Internal uuid of the widget (externalMetrics.id)"),
5286
+ clientId: z27.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
5287
+ dashboardName: z27.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
5288
+ workspaceName: z27.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
5289
+ embedId: z27.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
5290
+ dataAppName: z27.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank.")
4795
5291
  },
4796
5292
  DESTRUCTIVE,
4797
5293
  async (params) => {
@@ -4839,21 +5335,21 @@ Note: this is a per-client soft delete. The widget remains available for other c
4839
5335
  }
4840
5336
 
4841
5337
  // src/tools/widgets/generate-widget.ts
4842
- import { z as z25 } from "zod";
5338
+ import { z as z28 } from "zod";
4843
5339
  function registerGenerateWidget(server, client) {
4844
5340
  const services = createServiceBundle(client);
4845
5341
  server.tool(
4846
5342
  "generate_widget",
4847
5343
  "AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft to pass to `add_widget`. Non-technical flow: `clientId` + `dashboardName` (+ `workspaceName` if ambiguous); data app and datasource auto-resolve. Never ask the user for data app, datasource, or integration.",
4848
5344
  {
4849
- clientId: z25.string().describe("Client/tenant identifier"),
4850
- userInputs: z25.record(z25.unknown()).describe('Prompt + context object. Typical shape: { prompt: "calls per day over last 30 days", schema: {...}, examples: [...] }. Backend decides which keys it honors.'),
4851
- dashboardName: z25.string().optional().describe("Dashboard name (non-technical). Preferred input for CS/CX flows \u2014 pass this with optional `workspaceName` instead of `embedId`."),
4852
- workspaceName: z25.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
4853
- embedId: z25.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
4854
- dataAppName: z25.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
4855
- integrationId: z25.string().optional().describe("Technical override \u2014 the datasource UUID. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets."),
4856
- integrationName: z25.string().optional().describe("Technical override \u2014 datasource display name. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets.")
5345
+ clientId: z28.string().describe("Client/tenant identifier"),
5346
+ userInputs: z28.record(z28.unknown()).describe('Prompt + context object. Typical shape: { prompt: "calls per day over last 30 days", schema: {...}, examples: [...] }. Backend decides which keys it honors.'),
5347
+ dashboardName: z28.string().optional().describe("Dashboard name (non-technical). Preferred input for CS/CX flows \u2014 pass this with optional `workspaceName` instead of `embedId`."),
5348
+ workspaceName: z28.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
5349
+ embedId: z28.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
5350
+ dataAppName: z28.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
5351
+ integrationId: z28.string().optional().describe("Technical override \u2014 the datasource UUID. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets."),
5352
+ integrationName: z28.string().optional().describe("Technical override \u2014 datasource display name. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets.")
4857
5353
  },
4858
5354
  READ_ONLY,
4859
5355
  async (params) => {
@@ -4918,13 +5414,417 @@ function registerGenerateWidget(server, client) {
4918
5414
  );
4919
5415
  }
4920
5416
 
5417
+ // src/tools/widgets/create-or-update-metric-from-query.ts
5418
+ import { z as z29 } from "zod";
5419
+ var modeSchema = z29.enum(["dry_run", "apply"]).default("dry_run");
5420
+ var conflictStrategySchema = z29.enum(["fail", "update", "create_new"]).default("fail");
5421
+ function makePlanText(plan) {
5422
+ return JSON.stringify(plan, null, 2);
5423
+ }
5424
+ function externalMetricIdFrom(widget) {
5425
+ return typeof widget.externalMetricId === "string" && widget.externalMetricId || typeof widget.id === "string" && widget.id || typeof widget._id === "string" && widget._id || null;
5426
+ }
5427
+ function createSuffix() {
5428
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
5429
+ }
5430
+ function registerCreateOrUpdateMetricFromQuery(server, client) {
5431
+ const services = createServiceBundle(client);
5432
+ server.tool(
5433
+ "create_or_update_metric_from_query",
5434
+ "Create or update a production metric/widget from validated SQL. Defaults to dry-run; apply requires explicit confirmation.",
5435
+ {
5436
+ mode: modeSchema.describe("dry_run returns the plan only; apply executes after confirmation"),
5437
+ confirm: z29.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
5438
+ conflictStrategy: conflictStrategySchema.describe("What to do if metricId already exists on the target dashboard"),
5439
+ clientId: z29.string().describe("Client/tenant identifier for the dashboard widget scope"),
5440
+ workspaceName: z29.string().describe("Target workspace name"),
5441
+ dashboardName: z29.string().optional().describe("Dashboard name to attach to"),
5442
+ embedId: z29.string().optional().describe("Technical embed id alternative to dashboardName"),
5443
+ dataAppName: z29.string().optional().describe("Optional data app override"),
5444
+ name: z29.string().describe("Metric/widget display name"),
5445
+ metricId: z29.string().describe("Stable metric slug to create or update"),
5446
+ query: z29.string().describe("SQL query already validated against the warehouse"),
5447
+ description: z29.string().optional().describe("Metric description"),
5448
+ chartOptions: z29.record(z29.unknown()).optional().describe("Chart configuration JSON"),
5449
+ companyIntegrationId: z29.string().optional().describe("Datasource UUID override"),
5450
+ integrationName: z29.string().optional().describe("Datasource display name override"),
5451
+ isPublished: z29.boolean().optional().describe("Publish after apply. Defaults to false for production safety"),
5452
+ requestedBy: z29.string().optional().describe("Human operator/request context to store on the metric when supported")
5453
+ },
5454
+ WRITE_IDEMPOTENT,
5455
+ async (params) => {
5456
+ try {
5457
+ const mode = params.mode || "dry_run";
5458
+ const conflictStrategy = params.conflictStrategy || "fail";
5459
+ if (!params.name.trim()) return toolError("`name` cannot be empty.");
5460
+ if (!params.metricId.trim()) return toolError("`metricId` cannot be empty.");
5461
+ if (!params.query.trim()) return toolError("`query` cannot be empty.");
5462
+ const embed = await services.widgetService.resolveEmbedId({
5463
+ embedId: params.embedId,
5464
+ dashboardName: params.dashboardName,
5465
+ workspaceName: params.workspaceName,
5466
+ required: true
5467
+ });
5468
+ if (!embed.ok) return toolError(embed.error);
5469
+ const embedId = embed.value.embedId;
5470
+ const ctx = await services.widgetService.resolveEmbedContext(embedId);
5471
+ if (!ctx.externalDashboardId) {
5472
+ return toolError(
5473
+ `Dashboard "${params.dashboardName || embedId}" has no externalDashboardId. Verify with get_embed_details before creating metrics.`
5474
+ );
5475
+ }
5476
+ const dataAppName = await services.widgetService.resolveDataAppName(
5477
+ params.dataAppName,
5478
+ embedId
5479
+ );
5480
+ if (!dataAppName.ok) return toolError(dataAppName.error);
5481
+ const guestToken = await services.widgetService.mintGuestToken(
5482
+ params.clientId,
5483
+ dataAppName.value,
5484
+ [embedId]
5485
+ );
5486
+ const datasource = await services.widgetService.resolveDatasource(guestToken, {
5487
+ embedId,
5488
+ clientId: params.clientId,
5489
+ integrationId: params.companyIntegrationId,
5490
+ integrationName: params.integrationName
5491
+ });
5492
+ if (!datasource.ok) return toolError(datasource.error);
5493
+ const widgets = await services.widgetsApi.listWidgets(embedId, params.clientId);
5494
+ const existing = widgets.find((widget) => widget.metricId === params.metricId);
5495
+ const existingExternalMetricId = existing ? externalMetricIdFrom(existing) : null;
5496
+ if (existing && conflictStrategy === "fail") {
5497
+ return toolError(
5498
+ `MetricId "${params.metricId}" already exists on this dashboard. Retry with conflictStrategy="update" to update it or "create_new" to create a suffixed copy.`
5499
+ );
5500
+ }
5501
+ if (existing && conflictStrategy === "update" && !existingExternalMetricId) {
5502
+ return toolError(
5503
+ `MetricId "${params.metricId}" exists but no externalMetricId/id was returned, so it cannot be updated safely.`
5504
+ );
5505
+ }
5506
+ const finalMetricId = existing && conflictStrategy === "create_new" ? `${params.metricId}-${createSuffix()}` : params.metricId;
5507
+ const createBody = {
5508
+ name: params.name,
5509
+ metricId: finalMetricId,
5510
+ query: params.query,
5511
+ description: params.description || "",
5512
+ chartOptions: params.chartOptions || {},
5513
+ companyIntegrationId: datasource.value.integrationId,
5514
+ integrationName: datasource.value.integrationName,
5515
+ isPublished: params.isPublished ?? false,
5516
+ isCreatedByClient: false,
5517
+ createdBy: params.requestedBy,
5518
+ externalDashboardIds: [{ externalDashboardId: ctx.externalDashboardId }]
5519
+ };
5520
+ const updateSet = {
5521
+ name: params.name,
5522
+ query: params.query,
5523
+ description: params.description || "",
5524
+ chartOptions: params.chartOptions || {},
5525
+ companyIntegrationId: datasource.value.integrationId,
5526
+ integrationName: datasource.value.integrationName,
5527
+ isPublished: params.isPublished ?? false,
5528
+ createdBy: params.requestedBy
5529
+ };
5530
+ const operation = existing && conflictStrategy === "update" ? "update_metric" : "create_metric";
5531
+ const plan = {
5532
+ mode,
5533
+ operation,
5534
+ safety: productionSafetyNote(params.workspaceName),
5535
+ requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
5536
+ resolved: {
5537
+ workspaceName: params.workspaceName,
5538
+ dashboardName: embed.value.resolvedFrom?.dashboardName || params.dashboardName || null,
5539
+ embedId,
5540
+ dataAppName: dataAppName.value,
5541
+ externalDashboardId: ctx.externalDashboardId,
5542
+ clientId: params.clientId,
5543
+ datasource: datasource.value
5544
+ },
5545
+ conflict: existing ? {
5546
+ metricId: existing.metricId || params.metricId,
5547
+ externalMetricId: existingExternalMetricId,
5548
+ strategy: conflictStrategy
5549
+ } : null,
5550
+ metric: operation === "update_metric" ? { externalMetricId: existingExternalMetricId, set: updateSet } : createBody
5551
+ };
5552
+ if (mode === "dry_run") {
5553
+ return toolSuccess(
5554
+ makePlanText({
5555
+ ...plan,
5556
+ status: "dry_run_only",
5557
+ nextStep: `Review this plan. To execute, call again with mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}".`
5558
+ })
5559
+ );
5560
+ }
5561
+ const guardError = validateProductionApply({
5562
+ workspaceName: params.workspaceName,
5563
+ confirm: params.confirm
5564
+ });
5565
+ if (guardError) return toolError(guardError);
5566
+ if (operation === "update_metric") {
5567
+ const result2 = await services.widgetsApi.updateMetric(guestToken, {
5568
+ externalMetricId: existingExternalMetricId,
5569
+ set: updateSet
5570
+ });
5571
+ if (result2.error) {
5572
+ return toolError(`Metric update failed: ${stringify(result2.error)}`);
5573
+ }
5574
+ return toolSuccess(
5575
+ makePlanText({
5576
+ ...plan,
5577
+ status: "applied",
5578
+ result: result2,
5579
+ verifyWith: "list_widgets"
5580
+ })
5581
+ );
5582
+ }
5583
+ const result = await services.widgetsApi.createMetric(guestToken, createBody);
5584
+ if (result.error) {
5585
+ return toolError(`Metric creation failed: ${stringify(result.error)}`);
5586
+ }
5587
+ return toolSuccess(
5588
+ makePlanText({
5589
+ ...plan,
5590
+ status: "applied",
5591
+ result,
5592
+ verifyWith: "list_widgets"
5593
+ })
5594
+ );
5595
+ } catch (err) {
5596
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
5597
+ return toolError(`Unexpected error: ${err.message}`);
5598
+ }
5599
+ }
5600
+ );
5601
+ }
5602
+
5603
+ // src/tools/widgets/validate-metric-spec.ts
5604
+ import { z as z30 } from "zod";
5605
+ function registerValidateMetricSpec(server, client) {
5606
+ const services = createServiceBundle(client);
5607
+ server.tool(
5608
+ "validate_metric_spec",
5609
+ "Validate a production metric spec from SQL without saving it. Resolves dashboard, data app, datasource, conflicts, and required fields.",
5610
+ {
5611
+ clientId: z30.string().describe("Client/tenant identifier used to validate dashboard widget scope"),
5612
+ workspaceName: z30.string().describe("Target production-backed workspace name"),
5613
+ dashboardName: z30.string().optional().describe("Dashboard name to attach to"),
5614
+ embedId: z30.string().optional().describe("Technical embed id alternative to dashboardName"),
5615
+ dataAppName: z30.string().optional().describe("Optional data app override"),
5616
+ name: z30.string().describe("Metric/widget display name"),
5617
+ metricId: z30.string().describe("Stable metric slug to create or update"),
5618
+ query: z30.string().describe("SQL query already validated against the warehouse"),
5619
+ description: z30.string().optional().describe("Metric description"),
5620
+ chartOptions: z30.record(z30.unknown()).optional().describe("Chart configuration JSON"),
5621
+ companyIntegrationId: z30.string().optional().describe("Datasource UUID override"),
5622
+ integrationName: z30.string().optional().describe("Datasource display name override")
5623
+ },
5624
+ READ_ONLY,
5625
+ async (params) => {
5626
+ try {
5627
+ if (!params.name.trim()) return toolError("`name` cannot be empty.");
5628
+ if (!params.metricId.trim()) return toolError("`metricId` cannot be empty.");
5629
+ if (!params.query.trim()) return toolError("`query` cannot be empty.");
5630
+ const embed = await services.widgetService.resolveEmbedId({
5631
+ embedId: params.embedId,
5632
+ dashboardName: params.dashboardName,
5633
+ workspaceName: params.workspaceName,
5634
+ required: true
5635
+ });
5636
+ if (!embed.ok) return toolError(embed.error);
5637
+ const embedId = embed.value.embedId;
5638
+ const ctx = await services.widgetService.resolveEmbedContext(embedId);
5639
+ if (!ctx.externalDashboardId) {
5640
+ return toolError(
5641
+ `Dashboard "${params.dashboardName || embedId}" has no externalDashboardId. Verify with get_embed_details before creating metrics.`
5642
+ );
5643
+ }
5644
+ const dataAppName = await services.widgetService.resolveDataAppName(
5645
+ params.dataAppName,
5646
+ embedId
5647
+ );
5648
+ if (!dataAppName.ok) return toolError(dataAppName.error);
5649
+ const guestToken = await services.widgetService.mintGuestToken(
5650
+ params.clientId,
5651
+ dataAppName.value,
5652
+ [embedId]
5653
+ );
5654
+ const datasource = await services.widgetService.resolveDatasource(guestToken, {
5655
+ embedId,
5656
+ clientId: params.clientId,
5657
+ integrationId: params.companyIntegrationId,
5658
+ integrationName: params.integrationName
5659
+ });
5660
+ if (!datasource.ok) return toolError(datasource.error);
5661
+ const widgets = await services.widgetsApi.listWidgets(embedId, params.clientId);
5662
+ const existing = widgets.find((widget) => widget.metricId === params.metricId);
5663
+ return toolSuccess(
5664
+ JSON.stringify(
5665
+ {
5666
+ status: "valid_for_production_dry_run",
5667
+ safety: productionSafetyNote(params.workspaceName),
5668
+ resolved: {
5669
+ workspaceName: params.workspaceName,
5670
+ dashboardName: embed.value.resolvedFrom?.dashboardName || params.dashboardName || null,
5671
+ embedId,
5672
+ dataAppName: dataAppName.value,
5673
+ externalDashboardId: ctx.externalDashboardId,
5674
+ datasource
5675
+ },
5676
+ conflict: existing ? {
5677
+ strategyNeeded: true,
5678
+ metricId: existing.metricId,
5679
+ externalMetricId: existing.externalMetricId || existing.id || existing._id || null,
5680
+ name: existing.name || null
5681
+ } : null,
5682
+ proposedMetric: {
5683
+ name: params.name,
5684
+ metricId: params.metricId,
5685
+ query: params.query,
5686
+ description: params.description || "",
5687
+ chartOptions: params.chartOptions || {},
5688
+ isPublishedDefault: false
5689
+ },
5690
+ nextStep: `Call create_or_update_metric_from_query with mode="dry_run" first, then mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}" only after reviewing the plan.`
5691
+ },
5692
+ null,
5693
+ 2
5694
+ )
5695
+ );
5696
+ } catch (err) {
5697
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
5698
+ return toolError(`Unexpected error: ${err.message}`);
5699
+ }
5700
+ }
5701
+ );
5702
+ }
5703
+
5704
+ // src/tools/reports/list-scheduled-reports.ts
5705
+ import { z as z31 } from "zod";
5706
+ function registerListScheduledReports(server, client) {
5707
+ const services = createServiceBundle(client);
5708
+ server.tool(
5709
+ "list_scheduled_reports",
5710
+ "List configured scheduled reports for a specific client/dashboard embed. Shows schedule metadata, not delivery success history.",
5711
+ {
5712
+ clientId: z31.string().describe("Client/tenant identifier. Required by the current backend API for non-empty results"),
5713
+ dashboardName: z31.string().optional().describe("Dashboard name to inspect"),
5714
+ workspaceName: z31.string().optional().describe("Workspace name if dashboardName is ambiguous"),
5715
+ embedId: z31.string().optional().describe("Technical embed id alternative to dashboardName"),
5716
+ userIdentifier: z31.string().optional().describe("Optional creator/user filter"),
5717
+ pageNumber: z31.number().int().positive().optional().describe("Page number when pagination is enabled"),
5718
+ isPagination: z31.boolean().optional().describe("Enable backend pagination")
5719
+ },
5720
+ READ_ONLY,
5721
+ async (params) => {
5722
+ try {
5723
+ const embed = await services.widgetService.resolveEmbedId({
5724
+ embedId: params.embedId,
5725
+ dashboardName: params.dashboardName,
5726
+ workspaceName: params.workspaceName,
5727
+ required: true
5728
+ });
5729
+ if (!embed.ok) return toolError(embed.error);
5730
+ const embedId = embed.value.embedId;
5731
+ const reports = await services.scheduledReportsApi.listByEmbed({
5732
+ embedId,
5733
+ clientId: params.clientId,
5734
+ userIdentifier: params.userIdentifier,
5735
+ pageNumber: params.pageNumber,
5736
+ isPagination: params.isPagination
5737
+ });
5738
+ return toolSuccess(
5739
+ JSON.stringify(
5740
+ {
5741
+ count: Array.isArray(reports) ? reports.length : 0,
5742
+ dashboardName: embed.value.resolvedFrom?.dashboardName || params.dashboardName || null,
5743
+ embedId,
5744
+ clientId: params.clientId,
5745
+ note: "This lists configured schedules only. Delivery success/failure history is not persisted in the current MCP/API surface yet.",
5746
+ reports: (Array.isArray(reports) ? reports : []).map((report) => ({
5747
+ id: report.id || null,
5748
+ subject: report.subject || null,
5749
+ nextScheduledAt: report.nextScheduledAt || null,
5750
+ recipientCount: Array.isArray(report.emails) ? report.emails.length : null,
5751
+ chartCount: Array.isArray(report.scheduleEmailReportCharts) ? report.scheduleEmailReportCharts.length : null,
5752
+ isDownloadFile: report.isDownloadFile ?? null,
5753
+ downloadFileFormat: report.downloadFileFormat || null,
5754
+ timeConfigurations: report.timeConfigurations || null
5755
+ }))
5756
+ },
5757
+ null,
5758
+ 2
5759
+ )
5760
+ );
5761
+ } catch (err) {
5762
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
5763
+ return toolError(`Unexpected error: ${err.message}`);
5764
+ }
5765
+ }
5766
+ );
5767
+ }
5768
+
5769
+ // src/tools/auth/whoami.ts
5770
+ function registerWhoami(server, client) {
5771
+ server.tool(
5772
+ "whoami",
5773
+ "Return the credential currently used for API calls. Use to confirm which auth mode this MCP session is operating under before mutating actions.",
5774
+ {},
5775
+ READ_ONLY,
5776
+ async () => {
5777
+ const auth = client.getActiveAuth();
5778
+ if (auth.source === "service-token") {
5779
+ return toolSuccess(
5780
+ JSON.stringify(
5781
+ {
5782
+ status: "authenticated",
5783
+ activeBearer: "service-token",
5784
+ note: "Org-level service token (DATABRAIN_SERVICE_TOKEN). Per-user identity is NOT bound \u2014 mutations are audit-logged as actor=system. Per-user binding will be available once the OAuth login flow ships."
5785
+ },
5786
+ null,
5787
+ 2
5788
+ )
5789
+ );
5790
+ }
5791
+ if (auth.source === "api-token") {
5792
+ return toolSuccess(
5793
+ JSON.stringify(
5794
+ {
5795
+ status: "authenticated",
5796
+ activeBearer: "api-token",
5797
+ note: "Per-data-app API token. Limited to embedding operations on that data app. Org-level setup tools require DATABRAIN_SERVICE_TOKEN."
5798
+ },
5799
+ null,
5800
+ 2
5801
+ )
5802
+ );
5803
+ }
5804
+ return toolSuccess(
5805
+ JSON.stringify(
5806
+ {
5807
+ status: "not_authenticated",
5808
+ activeBearer: "none",
5809
+ note: "No usable credential is configured. Set DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN in the MCP config."
5810
+ },
5811
+ null,
5812
+ 2
5813
+ )
5814
+ );
5815
+ }
5816
+ );
5817
+ }
5818
+
4921
5819
  // src/tools/registry.ts
4922
5820
  function registerAllTools(server, client) {
5821
+ registerWhoami(server, client);
4923
5822
  registerListDatasources(server, client);
4924
5823
  registerDiscoverDatasourceSchema(server, client);
4925
5824
  registerListDatamarts(server, client);
4926
5825
  registerListWorkspaces(server, client);
4927
- registerLinkDatamartToWorkspace(server, client);
5826
+ registerSyncDatasource(server, client);
5827
+ registerCreateDatamart(server, client);
4928
5828
  registerListDataApps(server, client);
4929
5829
  registerCreateDataApp(server, client);
4930
5830
  registerListApiTokens(server, client);
@@ -4935,17 +5835,23 @@ function registerAllTools(server, client) {
4935
5835
  registerGetEmbedDetails(server, client);
4936
5836
  registerUpdateEmbed(server, client);
4937
5837
  registerCustomizeTheme(server, client);
5838
+ registerGetDemoLink(server, client);
4938
5839
  registerGenerateGuestToken(server, client);
4939
5840
  registerGenerateEmbedCode(server, client);
4940
5841
  registerListDashboards(server, client);
4941
5842
  registerAskQuestion(server, client);
4942
5843
  registerGetSemanticLayer(server, client);
4943
5844
  registerUpdateSemanticLayer(server, client);
5845
+ registerStartSemanticLayerGeneration(server, client);
5846
+ registerGetSemanticLayerGenerationStatus(server, client);
4944
5847
  registerListWidgets(server, client);
5848
+ registerValidateMetricSpec(server, client);
5849
+ registerCreateOrUpdateMetricFromQuery(server, client);
4945
5850
  registerAddWidget(server, client);
4946
5851
  registerUpdateWidget(server, client);
4947
5852
  registerRemoveWidget(server, client);
4948
5853
  registerGenerateWidget(server, client);
5854
+ registerListScheduledReports(server, client);
4949
5855
  }
4950
5856
 
4951
5857
  // src/resources/content/getting-started.ts
@@ -7308,7 +8214,7 @@ Then create an embed configuration with tenancy settings:
7308
8214
 
7309
8215
  ### Step 5: Generate Guest Tokens Per Tenant
7310
8216
 
7311
- On your backend, when a user from "Acme Corp" (org_id = "acme") logs in:
8217
+ On your backend, when a user from tenant A (org_id = "tenant_a") logs in:
7312
8218
 
7313
8219
  \`\`\`typescript
7314
8220
  // Server-side
@@ -7320,12 +8226,12 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
7320
8226
  },
7321
8227
  body: JSON.stringify({
7322
8228
  embedId: 'embed-789',
7323
- clientId: 'acme', // Maps to org_id values in the database
8229
+ clientId: 'tenant_a', // Maps to org_id values in the database
7324
8230
  }),
7325
8231
  });
7326
8232
  \`\`\`
7327
8233
 
7328
- When a user from "Globex" (org_id = "globex") logs in:
8234
+ When a user from tenant B (org_id = "tenant_b") logs in:
7329
8235
 
7330
8236
  \`\`\`typescript
7331
8237
  const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/create', {
@@ -7336,7 +8242,7 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
7336
8242
  },
7337
8243
  body: JSON.stringify({
7338
8244
  embedId: 'embed-789',
7339
- clientId: 'globex',
8245
+ clientId: 'tenant_b',
7340
8246
  }),
7341
8247
  });
7342
8248
  \`\`\`
@@ -7344,20 +8250,20 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
7344
8250
  ### Step 6: Render
7345
8251
 
7346
8252
  Both users see the same dashboard layout and metrics, but:
7347
- - Acme sees only their projects, tasks, and team members
7348
- - Globex sees only their projects, tasks, and team members
8253
+ - Tenant A sees only their projects, tasks, and team members
8254
+ - Tenant B sees only their projects, tasks, and team members
7349
8255
  - Both see all task statuses (shared reference table)
7350
8256
 
7351
8257
  ### Result
7352
8258
 
7353
8259
  \`\`\`
7354
- Acme User sees:
7355
- - 12 projects (all with org_id = 'acme')
8260
+ Tenant A user sees:
8261
+ - 12 projects (all with org_id = 'tenant_a')
7356
8262
  - 87 tasks
7357
8263
  - 15 team members
7358
8264
 
7359
- Globex User sees:
7360
- - 8 projects (all with org_id = 'globex')
8265
+ Tenant B user sees:
8266
+ - 8 projects (all with org_id = 'tenant_b')
7361
8267
  - 45 tasks
7362
8268
  - 9 team members
7363
8269
  \`\`\`
@@ -8018,8 +8924,10 @@ Wait for explicit confirmation before calling \`remove_widget({ externalMetricId
8018
8924
  **Step 4 \u2014 Verify**
8019
8925
  Call \`list_widgets({ clientId, dashboardName, workspaceName? })\` again. Present the updated list and confirm the changes match what the user asked for.
8020
8926
 
8021
- **Step 5 \u2014 (Optional) regenerate guest token for live preview**
8022
- If the user wants to see the changes in their running app, regenerate their guest token and reload. The MCP tools mint tokens internally for widget operations; a cached frontend token will not show the changes until refreshed.
8927
+ **Step 5 \u2014 Hand off the demo link**
8928
+ Always close with a demo link so the user can verify visually. Call \`get_demo_link({ clientId, dashboardName, workspaceName? })\` \u2014 it returns the same URL the Databrain UI's "Share Demo Link" button generates: a hosted preview page that loads the dashboard exactly as that \`clientId\` will see it. Default expiry is 24h; pass \`expiryHours\` to override.
8929
+
8930
+ Present the URL to the user and remind them: (a) only \`isPublished: true\` widgets appear in the preview, so flag any unpublished widgets from the Step 4 \`list_widgets\` output; (b) if they already have the embed wired up in their own app, the cached frontend guest token there must be refreshed before changes appear.
8023
8931
 
8024
8932
  **Important rules:**
8025
8933
  - STOP after each step. Present results and wait for user input before proceeding.
@@ -8029,6 +8937,108 @@ If the user wants to see the changes in their running app, regenerate their gues
8029
8937
 
8030
8938
  After completion, suggest: \`brand-my-embed\` if they want to adjust theme, or \`manage-embeds\` if they want to tweak access settings / options.`;
8031
8939
 
8940
+ // src/prompts/sql-to-metric-migration.ts
8941
+ var sqlToMetricMigrationPrompt = `You are helping migrate validated warehouse SQL into Databrain metrics/widgets for a production-backed workspace.
8942
+
8943
+ This workflow is built for agent-assisted SQL migration: the user already has SQL converted and validated against their warehouse. Your job is to resolve the Databrain dashboard/data-app/datasource context, validate the metric spec, produce a dry-run plan, and only apply after explicit confirmation.
8944
+
8945
+ **Critical safety rules**
8946
+ - Default to dry-run. Never apply on the first pass.
8947
+ - Applying requires the exact confirmation string: \`APPLY_TO_PRODUCTION\`.
8948
+ - Service-token mode means writes are attributed as system until OAuth ships. State that clearly before apply.
8949
+ - Do not create datasources or workspaces in this workflow.
8950
+ - If a datamart is needed, use \`create_datamart\` in \`dry_run\` first; apply only after confirmation.
8951
+ - Keep \`isPublished: false\` unless the user explicitly asks to publish.
8952
+
8953
+ **Minimum inputs**
8954
+ - \`clientId\`
8955
+ - \`workspaceName\`
8956
+ - \`dashboardName\` or \`embedId\`
8957
+ - \`metricId\`
8958
+ - \`name\`
8959
+ - \`query\`
8960
+ - Optional: \`description\`, \`chartOptions\`, \`integrationName\`, \`companyIntegrationId\`
8961
+
8962
+ **Step 1 \u2014 Check context**
8963
+ Call:
8964
+ \`\`\`
8965
+ list_workspaces
8966
+ list_dashboards({ workspaceName })
8967
+ list_widgets({ clientId, dashboardName, workspaceName })
8968
+ \`\`\`
8969
+ Show the current target dashboard and existing widgets. If \`metricId\` already exists, tell the user they must choose \`conflictStrategy: "update"\` or \`"create_new"\`.
8970
+
8971
+ **Step 2 \u2014 Validate without saving**
8972
+ Call:
8973
+ \`\`\`
8974
+ validate_metric_spec({
8975
+ clientId,
8976
+ workspaceName,
8977
+ dashboardName,
8978
+ metricId,
8979
+ name,
8980
+ query,
8981
+ description,
8982
+ chartOptions
8983
+ })
8984
+ \`\`\`
8985
+ Present the resolved data app, datasource, embed id, dashboard id, and any conflict.
8986
+
8987
+ **Step 3 \u2014 Dry-run the write**
8988
+ Call:
8989
+ \`\`\`
8990
+ create_or_update_metric_from_query({
8991
+ mode: "dry_run",
8992
+ conflictStrategy,
8993
+ clientId,
8994
+ workspaceName,
8995
+ dashboardName,
8996
+ metricId,
8997
+ name,
8998
+ query,
8999
+ description,
9000
+ chartOptions,
9001
+ isPublished: false
9002
+ })
9003
+ \`\`\`
9004
+ Show the exact plan. Stop and ask the user to review it.
9005
+
9006
+ **Step 4 \u2014 Apply only after explicit confirmation**
9007
+ Only after the user approves, call:
9008
+ \`\`\`
9009
+ create_or_update_metric_from_query({
9010
+ mode: "apply",
9011
+ confirm: "APPLY_TO_PRODUCTION",
9012
+ conflictStrategy,
9013
+ clientId,
9014
+ workspaceName,
9015
+ dashboardName,
9016
+ metricId,
9017
+ name,
9018
+ query,
9019
+ description,
9020
+ chartOptions,
9021
+ isPublished: false,
9022
+ requestedBy: "<human/operator context>"
9023
+ })
9024
+ \`\`\`
9025
+ Never invent the confirmation. The user must provide explicit approval.
9026
+
9027
+ **Step 5 \u2014 Verify**
9028
+ Call:
9029
+ \`\`\`
9030
+ list_widgets({ clientId, dashboardName, workspaceName })
9031
+ get_demo_link({ clientId, dashboardName, workspaceName })
9032
+ \`\`\`
9033
+ Tell the user that unpublished widgets may not appear in the preview until published.
9034
+
9035
+ **Scheduled reports**
9036
+ If the user asks about schedule visibility, call:
9037
+ \`\`\`
9038
+ list_scheduled_reports({ clientId, dashboardName, workspaceName })
9039
+ \`\`\`
9040
+ Clarify that this lists configured schedules only. Delivery success/failure history is not yet persisted in the MCP/API surface.`;
9041
+
8032
9042
  // src/prompts/registry.ts
8033
9043
  var prompts = [
8034
9044
  {
@@ -8060,6 +9070,11 @@ var prompts = [
8060
9070
  name: "customize-customer-dashboard",
8061
9071
  description: "Add, modify, or remove widgets on a specific customer's dashboard embed. Per-client scope \u2014 changes only affect that customer.",
8062
9072
  content: customizeCustomerDashboardPrompt
9073
+ },
9074
+ {
9075
+ name: "sql-to-metric-migration",
9076
+ description: "Production-safe SQL-to-metric migration workflow: validate, dry-run, then apply with explicit confirmation.",
9077
+ content: sqlToMetricMigrationPrompt
8063
9078
  }
8064
9079
  ];
8065
9080
  function registerAllPrompts(server) {
@@ -8080,24 +9095,38 @@ function registerAllPrompts(server) {
8080
9095
 
8081
9096
  // src/server.ts
8082
9097
  function createServer(config) {
9098
+ const client = new DatabrainClient(config);
9099
+ const auth = client.getActiveAuth();
9100
+ let operatorBlurb;
9101
+ if (auth.source === "service-token") {
9102
+ operatorBlurb = "OPERATOR IDENTITY: service-token mode. Calls are authenticated at org level \u2014 actions in the audit log are tagged actor=system, not a specific user. Per-user identity binding will be added when the OAuth-based login flow ships. Do NOT ask the user for credentials or tokens.\n\n";
9103
+ } else if (auth.source === "api-token") {
9104
+ operatorBlurb = "OPERATOR IDENTITY: API-token-only mode. A per-data-app API token is configured, but DATABRAIN_SERVICE_TOKEN is not. Embedding operations can use the configured API token, but org-level setup tools require DATABRAIN_SERVICE_TOKEN in the MCP config.\n\n";
9105
+ } else {
9106
+ operatorBlurb = "OPERATOR IDENTITY: no usable token is configured. The MCP server normally refuses this config at startup. Ask the user to add DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN to the MCP config before proceeding.\n\n";
9107
+ }
9108
+ const serviceTokenStatus = config.serviceToken ? "This MCP server has DATABRAIN_SERVICE_TOKEN configured; never ask the user for it." : "DATABRAIN_SERVICE_TOKEN is not configured. Ask the user to add it to the MCP config only if org-level setup is needed.";
9109
+ const apiTokenStatus = client.hasApiToken() ? "This MCP server has a per-data-app API token configured." : "Created/rotated via create_api_token or rotate_api_token during setup when DATABRAIN_SERVICE_TOKEN is available.";
8083
9110
  const server = new McpServer(
8084
9111
  {
8085
9112
  name: "databrain",
8086
- version: "0.1.0",
8087
- description: "Databrain MCP Server \u2014 embed analytics dashboards and charts into your app. Focused on embed discovery, deterministic embed updates, theming, filters, localization, permissions, and guest token generation."
9113
+ version: MCP_SERVER_VERSION,
9114
+ 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."
8088
9115
  },
8089
9116
  {
8090
- instructions: `Databrain uses two token types \u2014 never confuse them:
8091
- \u2022 SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, list_datamarts, list_workspaces, link_datamart_to_workspace, list_data_apps, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer. You already have this \u2014 never ask the user for it.
8092
- \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) and data-app scoped dashboard listing. Created/rotated via create_api_token or rotate_api_token during setup.
9117
+ instructions: operatorBlurb + `Databrain uses two token types \u2014 never confuse them:
9118
+ \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, 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}
9119
+ \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}
8093
9120
 
8094
- 3 core use cases \u2014 match the user's scenario to the right prompt:
9121
+ 4 core use cases \u2014 match the user's scenario to the right prompt:
8095
9122
  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)
8096
9123
  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)
8097
9124
  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)
9125
+ 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)
9126
+
9127
+ 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. Do not apply from a vague request.
8098
9128
 
8099
- Infrastructure CREATION (datasources, datamarts, workspaces) is done in the Databrain UI.
8100
- However, the **semantic layer** is populated and maintained entirely via MCP tools (get_semantic_layer, update_semantic_layer). When the semantic layer is empty, YOU (the assistant) should auto-generate descriptions from table/column names and push them with update_semantic_layer \u2014 do NOT send the user to the UI for this.
9129
+ Infrastructure creation is intentionally narrow in MCP: datasource and workspace creation still happen outside this workflow; sync_datasource and dry-run/apply create_datamart are available. The semantic layer is populated and maintained via MCP tools (get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status). When the semantic layer is empty, use these tools \u2014 do NOT send the user to the UI for semantic metadata.
8101
9130
 
8102
9131
  After embed setup, customize with: brand-my-embed prompt for theming. Use update_embed (body passthrough) for filters, localization, and permissions.
8103
9132
  Use generate_guest_token for frontend auth and generate_embed_code for framework snippets.
@@ -8114,7 +9143,6 @@ IMPORTANT \u2014 Be interactive. Present results from each tool call and ask the
8114
9143
  Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, filters, permissions, datasource, datamart, workspace, metric creation.`
8115
9144
  }
8116
9145
  );
8117
- const client = new DatabrainClient(config);
8118
9146
  registerAllTools(server, client);
8119
9147
  registerAllResources(server);
8120
9148
  registerAllPrompts(server);
@@ -8122,7 +9150,6 @@ Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, fi
8122
9150
  }
8123
9151
 
8124
9152
  // src/index.ts
8125
- var VERSION = "0.2.0";
8126
9153
  function banner(msg) {
8127
9154
  process.stderr.write(`[databrain] ${msg}
8128
9155
  `);
@@ -8133,7 +9160,7 @@ async function main() {
8133
9160
  const server = createServer(config);
8134
9161
  const transport = new StdioServerTransport();
8135
9162
  await server.connect(transport);
8136
- banner(`v${VERSION} ready (21 tools, 11 resources, 5 prompts)`);
9163
+ banner(`v${MCP_SERVER_VERSION} ready`);
8137
9164
  } catch (error) {
8138
9165
  logger.error("Failed to start server", error);
8139
9166
  process.exit(1);