@databrainhq/mcp-server 0.2.9 → 0.2.11

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,9 +8,11 @@ import { z, ZodError } from "zod";
8
8
  var configSchema = z.object({
9
9
  serviceToken: z.string().min(1).optional(),
10
10
  apiToken: z.string().min(1).optional(),
11
- apiUrl: z.string().url().default("https://api.usedatabrain.com")
12
- }).refine((c) => c.serviceToken || c.apiToken, {
13
- message: "At least one of DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN must be set"
11
+ pat: z.string().min(1).optional(),
12
+ apiUrl: z.string().url().default("https://api.usedatabrain.com"),
13
+ 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"
14
16
  });
15
17
  function formatConfigError(err) {
16
18
  const issues = err.issues.map((i) => i.message);
@@ -19,8 +21,12 @@ function formatConfigError(err) {
19
21
  lines.push(` - ${issue}`);
20
22
  }
21
23
  lines.push("");
22
- lines.push(" Get a service token at Settings > Service Tokens in your Databrain dashboard.");
23
- lines.push(' Then add to your MCP config: { "env": { "DATABRAIN_SERVICE_TOKEN": "..." } }');
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": "..." } }');
28
+ lines.push("");
29
+ lines.push(" Self-hosted? Also set DATABRAIN_API_URL to your instance's API origin (e.g. https://api.your-databrain.example.com). Tokens minted on cloud do not work against a self-hosted instance and vice versa.");
24
30
  return lines.join("\n");
25
31
  }
