@databrainhq/mcp-server 0.2.11 → 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,11 +8,10 @@ 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
- pat: z.string().min(1).optional(),
12
11
  apiUrl: z.string().url().default("https://api.usedatabrain.com"),
13
12
  demoDomain: z.string().url().default("https://demo.usedatabrain.com")
14
- }).refine((c) => c.serviceToken || c.apiToken || c.pat, {
15
- message: "At least one of DATABRAIN_PAT, DATABRAIN_SERVICE_TOKEN, or DATABRAIN_API_TOKEN must be set"
13
+ }).refine((c) => c.serviceToken || c.apiToken, {
14
+ message: "At least one of DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN must be set"
16
15
  });
17
16
  function formatConfigError(err) {
18
17
  const issues = err.issues.map((i) => i.message);
@@ -21,10 +20,8 @@ function formatConfigError(err) {
21
20
  lines.push(` - ${issue}`);
22
21
  }
23
22
  lines.push("");
24
- lines.push(" Either:");
25
- lines.push(" \u2022 Set DATABRAIN_PAT to a Personal Access Token (Settings > Personal Access Tokens) \u2014 preferred, binds this MCP session to your user.");
26
- lines.push(" \u2022 OR set DATABRAIN_SERVICE_TOKEN (Settings > Service Tokens) \u2014 org-level, no per-user audit; the `connect` tool can mint a PAT on top.");
27
- lines.push(' Then add to your MCP config: { "env": { "DATABRAIN_PAT": "..." } }');
23
+ lines.push(" Set DATABRAIN_SERVICE_TOKEN (Settings > Service Tokens) \u2014 org-level token for setup and management.");
24
+ lines.push(' Then add to your MCP config: { "env": { "DATABRAIN_SERVICE_TOKEN": "..." } }');
28
25
  lines.push("");
29
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.");
30
27
  return lines.join("\n");
@@ -34,7 +31,6 @@ function loadConfig() {
34
31
  return configSchema.parse({
35
32
  serviceToken: process.env.DATABRAIN_SERVICE_TOKEN || void 0,
36
33
  apiToken: process.env.DATABRAIN_API_TOKEN || void 0,
37
- pat: process.env.DATABRAIN_PAT || void 0,
38
34
  apiUrl: process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com",
39
35
  demoDomain: process.env.DATABRAIN_DEMO_DOMAIN || "https://demo.usedatabrain.com"
40
36
  });
@@ -188,124 +184,10 @@ var logger = {
188
184
  error: (msg, data) => log("error", msg, data)
189
185
  };
190
186
 
191
- // src/auth/credential-store.ts
192
- import { mkdirSync, readFileSync, writeFileSync, renameSync, chmodSync, unlinkSync, existsSync } from "fs";
193
- import { homedir } from "os";
194
- import { dirname, join } from "path";
195
- function defaultStorePath() {
196
- const override = process.env.DATABRAIN_CREDENTIAL_PATH;
197
- if (override) return override;
198
- return join(homedir(), ".databrain", "mcp", "credentials.json");
199
- }
200
- function readStore(path) {
201
- if (!existsSync(path)) return {};
202
- try {
203
- const raw = readFileSync(path, "utf8");
204
- if (!raw.trim()) return {};
205
- const parsed = JSON.parse(raw);
206
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
207
- return parsed;
208
- }
209
- return {};
210
- } catch (err) {
211
- logger.warn("Credential store unreadable, starting fresh", {
212
- error: err.message
213
- });
214
- return {};
215
- }
216
- }
217
- function writeStore(path, store) {
218
- const dir = dirname(path);
219
- mkdirSync(dir, { recursive: true, mode: 448 });
220
- const tmp = `${path}.tmp.${process.pid}`;
221
- writeFileSync(tmp, JSON.stringify(store, null, 2), { mode: 384 });
222
- renameSync(tmp, path);
223
- try {
224
- chmodSync(path, 384);
225
- } catch {
226
- }
227
- }
228
- var CredentialStore = class {
229
- constructor(path = defaultStorePath()) {
230
- this.path = path;
231
- }
232
- get(baseUrl) {
233
- const store = readStore(this.path);
234
- const cred = store[normalizeUrl(baseUrl)];
235
- return cred ?? null;
236
- }
237
- set(baseUrl, cred) {
238
- const store = readStore(this.path);
239
- store[normalizeUrl(baseUrl)] = cred;
240
- writeStore(this.path, store);
241
- }
242
- clear(baseUrl) {
243
- const store = readStore(this.path);
244
- const key = normalizeUrl(baseUrl);
245
- if (!(key in store)) return false;
246
- delete store[key];
247
- if (Object.keys(store).length === 0 && existsSync(this.path)) {
248
- unlinkSync(this.path);
249
- } else {
250
- writeStore(this.path, store);
251
- }
252
- return true;
253
- }
254
- getPath() {
255
- return this.path;
256
- }
257
- };
258
- function normalizeUrl(url) {
259
- return url.replace(/\/+$/, "");
260
- }
261
-
262
- // src/auth/operator.ts
263
- var OPERATOR_PAT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
264
- var storeSingleton;
265
- function getCredentialStore() {
266
- if (!storeSingleton) storeSingleton = new CredentialStore();
267
- return storeSingleton;
268
- }
269
- function loadCachedOperator(baseUrl) {
270
- let cred;
271
- try {
272
- cred = getCredentialStore().get(baseUrl);
273
- } catch (err) {
274
- logger.warn("Credential store unreadable, treating as empty", {
275
- error: err.message
276
- });
277
- return null;
278
- }
279
- if (!cred) return null;
280
- if (cred.expiresAt && cred.expiresAt < Date.now()) return null;
281
- return cred;
282
- }
283
- function persistOperator(baseUrl, response) {
284
- const cred = {
285
- pat: response.pat,
286
- email: response.email,
287
- userId: response.userId,
288
- role: response.role,
289
- workspaces: response.workspaces,
290
- expiresAt: response.expiresAt
291
- };
292
- try {
293
- getCredentialStore().set(baseUrl, cred);
294
- return { cred, persisted: true };
295
- } catch (err) {
296
- logger.warn(
297
- "Could not write operator credential to disk \u2014 using in-memory only for this session",
298
- { error: err.message }
299
- );
300
- return { cred, persisted: false };
301
- }
302
- }
303
- function clearOperator(baseUrl) {
304
- return getCredentialStore().clear(baseUrl);
305
- }
187
+ // src/version.ts
188
+ var MCP_SERVER_VERSION = "0.2.10";
306
189
 
307
190
  // src/client/databrain-client.ts
308
- var MCP_SERVER_VERSION = "0.2.0";
309
191
  var MCP_USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION}`;
310
192
  var MAX_RETRIES = 3;
311
193
  var REQUEST_TIMEOUT_MS = 3e4;
@@ -325,8 +207,6 @@ var DatabrainClient = class {
325
207
  }
326
208
  cache = new TtlCache();
327
209
  runtimeApiToken;
328
- operator = null;
329
- operatorLoaded = false;
330
210
  hasApiToken() {
331
211
  return !!(this.runtimeApiToken || this.config.apiToken);
332
212
  }
@@ -343,85 +223,22 @@ var DatabrainClient = class {
343
223
  getDemoDomain() {
344
224
  return this.config.demoDomain;
345
225
  }
346
- getOperator() {
347
- if (!this.operatorLoaded) {
348
- this.operator = loadCachedOperator(this.config.apiUrl);
349
- this.operatorLoaded = true;
350
- }
351
- return this.operator;
352
- }
353
- setOperator(cred) {
354
- this.operator = cred;
355
- this.operatorLoaded = true;
356
- this.cache.invalidateByPrefix("");
357
- }
358
- clearOperator() {
359
- this.operator = null;
360
- this.operatorLoaded = true;
361
- this.cache.invalidateByPrefix("");
362
- return clearOperator(this.config.apiUrl);
363
- }
364
- async bootstrapOperator(email) {
365
- if (!this.config.serviceToken) {
366
- throw new DatabrainApiError(
367
- 401,
368
- "MISSING_SERVICE_TOKEN",
369
- "Cannot connect \u2014 DATABRAIN_SERVICE_TOKEN is not configured. Set it in your MCP server config first."
370
- );
371
- }
372
- const response = await this.request({
373
- method: "POST",
374
- path: "/api/mcp/bootstrap",
375
- body: { email },
376
- auth: "service",
377
- // Bootstrap uses the service token explicitly — not the operator PAT.
378
- token: this.config.serviceToken
379
- });
380
- const result = persistOperator(this.config.apiUrl, response);
381
- this.setOperator(result.cred);
382
- return result;
383
- }
384
226
  /**
385
227
  * Describes which credential is currently used as the bearer for outgoing
386
228
  * API calls. Used by `whoami` so the LLM can give the user a truthful answer.
387
229
  */
388
230
  getActiveAuth() {
389
- if (this.config.pat) return { source: "env-pat" };
390
- const op = this.getOperator();
391
- if (op?.pat) return { source: "cached-pat", operator: op };
392
231
  if (this.config.serviceToken) return { source: "service-token" };
393
232
  if (this.runtimeApiToken || this.config.apiToken) return { source: "api-token" };
394
233
  return { source: "none" };
395
234
  }
396
235
  getToken(auth) {
397
236
  if (auth === "none") return void 0;
398
- if (this.config.pat) return this.config.pat;
399
- const op = this.getOperator();
400
- if (op?.pat) return op.pat;
401
237
  if (auth === "service") return this.config.serviceToken;
402
238
  return this.runtimeApiToken || this.config.apiToken;
403
239
  }
404
240
  async request(options) {
405
- try {
406
- return await this.requestInternal(options);
407
- } catch (err) {
408
- if (err instanceof DatabrainApiError && err.code === "PAT_EXPIRED" && !options.token && !this.config.pat && options.path !== "/api/mcp/bootstrap") {
409
- const op = this.getOperator();
410
- if (op?.email) {
411
- logger.info("Operator PAT expired \u2014 silently re-bootstrapping");
412
- try {
413
- await this.bootstrapOperator(op.email);
414
- } catch (bootstrapErr) {
415
- logger.warn("Silent re-bootstrap failed", {
416
- error: bootstrapErr.message
417
- });
418
- throw err;
419
- }
420
- return this.requestInternal(options);
421
- }
422
- }
423
- throw err;
424
- }
241
+ return this.requestInternal(options);
425
242
  }
426
243
  async requestInternal(options) {
427
244
  if (options.invalidateCache) {
@@ -826,6 +643,69 @@ var QueryApi = class {
826
643
  throw err;
827
644
  }
828
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
+ }
829
709
  };
830
710
 
831
711
  // src/api/widgets-api.ts
@@ -965,19 +845,19 @@ var DEFAULT_ACCESS_SETTINGS = {
965
845
  function isNonEmptyString2(value) {
966
846
  return typeof value === "string" && value.trim().length > 0;
967
847
  }
968
- function asRecord(value) {
848
+ function asRecord2(value) {
969
849
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
970
850
  return value;
971
851
  }
972
852
  function extractAccessSettings(embed) {
973
- const metadata = asRecord(embed.embedMetadata);
974
- const metadataConfig = asRecord(metadata?.embedConfig);
975
- const options = asRecord(embed.options);
853
+ const metadata = asRecord2(embed.embedMetadata);
854
+ const metadataConfig = asRecord2(metadata?.embedConfig);
855
+ const options = asRecord2(embed.options);
976
856
  const candidates = [
977
- asRecord(embed.accessSettings),
978
- asRecord(metadata?.accessSettings),
979
- asRecord(metadataConfig?.accessSettings),
980
- asRecord(options?.accessSettings)
857
+ asRecord2(embed.accessSettings),
858
+ asRecord2(metadata?.accessSettings),
859
+ asRecord2(metadataConfig?.accessSettings),
860
+ asRecord2(options?.accessSettings)
981
861
  ].filter((c) => !!c);
982
862
  for (const candidate of candidates) {
983
863
  if (Object.keys(candidate).length > 0) return candidate;
@@ -985,8 +865,8 @@ function extractAccessSettings(embed) {
985
865
  return null;
986
866
  }
987
867
  function extractDatamartName(embed, existingAccess) {
988
- const metadata = asRecord(embed.embedMetadata);
989
- const metadataConfig = asRecord(metadata?.embedConfig);
868
+ const metadata = asRecord2(embed.embedMetadata);
869
+ const metadataConfig = asRecord2(metadata?.embedConfig);
990
870
  const values = [
991
871
  existingAccess?.datamartName,
992
872
  existingAccess?.dataMartName,
@@ -1023,7 +903,7 @@ var EmbedService = class {
1023
903
  return this.embedsApi.listEmbeds(dataAppName);
1024
904
  }
1025
905
  async createEmbedStrict(body) {
1026
- const accessSettings = asRecord(body.accessSettings);
906
+ const accessSettings = asRecord2(body.accessSettings);
1027
907
  if (!accessSettings || Object.keys(accessSettings).length === 0) {
1028
908
  throw new Error(
1029
909
  "Embed creation requires `accessSettings` for future update compatibility."
@@ -1111,8 +991,8 @@ Run \`update_embed\` with explicit \`accessSettings.datamartName\`, or recreate
1111
991
  }
1112
992
  async updateEmbed(embedId, updatePatch) {
1113
993
  const body = await this.buildUpdateBase(embedId);
1114
- const baseAccessSettings = asRecord(body.accessSettings);
1115
- const patchAccessSettings = asRecord(updatePatch.accessSettings);
994
+ const baseAccessSettings = asRecord2(body.accessSettings);
995
+ const patchAccessSettings = asRecord2(updatePatch.accessSettings);
1116
996
  for (const [key, value] of Object.entries(updatePatch)) {
1117
997
  if (key === "accessSettings") continue;
1118
998
  if (value !== void 0) body[key] = value;
@@ -1172,7 +1052,7 @@ var GuestTokenService = class {
1172
1052
 
1173
1053
  // src/services/widget-service.ts
1174
1054
  var isNonEmptyString3 = (v) => typeof v === "string" && v.trim().length > 0;
1175
- var asRecord2 = (v) => {
1055
+ var asRecord3 = (v) => {
1176
1056
  if (!v || typeof v !== "object" || Array.isArray(v)) return null;
1177
1057
  return v;
1178
1058
  };
@@ -1493,8 +1373,8 @@ Do not guess between the two \u2014 wait for the user to choose.`
1493
1373
  if (!exact) {
1494
1374
  throw new Error(`Embed "${embedId}" not found. Run \`list_embeds\` to verify.`);
1495
1375
  }
