@databrainhq/mcp-server 0.2.10 → 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);
@@ -1990,79 +2220,8 @@ function registerListWorkspaces(server, client) {
1990
2220
  );
1991
2221
  }
1992
2222
 
1993
- // src/tools/infrastructure/link-datamart-to-workspace.ts
1994
- import { z as z9 } from "zod";
1995
- function registerLinkDatamartToWorkspace(server, client) {
1996
- server.tool(
1997
- "link_datamart_to_workspace",
1998
- "Link a datamart to an existing workspace. ALWAYS ask the user which workspace to use \u2014 do not auto-select.",
1999
- {
2000
- workspaceName: z9.string().optional().describe(
2001
- "Name of the workspace to update. Omit to list available workspaces and let the user choose."
2002
- ),
2003
- datamartName: z9.string().describe(
2004
- "Name of the datamart to link. Use `list_datamarts` to find it."
2005
- )
2006
- },
2007
- WRITE_IDEMPOTENT,
2008
- async ({ workspaceName, datamartName }) => {
2009
- try {
2010
- if (!workspaceName) {
2011
- const workspaces = await client.request({
2012
- method: "GET",
2013
- path: "/api/v2/workspace",
2014
- auth: "service",
2015
- cacheKey: "list_workspaces",
2016
- cacheTtlMs: 5 * 60 * 1e3
2017
- });
2018
- if (!Array.isArray(workspaces) || workspaces.length === 0) {
2019
- return toolError(
2020
- "No workspaces found. Create a workspace in the **Databrain UI** first, then come back here to link the datamart."
2021
- );
2022
- }
2023
- const list = workspaces.map((ws) => `- **${ws.name}** (ID: ${ws._id || ws.id})`).join("\n");
2024
- return toolSuccess(
2025
- `Found ${workspaces.length} workspace(s):
2026
-
2027
- ${list}
2028
-
2029
- **Ask the user** which workspace they want to link to datamart **${datamartName}**, then call \`link_datamart_to_workspace\` with both \`workspaceName\` and \`datamartName\`.`
2030
- );
2031
- }
2032
- const result = await client.request({
2033
- method: "PUT",
2034
- path: "/api/v2/workspace",
2035
- body: {
2036
- name: workspaceName,
2037
- connectionType: "DATAMART",
2038
- datamartName
2039
- },
2040
- auth: "service",
2041
- invalidateCache: ["list_workspaces"]
2042
- });
2043
- if (result.error) {
2044
- return toolError(
2045
- `Failed to link datamart: ${typeof result.error === "object" ? JSON.stringify(result.error) : result.error}`
2046
- );
2047
- }
2048
- return toolSuccess(
2049
- `Workspace **${workspaceName}** is now linked to datamart **${datamartName}**.
2050
-
2051
- Next steps:
2052
- - Build dashboards in the Databrain UI using this workspace
2053
- - Use \`list_dashboards\` to find dashboards in this workspace
2054
- - Use \`create_embed\` to embed dashboards into your app`
2055
- );
2056
- } catch (err) {
2057
- if (err instanceof DatabrainApiError) return err.toToolResponse();
2058
- return toolError(`Unexpected error: ${err.message}`);
2059
- }
2060
- }
2061
- );
2062
- }
2063
-
2064
2223
  // src/tools/embed/create-embed.ts