26
32
  function loadConfig() {
@@ -28,7 +34,9 @@ function loadConfig() {
28
34
  return configSchema.parse({
29
35
  serviceToken: process.env.DATABRAIN_SERVICE_TOKEN || void 0,
30
36
  apiToken: process.env.DATABRAIN_API_TOKEN || void 0,
31
- apiUrl: process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com"
37
+ pat: process.env.DATABRAIN_PAT || void 0,
38
+ apiUrl: process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com",
39
+ demoDomain: process.env.DATABRAIN_DEMO_DOMAIN || "https://demo.usedatabrain.com"
32
40
  });
33
41
  } catch (err) {
34
42
  if (err instanceof ZodError) {
@@ -78,13 +86,39 @@ var DatabrainApiError = class extends Error {
78
86
  this.name = "DatabrainApiError";
79
87
  }
80
88
  toToolResponse() {
89
+ const parts = [`Error: ${this.message}`];
90
+ const detailDump = this.formatDetails();
91
+ if (detailDump) parts.push(detailDump);
92
+ const hint = this.hint();
93
+ if (hint) parts.push(hint);
81
94
  return {
82
- content: [{ type: "text", text: `Error: ${this.message}
83
-
84
- ${this.hint()}` }],
95
+ content: [{ type: "text", text: parts.join("\n\n") }],
85
96
  isError: true
86
97
  };
87
98
  }
99
+ /**
100
+ * Surface API-returned details for 4xx errors so the LLM can recover or
101
+ * report the real reason (validation messages, field names, etc.). Skips
102
+ * fields already collapsed into `message`/`code`, and skips 5xx where the
103
+ * payload usually isn't actionable.
104
+ */
105
+ formatDetails() {
106
+ if (this.status < 400 || this.status >= 500) return "";
107
+ if (!this.details || typeof this.details !== "object") return "";
108
+ const skipKeys = /* @__PURE__ */ new Set(["message", "detail", "error", "code"]);
109
+ const extra = {};
110
+ for (const [k, v] of Object.entries(this.details)) {
111
+ if (skipKeys.has(k)) continue;
112
+ if (v === null || v === void 0) continue;
113
+ extra[k] = v;
114
+ }
115
+ if (Object.keys(extra).length === 0) return "";
116
+ try {
117
+ return `API details: ${JSON.stringify(extra)}`;
118
+ } catch {
119
+ return "";
120
+ }
121
+ }
88
122
  hint() {
89
123
  if (this.status === 401 || this.status === 403) {
90
124
  if (this.code === "INVALID_SERVICE_TOKEN" || this.code === "MISSING_SERVICE_TOKEN" || this.message.toLowerCase().includes("service token")) {
@@ -154,6 +188,122 @@ var logger = {
154
188
  error: (msg, data) => log("error", msg, data)
155
189
  };
156
190
 
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
+ }
306
+
157
307
  // src/client/databrain-client.ts
158
308
  var MCP_SERVER_VERSION = "0.2.0";
159
309
  var MCP_USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION}`;
@@ -175,6 +325,8 @@ var DatabrainClient = class {
175
325
  }
176
326
  cache = new TtlCache();
177
327
  runtimeApiToken;
328
+ operator = null;
329
+ operatorLoaded = false;
178
330
  hasApiToken() {
179
331
  return !!(this.runtimeApiToken || this.config.apiToken);
180
332
  }
@@ -188,12 +340,90 @@ var DatabrainClient = class {
188
340
  getApiUrl() {
189
341
  return this.config.apiUrl;
190
342
  }
343
+ getDemoDomain() {
344
+ return this.config.demoDomain;
345
+ }
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
+ /**
385
+ * Describes which credential is currently used as the bearer for outgoing
386
+ * API calls. Used by `whoami` so the LLM can give the user a truthful answer.
387
+ */
388
+ 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
+ if (this.config.serviceToken) return { source: "service-token" };
393
+ if (this.runtimeApiToken || this.config.apiToken) return { source: "api-token" };
394
+ return { source: "none" };
395
+ }
191
396
  getToken(auth) {
192
397
  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;
193
401
  if (auth === "service") return this.config.serviceToken;
194
402
  return this.runtimeApiToken || this.config.apiToken;
195
403
  }
196
404
  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
+ }
425
+ }
426
+ async requestInternal(options) {
197
427
  if (options.invalidateCache) {
198
428
  for (const prefix of options.invalidateCache) {
199
429
  this.cache.invalidateByPrefix(prefix);
@@ -479,6 +709,14 @@ var DatasourcesApi = class {
479
709
  cacheTtlMs: 5 * 60 * 1e3
480
710
  });
481
711
  }
712
+ async resolveIdByName(datasourceName) {
713
+ try {
714
+ const schema = await this.getDatasourceSchema(datasourceName);
715
+ return schema?.datasourceId || null;
716
+ } catch {
717
+ return null;
718
+ }
719
+ }
482
720
  };
483
721
 
484
722
  // src/api/embeds-api.ts
@@ -691,6 +929,7 @@ var DiscoveryService = class {
691
929
  return {
692
930
  name: isNonEmptyString(dashboard.name) && dashboard.name || isNonEmptyString(dashboard.dashboardName) && dashboard.dashboardName || "Unnamed Dashboard",
693
931
  dashboardId: isNonEmptyString(dashboard.dashboardId) && dashboard.dashboardId || isNonEmptyString(dashboard.externalDashboardId) && dashboard.externalDashboardId || isNonEmptyString(dashboard._id) && dashboard._id || isNonEmptyString(dashboard.id) && dashboard.id || isNonEmptyString(dashboard.uid) && dashboard.uid || null,
932
+ externalDashboardId: isNonEmptyString(dashboard.externalDashboardId) && dashboard.externalDashboardId || null,
694
933
  workspaceName: isNonEmptyString(dashboard.workspaceName) && dashboard.workspaceName || isNonEmptyString(dashboard.workspace) && dashboard.workspace || isNonEmptyString(dashboard.workspaceId) && dashboard.workspaceId || null,
695
934
  metricCount: typeof dashboard.metricCount === "number" ? dashboard.metricCount : null
696
935
  };
@@ -703,6 +942,7 @@ var DiscoveryService = class {
703
942
  embedId: isNonEmptyString(embed.embedId) ? embed.embedId : null,
704
943
  internalId: isNonEmptyString(embed._id) && embed._id || isNonEmptyString(embed.id) && embed.id || null,
705
944
  dashboardId: metadata && isNonEmptyString(metadata.dashboardId) && metadata.dashboardId || isNonEmptyString(embed.dashboardId) && embed.dashboardId || null,
945
+ externalDashboardId: isNonEmptyString(embed.externalDashboardId) && embed.externalDashboardId || externalDashboard && isNonEmptyString(externalDashboard.externalDashboardId) && externalDashboard.externalDashboardId || metadata && isNonEmptyString(metadata.externalDashboardId) && metadata.externalDashboardId || null,
706
946
  dataAppName: metadata && isNonEmptyString(metadata.dataAppName) && metadata.dataAppName || null,
707
947
  embedType: isNonEmptyString(embed.embedType) ? embed.embedType : null
708
948
  };
@@ -938,14 +1178,135 @@ var asRecord2 = (v) => {
938
1178
  };
939
1179
  var isActiveDatasource = (ds) => Boolean(ds.isAuthenticated) && !ds.isDisabled;
940
1180
  var WidgetService = class {
941
- constructor(widgetsApi, embedsApi, dataAppsApi, datasourcesApi, guestTokenService, discoveryService) {
1181
+ constructor(widgetsApi, embedsApi, dataAppsApi, datasourcesApi, dashboardsApi, guestTokenService, discoveryService) {
942
1182
  this.widgetsApi = widgetsApi;
943
1183
  this.embedsApi = embedsApi;
944
1184
  this.dataAppsApi = dataAppsApi;
945
1185
  this.datasourcesApi = datasourcesApi;
1186
+ this.dashboardsApi = dashboardsApi;
946
1187
  this.guestTokenService = guestTokenService;
947
1188
  this.discoveryService = discoveryService;
948
1189
  }
1190
+ /**
1191
+ * Resolve the target embedId from a non-technical dashboard name (optionally
1192
+ * scoped to a workspace). This is the preferred entry point for CS/CX flows
1193
+ * where the user only knows the dashboard + workspace, not the embedId or
1194
+ * data app name. Returns a business-user-friendly error on ambiguity.
1195
+ */
1196
+ async resolveEmbedFromDashboard(dashboardName, workspaceName) {
1197
+ const wsProvided = workspaceName && workspaceName.trim().length > 0 ? workspaceName.trim() : void 0;
1198
+ let dashboards;
1199
+ try {
1200
+ dashboards = wsProvided ? await this.dashboardsApi.listDashboardsForWorkspace(wsProvided) : await this.dashboardsApi.listDashboardsAcrossWorkspaces();
1201
+ } catch (err) {
1202
+ return {
1203
+ ok: false,
1204
+ error: `Could not list dashboards: ${err.message}`
1205
+ };
1206
+ }
1207
+ const normalized = dashboards.map(
1208
+ (d) => this.discoveryService.normalizeDashboard(d)
1209
+ );
1210
+ const target = dashboardName.trim().toLowerCase();
1211
+ const matches = normalized.filter(
1212
+ (d) => d.name.trim().toLowerCase() === target
1213
+ );
1214
+ if (matches.length === 0) {
1215
+ const available = normalized.filter((d) => d.name && d.name !== "Unnamed Dashboard").map(
1216
+ (d) => d.workspaceName ? `"${d.name}" (workspace: ${d.workspaceName})` : `"${d.name}"`
1217
+ ).join(", ");
1218
+ return {
1219
+ ok: false,
1220
+ error: `No dashboard named "${dashboardName}"${wsProvided ? ` in workspace "${wsProvided}"` : ""}. ` + (available ? `Available dashboards: ${available}. Ask the user to pick by dashboard name.` : "No dashboards exist in this organization.")
1221
+ };
1222
+ }
1223
+ if (matches.length > 1) {
1224
+ const workspaces = matches.map((d) => d.workspaceName || "(unknown workspace)").join(", ");
1225
+ return {
1226
+ ok: false,
1227
+ error: `Dashboard name "${dashboardName}" is ambiguous \u2014 it exists in multiple workspaces (${workspaces}). Ask the user which workspace and retry with \`workspaceName\`.`
1228
+ };
1229
+ }
1230
+ const dashboard = matches[0];
1231
+ if (!dashboard.dashboardId) {
1232
+ return {
1233
+ ok: false,
1234
+ error: `Dashboard "${dashboard.name}" has no ID. This is unexpected \u2014 ask the user to verify the dashboard in the Databrain UI.`
1235
+ };
1236
+ }
1237
+ let embeds;
1238
+ try {
1239
+ embeds = await this.embedsApi.listEmbeds();
1240
+ } catch (err) {
1241
+ return {
1242
+ ok: false,
1243
+ error: `Could not list embeds: ${err.message}`
1244
+ };
1245
+ }
1246
+ const dashboardIds = new Set(
1247
+ [dashboard.dashboardId, dashboard.externalDashboardId].filter(
1248
+ (v) => typeof v === "string" && v.length > 0
1249
+ )
1250
+ );
1251
+ const embedMatches = embeds.map((e) => ({ raw: e, n: this.discoveryService.normalizeEmbed(e) })).filter((x) => {
1252
+ if (!x.n.embedId) return false;
1253
+ const embedIds = [x.n.dashboardId, x.n.externalDashboardId].filter(
1254
+ (v) => typeof v === "string" && v.length > 0
1255
+ );
1256
+ return embedIds.some((id) => dashboardIds.has(id));
1257
+ });
1258
+ if (embedMatches.length === 0) {
1259
+ return {
1260
+ ok: false,
1261
+ error: `Dashboard "${dashboard.name}" has no embed configured. An embed must be created in the Databrain UI before widgets can be managed for customers.`
1262
+ };
1263
+ }
1264
+ if (embedMatches.length > 1) {
1265
+ const names = embedMatches.map((x) => `"${x.n.name}"`).join(", ");
1266
+ return {
1267
+ ok: false,
1268
+ error: `Dashboard "${dashboard.name}" has multiple embeds (${names}). Ask the user which embed name they want to customize.`
1269
+ };
1270
+ }
1271
+ return {
1272
+ ok: true,
1273
+ value: {
1274
+ embedId: embedMatches[0].n.embedId,
1275
+ dashboardName: dashboard.name,
1276
+ workspaceName: dashboard.workspaceName
1277
+ }
1278
+ };
1279
+ }
1280
+ /**
1281
+ * Normalize widget-tool input: callers may supply `embedId` directly (legacy
1282
+ * / technical callers) or `dashboardName` + optional `workspaceName`
1283
+ * (non-technical CS/CX flow). Returns a single embedId and the resolved
1284
+ * dashboard info if lookup happened. Pass `required: true` to enforce that
1285
+ * the user named a target dashboard (recommended for CS/CX flows).
1286
+ */
1287
+ async resolveEmbedId(params) {
1288
+ if (params.embedId && params.embedId.trim().length > 0) {
1289
+ return { ok: true, value: { embedId: params.embedId.trim() } };
1290
+ }
1291
+ if (params.dashboardName && params.dashboardName.trim().length > 0) {
1292
+ const res = await this.resolveEmbedFromDashboard(
1293
+ params.dashboardName,
1294
+ params.workspaceName
1295
+ );
1296
+ if (!res.ok) return res;
1297
+ return {
1298
+ ok: true,
1299
+ value: { embedId: res.value.embedId, resolvedFrom: res.value }
1300
+ };
1301
+ }
1302
+ if (params.required) {
1303
+ return {
1304
+ ok: false,
1305
+ error: "Missing dashboard. Ask the user which dashboard to customize (by name) and optionally which workspace, then pass `dashboardName` (and `workspaceName` if needed). Technical callers may pass `embedId` directly."
1306
+ };
1307
+ }
1308
+ return { ok: true, value: { embedId: null } };
1309
+ }
949
1310
  /**
950
1311
  * Resolve the data app name from either a caller override, an embed's
951
1312
  * metadata, or the sole data app in the workspace. Used by every widget
@@ -1014,8 +1375,8 @@ var WidgetService = class {
1014
1375
  );
1015
1376
  if (named.length === 1) {
1016
1377
  const ds = named[0];
1017
- const id = ds._id || ds.id || "";
1018
1378
  const name = ds.name;
1379
+ const id = ds._id || ds.id || await this.datasourcesApi.resolveIdByName(name) || "";
1019
1380
  if (id && name) {
1020
1381
  return { ok: true, value: { integrationId: id, integrationName: name } };
1021
1382
  }
@@ -1030,14 +1391,17 @@ var WidgetService = class {
1030
1391
  let embedReason = "";
1031
1392
  if (params.embedId && embedProbe) {
1032
1393
  if (embedProbe.kind === "no_widgets") {
1033
- embedReason = ` Embed "${params.embedId}" has no widgets for clientId "${params.clientId}" to inherit from, so the datasource can't be auto-detected.`;
1394
+ embedReason = ` This dashboard has no widgets for client "${params.clientId}" yet, so there's no existing widget to inherit the datasource from.`;
1034
1395
  } else if (embedProbe.kind === "no_metric_fields") {
1035
- embedReason = ` Checked ${embedProbe.widgetsTried} widget(s) on embed "${params.embedId}"` + (embedProbe.fetchErrors > 0 ? ` (${embedProbe.fetchErrors} could not be fetched)` : "") + `, but none had datasource metadata to inherit from.`;
1396
+ embedReason = ` Checked ${embedProbe.widgetsTried} existing widget(s) on this dashboard` + (embedProbe.fetchErrors > 0 ? ` (${embedProbe.fetchErrors} could not be fetched)` : "") + `, but none had datasource metadata to inherit from.`;
1036
1397
  }
1037
1398
  }
1038
1399
  return {
1039
1400
  ok: false,
1040
- error: `Could not auto-resolve a datasource.${embedReason} Ask the user which datasource this widget should query \u2014 available options: ${nameList}. Then retry with \`integrationName: "<chosen name>"\` (no other fields needed). Technical callers can pass \`integrationId\` instead \u2014 see \`list_datasources\` for UUIDs.`
1401
+ error: `Could not auto-detect the datasource for this dashboard.${embedReason} STOP and ASK the user to choose one of these two options:
1402
+ (1) Pick an existing chart to copy from \u2014 call \`list_widgets\` and present the widgets by name, then retry via \`add_widget\` with \`templateMetricId\` (datasource inherited automatically). This is the recommended path for non-technical users.
1403
+ (2) Pick a datasource by name from: ${nameList}. Retry with \`integrationName: "<chosen name>"\`. Only use this if the user already knows their datasource.
1404
+ Do not guess between the two \u2014 wait for the user to choose.`
1041
1405
  };
1042
1406
  }
1043
1407
  async completeFromDatasources(provided) {
@@ -1068,12 +1432,12 @@ var WidgetService = class {
1068
1432
  const hint = provided.integrationId ? `No active datasource matches integrationId "${provided.integrationId}". Available datasources: ${available || "(none)"}.` : `No active datasource matches integrationName "${provided.integrationName}". Available datasources: ${available || "(none)"}. Match is case-insensitive \u2014 double-check the name with the user.`;
1069
1433
  return { ok: false, error: hint };
1070
1434
  }
1071
- const id = target._id || target.id || "";
1072
1435
  const name = typeof target.name === "string" ? target.name : "";
1436
+ const id = target._id || target.id || (name ? await this.datasourcesApi.resolveIdByName(name) : null) || "";
1073
1437
  if (!id || !name) {
1074
1438
  return {
1075
1439
  ok: false,
1076
- error: `Datasource match is missing id or name (id: "${id}", name: "${name}").`
1440
+ error: `Could not resolve datasource "${name || provided.integrationName || provided.integrationId}" to a UUID. The datasource list API does not return IDs; the schema lookup also failed \u2014 the datasource may need to be synced in the Databrain UI.`
1077
1441
  };
1078
1442
  }
1079
1443
  return { ok: true, value: { integrationId: id, integrationName: name } };
@@ -1258,6 +1622,7 @@ function createServiceBundle(client) {
1258
1622
  embedsApi,
1259
1623
  dataAppsApi,
1260
1624
  datasourcesApi,
1625
+ dashboardsApi,
1261
1626
  guestTokenService,
1262
1627
  discoveryService
1263
1628
  )
@@ -1658,9 +2023,7 @@ function registerListDatasources(server, client) {
1658
2023
  return toolSuccess(JSON.stringify({
1659
2024
  count: active.length,
1660
2025
  datasources: active.map((ds) => ({
1661
- id: ds._id || ds.id || null,
1662
- name: ds.name,
1663
- type: ds.dbName || ds.datasourceType || "unknown"
2026
+ name: ds.name
1664
2027
  })),
1665
2028
  inactive: inactive.length > 0 ? inactive.map((ds) => ({ name: ds.name })) : void 0
1666
2029
  }));
@@ -1857,79 +2220,8 @@ function registerListWorkspaces(server, client) {
1857
2220
  );
1858
2221
  }
1859
2222
 
1860
- // src/tools/infrastructure/link-datamart-to-workspace.ts
1861
- import { z as z9 } from "zod";
1862
- function registerLinkDatamartToWorkspace(server, client) {
1863
- server.tool(
1864
- "link_datamart_to_workspace",
1865
- "Link a datamart to an existing workspace. ALWAYS ask the user which workspace to use \u2014 do not auto-select.",
1866
- {
1867
- workspaceName: z9.string().optional().describe(
1868
- "Name of the workspace to update. Omit to list available workspaces and let the user choose."
1869
- ),
1870
- datamartName: z9.string().describe(
1871
- "Name of the datamart to link. Use `list_datamarts` to find it."
1872
- )
1873
- },
1874
- WRITE_IDEMPOTENT,
1875
- async ({ workspaceName, datamartName }) => {
1876
- try {
1877
- if (!workspaceName) {
1878
- const workspaces = await client.request({
1879
- method: "GET",
1880
- path: "/api/v2/workspace",
1881
- auth: "service",
1882
- cacheKey: "list_workspaces",
1883
- cacheTtlMs: 5 * 60 * 1e3
1884
- });
1885
- if (!Array.isArray(workspaces) || workspaces.length === 0) {
1886
- return toolError(
1887
- "No workspaces found. Create a workspace in the **Databrain UI** first, then come back here to link the datamart."
1888
- );
1889
- }
1890
- const list = workspaces.map((ws) => `- **${ws.name}** (ID: ${ws._id || ws.id})`).join("\n");
1891
- return toolSuccess(
1892
- `Found ${workspaces.length} workspace(s):
1893
-
1894
- ${list}
1895
-
1896
- **Ask the user** which workspace they want to link to datamart **${datamartName}**, then call \`link_datamart_to_workspace\` with both \`workspaceName\` and \`datamartName\`.`
1897
- );
1898
- }
1899
- const result = await client.request({
1900
- method: "PUT",
1901
- path: "/api/v2/workspace",
1902
- body: {
1903
- name: workspaceName,
1904
- connectionType: "DATAMART",
1905
- datamartName
1906
- },
1907
- auth: "service",
1908
- invalidateCache: ["list_workspaces"]
1909
- });
1910
- if (result.error) {
1911
- return toolError(
1912
- `Failed to link datamart: ${typeof result.error === "object" ? JSON.stringify(result.error) : result.error}`
1913
- );
1914
- }
1915
- return toolSuccess(
1916
- `Workspace **${workspaceName}** is now linked to datamart **${datamartName}**.
1917
-
1918
- Next steps:
1919
- - Build dashboards in the Databrain UI using this workspace
1920
- - Use \`list_dashboards\` to find dashboards in this workspace
1921
- - Use \`create_embed\` to embed dashboards into your app`
1922
- );
1923
- } catch (err) {
1924
- if (err instanceof DatabrainApiError) return err.toToolResponse();
1925
- return toolError(`Unexpected error: ${err.message}`);
1926
- }
1927
- }
1928
- );
1929
- }
1930
-
1931
2223
  // src/tools/embed/create-embed.ts
1932
- import { z as z10 } from "zod";
2224
+ import { z as z9 } from "zod";
1933
2225
  var DEFAULT_ACCESS_SETTINGS2 = {
1934
2226
  isAllowMetricCreation: false,
1935
2227
  isAllowMetricDeletion: false,
@@ -1942,18 +2234,18 @@ var DEFAULT_ACCESS_SETTINGS2 = {
1942
2234
  isAllowCreateDashboardView: false,
1943
2235
  metricCreationMode: "DRAG_DROP"
1944
2236
  };
1945
- var accessSettingsSchema = z10.object({
1946
- datamartName: z10.string().describe("Name of the datamart (required)"),
1947
- isAllowMetricCreation: z10.boolean().optional().describe("Allow end-users to create metrics"),
1948
- metricCreationMode: z10.enum(["DRAG_DROP", "CHAT"]).optional().describe("Metric creation mode"),
1949
- isAllowMetricDeletion: z10.boolean().optional().describe("Allow metric deletion"),
1950
- isAllowMetricUpdate: z10.boolean().optional().describe("Allow metric editing"),
1951
- isAllowMetricLayoutChange: z10.boolean().optional().describe("Allow layout changes"),
1952
- isAllowManageMetrics: z10.boolean().optional().describe("Show manage metrics UI"),
1953
- isAllowUnderlyingData: z10.boolean().optional().describe("Allow viewing underlying data"),
1954
- isAllowAiPilot: z10.boolean().optional().describe("Enable AI pilot features"),
1955
- isAllowEmailReports: z10.boolean().optional().describe("Allow email report scheduling"),
1956
- isAllowCreateDashboardView: z10.boolean().optional().describe("Allow creating dashboard views")
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")
1957
2249
  }).describe("Embed access settings. Only datamartName is required \u2014 all flags default to false.");
1958
2250
  function registerCreateEmbed(server, client) {
1959
2251
  const services = createServiceBundle(client);
@@ -1961,23 +2253,23 @@ function registerCreateEmbed(server, client) {
1961
2253
  "create_embed",
1962
2254
  "Create an embed. Provide dashboardId, workspaceName, accessSettings.datamartName. Add clientId for multi-tenant per-client dashboards. All isAllow* flags default to false.",
1963
2255
  {
1964
- body: z10.record(z10.unknown()).optional().describe(
2256
+ body: z9.record(z9.unknown()).optional().describe(
1965
2257
  "Raw API body passthrough. If provided, all other fields are ignored."
1966
2258
  ),
1967
- dashboardId: z10.string().optional().describe("Dashboard identifier for embed creation"),
1968
- workspaceName: z10.string().optional().describe("Workspace name. Omit to auto-resolve if only one exists."),
1969
- embedType: z10.enum(["dashboard", "metric"]).optional().describe("Embed type"),
1970
- metricId: z10.string().optional().describe("Required for metric embed type"),
1971
- name: z10.string().optional().describe("Human-readable embed config name"),
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"),
1972
2264
  accessSettings: accessSettingsSchema.optional(),
1973
2265
  // Multi-tenant (dashboard embed) params
1974
- clientId: z10.string().optional().describe(
2266
+ clientId: z9.string().optional().describe(
1975
2267
  "Client identifier for multi-tenant isolation. When provided, creates a dashboard embed (each client gets their own dashboard copy)."
1976
2268
  ),
1977
- templateDashboardId: z10.string().optional().describe("Template dashboard ID to clone metrics from"),
1978
- isImportMetrics: z10.boolean().optional().describe("Import metrics from template dashboard"),
1979
- metricImportMode: z10.enum(["COPY", "LINK"]).optional().describe("COPY duplicates metrics, LINK shares them"),
1980
- isRenameDashboard: z10.boolean().optional().describe("Rename the dashboard for this client")
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")
1981
2273
  },
1982
2274
  WRITE_CREATE,
1983
2275
  async (params) => {
@@ -2079,14 +2371,14 @@ Next: run \`get_embed_details\` to verify the embed configuration.`
2079
2371
  }
2080
2372
 
2081
2373
  // src/tools/embed/list-embeds.ts
2082
- import { z as z11 } from "zod";
2374
+ import { z as z10 } from "zod";
2083
2375
  function registerListEmbeds(server, client) {
2084
2376
  const services = createServiceBundle(client);
2085
2377
  server.tool(
2086
2378
  "list_embeds",
2087
- "List embed configurations. Requires an API token (use create_api_token first).",
2379
+ "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.",
2088
2380
  {
2089
- dataAppName: z11.string().optional().describe("Filter by data app name")
2381
+ dataAppName: z10.string().optional().describe("Filter by data app name")
2090
2382
  },
2091
2383
  READ_ONLY,
2092
2384
  async (params) => {
@@ -2097,19 +2389,28 @@ function registerListEmbeds(server, client) {
2097
2389
  "No embeds found. Create an embed in the Databrain UI, then run `list_embeds` again."
2098
2390
  );
2099
2391
  }
2392
+ const embeds = result.map((e) => {
2393
+ const n = services.discoveryService.normalizeEmbed(e);
2394
+ const clientId = typeof e.clientId === "string" && e.clientId.trim().length > 0 ? e.clientId : null;
2395
+ return {
2396
+ embedId: n.embedId || null,
2397
+ internalId: !n.embedId && n.internalId ? n.internalId : void 0,
2398
+ name: n.name,
2399
+ type: n.embedType || "dashboard",
2400
+ dashboardId: n.dashboardId || null,
2401
+ dataApp: n.dataAppName || null,
2402
+ clientId
2403
+ };
2404
+ });
2405
+ const distinctClientIds = Array.from(
2406
+ new Set(
2407
+ embeds.map((e) => e.clientId).filter((id) => typeof id === "string" && id.length > 0)
2408
+ )
2409
+ ).sort();
2100
2410
  return toolSuccess(JSON.stringify({
2101
- count: result.length,
2102
- embeds: result.map((e) => {
2103
- const n = services.discoveryService.normalizeEmbed(e);
2104
- return {
2105
- embedId: n.embedId || null,
2106
- internalId: !n.embedId && n.internalId ? n.internalId : void 0,
2107
- name: n.name,
2108
- type: n.embedType || "dashboard",
2109
- dashboardId: n.dashboardId || null,
2110
- dataApp: n.dataAppName || null
2111
- };
2112
- })
2411
+ count: embeds.length,
2412
+ distinctClientIds,
2413
+ embeds
2113
2414
  }));
2114
2415
  } catch (err) {
2115
2416
  if (err instanceof DatabrainApiError) return err.toToolResponse();
@@ -2120,14 +2421,14 @@ function registerListEmbeds(server, client) {
2120
2421
  }
2121
2422
 
2122
2423
  // src/tools/embed/get-embed-details.ts
2123
- import { z as z12 } from "zod";
2424
+ import { z as z11 } from "zod";
2124
2425
  function registerGetEmbedDetails(server, client) {
2125
2426
  const services = createServiceBundle(client);
2126
2427
  server.tool(
2127
2428
  "get_embed_details",
2128
2429
  "Get full embed configuration including access settings and theme.",
2129
2430
  {
2130
- embedId: z12.string().describe("ID of the embed")
2431
+ embedId: z11.string().describe("ID of the embed")
2131
2432
  },
2132
2433
  READ_ONLY,
2133
2434
  async (params) => {
@@ -2143,7 +2444,7 @@ function registerGetEmbedDetails(server, client) {
2143
2444
  }
2144
2445
 
2145
2446
  // src/tools/embed/update-embed.ts
2146
- import { z as z13 } from "zod";
2447
+ import { z as z12 } from "zod";
2147
2448
 
2148
2449
  // src/tools/embed/helpers.ts
2149
2450
  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.";
@@ -2181,15 +2482,15 @@ function registerUpdateEmbed(server, client) {
2181
2482
  "update_embed",
2182
2483
  "Update embed access settings, permissions, theme, or name.",
2183
2484
  {
2184
- embedId: z13.string().describe("ID of the embed to update"),
2185
- body: z13.record(z13.unknown()).optional().describe(
2485
+ embedId: z12.string().describe("ID of the embed to update"),
2486
+ body: z12.record(z12.unknown()).optional().describe(
2186
2487
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, it is sent directly (passthrough mode)."
2187
2488
  ),
2188
- name: z13.string().optional().describe("New name for the embed"),
2189
- accessSettings: z13.record(z13.unknown()).optional().describe(
2489
+ name: z12.string().optional().describe("New name for the embed"),
2490
+ accessSettings: z12.record(z12.unknown()).optional().describe(
2190
2491
  "Partial update of access settings. See `get_embed_details` for current settings."
2191
2492
  ),
2192
- theme: z13.record(z13.unknown()).optional().describe(
2493
+ theme: z12.record(z12.unknown()).optional().describe(
2193
2494
  "Theme configuration object. See databrain://theme-reference for the full schema."
2194
2495
  )
2195
2496
  },
@@ -2235,7 +2536,7 @@ function registerUpdateEmbed(server, client) {
2235
2536
  }
2236
2537
 
2237
2538
  // src/tools/embed/customize-theme.ts
2238
- import { z as z14 } from "zod";
2539
+ import { z as z13 } from "zod";
2239
2540
  var THEME_PRESETS = {
2240
2541
  light: {
2241
2542
  general: {
@@ -2369,7 +2670,7 @@ var ACCESS_PRESETS = {
2369
2670
  metricCreationMode: "CHAT"
2370
2671
  }
2371
2672
  };
2372
- var fontFamilyEnum = z14.enum([
2673
+ var fontFamilyEnum = z13.enum([
2373
2674
  "Inter",
2374
2675
  "Roboto",
2375
2676
  "Open Sans",
@@ -2381,17 +2682,17 @@ var fontFamilyEnum = z14.enum([
2381
2682
  "Nunito",
2382
2683
  "system-ui"
2383
2684
  ]).describe("Font family");
2384
- var tooltipFontSchema = z14.object({
2385
- fontFamily: z14.string().optional(),
2386
- fontSize: z14.number().optional(),
2387
- fontWeight: z14.number().optional(),
2388
- fontColor: z14.string().optional()
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()
2389
2690
  }).optional();
2390
- var fontConfigInputSchema = z14.object({
2391
- fontSize: z14.number().optional(),
2392
- fontFamily: z14.string().optional(),
2393
- fontWeight: z14.number().optional(),
2394
- color: z14.string().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()
2395
2696
  }).optional();
2396
2697
  function mapToFrontendFontConfig(input) {
2397
2698
  if (!input) return void 0;
@@ -2402,23 +2703,23 @@ function mapToFrontendFontConfig(input) {
2402
2703
  ...input.color && { color: input.color }
2403
2704
  };
2404
2705
  }
2405
- var axisSchema = z14.object({
2406
- hideAxisLines: z14.boolean().optional().describe("Hide axis lines"),
2407
- hideSplitLines: z14.boolean().optional().describe("Hide grid/split lines"),
2408
- hideAxisLabels: z14.boolean().optional().describe("Hide axis labels"),
2409
- hideAxisTicks: z14.boolean().optional().describe("Hide axis tick marks"),
2410
- axisName: z14.string().optional().describe("Axis title text"),
2411
- axisNameOffset: z14.number().optional().describe("Axis title offset from axis"),
2412
- axisLabelMargin: z14.number().optional().describe("Margin between axis labels and chart area in pixels"),
2413
- fontSize: z14.number().optional().describe("Axis label font size"),
2414
- fontFamily: z14.string().optional().describe("Axis label font family"),
2415
- fontWeight: z14.number().optional().describe("Axis label font weight"),
2416
- color: z14.string().optional().describe("Axis label color (hex)"),
2417
- axisColor: z14.string().optional().describe("Axis line color (hex)"),
2418
- axisNameFontConfig: z14.object({
2419
- fontFamily: z14.string().optional(),
2420
- fontSize: z14.number().optional(),
2421
- fontWeight: z14.number().optional()
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()
2422
2723
  }).optional().describe("Axis title font configuration")
2423
2724
  }).optional();
2424
2725
  function buildAdminTheme(p, presetBase) {
@@ -2574,94 +2875,94 @@ function registerCustomizeTheme(server, client) {
2574
2875
  "customize_embed_theme",
2575
2876
  `Theme an embed: presets (${Object.keys(THEME_PRESETS).join("/")}), colors, fonts, cards, chart appearance, responsive layout, or access presets (${Object.keys(ACCESS_PRESETS).join("/")}).`,
2576
2877
  {
2577
- embedId: z14.string().describe("ID of the embed to theme"),
2578
- body: z14.record(z14.unknown()).optional().describe(
2878
+ embedId: z13.string().describe("ID of the embed to theme"),
2879
+ body: z13.record(z13.unknown()).optional().describe(
2579
2880
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, sent directly and all convenience fields are ignored."
2580
2881
  ),
2581
2882
  // --- Presets ---
2582
- themePreset: z14.enum(["light", "dark", "corporate", "minimal"]).optional().describe("Apply a theme preset as the base. Granular params below override preset values."),
2583
- accessPreset: z14.enum(["view-only", "power-user", "export-only", "ai-enabled"]).optional().describe("Apply an access settings preset"),
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"),
2584
2885
  // --- General ---
2585
2886
  fontFamily: fontFamilyEnum.optional().describe("Global font family"),
2586
- datePickerFormat: z14.string().optional().describe("Date picker display format"),
2887
+ datePickerFormat: z13.string().optional().describe("Date picker display format"),
2587
2888
  // --- Colors ---
2588
- primaryColor: z14.string().optional().describe("Primary accent color (hex)"),
2589
- backgroundColor: z14.string().optional().describe("Overall background color (hex)"),
2590
- textColor: z14.string().optional().describe("Default text color (hex)"),
2591
- ctaColor: z14.string().optional().describe("Call-to-action button color (hex)"),
2592
- ctaTextColor: z14.string().optional().describe("CTA button text color (hex)"),
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)"),
2593
2894
  // --- Dashboard ---
2594
- dashboardBackgroundColor: z14.string().optional().describe("Dashboard container background (hex)"),
2595
- selectBoxSize: z14.enum(["small", "medium", "large"]).optional().describe("Filter select box size"),
2596
- selectBoxVariant: z14.enum(["floating", "outlined", "standard"]).optional().describe("Filter select box style"),
2597
- selectBoxBorderRadius: z14.string().optional().describe('Select box border radius (e.g. "20px")'),
2598
- selectBoxTextColor: z14.string().optional().describe("Select box text color (hex)"),
2599
- metricCardColor: z14.string().optional().describe("Metric card background color (hex)"),
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)"),
2600
2901
  // --- Card Title ---
2601
- cardTitleFontSize: z14.string().optional().describe('Card title font size (e.g. "16px")'),
2602
- cardTitleFontWeight: z14.string().optional().describe('Card title font weight (e.g. "600")'),
2603
- cardTitleColor: z14.string().optional().describe("Card title text color (hex)"),
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)"),
2604
2905
  // --- Card Description ---
2605
- cardDescriptionFontSize: z14.string().optional().describe("Card description font size"),
2606
- cardDescriptionFontWeight: z14.string().optional().describe("Card description font weight"),
2607
- cardDescriptionColor: z14.string().optional().describe("Card description text color (hex)"),
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)"),
2608
2909
  // --- Card Customization ---
2609
- cardPadding: z14.string().optional().describe("Card internal padding"),
2610
- cardBorderRadius: z14.string().optional().describe("Card border radius"),
2611
- cardShadow: z14.string().optional().describe("Card box-shadow CSS value"),
2612
- disableShadowOnHover: z14.boolean().optional().describe("Disable shadow on card hover"),
2613
- disableCardStroke: z14.boolean().optional().describe("Remove card border stroke"),
2614
- cardStrokeColor: z14.string().optional().describe("Card border stroke color (hex)"),
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)"),
2615
2916
  // --- Chart Colors (admin theme level) ---
2616
- chartFontFamily: z14.string().optional().describe("Font family for chart text"),
2617
- chartFontColor: z14.string().optional().describe("Font color for chart text (hex)"),
2618
- chartColors: z14.array(z14.string()).optional().describe("Array of chart series colors (hex)"),
2619
- chartPaletteName: z14.string().optional().describe("Name for a custom palette"),
2620
- chartPaletteColors: z14.array(z14.string()).optional().describe("9 colors for a custom palette"),
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"),
2621
2922
  // --- Chart Tooltip (admin theme level) ---
2622
2923
  tooltipHeader: tooltipFontSchema.describe("Tooltip header font config"),
2623
2924
  tooltipLabel: tooltipFontSchema.describe("Tooltip label font config"),
2624
2925
  tooltipValue: tooltipFontSchema.describe("Tooltip value font config"),
2625
2926
  // --- Component Theme ---
2626
- buttonPrimaryColor: z14.string().optional().describe("Primary button background (hex)"),
2627
- buttonPrimaryTextColor: z14.string().optional().describe("Primary button text (hex)"),
2628
- buttonSecondaryColor: z14.string().optional().describe("Secondary button background (hex)"),
2629
- buttonSecondaryTextColor: z14.string().optional().describe("Secondary button text (hex)"),
2630
- checkboxCheckedColor: z14.string().optional().describe("Checked checkbox color (hex)"),
2631
- checkboxUncheckedColor: z14.string().optional().describe("Unchecked checkbox color (hex)"),
2632
- switchEnabledColor: z14.string().optional().describe("Enabled switch color (hex)"),
2633
- switchDisabledColor: z14.string().optional().describe("Disabled switch color (hex)"),
2634
- breadcrumbFontFamily: z14.string().optional().describe("Drill breadcrumb font family"),
2635
- breadcrumbFontColor: z14.string().optional().describe("Drill breadcrumb font color (hex)"),
2636
- breadcrumbActiveColor: z14.string().optional().describe("Active breadcrumb color (hex)"),
2637
- filterBadgeColor: z14.string().optional().describe("Multi-select filter badge color (hex)"),
2638
- filterBadgeTextColor: z14.string().optional().describe("Filter badge text color (hex)"),
2639
- datePickerColor: z14.string().optional().describe("Date picker accent color (hex)"),
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)"),
2640
2941
  // --- Responsive Breakpoints ---
2641
- breakpointXl: z14.number().optional().describe("XL breakpoint width in px"),
2642
- breakpointLg: z14.number().optional().describe("LG breakpoint width in px"),
2643
- breakpointMd: z14.number().optional().describe("MD breakpoint width in px"),
2644
- breakpointSm: z14.number().optional().describe("SM breakpoint width in px"),
2645
- breakpointXs: z14.number().optional().describe("XS breakpoint width in px"),
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"),
2646
2947
  // --- Metric Layout Columns ---
2647
- layoutColsXl: z14.number().optional().describe("Grid columns at XL breakpoint"),
2648
- layoutColsLg: z14.number().optional().describe("Grid columns at LG breakpoint"),
2649
- layoutColsMd: z14.number().optional().describe("Grid columns at MD breakpoint"),
2650
- layoutColsSm: z14.number().optional().describe("Grid columns at SM breakpoint"),
2651
- layoutColsXs: z14.number().optional().describe("Grid columns at XS breakpoint"),
2652
- layoutColsXxs: z14.number().optional().describe("Grid columns at XXS breakpoint"),
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"),
2653
2954
  // --- Chart Appearance (detailed) ---
2654
2955
  tooltipHeaderStyle: fontConfigInputSchema.describe("Tooltip header: fontSize, fontFamily, fontWeight, color"),
2655
2956
  tooltipLabelStyle: fontConfigInputSchema.describe("Tooltip label: fontSize, fontFamily, fontWeight, color"),
2656
2957
  tooltipValueStyle: fontConfigInputSchema.describe("Tooltip value: fontSize, fontFamily, fontWeight, color"),
2657
- labelPosition: z14.enum(["hidden", "top", "left", "right", "bottom", "inside"]).optional().describe("Data label position on charts"),
2658
- radialLabelPosition: z14.enum(["outside", "inside"]).optional().describe("Label position for radial/pie charts"),
2659
- marginTop: z14.number().optional().describe("Chart top margin in pixels"),
2660
- marginLeft: z14.number().optional().describe("Chart left margin in pixels"),
2661
- marginRight: z14.number().optional().describe("Chart right margin in pixels"),
2662
- marginBottom: z14.number().optional().describe("Chart bottom margin in pixels"),
2663
- legendShow: z14.boolean().optional().describe("Show or hide chart legend"),
2664
- legendPosition: z14.enum([
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([
2665
2966
  "top-left",
2666
2967
  "top-center",
2667
2968
  "top-right",
@@ -2671,17 +2972,17 @@ function registerCustomizeTheme(server, client) {
2671
2972
  "bottom-center",
2672
2973
  "bottom-right"
2673
2974
  ]).optional().describe("Legend position"),
2674
- legendLayout: z14.enum(["horizontal", "vertical"]).optional().describe("Legend layout direction"),
2675
- legendShape: z14.enum(["circle", "rect", "roundRect", "triangle", "diamond", "arrow", "none"]).optional().describe("Legend icon shape"),
2676
- legendFontSize: z14.number().optional().describe("Legend font size"),
2677
- legendFontWeight: z14.number().optional().describe("Legend font weight"),
2678
- legendFontFamily: z14.string().optional().describe("Legend font family"),
2679
- legendColor: z14.string().optional().describe("Legend text color (hex)"),
2680
- legendTruncateLength: z14.number().optional().describe("Max characters before truncating legend"),
2681
- disableLegendScrolling: z14.boolean().optional().describe("Disable scrolling in legend"),
2682
- enableVariableLegendPosition: z14.boolean().optional().describe("Allow user to drag legend"),
2683
- legendTop: z14.number().optional().describe("Custom legend top offset"),
2684
- legendLeft: z14.number().optional().describe("Custom legend left offset"),
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"),
2685
2986
  verticalAxis: axisSchema.describe("Vertical (Y) axis configuration"),
2686
2987
  horizontalAxis: axisSchema.describe("Horizontal (X) axis configuration")
2687
2988
  },
@@ -2755,6 +3056,76 @@ Use \`get_embed_details\` to view the full current theme configuration.` + GUEST
2755
3056
  );
2756
3057
  }
2757
3058
 
3059
+ // src/tools/embed/get-demo-link.ts
3060
+ import { z as z14 } from "zod";
3061
+ var HOUR_MS = 60 * 60 * 1e3;
3062
+ var DEFAULT_EXPIRY_HOURS = 24;
3063
+ var MAX_EXPIRY_HOURS = 24 * 30;
3064
+ function registerGetDemoLink(server, client) {
3065
+ const services = createServiceBundle(client);
3066
+ server.tool(
3067
+ "get_demo_link",
3068
+ `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
+ {
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).`)
3075
+ },
3076
+ READ_ONLY,
3077
+ async (params) => {
3078
+ try {
3079
+ const embed = await services.widgetService.resolveEmbedId({
3080
+ embedId: params.embedId,
3081
+ dashboardName: params.dashboardName,
3082
+ workspaceName: params.workspaceName,
3083
+ required: true
3084
+ });
3085
+ if (!embed.ok) return toolError(embed.error);
3086
+ const embedId = embed.value.embedId;
3087
+ const dashboardName = embed.value.resolvedFrom?.dashboardName || null;
3088
+ const ctx = await services.widgetService.resolveEmbedContext(embedId);
3089
+ if (!ctx.dataAppName) {
3090
+ return toolError(
3091
+ `Could not resolve the data app for embed "${embedId}". The demo link needs a data app to mint a guest token against. Run \`list_data_apps\` to confirm one exists, or pass \`embedId\` for an embed with metadata.dataAppName populated.`
3092
+ );
3093
+ }
3094
+ const dashboardSlug = ctx.externalDashboardId || ctx.dashboardId;
3095
+ if (!dashboardSlug) {
3096
+ return toolError(
3097
+ `Embed "${embedId}" has no dashboardId/externalDashboardId \u2014 the demo URL requires one to load the dashboard. This is unexpected; verify the embed in the Databrain UI.`
3098
+ );
3099
+ }
3100
+ const expiryHours = params.expiryHours ?? DEFAULT_EXPIRY_HOURS;
3101
+ const { token } = await services.guestTokenService.generateToken({
3102
+ clientId: params.clientId,
3103
+ dataAppName: ctx.dataAppName,
3104
+ allowedEmbeds: [embedId],
3105
+ expiryTime: expiryHours * HOUR_MS
3106
+ });
3107
+ const baseUrl = client.getDemoDomain().replace(/\/+$/, "");
3108
+ const demoUrl = `${baseUrl}/?token=${encodeURIComponent(token)}&dashboardId=${encodeURIComponent(dashboardSlug)}`;
3109
+ return toolSuccess(
3110
+ JSON.stringify({
3111
+ demoUrl,
3112
+ clientId: params.clientId,
3113
+ dashboardName,
3114
+ embedId,
3115
+ dashboardId: dashboardSlug,
3116
+ dataAppName: ctx.dataAppName,
3117
+ expiresInHours: expiryHours,
3118
+ note: `Open this URL in a browser to preview the dashboard exactly as the end-customer sees it. This matches the UI's "Share Demo Link" flow. Override the host with DATABRAIN_DEMO_DOMAIN for self-hosted / custom preview domains.`
3119
+ })
3120
+ );
3121
+ } catch (err) {
3122
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
3123
+ return toolError(`Unexpected error: ${err.message}`);
3124
+ }
3125
+ }
3126
+ );
3127
+ }
3128
+
2758
3129
  // src/tools/frontend/generate-guest-token.ts
2759
3130
  import { z as z15 } from "zod";
2760
3131
  function registerGenerateGuestToken(server, client) {
@@ -4396,29 +4767,41 @@ function registerListWidgets(server, client) {
4396
4767
  const services = createServiceBundle(client);
4397
4768
  server.tool(
4398
4769
  "list_widgets",
4399
- "List widgets (metrics) on a customer dashboard embed. Returns each widget's metricId, name, published state, and creator identifier.",
4770
+ "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.",
4400
4771
  {
4401
- embedId: z21.string().describe("Embed ID (customer dashboard instantiation)"),
4402
- clientId: z21.string().describe("Client/tenant identifier for this embed")
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.")
4403
4776
  },
4404
4777
  READ_ONLY,
4405
4778
  async (params) => {
4406
4779
  try {
4780
+ const embed = await services.widgetService.resolveEmbedId({
4781
+ embedId: params.embedId,
4782
+ dashboardName: params.dashboardName,
4783
+ workspaceName: params.workspaceName,
4784
+ required: true
4785
+ });
4786
+ if (!embed.ok) return toolError(embed.error);
4787
+ const embedId = embed.value.embedId;
4407
4788
  const widgets = await services.widgetsApi.listWidgets(
4408
- params.embedId,
4789
+ embedId,
4409
4790
  params.clientId
4410
4791
  );
4411
4792
  if (!Array.isArray(widgets) || widgets.length === 0) {
4412
4793
  return toolSuccess(
4413
- `No widgets found on embed "${params.embedId}" for client "${params.clientId}".`
4794
+ `No widgets found on dashboard "${embed.value.resolvedFrom?.dashboardName || embedId}" for client "${params.clientId}".`
4414
4795
  );
4415
4796
  }
4416
4797
  return toolSuccess(
4417
4798
  JSON.stringify({
4418
4799
  count: widgets.length,
4419
- embedId: params.embedId,
4800
+ dashboardName: embed.value.resolvedFrom?.dashboardName || null,
4801
+ embedId,
4420
4802
  clientId: params.clientId,
4421
4803
  widgets: widgets.map((w) => ({
4804
+ externalMetricId: typeof w.externalMetricId === "string" && w.externalMetricId || typeof w.id === "string" && w.id || typeof w._id === "string" && w._id || null,
4422
4805
  metricId: w.metricId || null,
4423
4806
  name: w.name || null,
4424
4807
  isPublished: w.isPublished ?? null,
@@ -4440,11 +4823,13 @@ function registerAddWidget(server, client) {
4440
4823
  const services = createServiceBundle(client);
4441
4824
  server.tool(
4442
4825
  "add_widget",
4443
- "Add a widget (metric) to a customer dashboard embed. Either clone via `templateMetricId` (recommended \u2014 inherits query, chartOptions, datasource), or supply a full spec (name + query + chartOptions). Datasource auto-resolves from the embed or sole active datasource; pass `integrationName` to override. Mints a guest token internally.",
4826
+ "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.",
4444
4827
  {
4445
- embedId: z22.string().describe("Embed ID the widget will be added to"),
4446
4828
  clientId: z22.string().describe("Client/tenant identifier (widget is created in their scope)"),
4447
- dataAppName: z22.string().optional().describe("Data app name (required for guest token). Auto-resolved from embedId if omitted."),
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."),
4448
4833
  templateMetricId: z22.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
4449
4834
  name: z22.string().optional().describe("Widget name (overrides template)"),
4450
4835
  metricId: z22.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
@@ -4459,21 +4844,29 @@ function registerAddWidget(server, client) {
4459
4844
  WRITE_CREATE,
4460
4845
  async (params) => {
4461
4846
  try {
4462
- const ctx = await services.widgetService.resolveEmbedContext(params.embedId);
4847
+ const embed = await services.widgetService.resolveEmbedId({
4848
+ embedId: params.embedId,
4849
+ dashboardName: params.dashboardName,
4850
+ workspaceName: params.workspaceName,
4851
+ required: true
4852
+ });
4853
+ if (!embed.ok) return toolError(embed.error);
4854
+ const embedId = embed.value.embedId;
4855
+ const ctx = await services.widgetService.resolveEmbedContext(embedId);
4463
4856
  if (!ctx.externalDashboardId) {
4464
4857
  return toolError(
4465
- `Embed "${params.embedId}" has no externalDashboardId \u2014 cannot attach a widget. Verify via \`get_embed_details\`.`
4858
+ `Dashboard "${params.dashboardName || embedId}" has no externalDashboardId \u2014 cannot attach a widget. Verify via \`get_embed_details\`.`
4466
4859
  );
4467
4860
  }
4468
4861
  const dataAppName = await services.widgetService.resolveDataAppName(
4469
4862
  params.dataAppName,
4470
- params.embedId
4863
+ embedId
4471
4864
  );
4472
4865
  if (!dataAppName.ok) return toolError(dataAppName.error);
4473
4866
  const guestToken = await services.widgetService.mintGuestToken(
4474
4867
  params.clientId,
4475
4868
  dataAppName.value,
4476
- [params.embedId]
4869
+ [embedId]
4477
4870
  );
4478
4871
  let createBody;
4479
4872
  if (params.body) {
@@ -4505,7 +4898,7 @@ function registerAddWidget(server, client) {
4505
4898
  if (!params.name) return toolError("`name` is required when not cloning a template.");
4506
4899
  if (!params.query) return toolError("`query` is required when not cloning a template.");
4507
4900
  const datasource = await services.widgetService.resolveDatasource(guestToken, {
4508
- embedId: params.embedId,
4901
+ embedId,
4509
4902
  clientId: params.clientId,
4510
4903
  integrationId: params.companyIntegrationId,
4511
4904
  integrationName: params.integrationName
@@ -4542,7 +4935,7 @@ ${JSON.stringify(createBody, null, 2).slice(0, 3e3)}`
4542
4935
  - externalMetricId: \`${newId}\`
4543
4936
  - metricId: \`${newMetricId}\`
4544
4937
  - name: \`${createBody.name}\`
4545
- - embedId: \`${params.embedId}\`
4938
+ - dashboard: \`${embed.value.resolvedFrom?.dashboardName || embedId}\`
4546
4939
  - clientId: \`${params.clientId}\`
4547
4940
 
4548
4941
  Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust.`
@@ -4561,12 +4954,14 @@ function registerUpdateWidget(server, client) {
4561
4954
  const services = createServiceBundle(client);
4562
4955
  server.tool(
4563
4956
  "update_widget",
4564
- "Update a widget on a customer dashboard embed. Applies a partial patch to the externalMetrics row (name, query, chartOptions, isPublished, etc.). Mints a guest token internally.",
4957
+ "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.",
4565
4958
  {
4566
4959
  externalMetricId: z23.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
4567
4960
  clientId: z23.string().describe("Client/tenant identifier"),
4568
- dataAppName: z23.string().optional().describe("Optional override. Auto-resolved from `embedId`, or from the sole data app when there is only one."),
4569
- embedId: z23.string().optional().describe("Optional \u2014 if provided, the guest token is scoped to just this embed and dataAppName is auto-resolved."),
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."),
4570
4965
  set: z23.record(z23.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
4571
4966
  },
4572
4967
  WRITE_IDEMPOTENT,
@@ -4575,15 +4970,22 @@ function registerUpdateWidget(server, client) {
4575
4970
  if (!params.set || Object.keys(params.set).length === 0) {
4576
4971
  return toolError("`set` must contain at least one field to update.");
4577
4972
  }
4973
+ const embed = await services.widgetService.resolveEmbedId({
4974
+ embedId: params.embedId,
4975
+ dashboardName: params.dashboardName,
4976
+ workspaceName: params.workspaceName
4977
+ });
4978
+ if (!embed.ok) return toolError(embed.error);
4979
+ const embedId = embed.value.embedId;
4578
4980
  const dataAppName = await services.widgetService.resolveDataAppName(
4579
4981
  params.dataAppName,
4580
- params.embedId
4982
+ embedId || void 0
4581
4983
  );
4582
4984
  if (!dataAppName.ok) return toolError(dataAppName.error);
4583
4985
  const guestToken = await services.widgetService.mintGuestToken(
4584
4986
  params.clientId,
4585
4987
  dataAppName.value,
4586
- params.embedId ? [params.embedId] : void 0
4988
+ embedId ? [embedId] : void 0
4587
4989
  );
4588
4990
  const result = await services.widgetsApi.updateMetric(guestToken, {
4589
4991
  externalMetricId: params.externalMetricId,
@@ -4620,25 +5022,34 @@ function registerRemoveWidget(server, client) {
4620
5022
  const services = createServiceBundle(client);
4621
5023
  server.tool(
4622
5024
  "remove_widget",
4623
- "Remove a widget from a customer dashboard embed. This is a per-client soft delete (writes to clientDeletedMetrics) \u2014 the widget stays on the template dashboard and on other customers' embeds.",
5025
+ "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.",
4624
5026
  {
4625
5027
  externalMetricId: z24.string().describe("Internal uuid of the widget (externalMetrics.id)"),
4626
5028
  clientId: z24.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
4627
- dataAppName: z24.string().optional().describe("Optional override. Auto-resolved from `embedId`, or from the sole data app when there is only one."),
4628
- embedId: z24.string().optional().describe("Optional \u2014 scopes the guest token to this embed and auto-resolves dataAppName.")
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.")
4629
5033
  },
4630
5034
  DESTRUCTIVE,
4631
5035
  async (params) => {
4632
5036
  try {
5037
+ const embed = await services.widgetService.resolveEmbedId({
5038
+ embedId: params.embedId,
5039
+ dashboardName: params.dashboardName,
5040
+ workspaceName: params.workspaceName
5041
+ });
5042
+ if (!embed.ok) return toolError(embed.error);
5043
+ const embedId = embed.value.embedId;
4633
5044
  const dataAppName = await services.widgetService.resolveDataAppName(
4634
5045
  params.dataAppName,
4635
- params.embedId
5046
+ embedId || void 0
4636
5047
  );
4637
5048
  if (!dataAppName.ok) return toolError(dataAppName.error);
4638
5049
  const guestToken = await services.widgetService.mintGuestToken(
4639
5050
  params.clientId,
4640
5051
  dataAppName.value,
4641
- params.embedId ? [params.embedId] : void 0
5052
+ embedId ? [embedId] : void 0
4642
5053
  );
4643
5054
  const result = await services.widgetsApi.markArchived(guestToken, {
4644
5055
  id: params.externalMetricId,
@@ -4671,30 +5082,40 @@ function registerGenerateWidget(server, client) {
4671
5082
  const services = createServiceBundle(client);
4672
5083
  server.tool(
4673
5084
  "generate_widget",
4674
- "AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft to review and pass to `add_widget`. Data app and datasource auto-resolve from `embedId` (preferred) or sole workspace defaults. On resolve failure, ask the user for the datasource by name and pass `integrationName`.",
5085
+ "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.",
4675
5086
  {
4676
5087
  clientId: z25.string().describe("Client/tenant identifier"),
4677
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.'),
4678
- embedId: z25.string().optional().describe("Optional. When provided, data app and datasource are inherited from the embed's existing widgets \u2014 the preferred path for CS/CX flows."),
4679
- dataAppName: z25.string().optional().describe("Optional override. Auto-resolved from embedId, or from the sole data app when there is only one."),
4680
- integrationId: z25.string().optional().describe("Technical override \u2014 the datasource UUID. Most flows should leave this blank and rely on auto-resolution from `embedId`. If you must override, prefer `integrationName` (business-user friendly)."),
4681
- integrationName: z25.string().optional().describe('Datasource display name (e.g. "Snowflake", "demo datasource"). Use this when the user picks a datasource by name \u2014 the id is looked up automatically. Leave blank if `embedId` is provided (auto-resolved).')
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.")
4682
5095
  },
4683
5096
  READ_ONLY,
4684
5097
  async (params) => {
4685
5098
  try {
5099
+ const embed = await services.widgetService.resolveEmbedId({
5100
+ embedId: params.embedId,
5101
+ dashboardName: params.dashboardName,
5102
+ workspaceName: params.workspaceName,
5103
+ required: true
5104
+ });
5105
+ if (!embed.ok) return toolError(embed.error);
5106
+ const embedId = embed.value.embedId;
4686
5107
  const dataAppName = await services.widgetService.resolveDataAppName(
4687
5108
  params.dataAppName,
4688
- params.embedId
5109
+ embedId
4689
5110
  );
4690
5111
  if (!dataAppName.ok) return toolError(dataAppName.error);
4691
5112
  const guestToken = await services.widgetService.mintGuestToken(
4692
5113
  params.clientId,
4693
5114
  dataAppName.value,
4694
- params.embedId ? [params.embedId] : void 0
5115
+ [embedId]
4695
5116
  );
4696
5117
  const datasource = await services.widgetService.resolveDatasource(guestToken, {
4697
- embedId: params.embedId,
5118
+ embedId,
4698
5119
  clientId: params.clientId,
4699
5120
  integrationId: params.integrationId,
4700
5121
  integrationName: params.integrationName
@@ -4735,13 +5156,173 @@ function registerGenerateWidget(server, client) {
4735
5156
  );
4736
5157
  }
4737
5158
 
5159
+ // src/tools/auth/connect.ts
5160
+ import { z as z26 } from "zod";
5161
+ function registerConnect(server, client) {
5162
+ 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.",
5165
+ {
5166
+ email: z26.string().email().describe("The Databrain login email for the user running this MCP session.")
5167
+ },
5168
+ WRITE_IDEMPOTENT,
5169
+ async ({ email }) => {
5170
+ 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))
5175
+ );
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."
5181
+ );
5182
+ } else {
5183
+ notes.push(
5184
+ "Mutations from this MCP session are now role-checked and audit-logged under this user."
5185
+ );
5186
+ }
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."
5190
+ );
5191
+ }
5192
+ return toolSuccess(
5193
+ JSON.stringify(
5194
+ {
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
5204
+ },
5205
+ null,
5206
+ 2
5207
+ )
5208
+ );
5209
+ } catch (err) {
5210
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
5211
+ return toolError(`Unexpected error during connect: ${err.message}`);
5212
+ }
5213
+ }
5214
+ );
5215
+ }
5216
+
5217
+ // src/tools/auth/whoami.ts
5218
+ function registerWhoami(server, client) {
5219
+ 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
+ {},
5223
+ 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
+ );
5253
+ return toolSuccess(
5254
+ JSON.stringify(
5255
+ {
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
5263
+ },
5264
+ null,
5265
+ 2
5266
+ )
5267
+ );
5268
+ }
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
+ }
5281
+ );
5282
+ }
5283
+
5284
+ // src/tools/auth/disconnect.ts
5285
+ function registerDisconnect(server, client) {
5286
+ 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.",
5289
+ {},
5290
+ DESTRUCTIVE,
5291
+ 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.");
5300
+ }
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."
5304
+ );
5305
+ }
5306
+ return toolSuccess(
5307
+ JSON.stringify(
5308
+ { status: "disconnected", wasConnected, notes },
5309
+ null,
5310
+ 2
5311
+ )
5312
+ );
5313
+ }
5314
+ );
5315
+ }
5316
+
4738
5317
  // src/tools/registry.ts
4739
5318
  function registerAllTools(server, client) {
5319
+ registerConnect(server, client);
5320
+ registerWhoami(server, client);
5321
+ registerDisconnect(server, client);
4740
5322
  registerListDatasources(server, client);
4741
5323
  registerDiscoverDatasourceSchema(server, client);
4742
5324
  registerListDatamarts(server, client);
4743
5325
  registerListWorkspaces(server, client);
4744
- registerLinkDatamartToWorkspace(server, client);
4745
5326
  registerListDataApps(server, client);
4746
5327
  registerCreateDataApp(server, client);
4747
5328
  registerListApiTokens(server, client);
@@ -4752,6 +5333,7 @@ function registerAllTools(server, client) {
4752
5333
  registerGetEmbedDetails(server, client);
4753
5334
  registerUpdateEmbed(server, client);
4754
5335
  registerCustomizeTheme(server, client);
5336
+ registerGetDemoLink(server, client);
4755
5337
  registerGenerateGuestToken(server, client);
4756
5338
  registerGenerateEmbedCode(server, client);
4757
5339
  registerListDashboards(server, client);
@@ -7764,75 +8346,87 @@ Suggest a sample question based on the tables you described and run it with \`as
7764
8346
  `;
7765
8347
 
7766
8348
  // src/prompts/customize-customer-dashboard.ts
7767
- var customizeCustomerDashboardPrompt = `You are guiding widget CRUD on a specific customer's dashboard embed. This is the "tweak per customer" flow: add, modify, or remove individual widgets (metrics) on a customer's embedded dashboard without touching the template or other customers.
8349
+ var customizeCustomerDashboardPrompt = `You are guiding widget CRUD on a specific customer's dashboard. This is the "tweak per customer" flow for a non-technical CS/CX/support user: add, modify, or remove individual widgets for one customer without touching the template or other customers.
8350
+
8351
+ **CRITICAL: This is an interactive, step-by-step guide. STOP after each step, show the user what you found, and wait for their input. Never batch multiple steps. Never assume the customer or dashboard.**
7768
8352
 
7769
- **CRITICAL: This is an interactive, step-by-step guide. STOP after each step, show the user what you found, and wait for their input. Never batch multiple steps. Never assume the customer/embed.**
8353
+ **Minimum info the user needs to provide: \`clientId\` + \`dashboardName\`.** Everything else (data app, datasource, embed id, integration id) is auto-resolved. NEVER ask the user for any of those.
7770
8354
 
7771
8355
  **Key concepts (read before starting):**
7772
- - **Embed** = a customer's instantiation of a dashboard. Identified by \`embedId\`. Each embed is scoped to one \`clientId\` (the customer/tenant).
7773
- - **Widget** = a single visualization on a dashboard. Stored as \`externalMetrics\` internally. Has two ids: \`metricId\` (user-facing slug, shown in lists) and \`externalMetricId\` (internal uuid, used for updates/deletes).
7774
- - **Per-client scope**: \`remove_widget\` is a per-client soft delete \u2014 it hides the widget for one client only. The widget stays on the template dashboard and on other customers' embeds.
7775
- - **Guest tokens auto-minted**: \`add_widget\`, \`update_widget\`, \`remove_widget\`, and \`generate_widget\` all mint a guest token per call. You do not need to call \`generate_guest_token\` manually for widget operations.
8356
+ - **Dashboard** = the named analytics view the customer sees (e.g. "Demo Dashboard", "Airline Marketing"). This is what the user will name.
8357
+ - **Workspace** = an optional container grouping dashboards. Only ask for this if the dashboard name is ambiguous.
8358
+ - **Client/customer** = the tenant whose copy of the dashboard we're editing. Identified by \`clientId\`.
8359
+ - **Widget** = a single visualization on a dashboard. Has two ids: \`metricId\` (slug, shown in lists) and \`externalMetricId\` (uuid, used for update/remove).
8360
+ - **Per-client scope**: \`remove_widget\` is a per-client soft delete \u2014 the widget stays on the template dashboard and on other customers' copies.
7776
8361
 
7777
- **Step 1 \u2014 Identify the target embed**
8362
+ **Forbidden questions \u2014 DO NOT ASK the user for any of these:**
8363
+ - Data app name, \`dataAppName\`
8364
+ - Datasource name, \`integrationName\`, \`integrationId\`, \`companyIntegrationId\`
8365
+ - Embed id, \`embedId\`
8366
+ - External dashboard id, dashboard id, internal UUIDs
8367
+
8368
+ If the user offers any of these proactively, accept and use them, but never prompt for them.
8369
+
8370
+ **Step 1 \u2014 Identify the customer and dashboard**
7778
8371
  STOP and ASK the user:
7779
- - "Which customer/client are we customizing for?" (the \`clientId\`)
7780
- - "Which embed/dashboard?" (the \`embedId\`)
8372
+ - "Which customer/client are we customizing for?" (capture as \`clientId\`)
8373
+ - "Which dashboard?" (capture as \`dashboardName\`)
7781
8374
 
7782
- If the user only knows the customer name, call \`list_data_apps\` then \`list_embeds\` (filtered by \`dataAppName\`) and present the matching embeds. Do NOT proceed until you have both \`embedId\` and \`clientId\` confirmed.
8375
+ Only ask about workspace if the tool reports the dashboard name is ambiguous across workspaces.
7783
8376
 
7784
8377
  **Step 2 \u2014 Survey the current widgets**
7785
- Call \`list_widgets({ embedId, clientId })\`.
7786
- STOP and present the list: \`{metricId, name, isPublished, createdByUser}\` per widget.
7787
- ASK: "What would you like to change? (add / modify / remove)" \u2014 or more specifically, "Which widgets should I add, update, or remove?"
8378
+ Call \`list_widgets({ clientId, dashboardName, workspaceName? })\`.
8379
+ STOP and present the list: \`{externalMetricId, metricId, name, isPublished, createdByUser}\` per widget. Keep the \`externalMetricId\` values from this response \u2014 you'll need them verbatim for update/remove. Show the user the human-readable names; the uuids are yours to reference internally.
8380
+ ASK: "What would you like to change? (add / modify / remove) \u2014 and which widgets?"
7788
8381
 
7789
8382
  **Step 3a \u2014 ADD a widget**
7790
- This is a business-user flow. NEVER ask the user for a datasource, \`companyIntegrationId\`, \`integrationId\`, or raw SQL unless they explicitly say they want to write SQL. Prefer the two paths below in order.
8383
+ Business-user flow. NEVER ask for SQL, datasource, or chart config unless the user explicitly says they want to write raw SQL.
7791
8384
 
7792
8385
  1. **Clone an existing chart** (default \u2014 try this first):
7793
- - ASK: "Which existing chart should I copy from?" Call \`list_widgets\` on this embed, or on the template embed / another customer's embed, and present the widgets by name. Let the user pick by name.
7794
- - Call \`add_widget({ embedId, clientId, templateMetricId, name? })\` using the picked widget's \`metricId\`. The tool inherits the query, chart config, and datasource automatically.
8386
+ - ASK: "Which existing chart should I copy from?" Call \`list_widgets\` and present the widgets by name. Let the user pick by name.
8387
+ - Call \`add_widget({ clientId, dashboardName, workspaceName?, templateMetricId, name? })\` using the picked widget's \`metricId\`. Query, chart config, and datasource are inherited automatically.
7795
8388
 
7796
8389
  2. **Describe the chart in plain English** (AI generate \u2014 use if no suitable template exists):
7797
8390
  - ASK: "Describe the chart you want" (e.g., "calls per day over last 30 days").
7798
- - Call \`generate_widget({ embedId, clientId, userInputs: { prompt: "..." } })\` \u2014 data app and datasource auto-resolve from the embed's existing widgets. Do NOT ask the user for a datasource or data app.
7799
- - \`embedId\` is optional on \`generate_widget\` (the tool will fall back to the sole data app / sole datasource if absent), but pass it when you have one \u2014 resolution is more reliable.
7800
- - Review the returned \`draft\` with the user. If they approve, call \`add_widget({ embedId, clientId, body: <draft + externalDashboardIds> })\` to persist.
8391
+ - Call \`generate_widget({ clientId, dashboardName, workspaceName?, userInputs: { prompt: "..." } })\`.
8392
+ - Review the returned \`draft\` with the user. If they approve, call \`add_widget({ clientId, dashboardName, workspaceName?, body: <draft + externalDashboardIds> })\` to persist.
7801
8393
  - If they want changes, re-run \`generate_widget\` with a refined prompt.
7802
- - If \`generate_widget\` reports "Multiple data apps", pass \`embedId\` or \`dataAppName\`. If it reports "Could not auto-resolve a datasource", ASK the user which datasource to use by NAME (the error lists available options) and retry with \`integrationName: "<name>"\`. Never ask the user for \`integrationId\` or any UUID \u2014 \`integrationName\` alone is enough.
8394
+ - If \`generate_widget\` reports "Could not auto-detect the datasource", STOP and ASK the user to choose: (a) pick an existing chart to copy from (recommended \u2014 call \`list_widgets\`, present widgets by name, then use \`add_widget\` with \`templateMetricId\`); or (b) pick a datasource by name from the list in the error (only if the user already knows it). Wait for the user's choice \u2014 do not guess.
7803
8395
 
7804
8396
  **Advanced \u2014 write SQL directly** (technical users only; ONLY if the user explicitly asks to write SQL):
7805
- - ASK for: \`name\`, \`query\` (SQL), \`chartOptions\`, \`companyIntegrationId\` (use \`list_datasources\`).
7806
- - Call \`add_widget({ embedId, clientId, name, query, chartOptions, companyIntegrationId, ... })\`.
8397
+ - ASK for: \`name\`, \`query\` (SQL), \`chartOptions\`.
8398
+ - Call \`add_widget({ clientId, dashboardName, name, query, chartOptions })\` \u2014 datasource still auto-resolves from the dashboard.
7807
8399
 
7808
8400
  Show the user the new widget's \`externalMetricId\` and \`metricId\` after each add.
7809
8401
 
7810
8402
  **Step 3b \u2014 UPDATE a widget**
7811
8403
  If the user wants to modify a widget, STOP and ASK:
7812
- - Which widget? (look up its \`externalMetricId\` from Step 2 \u2014 remember, this is the uuid, NOT the \`metricId\` slug)
8404
+ - Which widget? (let the user pick by name, then look up the widget's \`externalMetricId\` from the Step 2 \`list_widgets\` response \u2014 this is the uuid, NOT the \`metricId\` slug. If you skipped Step 2, call \`list_widgets\` first to fetch it \u2014 never ask the user for a uuid.)
7813
8405
  - What to change? (name, query, chartOptions, isPublished, etc.)
7814
8406
 
7815
- Call \`update_widget({ externalMetricId, clientId, dataAppName, set: {<partial patch>} })\`.
8407
+ Call \`update_widget({ externalMetricId, clientId, dashboardName, workspaceName?, set: {<partial patch>} })\`.
7816
8408
  Show the user the confirmation and list the changed fields.
7817
8409
 
7818
8410
  **Step 3c \u2014 REMOVE a widget**
7819
8411
  If the user wants to remove a widget, STOP and CONFIRM:
7820
- - "This is a per-client soft delete. The widget will be hidden for \`<clientId>\` only \u2014 other customers and the template dashboard are unaffected. Proceed?"
8412
+ - "This is a per-customer soft delete. The widget will be hidden for \`<clientId>\` only \u2014 other customers and the template dashboard are unaffected. Proceed?"
7821
8413
 
7822
- Wait for explicit confirmation before calling \`remove_widget({ externalMetricId, clientId, dataAppName })\`.
8414
+ Look up the widget's \`externalMetricId\` from the Step 2 \`list_widgets\` response (call it first if you skipped Step 2). Never ask the user for the uuid.
8415
+ Wait for explicit confirmation before calling \`remove_widget({ externalMetricId, clientId, dashboardName, workspaceName? })\`.
7823
8416
 
7824
8417
  **Step 4 \u2014 Verify**
7825
- Call \`list_widgets({ embedId, clientId })\` again. Present the updated list and confirm the changes match what the user asked for.
8418
+ Call \`list_widgets({ clientId, dashboardName, workspaceName? })\` again. Present the updated list and confirm the changes match what the user asked for.
8419
+
8420
+ **Step 5 \u2014 Hand off the demo link**
8421
+ Always close with a demo link so the user can verify visually. Call \`get_demo_link({ clientId, dashboardName, workspaceName? })\` \u2014 it returns the same URL the Databrain UI's "Share Demo Link" button generates: a hosted preview page that loads the dashboard exactly as that \`clientId\` will see it. Default expiry is 24h; pass \`expiryHours\` to override.
7826
8422
 
7827
- **Step 5 \u2014 (Optional) regenerate guest token for live preview**
7828
- If the user wants to see the changes in their running app:
7829
- - Remind them: "Existing guest tokens carry the old embed config. Regenerate via \`generate_guest_token({ clientId, dataAppName })\` and reload the frontend to see the changes."
8423
+ Present the URL to the user and remind them: (a) only \`isPublished: true\` widgets appear in the preview, so flag any unpublished widgets from the Step 4 \`list_widgets\` output; (b) if they already have the embed wired up in their own app, the cached frontend guest token there must be refreshed before changes appear.
7830
8424
 
7831
8425
  **Important rules:**
7832
8426
  - STOP after each step. Present results and wait for user input before proceeding.
7833
- - Always pass \`externalMetricId\` (uuid) to \`update_widget\` / \`remove_widget\`, NOT the \`metricId\` slug. When in doubt, use \`list_widgets\` output \u2014 but note that \`list_widgets\` shows \`metricId\`, so you may need \`get_embed_details\` or context from \`add_widget\`'s response to find the uuid.
8427
+ - Always pass \`externalMetricId\` (uuid) to \`update_widget\` / \`remove_widget\`, NOT the \`metricId\` slug.
8428
+ - Never ask for data app, datasource, integration, or embed identifiers \u2014 they all auto-resolve from \`dashboardName\`.
7834
8429
  - Never batch add + update + remove without confirming each one.
7835
- - If the customer-facing effect is not immediate (e.g., guest token cached), say so explicitly in your handoff message.
7836
8430
 
7837
8431
  After completion, suggest: \`brand-my-embed\` if they want to adjust theme, or \`manage-embeds\` if they want to tweak access settings / options.`;
7838
8432
 
@@ -7887,6 +8481,19 @@ function registerAllPrompts(server) {
7887
8481
 
7888
8482
  // src/server.ts
7889
8483
  function createServer(config) {
8484
+ const client = new DatabrainClient(config);
8485
+ const auth = client.getActiveAuth();
8486
+ 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
+ `;
8494
+ } 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";
8496
+ }
7890
8497
  const server = new McpServer(
7891
8498
  {
7892
8499
  name: "databrain",
@@ -7894,8 +8501,8 @@ function createServer(config) {
7894
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."
7895
8502
  },
7896
8503
  {
7897
- instructions: `Databrain uses two token types \u2014 never confuse them:
7898
- \u2022 SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, list_datamarts, list_workspaces, link_datamart_to_workspace, list_data_apps, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer. You already have this \u2014 never ask the user for it.
8504
+ 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.
7899
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.
7900
8507
 
7901
8508
  3 core use cases \u2014 match the user's scenario to the right prompt:
@@ -7921,7 +8528,6 @@ IMPORTANT \u2014 Be interactive. Present results from each tool call and ask the
7921
8528
  Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, filters, permissions, datasource, datamart, workspace, metric creation.`
7922
8529
  }
7923
8530
  );
7924
- const client = new DatabrainClient(config);
7925
8531
  registerAllTools(server, client);
7926
8532
  registerAllResources(server);
7927
8533
  registerAllPrompts(server);