1496
- const metadata = asRecord2(exact.embedMetadata);
1497
- const externalDashboard = asRecord2(exact.externalDashboard);
1376
+ const metadata = asRecord3(exact.embedMetadata);
1377
+ const externalDashboard = asRecord3(exact.externalDashboard);
1498
1378
  const dataAppId = isNonEmptyString3(exact.dataAppId) ? exact.dataAppId : null;
1499
1379
  const externalDashboardId = isNonEmptyString3(exact.externalDashboardId) && exact.externalDashboardId || (externalDashboard && isNonEmptyString3(externalDashboard.externalDashboardId) ? externalDashboard.externalDashboardId : null) || (metadata && isNonEmptyString3(metadata.externalDashboardId) ? metadata.externalDashboardId : null);
1500
1380
  const dashboardId = isNonEmptyString3(exact.dashboardId) && exact.dashboardId || (metadata && isNonEmptyString3(metadata.dashboardId) ? metadata.dashboardId : null);
@@ -1544,7 +1424,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
1544
1424
  guestToken,
1545
1425
  templateMetricId
1546
1426
  );
1547
- const metric = asRecord2(response.externalMetric);
1427
+ const metric = asRecord3(response.externalMetric);
1548
1428
  if (!metric) {
1549
1429
  throw new Error(
1550
1430
  `Template metric "${templateMetricId}" not found or could not be loaded. Response keys: ${Object.keys(
@@ -1560,7 +1440,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
1560
1440
  query: metric.query || void 0,
1561
1441
  metricQuery: metric.metricQuery || void 0,
1562
1442
  description: metric.description || "",
1563
- chartOptions: asRecord2(metric.chartOptions) || {},
1443
+ chartOptions: asRecord3(metric.chartOptions) || {},
1564
1444
  companyId: metric.companyId || void 0,
1565
1445
  companyIntegrationId: metric.companyIntegrationId || void 0,
1566
1446
  integrationName: metric.integrationName || void 0,
@@ -1601,6 +1481,7 @@ function createServiceBundle(client) {
1601
1481
  const datasourcesApi = new DatasourcesApi(client);
1602
1482
  const guestTokenApi = new GuestTokenApi(client);
1603
1483
  const queryApi = new QueryApi(client);
1484
+ const scheduledReportsApi = new ScheduledReportsApi(client);
1604
1485
  const widgetsApi = new WidgetsApi(client);
1605
1486
  const workspacesApi = new WorkspacesApi(client);
1606
1487
  const discoveryService = new DiscoveryService();
@@ -1613,6 +1494,7 @@ function createServiceBundle(client) {
1613
1494
  datasourcesApi,
1614
1495
  workspacesApi,
1615
1496
  queryApi,
1497
+ scheduledReportsApi,
1616
1498
  widgetsApi,
1617
1499
  embedService: new EmbedService(embedsApi, datamartsApi),
1618
1500
  discoveryService,
@@ -2220,8 +2102,234 @@ function registerListWorkspaces(server, client) {
2220
2102
  );
2221
2103
  }
2222
2104
 
2223
- // src/tools/embed/create-embed.ts
2105
+ // src/tools/infrastructure/create-datamart.ts
2224
2106
  import { z as z9 } from "zod";
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);
2163
+ server.tool(
2164
+ "create_datamart",
2165
+ "Create a production datamart from selected tables and relationships. Defaults to dry-run; apply requires explicit confirmation.",
2166
+ {
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")
2182
+ },
2183
+ WRITE_CREATE,
2184
+ async (params) => {
2185
+ try {
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"
2196
+ });
2197
+ if (resolution.kind !== "resolved") return resolution.response;
2198
+ if (!params.tableList || params.tableList.length === 0) {
2199
+ return toolError(
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
+ )
2223
+ );
2224
+ }
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") {
2240
+ return toolSuccess(
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
+ )
2252
+ );
2253
+ }
2254
+ const guardError = validateProductionApply({ confirm: params.confirm });
2255
+ if (guardError) return toolError(guardError);
2256
+ const result = await client.request({
2257
+ method: "POST",
2258
+ path: "/api/v2/data-app/datamarts",
2259
+ body: params.body,
2260
+ auth: "service",
2261
+ invalidateCache: ["list_datamarts"]
2262
+ });
2263
+ if (result.error) {
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.`);
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";
2317
+ return toolSuccess(
2318
+ `Datasource "${selected.name}" sync triggered (ID: ${datasourceId}).
2319
+ Status: ${message}
2320
+
2321
+ The schema metadata will be refreshed. You can now use \`discover_datasource_schema\` or \`create_datamart\` to select tables.`
2322
+ );
2323
+ } catch (err) {
2324
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
2325
+ return toolError(`Unexpected error: ${err.message}`);
2326
+ }
2327
+ }
2328
+ );
2329
+ }
2330
+
2331
+ // src/tools/embed/create-embed.ts
2332
+ import { z as z11 } from "zod";
2225
2333
  var DEFAULT_ACCESS_SETTINGS2 = {
2226
2334
  isAllowMetricCreation: false,
2227
2335
  isAllowMetricDeletion: false,
@@ -2234,18 +2342,18 @@ var DEFAULT_ACCESS_SETTINGS2 = {
2234
2342
  isAllowCreateDashboardView: false,
2235
2343
  metricCreationMode: "DRAG_DROP"
2236
2344
  };
2237
- var accessSettingsSchema = z9.object({
2238
- datamartName: z9.string().describe("Name of the datamart (required)"),
2239
- isAllowMetricCreation: z9.boolean().optional().describe("Allow end-users to create metrics"),
2240
- metricCreationMode: z9.enum(["DRAG_DROP", "CHAT"]).optional().describe("Metric creation mode"),
2241
- isAllowMetricDeletion: z9.boolean().optional().describe("Allow metric deletion"),
2242
- isAllowMetricUpdate: z9.boolean().optional().describe("Allow metric editing"),
2243
- isAllowMetricLayoutChange: z9.boolean().optional().describe("Allow layout changes"),
2244
- isAllowManageMetrics: z9.boolean().optional().describe("Show manage metrics UI"),
2245
- isAllowUnderlyingData: z9.boolean().optional().describe("Allow viewing underlying data"),
2246
- isAllowAiPilot: z9.boolean().optional().describe("Enable AI pilot features"),
2247
- isAllowEmailReports: z9.boolean().optional().describe("Allow email report scheduling"),
2248
- isAllowCreateDashboardView: z9.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")
2249
2357
  }).describe("Embed access settings. Only datamartName is required \u2014 all flags default to false.");
2250
2358
  function registerCreateEmbed(server, client) {
2251
2359
  const services = createServiceBundle(client);
@@ -2253,23 +2361,23 @@ function registerCreateEmbed(server, client) {
2253
2361
  "create_embed",
2254
2362
  "Create an embed. Provide dashboardId, workspaceName, accessSettings.datamartName. Add clientId for multi-tenant per-client dashboards. All isAllow* flags default to false.",
2255
2363
  {
2256
- body: z9.record(z9.unknown()).optional().describe(
2364
+ body: z11.record(z11.unknown()).optional().describe(
2257
2365
  "Raw API body passthrough. If provided, all other fields are ignored."
2258
2366
  ),
2259
- dashboardId: z9.string().optional().describe("Dashboard identifier for embed creation"),
2260
- workspaceName: z9.string().optional().describe("Workspace name. Omit to auto-resolve if only one exists."),
2261
- embedType: z9.enum(["dashboard", "metric"]).optional().describe("Embed type"),
2262
- metricId: z9.string().optional().describe("Required for metric embed type"),
2263
- name: z9.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"),
2264
2372
  accessSettings: accessSettingsSchema.optional(),
2265
2373
  // Multi-tenant (dashboard embed) params
2266
- clientId: z9.string().optional().describe(
2374
+ clientId: z11.string().optional().describe(
2267
2375
  "Client identifier for multi-tenant isolation. When provided, creates a dashboard embed (each client gets their own dashboard copy)."
2268
2376
  ),
2269
- templateDashboardId: z9.string().optional().describe("Template dashboard ID to clone metrics from"),
2270
- isImportMetrics: z9.boolean().optional().describe("Import metrics from template dashboard"),
2271
- metricImportMode: z9.enum(["COPY", "LINK"]).optional().describe("COPY duplicates metrics, LINK shares them"),
2272
- isRenameDashboard: z9.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")
2273
2381
  },
2274
2382
  WRITE_CREATE,
2275
2383
  async (params) => {
@@ -2371,14 +2479,14 @@ Next: run \`get_embed_details\` to verify the embed configuration.`
2371
2479
  }
2372
2480
 
2373
2481
  // src/tools/embed/list-embeds.ts
2374
- import { z as z10 } from "zod";
2482
+ import { z as z12 } from "zod";
2375
2483
  function registerListEmbeds(server, client) {
2376
2484
  const services = createServiceBundle(client);
2377
2485
  server.tool(
2378
2486
  "list_embeds",
2379
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.",
2380
2488
  {
2381
- dataAppName: z10.string().optional().describe("Filter by data app name")
2489
+ dataAppName: z12.string().optional().describe("Filter by data app name")
2382
2490
  },
2383
2491
  READ_ONLY,
2384
2492
  async (params) => {
@@ -2421,14 +2529,14 @@ function registerListEmbeds(server, client) {
2421
2529
  }
2422
2530
 
2423
2531
  // src/tools/embed/get-embed-details.ts
2424
- import { z as z11 } from "zod";
2532
+ import { z as z13 } from "zod";
2425
2533
  function registerGetEmbedDetails(server, client) {
2426
2534
  const services = createServiceBundle(client);
2427
2535
  server.tool(
2428
2536
  "get_embed_details",
2429
2537
  "Get full embed configuration including access settings and theme.",
2430
2538
  {
2431
- embedId: z11.string().describe("ID of the embed")
2539
+ embedId: z13.string().describe("ID of the embed")
2432
2540
  },
2433
2541
  READ_ONLY,
2434
2542
  async (params) => {
@@ -2444,7 +2552,7 @@ function registerGetEmbedDetails(server, client) {
2444
2552
  }
2445
2553
 
2446
2554
  // src/tools/embed/update-embed.ts
2447
- import { z as z12 } from "zod";
2555
+ import { z as z14 } from "zod";
2448
2556
 
2449
2557
  // src/tools/embed/helpers.ts
2450
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.";
@@ -2482,15 +2590,15 @@ function registerUpdateEmbed(server, client) {
2482
2590
  "update_embed",
2483
2591
  "Update embed access settings, permissions, theme, or name.",
2484
2592
  {
2485
- embedId: z12.string().describe("ID of the embed to update"),
2486
- body: z12.record(z12.unknown()).optional().describe(
2593
+ embedId: z14.string().describe("ID of the embed to update"),
2594
+ body: z14.record(z14.unknown()).optional().describe(
2487
2595
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, it is sent directly (passthrough mode)."
2488
2596
  ),
2489
- name: z12.string().optional().describe("New name for the embed"),
2490
- accessSettings: z12.record(z12.unknown()).optional().describe(
2597
+ name: z14.string().optional().describe("New name for the embed"),
2598
+ accessSettings: z14.record(z14.unknown()).optional().describe(
2491
2599
  "Partial update of access settings. See `get_embed_details` for current settings."
2492
2600
  ),
2493
- theme: z12.record(z12.unknown()).optional().describe(
2601
+ theme: z14.record(z14.unknown()).optional().describe(
2494
2602
  "Theme configuration object. See databrain://theme-reference for the full schema."
2495
2603
  )
2496
2604
  },
@@ -2536,7 +2644,7 @@ function registerUpdateEmbed(server, client) {
2536
2644
  }
2537
2645
 
2538
2646
  // src/tools/embed/customize-theme.ts
2539
- import { z as z13 } from "zod";
2647
+ import { z as z15 } from "zod";
2540
2648
  var THEME_PRESETS = {
2541
2649
  light: {
2542
2650
  general: {
@@ -2670,7 +2778,7 @@ var ACCESS_PRESETS = {
2670
2778
  metricCreationMode: "CHAT"
2671
2779
  }
2672
2780
  };
2673
- var fontFamilyEnum = z13.enum([
2781
+ var fontFamilyEnum = z15.enum([
2674
2782
  "Inter",
2675
2783
  "Roboto",
2676
2784
  "Open Sans",
@@ -2682,17 +2790,17 @@ var fontFamilyEnum = z13.enum([
2682
2790
  "Nunito",
2683
2791
  "system-ui"
2684
2792
  ]).describe("Font family");
2685
- var tooltipFontSchema = z13.object({
2686
- fontFamily: z13.string().optional(),
2687
- fontSize: z13.number().optional(),
2688
- fontWeight: z13.number().optional(),
2689
- fontColor: z13.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()
2690
2798
  }).optional();
2691
- var fontConfigInputSchema = z13.object({
2692
- fontSize: z13.number().optional(),
2693
- fontFamily: z13.string().optional(),
2694
- fontWeight: z13.number().optional(),
2695
- color: z13.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()
2696
2804
  }).optional();
2697
2805
  function mapToFrontendFontConfig(input) {
2698
2806
  if (!input) return void 0;
@@ -2703,23 +2811,23 @@ function mapToFrontendFontConfig(input) {
2703
2811
  ...input.color && { color: input.color }
2704
2812
  };
2705
2813
  }
2706
- var axisSchema = z13.object({
2707
- hideAxisLines: z13.boolean().optional().describe("Hide axis lines"),
2708
- hideSplitLines: z13.boolean().optional().describe("Hide grid/split lines"),
2709
- hideAxisLabels: z13.boolean().optional().describe("Hide axis labels"),
2710
- hideAxisTicks: z13.boolean().optional().describe("Hide axis tick marks"),
2711
- axisName: z13.string().optional().describe("Axis title text"),
2712
- axisNameOffset: z13.number().optional().describe("Axis title offset from axis"),
2713
- axisLabelMargin: z13.number().optional().describe("Margin between axis labels and chart area in pixels"),
2714
- fontSize: z13.number().optional().describe("Axis label font size"),
2715
- fontFamily: z13.string().optional().describe("Axis label font family"),
2716
- fontWeight: z13.number().optional().describe("Axis label font weight"),
2717
- color: z13.string().optional().describe("Axis label color (hex)"),
2718
- axisColor: z13.string().optional().describe("Axis line color (hex)"),
2719
- axisNameFontConfig: z13.object({
2720
- fontFamily: z13.string().optional(),
2721
- fontSize: z13.number().optional(),
2722
- fontWeight: z13.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()
2723
2831
  }).optional().describe("Axis title font configuration")
2724
2832
  }).optional();
2725
2833
  function buildAdminTheme(p, presetBase) {
@@ -2778,37 +2886,37 @@ function buildAdminTheme(p, presetBase) {
2778
2886
  function buildComponentTheme(p) {
2779
2887
  const componentTheme = {};
2780
2888
  if (p.buttonPrimaryColor || p.buttonPrimaryTextColor || p.buttonSecondaryColor || p.buttonSecondaryTextColor) {
2781
- componentTheme.button = {
2782
- ...p.buttonPrimaryColor && { primary: p.buttonPrimaryColor },
2783
- ...p.buttonPrimaryTextColor && { primaryText: p.buttonPrimaryTextColor },
2784
- ...p.buttonSecondaryColor && { secondary: p.buttonSecondaryColor },
2785
- ...p.buttonSecondaryTextColor && { secondaryText: p.buttonSecondaryTextColor }
2786
- };
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;
2787
2895
  }
2788
2896
  if (p.checkboxCheckedColor || p.checkboxUncheckedColor) {
2789
- componentTheme.checkbox = {
2790
- ...p.checkboxCheckedColor && { checked: p.checkboxCheckedColor },
2791
- ...p.checkboxUncheckedColor && { unChecked: p.checkboxUncheckedColor }
2792
- };
2897
+ const checkbox = {};
2898
+ if (p.checkboxCheckedColor) checkbox.checked = p.checkboxCheckedColor;
2899
+ if (p.checkboxUncheckedColor) checkbox.unChecked = p.checkboxUncheckedColor;
2900
+ componentTheme.checkbox = checkbox;
2793
2901
  }
2794
2902
  if (p.switchEnabledColor || p.switchDisabledColor) {
2795
- componentTheme.switch = {
2796
- ...p.switchEnabledColor && { enabled: p.switchEnabledColor },
2797
- ...p.switchDisabledColor && { disabled: p.switchDisabledColor }
2798
- };
2903
+ const switchTheme = {};
2904
+ if (p.switchEnabledColor) switchTheme.enabled = p.switchEnabledColor;
2905
+ if (p.switchDisabledColor) switchTheme.disabled = p.switchDisabledColor;
2906
+ componentTheme.switch = switchTheme;
2799
2907
  }
2800
2908
  if (p.breadcrumbFontFamily || p.breadcrumbFontColor || p.breadcrumbActiveColor) {
2801
- componentTheme.drillBreadCrumbs = {
2802
- ...p.breadcrumbFontFamily && { fontFamily: p.breadcrumbFontFamily },
2803
- ...p.breadcrumbFontColor && { fontColor: p.breadcrumbFontColor },
2804
- ...p.breadcrumbActiveColor && { activeColor: p.breadcrumbActiveColor }
2805
- };
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;
2806
2914
  }
2807
2915
  if (p.filterBadgeColor || p.filterBadgeTextColor) {
2808
- componentTheme.multiSelectFilterDropdown = {
2809
- ...p.filterBadgeColor && { badgeColor: p.filterBadgeColor },
2810
- ...p.filterBadgeTextColor && { badgeTextColor: p.filterBadgeTextColor }
2811
- };
2916
+ const multiSelectFilterDropdown = {};
2917
+ if (p.filterBadgeColor) multiSelectFilterDropdown.badgeColor = p.filterBadgeColor;
2918
+ if (p.filterBadgeTextColor) multiSelectFilterDropdown.badgeTextColor = p.filterBadgeTextColor;
2919
+ componentTheme.multiSelectFilterDropdown = multiSelectFilterDropdown;
2812
2920
  }
2813
2921
  if (p.datePickerColor) componentTheme.datePickerColor = p.datePickerColor;
2814
2922
  const breakpoint = {};
@@ -2875,94 +2983,94 @@ function registerCustomizeTheme(server, client) {
2875
2983
  "customize_embed_theme",
2876
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("/")}).`,
2877
2985
  {
2878
- embedId: z13.string().describe("ID of the embed to theme"),
2879
- body: z13.record(z13.unknown()).optional().describe(
2986
+ embedId: z15.string().describe("ID of the embed to theme"),
2987
+ body: z15.record(z15.unknown()).optional().describe(
2880
2988
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, sent directly and all convenience fields are ignored."
2881
2989
  ),
2882
2990
  // --- Presets ---
2883
- themePreset: z13.enum(["light", "dark", "corporate", "minimal"]).optional().describe("Apply a theme preset as the base. Granular params below override preset values."),
2884
- accessPreset: z13.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"),
2885
2993
  // --- General ---
2886
2994
  fontFamily: fontFamilyEnum.optional().describe("Global font family"),
2887
- datePickerFormat: z13.string().optional().describe("Date picker display format"),
2995
+ datePickerFormat: z15.string().optional().describe("Date picker display format"),
2888
2996
  // --- Colors ---
2889
- primaryColor: z13.string().optional().describe("Primary accent color (hex)"),
2890
- backgroundColor: z13.string().optional().describe("Overall background color (hex)"),
2891
- textColor: z13.string().optional().describe("Default text color (hex)"),
2892
- ctaColor: z13.string().optional().describe("Call-to-action button color (hex)"),
2893
- ctaTextColor: z13.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)"),
2894
3002
  // --- Dashboard ---
2895
- dashboardBackgroundColor: z13.string().optional().describe("Dashboard container background (hex)"),
2896
- selectBoxSize: z13.enum(["small", "medium", "large"]).optional().describe("Filter select box size"),
2897
- selectBoxVariant: z13.enum(["floating", "outlined", "standard"]).optional().describe("Filter select box style"),
2898
- selectBoxBorderRadius: z13.string().optional().describe('Select box border radius (e.g. "20px")'),
2899
- selectBoxTextColor: z13.string().optional().describe("Select box text color (hex)"),
2900
- metricCardColor: z13.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)"),
2901
3009
  // --- Card Title ---
2902
- cardTitleFontSize: z13.string().optional().describe('Card title font size (e.g. "16px")'),
2903
- cardTitleFontWeight: z13.string().optional().describe('Card title font weight (e.g. "600")'),
2904
- cardTitleColor: z13.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)"),
2905
3013
  // --- Card Description ---
2906
- cardDescriptionFontSize: z13.string().optional().describe("Card description font size"),
2907
- cardDescriptionFontWeight: z13.string().optional().describe("Card description font weight"),
2908
- cardDescriptionColor: z13.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)"),
2909
3017
  // --- Card Customization ---
2910
- cardPadding: z13.string().optional().describe("Card internal padding"),
2911
- cardBorderRadius: z13.string().optional().describe("Card border radius"),
2912
- cardShadow: z13.string().optional().describe("Card box-shadow CSS value"),
2913
- disableShadowOnHover: z13.boolean().optional().describe("Disable shadow on card hover"),
2914
- disableCardStroke: z13.boolean().optional().describe("Remove card border stroke"),
2915
- cardStrokeColor: z13.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)"),
2916
3024
  // --- Chart Colors (admin theme level) ---
2917
- chartFontFamily: z13.string().optional().describe("Font family for chart text"),
2918
- chartFontColor: z13.string().optional().describe("Font color for chart text (hex)"),
2919
- chartColors: z13.array(z13.string()).optional().describe("Array of chart series colors (hex)"),
2920
- chartPaletteName: z13.string().optional().describe("Name for a custom palette"),
2921
- chartPaletteColors: z13.array(z13.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"),
2922
3030
  // --- Chart Tooltip (admin theme level) ---
2923
3031
  tooltipHeader: tooltipFontSchema.describe("Tooltip header font config"),
2924
3032
  tooltipLabel: tooltipFontSchema.describe("Tooltip label font config"),
2925
3033
  tooltipValue: tooltipFontSchema.describe("Tooltip value font config"),
2926
3034
  // --- Component Theme ---
2927
- buttonPrimaryColor: z13.string().optional().describe("Primary button background (hex)"),
2928
- buttonPrimaryTextColor: z13.string().optional().describe("Primary button text (hex)"),
2929
- buttonSecondaryColor: z13.string().optional().describe("Secondary button background (hex)"),
2930
- buttonSecondaryTextColor: z13.string().optional().describe("Secondary button text (hex)"),
2931
- checkboxCheckedColor: z13.string().optional().describe("Checked checkbox color (hex)"),
2932
- checkboxUncheckedColor: z13.string().optional().describe("Unchecked checkbox color (hex)"),
2933
- switchEnabledColor: z13.string().optional().describe("Enabled switch color (hex)"),
2934
- switchDisabledColor: z13.string().optional().describe("Disabled switch color (hex)"),
2935
- breadcrumbFontFamily: z13.string().optional().describe("Drill breadcrumb font family"),
2936
- breadcrumbFontColor: z13.string().optional().describe("Drill breadcrumb font color (hex)"),
2937
- breadcrumbActiveColor: z13.string().optional().describe("Active breadcrumb color (hex)"),
2938
- filterBadgeColor: z13.string().optional().describe("Multi-select filter badge color (hex)"),
2939
- filterBadgeTextColor: z13.string().optional().describe("Filter badge text color (hex)"),
2940
- datePickerColor: z13.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)"),
2941
3049
  // --- Responsive Breakpoints ---
2942
- breakpointXl: z13.number().optional().describe("XL breakpoint width in px"),
2943
- breakpointLg: z13.number().optional().describe("LG breakpoint width in px"),
2944
- breakpointMd: z13.number().optional().describe("MD breakpoint width in px"),
2945
- breakpointSm: z13.number().optional().describe("SM breakpoint width in px"),
2946
- breakpointXs: z13.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"),
2947
3055
  // --- Metric Layout Columns ---
2948
- layoutColsXl: z13.number().optional().describe("Grid columns at XL breakpoint"),
2949
- layoutColsLg: z13.number().optional().describe("Grid columns at LG breakpoint"),
2950
- layoutColsMd: z13.number().optional().describe("Grid columns at MD breakpoint"),
2951
- layoutColsSm: z13.number().optional().describe("Grid columns at SM breakpoint"),
2952
- layoutColsXs: z13.number().optional().describe("Grid columns at XS breakpoint"),
2953
- layoutColsXxs: z13.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"),
2954
3062
  // --- Chart Appearance (detailed) ---
2955
3063
  tooltipHeaderStyle: fontConfigInputSchema.describe("Tooltip header: fontSize, fontFamily, fontWeight, color"),
2956
3064
  tooltipLabelStyle: fontConfigInputSchema.describe("Tooltip label: fontSize, fontFamily, fontWeight, color"),
2957
3065
  tooltipValueStyle: fontConfigInputSchema.describe("Tooltip value: fontSize, fontFamily, fontWeight, color"),
2958
- labelPosition: z13.enum(["hidden", "top", "left", "right", "bottom", "inside"]).optional().describe("Data label position on charts"),
2959
- radialLabelPosition: z13.enum(["outside", "inside"]).optional().describe("Label position for radial/pie charts"),
2960
- marginTop: z13.number().optional().describe("Chart top margin in pixels"),
2961
- marginLeft: z13.number().optional().describe("Chart left margin in pixels"),
2962
- marginRight: z13.number().optional().describe("Chart right margin in pixels"),
2963
- marginBottom: z13.number().optional().describe("Chart bottom margin in pixels"),
2964
- legendShow: z13.boolean().optional().describe("Show or hide chart legend"),
2965
- legendPosition: z13.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([
2966
3074
  "top-left",
2967
3075
  "top-center",
2968
3076
  "top-right",
@@ -2972,17 +3080,17 @@ function registerCustomizeTheme(server, client) {
2972
3080
  "bottom-center",
2973
3081
  "bottom-right"
2974
3082
  ]).optional().describe("Legend position"),
2975
- legendLayout: z13.enum(["horizontal", "vertical"]).optional().describe("Legend layout direction"),
2976
- legendShape: z13.enum(["circle", "rect", "roundRect", "triangle", "diamond", "arrow", "none"]).optional().describe("Legend icon shape"),
2977
- legendFontSize: z13.number().optional().describe("Legend font size"),
2978
- legendFontWeight: z13.number().optional().describe("Legend font weight"),
2979
- legendFontFamily: z13.string().optional().describe("Legend font family"),
2980
- legendColor: z13.string().optional().describe("Legend text color (hex)"),
2981
- legendTruncateLength: z13.number().optional().describe("Max characters before truncating legend"),
2982
- disableLegendScrolling: z13.boolean().optional().describe("Disable scrolling in legend"),
2983
- enableVariableLegendPosition: z13.boolean().optional().describe("Allow user to drag legend"),
2984
- legendTop: z13.number().optional().describe("Custom legend top offset"),
2985
- legendLeft: z13.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"),
2986
3094
  verticalAxis: axisSchema.describe("Vertical (Y) axis configuration"),
2987
3095
  horizontalAxis: axisSchema.describe("Horizontal (X) axis configuration")
2988
3096
  },
@@ -3057,7 +3165,7 @@ Use \`get_embed_details\` to view the full current theme configuration.` + GUEST
3057
3165
  }
3058
3166
 
3059
3167
  // src/tools/embed/get-demo-link.ts
3060
- import { z as z14 } from "zod";
3168
+ import { z as z16 } from "zod";
3061
3169
  var HOUR_MS = 60 * 60 * 1e3;
3062
3170
  var DEFAULT_EXPIRY_HOURS = 24;
3063
3171
  var MAX_EXPIRY_HOURS = 24 * 30;
@@ -3067,13 +3175,13 @@ function registerGetDemoLink(server, client) {
3067
3175
  "get_demo_link",
3068
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.`,
3069
3177
  {
3070
- clientId: z14.string().describe("End-customer identifier to scope the preview to."),
3071
- dashboardName: z14.string().optional().describe("Dashboard name (non-technical). Preferred \u2014 pass with optional `workspaceName`."),
3072
- workspaceName: z14.string().optional().describe("Workspace name. Only needed if `dashboardName` is ambiguous."),
3073
- embedId: z14.string().optional().describe("Technical alternative to `dashboardName`."),
3074
- expiryHours: z14.number().min(1).max(MAX_EXPIRY_HOURS).optional().describe(`Token TTL in hours. Defaults to ${DEFAULT_EXPIRY_HOURS}. Max ${MAX_EXPIRY_HOURS} (30 days).`)
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).`)
3075
3183
  },
3076
- READ_ONLY,
3184
+ WRITE_CREATE,
3077
3185
  async (params) => {
3078
3186
  try {
3079
3187
  const embed = await services.widgetService.resolveEmbedId({
@@ -3127,70 +3235,70 @@ function registerGetDemoLink(server, client) {
3127
3235
  }
3128
3236
 
3129
3237
  // src/tools/frontend/generate-guest-token.ts
3130
- import { z as z15 } from "zod";
3238
+ import { z as z17 } from "zod";
3131
3239
  function registerGenerateGuestToken(server, client) {
3132
3240
  const services = createServiceBundle(client);
3133
3241
  server.tool(
3134
3242
  "generate_guest_token",
3135
3243
  "Generate a guest token for frontend embedding. Requires clientId. Omit dataAppName to auto-resolve.",
3136
3244
  {
3137
- clientId: z15.string().describe('Unique end-user identifier for multi-tenant filtering. Use "None" if no tenancy.'),
3138
- dataAppName: z15.string().optional().describe("Name of the data app. Omit to list available data apps."),
3139
- 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(
3140
3248
  "Raw API body for POST /api/v2/guest-token/create. If provided, sent directly and convenience fields are ignored."
3141
3249
  ),
3142
- params: z15.object({
3143
- allowedEmbeds: z15.array(z15.string()).optional().describe("Restrict token to specific embed IDs"),
3144
- rlsSettings: z15.array(z15.object({
3145
- metricId: z15.string().describe("Metric ID"),
3146
- 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")
3147
3255
  })).optional().describe("Legacy alias used in docs/examples; forwarded as params.rlsSettings"),
3148
- appFilters: z15.array(z15.object({
3149
- metricId: z15.string().describe("Metric ID"),
3150
- 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")
3151
3259
  })).optional().describe("Metric-level app filters"),
3152
- dashboardAppFilters: z15.array(z15.object({
3153
- dashboardId: z15.string().describe("Dashboard ID"),
3154
- values: z15.record(z15.unknown()).describe("Filter values by dashboard filter name"),
3155
- 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")
3156
3264
  })).optional().describe("Dashboard-level app filters"),
3157
- hideDashboardFilters: z15.array(z15.string()).optional().describe("Dashboard filter names to hide"),
3158
- userIdentifier: z15.string().optional().describe("End-user identifier for private metrics support"),
3159
- 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"')
3160
3268
  }).optional().describe("Advanced token params"),
3161
3269
  // Legacy convenience fields (auto-mapped into params for backward compatibility)
3162
- timezone: z15.string().optional().describe("Legacy: moved under params.timezone"),
3163
- allowedEmbeds: z15.array(z15.string()).optional().describe("Legacy: moved under params.allowedEmbeds"),
3164
- rlsSettings: z15.array(z15.object({
3165
- metricId: z15.string().describe("Metric ID"),
3166
- 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")
3167
3275
  })).optional().describe("Legacy: moved under params.rlsSettings"),
3168
- appFilters: z15.array(z15.object({
3169
- metricId: z15.string().describe("Metric ID"),
3170
- 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")
3171
3279
  })).optional().describe("Legacy: moved under params.appFilters"),
3172
- dashboardAppFilters: z15.array(z15.object({
3173
- dashboardId: z15.string().describe("Dashboard ID to apply filters to"),
3174
- values: z15.record(z15.unknown()).describe("Filter values for this dashboard"),
3175
- 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")
3176
3284
  })).optional().describe("Legacy: moved under params.dashboardAppFilters"),
3177
- permissions: z15.object({
3178
- isEnableArchiveMetrics: z15.boolean().optional(),
3179
- isEnableManageMetrics: z15.boolean().optional(),
3180
- isEnableCreateDashboardView: z15.boolean().optional(),
3181
- isEnableMetricUpdation: z15.boolean().optional(),
3182
- isEnableCustomizeLayout: z15.boolean().optional(),
3183
- isEnableUnderlyingData: z15.boolean().optional(),
3184
- isEnableDownloadMetrics: z15.boolean().optional(),
3185
- isShowSideBar: z15.boolean().optional(),
3186
- isShowDashboardName: z15.boolean().optional(),
3187
- 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()
3188
3296
  }).optional().describe("Runtime permission overrides (merge with and override embed-level settings)"),
3189
- theme: z15.record(z15.unknown()).optional().describe("Runtime theme overrides (overrides embed-level theme)"),
3190
- hideDashboardFilters: z15.array(z15.string()).optional().describe("Legacy: moved under params.hideDashboardFilters"),
3191
- userIdentifier: z15.string().optional().describe("Legacy: moved under params.userIdentifier"),
3192
- expiryTime: z15.number().optional().describe("Token TTL in milliseconds"),
3193
- 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")
3194
3302
  },
3195
3303
  WRITE_CREATE,
3196
3304
  async (params) => {
@@ -3243,7 +3351,7 @@ Next step: Use \`generate_embed_code\` to get copy-paste-ready frontend code.`
3243
3351
  }
3244
3352
 
3245
3353
  // src/tools/frontend/generate-embed-code.ts
3246
- import { z as z16 } from "zod";
3354
+ import { z as z18 } from "zod";
3247
3355
 
3248
3356
  // src/templates/react.ts
3249
3357
  function kebabCase(str) {
@@ -3352,7 +3460,7 @@ export default function DatabrainEmbed() {
3352
3460
  ### Environment Variables
3353
3461
  Set the following environment variables on your backend server:
3354
3462
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3355
- - \`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)
3356
3464
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3357
3465
 
3358
3466
  ### Security
@@ -3485,7 +3593,7 @@ export default function DatabrainEmbed() {
3485
3593
  ### Environment Variables
3486
3594
  Add the following to your \`.env.local\` file:
3487
3595
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3488
- - \`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)
3489
3597
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3490
3598
 
3491
3599
  ### Security
@@ -3617,7 +3725,7 @@ onMounted(async () => {
3617
3725
  ### Environment Variables
3618
3726
  Set the following environment variables on your backend server:
3619
3727
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3620
- - \`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)
3621
3729
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3622
3730
 
3623
3731
  ### Security
@@ -3767,7 +3875,7 @@ export class DatabrainEmbedComponent implements OnInit {
3767
3875
  ### Environment Variables
3768
3876
  Set the following environment variables on your backend server:
3769
3877
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3770
- - \`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)
3771
3879
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3772
3880
 
3773
3881
  ### Security
@@ -3909,7 +4017,7 @@ app.listen(3001, () => {
3909
4017
  ### Environment Variables
3910
4018
  Set the following environment variables on your backend server:
3911
4019
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
3912
- - \`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)
3913
4021
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
3914
4022
 
3915
4023
  ### Security
@@ -4041,7 +4149,7 @@ export default function DatabrainEmbed() {
4041
4149
  ### Environment Variables
4042
4150
  Set the following environment variables on your backend server:
4043
4151
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
4044
- - \`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)
4045
4153
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
4046
4154
 
4047
4155
  ### Security
@@ -4188,7 +4296,7 @@ app.listen(3001, () => {
4188
4296
  ### Environment Variables
4189
4297
  Set the following environment variables on your backend server:
4190
4298
  - \`DATABRAIN_API_TOKEN\` - Your Databrain API token
4191
- - \`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)
4192
4300
  - \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
4193
4301
 
4194
4302
  ### Security
@@ -4239,22 +4347,22 @@ function registerGenerateEmbedCode(server, _client) {
4239
4347
  "generate_embed_code",
4240
4348
  "Generate framework-specific embed code (React, Next.js, Vue, Angular, Svelte, SolidJS, vanilla HTML).",
4241
4349
  {
4242
- embedId: z16.string().describe("The embed ID to generate code for"),
4243
- framework: z16.enum(FRAMEWORKS).describe("Frontend framework"),
4244
- embedType: z16.enum(["dashboard", "metric"]).optional().default("dashboard"),
4245
- options: z16.object({
4246
- enableDownloadCsv: z16.boolean().optional(),
4247
- enableEmailCsv: z16.boolean().optional(),
4248
- enableDownloadAllMetrics: z16.boolean().optional(),
4249
- enableDownloadAllPdf: z16.boolean().optional(),
4250
- disableFullscreen: z16.boolean().optional(),
4251
- isHideTablePreview: z16.boolean().optional(),
4252
- isHideChartSettings: z16.boolean().optional(),
4253
- isStickyDashboardFilters: z16.boolean().optional(),
4254
- enableMultiMetricFilters: z16.boolean().optional(),
4255
- themeName: z16.string().optional(),
4256
- customTheme: z16.record(z16.unknown()).optional(),
4257
- 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()
4258
4366
  }).optional()
4259
4367
  },
4260
4368
  READ_ONLY,
@@ -4294,17 +4402,17 @@ ${generated.instructions}`;
4294
4402
  }
4295
4403
 
4296
4404
  // src/tools/analytics/list-dashboards.ts
4297
- import { z as z17 } from "zod";
4405
+ import { z as z19 } from "zod";
4298
4406
  function registerListDashboards(server, client) {
4299
4407
  const services = createServiceBundle(client);
4300
4408
  server.tool(
4301
4409
  "list_dashboards",
4302
4410
  "List dashboards globally or for a specific data app.",
4303
4411
  {
4304
- scope: z17.enum(["auto", "global", "data_app"]).optional().describe(
4412
+ scope: z19.enum(["auto", "global", "data_app"]).optional().describe(
4305
4413
  "Dashboard listing scope. auto: use data_app when dataAppName + API token are available, otherwise global."
4306
4414
  ),
4307
- 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.")
4308
4416
  },
4309
4417
  READ_ONLY,
4310
4418
  async (params) => {
@@ -4339,7 +4447,7 @@ function registerListDashboards(server, client) {
4339
4447
  }
4340
4448
 
4341
4449
  // src/tools/query/ask-question.ts
4342
- import { z as z18 } from "zod";
4450
+ import { z as z20 } from "zod";
4343
4451
  var MAX_DISPLAY_ROWS = 50;
4344
4452
  function formatDataTable(data, columns) {
4345
4453
  if (!Array.isArray(data) || data.length === 0) return "_No data returned._";
@@ -4369,16 +4477,16 @@ function registerAskQuestion(server, client) {
4369
4477
  "ask_question",
4370
4478
  "Ask a natural language question \u2014 converts to SQL, returns data + chart suggestion. Requires datamartName with a populated semantic layer.",
4371
4479
  {
4372
- question: z18.string().describe(
4480
+ question: z20.string().describe(
4373
4481
  'The natural language question to ask (e.g. "What was total revenue last month?")'
4374
4482
  ),
4375
- datamartName: z18.string().optional().describe(
4483
+ datamartName: z20.string().optional().describe(
4376
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."
4377
4485
  ),
4378
- sessionId: z18.string().optional().describe(
4486
+ sessionId: z20.string().optional().describe(
4379
4487
  "Session ID from a previous ask_question response, for follow-up questions."
4380
4488
  ),
4381
- 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.')
4382
4490
  },
4383
4491
  WRITE_CREATE,
4384
4492
  async (params) => {
@@ -4392,7 +4500,13 @@ function registerAskQuestion(server, client) {
4392
4500
  );
4393
4501
  }
4394
4502
  if (datamarts.length === 1) {
4395
- 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;
4396
4510
  } else {
4397
4511
  const list = datamarts.map(formatDatamart).join("\n");
4398
4512
  return toolSuccess(
@@ -4404,6 +4518,11 @@ Call \`ask_question\` again with the \`datamartName\` parameter.`
4404
4518
  );
4405
4519
  }
4406
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
+ }
4407
4526
  try {
4408
4527
  const semanticLayer = await services.queryApi.getSemanticLayer(
4409
4528
  datamartName
@@ -4543,14 +4662,14 @@ To fix this:
4543
4662
  }
4544
4663
 
4545
4664
  // src/tools/query/get-semantic-layer.ts
4546
- import { z as z19 } from "zod";
4665
+ import { z as z21 } from "zod";
4547
4666
  function registerGetSemanticLayer(server, client) {
4548
4667
  const services = createServiceBundle(client);
4549
4668
  server.tool(
4550
4669
  "get_semantic_layer",
4551
4670
  "Get semantic layer metadata for a datamart: table/column descriptions, synonyms, completion score. Omit datamartName to auto-resolve.",
4552
4671
  {
4553
- datamartName: z19.string().optional().describe(
4672
+ datamartName: z21.string().optional().describe(
4554
4673
  "Name of the datamart to inspect. Omit to list available datamarts."
4555
4674
  )
4556
4675
  },
@@ -4660,15 +4779,15 @@ ${result.feedback}`);
4660
4779
  }
4661
4780
 
4662
4781
  // src/tools/query/update-semantic-layer.ts
4663
- import { z as z20 } from "zod";
4664
- var columnSchema = z20.object({
4665
- name: z20.string().describe("Column name (must exist in the datamart)"),
4666
- description: z20.string().optional().describe("Business-friendly description of what this column represents"),
4667
- 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(
4668
4787
  'Alternative names users might use for this column (e.g. ["revenue", "sales amount"])'
4669
4788
  ),
4670
- miscellaneousInfo: z20.string().optional().describe("Additional context such as valid values, units, or caveats"),
4671
- columnType: z20.enum([
4789
+ miscellaneousInfo: z22.string().optional().describe("Additional context such as valid values, units, or caveats"),
4790
+ columnType: z22.enum([
4672
4791
  "String",
4673
4792
  "Long String",
4674
4793
  "String (Custom)",
@@ -4681,30 +4800,58 @@ var columnSchema = z20.object({
4681
4800
  "JSON"
4682
4801
  ]).nullable().optional().describe('Semantic column type. Use "Identifier" for IDs/foreign keys, "Number" for numeric values, "String" for text. Omit for date columns.')
4683
4802
  });
4684
- var tableSchema = z20.object({
4685
- name: z20.string().describe("Table name (must exist in the datamart)"),
4686
- 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(
4687
4806
  "Business-friendly description of what this table represents"
4688
4807
  ),
4689
- synonyms: z20.array(z20.string()).optional().describe("Alternative names for this table"),
4690
- miscellaneousInfo: z20.string().optional().describe(
4808
+ synonyms: z22.array(z22.string()).optional().describe("Alternative names for this table"),
4809
+ miscellaneousInfo: z22.string().optional().describe(
4691
4810
  "Additional context such as how the table relates to other tables"
4692
4811
  ),
4693
- 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")
4694
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
+ }
4695
4842
  function registerUpdateSemanticLayer(server, client) {
4696
4843
  const services = createServiceBundle(client);
4697
4844
  server.tool(
4698
4845
  "update_semantic_layer",
4699
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.",
4700
4847
  {
4701
- datamartName: z20.string().optional().describe(
4848
+ datamartName: z22.string().optional().describe(
4702
4849
  "Name of the datamart to update. Omit to list available datamarts."
4703
4850
  ),
4704
- tables: z20.array(tableSchema).optional().describe(
4851
+ tables: z22.array(tableSchema2).optional().describe(
4705
4852
  "Table and column definitions to update. Only include tables/columns you want to change."
4706
4853
  ),
4707
- feedback: z20.string().optional().describe(
4854
+ feedback: z22.string().optional().describe(
4708
4855
  'General feedback or context about the datamart (e.g. "This datamart contains e-commerce data for the US market").'
4709
4856
  )
4710
4857
  },
@@ -4727,9 +4874,10 @@ function registerUpdateSemanticLayer(server, client) {
4727
4874
  });
4728
4875
  if (resolution.kind !== "resolved") return resolution.response;
4729
4876
  const datamartName = resolution.name;
4877
+ const tables = normalizeTablesForApi(params.tables);
4730
4878
  await services.queryApi.updateSemanticLayer({
4731
4879
  datamartName,
4732
- tables: params.tables,
4880
+ tables,
4733
4881
  feedback: params.feedback
4734
4882
  });
4735
4883
  const updatedParts = [];
@@ -4761,18 +4909,128 @@ You can now:
4761
4909
  );
4762
4910
  }
4763
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
+
4764
5022
  // src/tools/widgets/list-widgets.ts
4765
- import { z as z21 } from "zod";
5023
+ import { z as z24 } from "zod";
4766
5024
  function registerListWidgets(server, client) {
4767
5025
  const services = createServiceBundle(client);
4768
5026
  server.tool(
4769
5027
  "list_widgets",
4770
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.",
4771
5029
  {
4772
- clientId: z21.string().describe("Client/tenant identifier for this dashboard"),
4773
- dashboardName: z21.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
4774
- workspaceName: z21.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
4775
- 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.")
4776
5034
  },
4777
5035
  READ_ONLY,
4778
5036
  async (params) => {
@@ -4818,28 +5076,28 @@ function registerListWidgets(server, client) {
4818
5076
  }
4819
5077
 
4820
5078
  // src/tools/widgets/add-widget.ts
4821
- import { z as z22 } from "zod";
5079
+ import { z as z25 } from "zod";
4822
5080
  function registerAddWidget(server, client) {
4823
5081
  const services = createServiceBundle(client);
4824
5082
  server.tool(
4825
5083
  "add_widget",
4826
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.",
4827
5085
  {
4828
- clientId: z22.string().describe("Client/tenant identifier (widget is created in their scope)"),
4829
- dashboardName: z22.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
4830
- workspaceName: z22.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
4831
- embedId: z22.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
4832
- dataAppName: z22.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
4833
- templateMetricId: z22.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
4834
- name: z22.string().optional().describe("Widget name (overrides template)"),
4835
- metricId: z22.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
4836
- query: z22.string().optional().describe("SQL query body (overrides template)"),
4837
- chartOptions: z22.record(z22.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
4838
- description: z22.string().optional(),
4839
- 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."),
4840
- 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.'),
4841
- isPublished: z22.boolean().optional().describe("Publish immediately (default true)"),
4842
- 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.")
4843
5101
  },
4844
5102
  WRITE_CREATE,
4845
5103
  async (params) => {
@@ -4949,20 +5207,20 @@ Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust
4949
5207
  }
4950
5208
 
4951
5209
  // src/tools/widgets/update-widget.ts
4952
- import { z as z23 } from "zod";
5210
+ import { z as z26 } from "zod";
4953
5211
  function registerUpdateWidget(server, client) {
4954
5212
  const services = createServiceBundle(client);
4955
5213
  server.tool(
4956
5214
  "update_widget",
4957
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.",
4958
5216
  {
4959
- externalMetricId: z23.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
4960
- clientId: z23.string().describe("Client/tenant identifier"),
4961
- dashboardName: z23.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
4962
- workspaceName: z23.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
4963
- embedId: z23.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
4964
- dataAppName: z23.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank."),
4965
- 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.")
4966
5224
  },
4967
5225
  WRITE_IDEMPOTENT,
4968
5226
  async (params) => {
@@ -5017,19 +5275,19 @@ Use \`list_widgets\` to verify.`
5017
5275
  }
5018
5276
 
5019
5277
  // src/tools/widgets/remove-widget.ts
5020
- import { z as z24 } from "zod";
5278
+ import { z as z27 } from "zod";
5021
5279
  function registerRemoveWidget(server, client) {
5022
5280
  const services = createServiceBundle(client);
5023
5281
  server.tool(
5024
5282
  "remove_widget",
5025
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.",
5026
5284
  {
5027
- externalMetricId: z24.string().describe("Internal uuid of the widget (externalMetrics.id)"),
5028
- clientId: z24.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
5029
- dashboardName: z24.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
5030
- workspaceName: z24.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
5031
- embedId: z24.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
5032
- 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.")
5033
5291
  },
5034
5292
  DESTRUCTIVE,
5035
5293
  async (params) => {
@@ -5077,21 +5335,21 @@ Note: this is a per-client soft delete. The widget remains available for other c
5077
5335
  }
5078
5336
 
5079
5337
  // src/tools/widgets/generate-widget.ts
5080
- import { z as z25 } from "zod";
5338
+ import { z as z28 } from "zod";
5081
5339
  function registerGenerateWidget(server, client) {
5082
5340
  const services = createServiceBundle(client);
5083
5341
  server.tool(
5084
5342
  "generate_widget",
5085
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.",
5086
5344
  {
5087
- clientId: z25.string().describe("Client/tenant identifier"),
5088
- 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.'),
5089
- dashboardName: z25.string().optional().describe("Dashboard name (non-technical). Preferred input for CS/CX flows \u2014 pass this with optional `workspaceName` instead of `embedId`."),
5090
- workspaceName: z25.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
5091
- embedId: z25.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
5092
- dataAppName: z25.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
5093
- 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."),
5094
- 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.")
5095
5353
  },
5096
5354
  READ_ONLY,
5097
5355
  async (params) => {
@@ -5156,51 +5414,280 @@ function registerGenerateWidget(server, client) {
5156
5414
  );
5157
5415
  }
5158
5416
 
5159
- // src/tools/auth/connect.ts
5160
- import { z as z26 } from "zod";
5161
- function registerConnect(server, client) {
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);
5162
5432
  server.tool(
5163
- "connect",
5164
- "Bind this MCP session to a Databrain user. Pass the user's Databrain login email; the server mints a personal token cached locally so subsequent actions are role-checked and audited under that user. One-time per machine.",
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.",
5165
5435
  {
5166
- email: z26.string().email().describe("The Databrain login email for the user running this MCP session.")
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")
5167
5453
  },
5168
5454
  WRITE_IDEMPOTENT,
5169
- async ({ email }) => {
5455
+ async (params) => {
5170
5456
  try {
5171
- const { cred, persisted } = await client.bootstrapOperator(email);
5172
- const expiresInDays = Math.max(
5173
- 0,
5174
- Math.round((cred.expiresAt - Date.now()) / (24 * 60 * 60 * 1e3))
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
5175
5479
  );
5176
- const envPatActive = client.getActiveAuth().source === "env-pat";
5177
- const notes = [];
5178
- if (envPatActive) {
5179
- notes.push(
5180
- "DATABRAIN_PAT is set in your MCP env, so that token continues to be used as the bearer for API calls. The cached identity is now available for `whoami`. Unset DATABRAIN_PAT if you want this freshly minted PAT to be used instead."
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.`
5181
5499
  );
5182
- } else {
5183
- notes.push(
5184
- "Mutations from this MCP session are now role-checked and audit-logged under this user."
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
+ })
5185
5559
  );
5186
5560
  }
5187
- if (!persisted) {
5188
- notes.push(
5189
- "Could not persist the credential to disk \u2014 likely a sandboxed or read-only home directory. The PAT is held in memory for this session only; you'll be asked to connect again next time the MCP server restarts. To avoid this, generate a PAT in Databrain Settings and paste it into your MCP config as DATABRAIN_PAT."
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.`
5190
5642
  );
5191
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);
5192
5663
  return toolSuccess(
5193
5664
  JSON.stringify(
5194
5665
  {
5195
- status: "connected",
5196
- email: cred.email,
5197
- userId: cred.userId,
5198
- role: cred.role,
5199
- workspaces: cred.workspaces,
5200
- expiresInDays,
5201
- persisted,
5202
- activeBearer: envPatActive ? "env-pat" : "cached-pat",
5203
- notes
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.`
5204
5691
  },
5205
5692
  null,
5206
5693
  2
@@ -5208,104 +5695,119 @@ function registerConnect(server, client) {
5208
5695
  );
5209
5696
  } catch (err) {
5210
5697
  if (err instanceof DatabrainApiError) return err.toToolResponse();
5211
- return toolError(`Unexpected error during connect: ${err.message}`);
5698
+ return toolError(`Unexpected error: ${err.message}`);
5212
5699
  }
5213
5700
  }
5214
5701
  );
5215
5702
  }
5216
5703
 
5217
- // src/tools/auth/whoami.ts
5218
- function registerWhoami(server, client) {
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);
5219
5708
  server.tool(
5220
- "whoami",
5221
- "Return the credential currently used for API calls and (if known) the Databrain user it belongs to. Use to confirm operator identity before mutating actions.",
5222
- {},
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
+ },
5223
5720
  READ_ONLY,
5224
- async () => {
5225
- const auth = client.getActiveAuth();
5226
- const cached = client.getOperator();
5227
- if (auth.source === "env-pat") {
5228
- const body = {
5229
- status: "connected",
5230
- activeBearer: "env-pat",
5231
- note: "Using DATABRAIN_PAT from your MCP env. The backend resolves the user behind this PAT \u2014 actions are role-checked and audited under their account."
5232
- };
5233
- if (cached) {
5234
- body.cachedIdentity = {
5235
- email: cached.email,
5236
- userId: cached.userId,
5237
- role: cached.role,
5238
- workspaces: cached.workspaces,
5239
- note: "Local cache from a prior `connect` \u2014 informational only; the env PAT is the active bearer."
5240
- };
5241
- } else {
5242
- body.cachedIdentity = null;
5243
- body.hint = "Run `connect <your-databrain-email>` to populate identity metadata locally for richer whoami output. The env PAT will continue to be the active bearer regardless.";
5244
- }
5245
- return toolSuccess(JSON.stringify(body, null, 2));
5246
- }
5247
- if (auth.source === "cached-pat") {
5248
- const op = auth.operator;
5249
- const expiresInDays = Math.max(
5250
- 0,
5251
- Math.round((op.expiresAt - Date.now()) / (24 * 60 * 60 * 1e3))
5252
- );
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
+ });
5253
5738
  return toolSuccess(
5254
5739
  JSON.stringify(
5255
5740
  {
5256
- status: "connected",
5257
- activeBearer: "cached-pat",
5258
- email: op.email,
5259
- userId: op.userId,
5260
- role: op.role,
5261
- workspaces: op.workspaces,
5262
- expiresInDays
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
+ }))
5263
5756
  },
5264
5757
  null,
5265
5758
  2
5266
5759
  )
5267
5760
  );
5761
+ } catch (err) {
5762
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
5763
+ return toolError(`Unexpected error: ${err.message}`);
5268
5764
  }
5269
- return toolSuccess(
5270
- JSON.stringify(
5271
- {
5272
- status: "not_connected",
5273
- activeBearer: auth.source,
5274
- note: "No operator bound to this MCP session. Calls fall back to the org-level service token (no per-user audit). Either run `connect <email>` to bootstrap interactively, or paste a Personal Access Token into your MCP config as DATABRAIN_PAT."
5275
- },
5276
- null,
5277
- 2
5278
- )
5279
- );
5280
5765
  }
5281
5766
  );
5282
5767
  }
5283
5768
 
5284
- // src/tools/auth/disconnect.ts
5285
- function registerDisconnect(server, client) {
5769
+ // src/tools/auth/whoami.ts
5770
+ function registerWhoami(server, client) {
5286
5771
  server.tool(
5287
- "disconnect",
5288
- "Wipe the locally cached operator credential for this Databrain instance. Use when switching Databrain accounts or revoking access on a shared machine. Subsequent calls fall back to the service token until `connect` is run again.",
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.",
5289
5774
  {},
5290
- DESTRUCTIVE,
5775
+ READ_ONLY,
5291
5776
  async () => {
5292
- const wasConnected = client.getOperator() !== null;
5293
- const envPatActive = client.getActiveAuth().source === "env-pat";
5294
- client.clearOperator();
5295
- const notes = [];
5296
- if (wasConnected) {
5297
- notes.push("Local operator credential cleared. Run `connect` again to rebind.");
5298
- } else {
5299
- notes.push("No cached operator to clear.");
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
+ );
5300
5790
  }
5301
- if (envPatActive) {
5302
- notes.push(
5303
- "DATABRAIN_PAT is still set in your MCP env, so API calls continue using that token. Unset it in your MCP config to fully disconnect."
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
+ )
5304
5802
  );
5305
5803
  }
5306
5804
  return toolSuccess(
5307
5805
  JSON.stringify(
5308
- { status: "disconnected", wasConnected, notes },
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
+ },
5309
5811
  null,
5310
5812
  2
5311
5813
  )
@@ -5316,13 +5818,13 @@ function registerDisconnect(server, client) {
5316
5818
 
5317
5819
  // src/tools/registry.ts
5318
5820
  function registerAllTools(server, client) {
5319
- registerConnect(server, client);
5320
5821
  registerWhoami(server, client);
5321
- registerDisconnect(server, client);
5322
5822
  registerListDatasources(server, client);
5323
5823
  registerDiscoverDatasourceSchema(server, client);
5324
5824
  registerListDatamarts(server, client);
5325
5825
  registerListWorkspaces(server, client);
5826
+ registerSyncDatasource(server, client);
5827
+ registerCreateDatamart(server, client);
5326
5828
  registerListDataApps(server, client);
5327
5829
  registerCreateDataApp(server, client);
5328
5830
  registerListApiTokens(server, client);
@@ -5340,11 +5842,16 @@ function registerAllTools(server, client) {
5340
5842
  registerAskQuestion(server, client);
5341
5843
  registerGetSemanticLayer(server, client);
5342
5844
  registerUpdateSemanticLayer(server, client);
5845
+ registerStartSemanticLayerGeneration(server, client);
5846
+ registerGetSemanticLayerGenerationStatus(server, client);
5343
5847
  registerListWidgets(server, client);
5848
+ registerValidateMetricSpec(server, client);
5849
+ registerCreateOrUpdateMetricFromQuery(server, client);
5344
5850
  registerAddWidget(server, client);
5345
5851
  registerUpdateWidget(server, client);
5346
5852
  registerRemoveWidget(server, client);
5347
5853
  registerGenerateWidget(server, client);
5854
+ registerListScheduledReports(server, client);
5348
5855
  }
5349
5856
 
5350
5857
  // src/resources/content/getting-started.ts
@@ -7707,7 +8214,7 @@ Then create an embed configuration with tenancy settings:
7707
8214
 
7708
8215
  ### Step 5: Generate Guest Tokens Per Tenant
7709
8216
 
7710
- 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:
7711
8218
 
7712
8219
  \`\`\`typescript
7713
8220
  // Server-side
@@ -7719,12 +8226,12 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
7719
8226
  },
7720
8227
  body: JSON.stringify({
7721
8228
  embedId: 'embed-789',
7722
- clientId: 'acme', // Maps to org_id values in the database
8229
+ clientId: 'tenant_a', // Maps to org_id values in the database
7723
8230
  }),
7724
8231
  });
7725
8232
  \`\`\`
7726
8233
 
7727
- When a user from "Globex" (org_id = "globex") logs in:
8234
+ When a user from tenant B (org_id = "tenant_b") logs in:
7728
8235
 
7729
8236
  \`\`\`typescript
7730
8237
  const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/create', {
@@ -7735,7 +8242,7 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
7735
8242
  },
7736
8243
  body: JSON.stringify({
7737
8244
  embedId: 'embed-789',
7738
- clientId: 'globex',
8245
+ clientId: 'tenant_b',
7739
8246
  }),
7740
8247
  });
7741
8248
  \`\`\`
@@ -7743,20 +8250,20 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
7743
8250
  ### Step 6: Render
7744
8251
 
7745
8252
  Both users see the same dashboard layout and metrics, but:
7746
- - Acme sees only their projects, tasks, and team members
7747
- - 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
7748
8255
  - Both see all task statuses (shared reference table)
7749
8256
 
7750
8257
  ### Result
7751
8258
 
7752
8259
  \`\`\`
7753
- Acme User sees:
7754
- - 12 projects (all with org_id = 'acme')
8260
+ Tenant A user sees:
8261
+ - 12 projects (all with org_id = 'tenant_a')
7755
8262
  - 87 tasks
7756
8263
  - 15 team members
7757
8264
 
7758
- Globex User sees:
7759
- - 8 projects (all with org_id = 'globex')
8265
+ Tenant B user sees:
8266
+ - 8 projects (all with org_id = 'tenant_b')
7760
8267
  - 45 tasks
7761
8268
  - 9 team members
7762
8269
  \`\`\`
@@ -8430,6 +8937,108 @@ Present the URL to the user and remind them: (a) only \`isPublished: true\` widg
8430
8937
 
8431
8938
  After completion, suggest: \`brand-my-embed\` if they want to adjust theme, or \`manage-embeds\` if they want to tweak access settings / options.`;
8432
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
+
8433
9042
  // src/prompts/registry.ts
8434
9043
  var prompts = [
8435
9044
  {
@@ -8461,6 +9070,11 @@ var prompts = [
8461
9070
  name: "customize-customer-dashboard",
8462
9071
  description: "Add, modify, or remove widgets on a specific customer's dashboard embed. Per-client scope \u2014 changes only affect that customer.",
8463
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
8464
9078
  }
8465
9079
  ];
8466
9080
  function registerAllPrompts(server) {
@@ -8484,34 +9098,35 @@ function createServer(config) {
8484
9098
  const client = new DatabrainClient(config);
8485
9099
  const auth = client.getActiveAuth();
8486
9100
  let operatorBlurb;
8487
- if (auth.source === "env-pat") {
8488
- operatorBlurb = "OPERATOR IDENTITY: a Personal Access Token is active via DATABRAIN_PAT. The backend resolves the user behind it \u2014 all calls are role-checked and audited under their account. Do NOT ask the user to connect; their identity is already bound. Use `whoami` if you need to confirm details.\n\n";
8489
- } else if (auth.source === "cached-pat") {
8490
- const op = auth.operator;
8491
- operatorBlurb = `OPERATOR IDENTITY: connected as ${op.email} (${op.role}) via locally cached PAT. All mutations are role-checked and audit-logged under this user. Use \`whoami\` to confirm, \`disconnect\` to switch users.
8492
-
8493
- `;
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";
8494
9105
  } else {
8495
- operatorBlurb = "OPERATOR IDENTITY: not connected. On the user's first request, ask once: \"What's your Databrain login email? I'll bind this MCP session to your account so actions are role-checked and audited.\" Then call `connect` with their email. If `connect` reports it could not persist the credential (sandboxed host), suggest the user generate a PAT in Databrain Settings and paste it into their MCP config as DATABRAIN_PAT for a stable bind. If they decline to connect, fall back to service-token mode (calls still work, but mutations are tagged actor=system in the audit log). Do not pester after one ask.\n\n";
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";
8496
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.";
8497
9110
  const server = new McpServer(
8498
9111
  {
8499
9112
  name: "databrain",
8500
- version: "0.1.0",
8501
- 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."
8502
9115
  },
8503
9116
  {
8504
9117
  instructions: operatorBlurb + `Databrain uses two token types \u2014 never confuse them:
8505
- \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, 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.
8506
- \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.
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}
8507
9120
 
8508
- 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:
8509
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)
8510
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)
8511
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.
8512
9128
 
8513
- Infrastructure CREATION (datasources, datamarts, workspaces) is done in the Databrain UI.
8514
- 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.
8515
9130
 
8516
9131
  After embed setup, customize with: brand-my-embed prompt for theming. Use update_embed (body passthrough) for filters, localization, and permissions.
8517
9132
  Use generate_guest_token for frontend auth and generate_embed_code for framework snippets.
@@ -8535,7 +9150,6 @@ Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, fi
8535
9150
  }
8536
9151
 
8537
9152
  // src/index.ts
8538
- var VERSION = "0.2.0";
8539
9153
  function banner(msg) {
8540
9154
  process.stderr.write(`[databrain] ${msg}
8541
9155
  `);
@@ -8546,7 +9160,7 @@ async function main() {
8546
9160
  const server = createServer(config);
8547
9161
  const transport = new StdioServerTransport();
8548
9162
  await server.connect(transport);
8549
- banner(`v${VERSION} ready (21 tools, 11 resources, 5 prompts)`);
9163
+ banner(`v${MCP_SERVER_VERSION} ready`);
8550
9164
  } catch (error) {
8551
9165
  logger.error("Failed to start server", error);
8552
9166
  process.exit(1);