2065
- import { z as z10 } from "zod";
2224
+ import { z as z9 } from "zod";
2066
2225
  var DEFAULT_ACCESS_SETTINGS2 = {
2067
2226
  isAllowMetricCreation: false,
2068
2227
  isAllowMetricDeletion: false,
@@ -2075,18 +2234,18 @@ var DEFAULT_ACCESS_SETTINGS2 = {
2075
2234
  isAllowCreateDashboardView: false,
2076
2235
  metricCreationMode: "DRAG_DROP"
2077
2236
  };
2078
- var accessSettingsSchema = z10.object({
2079
- datamartName: z10.string().describe("Name of the datamart (required)"),
2080
- isAllowMetricCreation: z10.boolean().optional().describe("Allow end-users to create metrics"),
2081
- metricCreationMode: z10.enum(["DRAG_DROP", "CHAT"]).optional().describe("Metric creation mode"),
2082
- isAllowMetricDeletion: z10.boolean().optional().describe("Allow metric deletion"),
2083
- isAllowMetricUpdate: z10.boolean().optional().describe("Allow metric editing"),
2084
- isAllowMetricLayoutChange: z10.boolean().optional().describe("Allow layout changes"),
2085
- isAllowManageMetrics: z10.boolean().optional().describe("Show manage metrics UI"),
2086
- isAllowUnderlyingData: z10.boolean().optional().describe("Allow viewing underlying data"),
2087
- isAllowAiPilot: z10.boolean().optional().describe("Enable AI pilot features"),
2088
- isAllowEmailReports: z10.boolean().optional().describe("Allow email report scheduling"),
2089
- isAllowCreateDashboardView: z10.boolean().optional().describe("Allow creating dashboard views")
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")
2090
2249
  }).describe("Embed access settings. Only datamartName is required \u2014 all flags default to false.");
2091
2250
  function registerCreateEmbed(server, client) {
2092
2251
  const services = createServiceBundle(client);
@@ -2094,23 +2253,23 @@ function registerCreateEmbed(server, client) {
2094
2253
  "create_embed",
2095
2254
  "Create an embed. Provide dashboardId, workspaceName, accessSettings.datamartName. Add clientId for multi-tenant per-client dashboards. All isAllow* flags default to false.",
2096
2255
  {
2097
- body: z10.record(z10.unknown()).optional().describe(
2256
+ body: z9.record(z9.unknown()).optional().describe(
2098
2257
  "Raw API body passthrough. If provided, all other fields are ignored."
2099
2258
  ),
2100
- dashboardId: z10.string().optional().describe("Dashboard identifier for embed creation"),
2101
- workspaceName: z10.string().optional().describe("Workspace name. Omit to auto-resolve if only one exists."),
2102
- embedType: z10.enum(["dashboard", "metric"]).optional().describe("Embed type"),
2103
- metricId: z10.string().optional().describe("Required for metric embed type"),
2104
- name: z10.string().optional().describe("Human-readable embed config name"),
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"),
2105
2264
  accessSettings: accessSettingsSchema.optional(),
2106
2265
  // Multi-tenant (dashboard embed) params
2107
- clientId: z10.string().optional().describe(
2266
+ clientId: z9.string().optional().describe(
2108
2267
  "Client identifier for multi-tenant isolation. When provided, creates a dashboard embed (each client gets their own dashboard copy)."
2109
2268
  ),
2110
- templateDashboardId: z10.string().optional().describe("Template dashboard ID to clone metrics from"),
2111
- isImportMetrics: z10.boolean().optional().describe("Import metrics from template dashboard"),
2112
- metricImportMode: z10.enum(["COPY", "LINK"]).optional().describe("COPY duplicates metrics, LINK shares them"),
2113
- isRenameDashboard: z10.boolean().optional().describe("Rename the dashboard for this client")
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")
2114
2273
  },
2115
2274
  WRITE_CREATE,
2116
2275
  async (params) => {
@@ -2212,14 +2371,14 @@ Next: run \`get_embed_details\` to verify the embed configuration.`
2212
2371
  }
2213
2372
 
2214
2373
  // src/tools/embed/list-embeds.ts
2215
- import { z as z11 } from "zod";
2374
+ import { z as z10 } from "zod";
2216
2375
  function registerListEmbeds(server, client) {
2217
2376
  const services = createServiceBundle(client);
2218
2377
  server.tool(
2219
2378
  "list_embeds",
2220
- "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.",
2221
2380
  {
2222
- dataAppName: z11.string().optional().describe("Filter by data app name")
2381
+ dataAppName: z10.string().optional().describe("Filter by data app name")
2223
2382
  },
2224
2383
  READ_ONLY,
2225
2384
  async (params) => {
@@ -2230,19 +2389,28 @@ function registerListEmbeds(server, client) {
2230
2389
  "No embeds found. Create an embed in the Databrain UI, then run `list_embeds` again."
2231
2390
  );
2232
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();
2233
2410
  return toolSuccess(JSON.stringify({
2234
- count: result.length,
2235
- embeds: result.map((e) => {
2236
- const n = services.discoveryService.normalizeEmbed(e);
2237
- return {
2238
- embedId: n.embedId || null,
2239
- internalId: !n.embedId && n.internalId ? n.internalId : void 0,
2240
- name: n.name,
2241
- type: n.embedType || "dashboard",
2242
- dashboardId: n.dashboardId || null,
2243
- dataApp: n.dataAppName || null
2244
- };
2245
- })
2411
+ count: embeds.length,
2412
+ distinctClientIds,
2413
+ embeds
2246
2414
  }));
2247
2415
  } catch (err) {
2248
2416
  if (err instanceof DatabrainApiError) return err.toToolResponse();
@@ -2253,14 +2421,14 @@ function registerListEmbeds(server, client) {
2253
2421
  }
2254
2422
 
2255
2423
  // src/tools/embed/get-embed-details.ts
2256
- import { z as z12 } from "zod";
2424
+ import { z as z11 } from "zod";
2257
2425
  function registerGetEmbedDetails(server, client) {
2258
2426
  const services = createServiceBundle(client);
2259
2427
  server.tool(
2260
2428
  "get_embed_details",
2261
2429
  "Get full embed configuration including access settings and theme.",
2262
2430
  {
2263
- embedId: z12.string().describe("ID of the embed")
2431
+ embedId: z11.string().describe("ID of the embed")
2264
2432
  },
2265
2433
  READ_ONLY,
2266
2434
  async (params) => {
@@ -2276,7 +2444,7 @@ function registerGetEmbedDetails(server, client) {
2276
2444
  }
2277
2445
 
2278
2446
  // src/tools/embed/update-embed.ts
2279
- import { z as z13 } from "zod";
2447
+ import { z as z12 } from "zod";
2280
2448
 
2281
2449
  // src/tools/embed/helpers.ts
2282
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.";
@@ -2314,15 +2482,15 @@ function registerUpdateEmbed(server, client) {
2314
2482
  "update_embed",
2315
2483
  "Update embed access settings, permissions, theme, or name.",
2316
2484
  {
2317
- embedId: z13.string().describe("ID of the embed to update"),
2318
- 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(
2319
2487
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, it is sent directly (passthrough mode)."
2320
2488
  ),
2321
- name: z13.string().optional().describe("New name for the embed"),
2322
- 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(
2323
2491
  "Partial update of access settings. See `get_embed_details` for current settings."
2324
2492
  ),
2325
- theme: z13.record(z13.unknown()).optional().describe(
2493
+ theme: z12.record(z12.unknown()).optional().describe(
2326
2494
  "Theme configuration object. See databrain://theme-reference for the full schema."
2327
2495
  )
2328
2496
  },
@@ -2368,7 +2536,7 @@ function registerUpdateEmbed(server, client) {
2368
2536
  }
2369
2537
 
2370
2538
  // src/tools/embed/customize-theme.ts
2371
- import { z as z14 } from "zod";
2539
+ import { z as z13 } from "zod";
2372
2540
  var THEME_PRESETS = {
2373
2541
  light: {
2374
2542
  general: {
@@ -2502,7 +2670,7 @@ var ACCESS_PRESETS = {
2502
2670
  metricCreationMode: "CHAT"
2503
2671
  }
2504
2672
  };
2505
- var fontFamilyEnum = z14.enum([
2673
+ var fontFamilyEnum = z13.enum([
2506
2674
  "Inter",
2507
2675
  "Roboto",
2508
2676
  "Open Sans",
@@ -2514,17 +2682,17 @@ var fontFamilyEnum = z14.enum([
2514
2682
  "Nunito",
2515
2683
  "system-ui"
2516
2684
  ]).describe("Font family");
2517
- var tooltipFontSchema = z14.object({
2518
- fontFamily: z14.string().optional(),
2519
- fontSize: z14.number().optional(),
2520
- fontWeight: z14.number().optional(),
2521
- fontColor: z14.string().optional()
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()
2522
2690
  }).optional();
2523
- var fontConfigInputSchema = z14.object({
2524
- fontSize: z14.number().optional(),
2525
- fontFamily: z14.string().optional(),
2526
- fontWeight: z14.number().optional(),
2527
- color: z14.string().optional()
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()
2528
2696
  }).optional();
2529
2697
  function mapToFrontendFontConfig(input) {
2530
2698
  if (!input) return void 0;
@@ -2535,23 +2703,23 @@ function mapToFrontendFontConfig(input) {
2535
2703
  ...input.color && { color: input.color }
2536
2704
  };
2537
2705
  }
2538
- var axisSchema = z14.object({
2539
- hideAxisLines: z14.boolean().optional().describe("Hide axis lines"),
2540
- hideSplitLines: z14.boolean().optional().describe("Hide grid/split lines"),
2541
- hideAxisLabels: z14.boolean().optional().describe("Hide axis labels"),
2542
- hideAxisTicks: z14.boolean().optional().describe("Hide axis tick marks"),
2543
- axisName: z14.string().optional().describe("Axis title text"),
2544
- axisNameOffset: z14.number().optional().describe("Axis title offset from axis"),
2545
- axisLabelMargin: z14.number().optional().describe("Margin between axis labels and chart area in pixels"),
2546
- fontSize: z14.number().optional().describe("Axis label font size"),
2547
- fontFamily: z14.string().optional().describe("Axis label font family"),
2548
- fontWeight: z14.number().optional().describe("Axis label font weight"),
2549
- color: z14.string().optional().describe("Axis label color (hex)"),
2550
- axisColor: z14.string().optional().describe("Axis line color (hex)"),
2551
- axisNameFontConfig: z14.object({
2552
- fontFamily: z14.string().optional(),
2553
- fontSize: z14.number().optional(),
2554
- fontWeight: z14.number().optional()
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()
2555
2723
  }).optional().describe("Axis title font configuration")
2556
2724
  }).optional();
2557
2725
  function buildAdminTheme(p, presetBase) {
@@ -2707,94 +2875,94 @@ function registerCustomizeTheme(server, client) {
2707
2875
  "customize_embed_theme",
2708
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("/")}).`,
2709
2877
  {
2710
- embedId: z14.string().describe("ID of the embed to theme"),
2711
- 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(
2712
2880
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, sent directly and all convenience fields are ignored."
2713
2881
  ),
2714
2882
  // --- Presets ---
2715
- themePreset: z14.enum(["light", "dark", "corporate", "minimal"]).optional().describe("Apply a theme preset as the base. Granular params below override preset values."),
2716
- accessPreset: z14.enum(["view-only", "power-user", "export-only", "ai-enabled"]).optional().describe("Apply an access settings preset"),
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"),
2717
2885
  // --- General ---
2718
2886
  fontFamily: fontFamilyEnum.optional().describe("Global font family"),
2719
- datePickerFormat: z14.string().optional().describe("Date picker display format"),
2887
+ datePickerFormat: z13.string().optional().describe("Date picker display format"),
2720
2888
  // --- Colors ---
2721
- primaryColor: z14.string().optional().describe("Primary accent color (hex)"),
2722
- backgroundColor: z14.string().optional().describe("Overall background color (hex)"),
2723
- textColor: z14.string().optional().describe("Default text color (hex)"),
2724
- ctaColor: z14.string().optional().describe("Call-to-action button color (hex)"),
2725
- ctaTextColor: z14.string().optional().describe("CTA button text color (hex)"),
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)"),
2726
2894
  // --- Dashboard ---
2727
- dashboardBackgroundColor: z14.string().optional().describe("Dashboard container background (hex)"),
2728
- selectBoxSize: z14.enum(["small", "medium", "large"]).optional().describe("Filter select box size"),
2729
- selectBoxVariant: z14.enum(["floating", "outlined", "standard"]).optional().describe("Filter select box style"),
2730
- selectBoxBorderRadius: z14.string().optional().describe('Select box border radius (e.g. "20px")'),
2731
- selectBoxTextColor: z14.string().optional().describe("Select box text color (hex)"),
2732
- metricCardColor: z14.string().optional().describe("Metric card background color (hex)"),
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)"),
2733
2901
  // --- Card Title ---
2734
- cardTitleFontSize: z14.string().optional().describe('Card title font size (e.g. "16px")'),
2735
- cardTitleFontWeight: z14.string().optional().describe('Card title font weight (e.g. "600")'),
2736
- cardTitleColor: z14.string().optional().describe("Card title text color (hex)"),
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)"),
2737
2905
  // --- Card Description ---
2738
- cardDescriptionFontSize: z14.string().optional().describe("Card description font size"),
2739
- cardDescriptionFontWeight: z14.string().optional().describe("Card description font weight"),
2740
- cardDescriptionColor: z14.string().optional().describe("Card description text color (hex)"),
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)"),
2741
2909
  // --- Card Customization ---
2742
- cardPadding: z14.string().optional().describe("Card internal padding"),
2743
- cardBorderRadius: z14.string().optional().describe("Card border radius"),
2744
- cardShadow: z14.string().optional().describe("Card box-shadow CSS value"),
2745
- disableShadowOnHover: z14.boolean().optional().describe("Disable shadow on card hover"),
2746
- disableCardStroke: z14.boolean().optional().describe("Remove card border stroke"),
2747
- cardStrokeColor: z14.string().optional().describe("Card border stroke color (hex)"),
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)"),
2748
2916
  // --- Chart Colors (admin theme level) ---
2749
- chartFontFamily: z14.string().optional().describe("Font family for chart text"),
2750
- chartFontColor: z14.string().optional().describe("Font color for chart text (hex)"),
2751
- chartColors: z14.array(z14.string()).optional().describe("Array of chart series colors (hex)"),
2752
- chartPaletteName: z14.string().optional().describe("Name for a custom palette"),
2753
- chartPaletteColors: z14.array(z14.string()).optional().describe("9 colors for a custom palette"),
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"),
2754
2922
  // --- Chart Tooltip (admin theme level) ---
2755
2923
  tooltipHeader: tooltipFontSchema.describe("Tooltip header font config"),
2756
2924
  tooltipLabel: tooltipFontSchema.describe("Tooltip label font config"),
2757
2925
  tooltipValue: tooltipFontSchema.describe("Tooltip value font config"),
2758
2926
  // --- Component Theme ---
2759
- buttonPrimaryColor: z14.string().optional().describe("Primary button background (hex)"),
2760
- buttonPrimaryTextColor: z14.string().optional().describe("Primary button text (hex)"),
2761
- buttonSecondaryColor: z14.string().optional().describe("Secondary button background (hex)"),
2762
- buttonSecondaryTextColor: z14.string().optional().describe("Secondary button text (hex)"),
2763
- checkboxCheckedColor: z14.string().optional().describe("Checked checkbox color (hex)"),
2764
- checkboxUncheckedColor: z14.string().optional().describe("Unchecked checkbox color (hex)"),
2765
- switchEnabledColor: z14.string().optional().describe("Enabled switch color (hex)"),
2766
- switchDisabledColor: z14.string().optional().describe("Disabled switch color (hex)"),
2767
- breadcrumbFontFamily: z14.string().optional().describe("Drill breadcrumb font family"),
2768
- breadcrumbFontColor: z14.string().optional().describe("Drill breadcrumb font color (hex)"),
2769
- breadcrumbActiveColor: z14.string().optional().describe("Active breadcrumb color (hex)"),
2770
- filterBadgeColor: z14.string().optional().describe("Multi-select filter badge color (hex)"),
2771
- filterBadgeTextColor: z14.string().optional().describe("Filter badge text color (hex)"),
2772
- datePickerColor: z14.string().optional().describe("Date picker accent color (hex)"),
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)"),
2773
2941
  // --- Responsive Breakpoints ---
2774
- breakpointXl: z14.number().optional().describe("XL breakpoint width in px"),
2775
- breakpointLg: z14.number().optional().describe("LG breakpoint width in px"),
2776
- breakpointMd: z14.number().optional().describe("MD breakpoint width in px"),
2777
- breakpointSm: z14.number().optional().describe("SM breakpoint width in px"),
2778
- breakpointXs: z14.number().optional().describe("XS breakpoint width in px"),
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"),
2779
2947
  // --- Metric Layout Columns ---
2780
- layoutColsXl: z14.number().optional().describe("Grid columns at XL breakpoint"),
2781
- layoutColsLg: z14.number().optional().describe("Grid columns at LG breakpoint"),
2782
- layoutColsMd: z14.number().optional().describe("Grid columns at MD breakpoint"),
2783
- layoutColsSm: z14.number().optional().describe("Grid columns at SM breakpoint"),
2784
- layoutColsXs: z14.number().optional().describe("Grid columns at XS breakpoint"),
2785
- layoutColsXxs: z14.number().optional().describe("Grid columns at XXS breakpoint"),
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"),
2786
2954
  // --- Chart Appearance (detailed) ---
2787
2955
  tooltipHeaderStyle: fontConfigInputSchema.describe("Tooltip header: fontSize, fontFamily, fontWeight, color"),
2788
2956
  tooltipLabelStyle: fontConfigInputSchema.describe("Tooltip label: fontSize, fontFamily, fontWeight, color"),
2789
2957
  tooltipValueStyle: fontConfigInputSchema.describe("Tooltip value: fontSize, fontFamily, fontWeight, color"),
2790
- labelPosition: z14.enum(["hidden", "top", "left", "right", "bottom", "inside"]).optional().describe("Data label position on charts"),
2791
- radialLabelPosition: z14.enum(["outside", "inside"]).optional().describe("Label position for radial/pie charts"),
2792
- marginTop: z14.number().optional().describe("Chart top margin in pixels"),
2793
- marginLeft: z14.number().optional().describe("Chart left margin in pixels"),
2794
- marginRight: z14.number().optional().describe("Chart right margin in pixels"),
2795
- marginBottom: z14.number().optional().describe("Chart bottom margin in pixels"),
2796
- legendShow: z14.boolean().optional().describe("Show or hide chart legend"),
2797
- legendPosition: z14.enum([
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([
2798
2966
  "top-left",
2799
2967
  "top-center",
2800
2968
  "top-right",
@@ -2804,17 +2972,17 @@ function registerCustomizeTheme(server, client) {
2804
2972
  "bottom-center",
2805
2973
  "bottom-right"
2806
2974
  ]).optional().describe("Legend position"),
2807
- legendLayout: z14.enum(["horizontal", "vertical"]).optional().describe("Legend layout direction"),
2808
- legendShape: z14.enum(["circle", "rect", "roundRect", "triangle", "diamond", "arrow", "none"]).optional().describe("Legend icon shape"),
2809
- legendFontSize: z14.number().optional().describe("Legend font size"),
2810
- legendFontWeight: z14.number().optional().describe("Legend font weight"),
2811
- legendFontFamily: z14.string().optional().describe("Legend font family"),
2812
- legendColor: z14.string().optional().describe("Legend text color (hex)"),
2813
- legendTruncateLength: z14.number().optional().describe("Max characters before truncating legend"),
2814
- disableLegendScrolling: z14.boolean().optional().describe("Disable scrolling in legend"),
2815
- enableVariableLegendPosition: z14.boolean().optional().describe("Allow user to drag legend"),
2816
- legendTop: z14.number().optional().describe("Custom legend top offset"),
2817
- legendLeft: z14.number().optional().describe("Custom legend left offset"),
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"),
2818
2986
  verticalAxis: axisSchema.describe("Vertical (Y) axis configuration"),
2819
2987
  horizontalAxis: axisSchema.describe("Horizontal (X) axis configuration")
2820
2988
  },
@@ -2888,6 +3056,76 @@ Use \`get_embed_details\` to view the full current theme configuration.` + GUEST
2888
3056
  );
2889
3057
  }
2890
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
+
2891
3129
  // src/tools/frontend/generate-guest-token.ts
2892
3130
  import { z as z15 } from "zod";
2893
3131
  function registerGenerateGuestToken(server, client) {
@@ -4918,13 +5156,173 @@ function registerGenerateWidget(server, client) {
4918
5156
  );
4919
5157
  }
4920
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
+
4921
5317
  // src/tools/registry.ts
4922
5318
  function registerAllTools(server, client) {
5319
+ registerConnect(server, client);
5320
+ registerWhoami(server, client);
5321
+ registerDisconnect(server, client);
4923
5322
  registerListDatasources(server, client);
4924
5323
  registerDiscoverDatasourceSchema(server, client);
4925
5324
  registerListDatamarts(server, client);
4926
5325
  registerListWorkspaces(server, client);
4927
- registerLinkDatamartToWorkspace(server, client);
4928
5326
  registerListDataApps(server, client);
4929
5327
  registerCreateDataApp(server, client);
4930
5328
  registerListApiTokens(server, client);
@@ -4935,6 +5333,7 @@ function registerAllTools(server, client) {
4935
5333
  registerGetEmbedDetails(server, client);
4936
5334
  registerUpdateEmbed(server, client);
4937
5335
  registerCustomizeTheme(server, client);
5336
+ registerGetDemoLink(server, client);
4938
5337
  registerGenerateGuestToken(server, client);
4939
5338
  registerGenerateEmbedCode(server, client);
4940
5339
  registerListDashboards(server, client);
@@ -8018,8 +8417,10 @@ Wait for explicit confirmation before calling \`remove_widget({ externalMetricId
8018
8417
  **Step 4 \u2014 Verify**
8019
8418
  Call \`list_widgets({ clientId, dashboardName, workspaceName? })\` again. Present the updated list and confirm the changes match what the user asked for.
8020
8419
 
8021
- **Step 5 \u2014 (Optional) regenerate guest token for live preview**
8022
- If the user wants to see the changes in their running app, regenerate their guest token and reload. The MCP tools mint tokens internally for widget operations; a cached frontend token will not show the changes until refreshed.
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.
8422
+
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.
8023
8424
 
8024
8425
  **Important rules:**
8025
8426
  - STOP after each step. Present results and wait for user input before proceeding.
@@ -8080,6 +8481,19 @@ function registerAllPrompts(server) {
8080
8481
 
8081
8482
  // src/server.ts
8082
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
+ }
8083
8497
  const server = new McpServer(
8084
8498
  {
8085
8499
  name: "databrain",
@@ -8087,8 +8501,8 @@ function createServer(config) {
8087
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."
8088
8502
  },
8089
8503
  {
8090
- instructions: `Databrain uses two token types \u2014 never confuse them:
8091
- \u2022 SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, list_datamarts, list_workspaces, link_datamart_to_workspace, list_data_apps, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer. You already have this \u2014 never ask the user for it.
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.
8092
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.
8093
8507
 
8094
8508
  3 core use cases \u2014 match the user's scenario to the right prompt:
@@ -8114,7 +8528,6 @@ IMPORTANT \u2014 Be interactive. Present results from each tool call and ask the
8114
8528
  Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, filters, permissions, datasource, datamart, workspace, metric creation.`
8115
8529
  }
8116
8530
  );
8117
- const client = new DatabrainClient(config);
8118
8531
  registerAllTools(server, client);
8119
8532
  registerAllResources(server);
8120
8533
  registerAllPrompts(server);