@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/README.md +46 -10
- package/dist/index.js +918 -312
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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("
|
|
23
|
-
lines.push(
|
|
24
|
+
lines.push(" Either:");
|
|
25
|
+
lines.push(" \u2022 Set DATABRAIN_PAT to a Personal Access Token (Settings > Personal Access Tokens) \u2014 preferred, binds this MCP session to your user.");
|
|
26
|
+
lines.push(" \u2022 OR set DATABRAIN_SERVICE_TOKEN (Settings > Service Tokens) \u2014 org-level, no per-user audit; the `connect` tool can mint a PAT on top.");
|
|
27
|
+
lines.push(' Then add to your MCP config: { "env": { "DATABRAIN_PAT": "..." } }');
|
|
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
|
-
|
|
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:
|
|
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 = `
|
|
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
|
|
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-
|
|
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: `
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1946
|
-
datamartName:
|
|
1947
|
-
isAllowMetricCreation:
|
|
1948
|
-
metricCreationMode:
|
|
1949
|
-
isAllowMetricDeletion:
|
|
1950
|
-
isAllowMetricUpdate:
|
|
1951
|
-
isAllowMetricLayoutChange:
|
|
1952
|
-
isAllowManageMetrics:
|
|
1953
|
-
isAllowUnderlyingData:
|
|
1954
|
-
isAllowAiPilot:
|
|
1955
|
-
isAllowEmailReports:
|
|
1956
|
-
isAllowCreateDashboardView:
|
|
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:
|
|
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:
|
|
1968
|
-
workspaceName:
|
|
1969
|
-
embedType:
|
|
1970
|
-
metricId:
|
|
1971
|
-
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:
|
|
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:
|
|
1978
|
-
isImportMetrics:
|
|
1979
|
-
metricImportMode:
|
|
1980
|
-
isRenameDashboard:
|
|
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
|
|
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:
|
|
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:
|
|
2102
|
-
|
|
2103
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
2185
|
-
body:
|
|
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:
|
|
2189
|
-
accessSettings:
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
2385
|
-
fontFamily:
|
|
2386
|
-
fontSize:
|
|
2387
|
-
fontWeight:
|
|
2388
|
-
fontColor:
|
|
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 =
|
|
2391
|
-
fontSize:
|
|
2392
|
-
fontFamily:
|
|
2393
|
-
fontWeight:
|
|
2394
|
-
color:
|
|
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 =
|
|
2406
|
-
hideAxisLines:
|
|
2407
|
-
hideSplitLines:
|
|
2408
|
-
hideAxisLabels:
|
|
2409
|
-
hideAxisTicks:
|
|
2410
|
-
axisName:
|
|
2411
|
-
axisNameOffset:
|
|
2412
|
-
axisLabelMargin:
|
|
2413
|
-
fontSize:
|
|
2414
|
-
fontFamily:
|
|
2415
|
-
fontWeight:
|
|
2416
|
-
color:
|
|
2417
|
-
axisColor:
|
|
2418
|
-
axisNameFontConfig:
|
|
2419
|
-
fontFamily:
|
|
2420
|
-
fontSize:
|
|
2421
|
-
fontWeight:
|
|
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:
|
|
2578
|
-
body:
|
|
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:
|
|
2583
|
-
accessPreset:
|
|
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:
|
|
2887
|
+
datePickerFormat: z13.string().optional().describe("Date picker display format"),
|
|
2587
2888
|
// --- Colors ---
|
|
2588
|
-
primaryColor:
|
|
2589
|
-
backgroundColor:
|
|
2590
|
-
textColor:
|
|
2591
|
-
ctaColor:
|
|
2592
|
-
ctaTextColor:
|
|
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:
|
|
2595
|
-
selectBoxSize:
|
|
2596
|
-
selectBoxVariant:
|
|
2597
|
-
selectBoxBorderRadius:
|
|
2598
|
-
selectBoxTextColor:
|
|
2599
|
-
metricCardColor:
|
|
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:
|
|
2602
|
-
cardTitleFontWeight:
|
|
2603
|
-
cardTitleColor:
|
|
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:
|
|
2606
|
-
cardDescriptionFontWeight:
|
|
2607
|
-
cardDescriptionColor:
|
|
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:
|
|
2610
|
-
cardBorderRadius:
|
|
2611
|
-
cardShadow:
|
|
2612
|
-
disableShadowOnHover:
|
|
2613
|
-
disableCardStroke:
|
|
2614
|
-
cardStrokeColor:
|
|
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:
|
|
2617
|
-
chartFontColor:
|
|
2618
|
-
chartColors:
|
|
2619
|
-
chartPaletteName:
|
|
2620
|
-
chartPaletteColors:
|
|
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:
|
|
2627
|
-
buttonPrimaryTextColor:
|
|
2628
|
-
buttonSecondaryColor:
|
|
2629
|
-
buttonSecondaryTextColor:
|
|
2630
|
-
checkboxCheckedColor:
|
|
2631
|
-
checkboxUncheckedColor:
|
|
2632
|
-
switchEnabledColor:
|
|
2633
|
-
switchDisabledColor:
|
|
2634
|
-
breadcrumbFontFamily:
|
|
2635
|
-
breadcrumbFontColor:
|
|
2636
|
-
breadcrumbActiveColor:
|
|
2637
|
-
filterBadgeColor:
|
|
2638
|
-
filterBadgeTextColor:
|
|
2639
|
-
datePickerColor:
|
|
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:
|
|
2642
|
-
breakpointLg:
|
|
2643
|
-
breakpointMd:
|
|
2644
|
-
breakpointSm:
|
|
2645
|
-
breakpointXs:
|
|
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:
|
|
2648
|
-
layoutColsLg:
|
|
2649
|
-
layoutColsMd:
|
|
2650
|
-
layoutColsSm:
|
|
2651
|
-
layoutColsXs:
|
|
2652
|
-
layoutColsXxs:
|
|
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:
|
|
2658
|
-
radialLabelPosition:
|
|
2659
|
-
marginTop:
|
|
2660
|
-
marginLeft:
|
|
2661
|
-
marginRight:
|
|
2662
|
-
marginBottom:
|
|
2663
|
-
legendShow:
|
|
2664
|
-
legendPosition:
|
|
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:
|
|
2675
|
-
legendShape:
|
|
2676
|
-
legendFontSize:
|
|
2677
|
-
legendFontWeight:
|
|
2678
|
-
legendFontFamily:
|
|
2679
|
-
legendColor:
|
|
2680
|
-
legendTruncateLength:
|
|
2681
|
-
disableLegendScrolling:
|
|
2682
|
-
enableVariableLegendPosition:
|
|
2683
|
-
legendTop:
|
|
2684
|
-
legendLeft:
|
|
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
|
|
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
|
-
|
|
4402
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
4569
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4628
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5115
|
+
[embedId]
|
|
4695
5116
|
);
|
|
4696
5117
|
const datasource = await services.widgetService.resolveDatasource(guestToken, {
|
|
4697
|
-
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
|
|
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
|
-
**
|
|
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
|
-
- **
|
|
7773
|
-
- **
|
|
7774
|
-
- **
|
|
7775
|
-
- **
|
|
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
|
-
**
|
|
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?" (
|
|
7780
|
-
- "Which
|
|
8372
|
+
- "Which customer/client are we customizing for?" (capture as \`clientId\`)
|
|
8373
|
+
- "Which dashboard?" (capture as \`dashboardName\`)
|
|
7781
8374
|
|
|
7782
|
-
|
|
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({
|
|
7786
|
-
STOP and present the list: \`{metricId, name, isPublished, createdByUser}\` per widget.
|
|
7787
|
-
ASK: "What would you like to change? (add / modify / 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
|
-
|
|
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\`
|
|
7794
|
-
- Call \`add_widget({
|
|
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({
|
|
7799
|
-
-
|
|
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 "
|
|
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
|
|
7806
|
-
- Call \`add_widget({
|
|
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
|
|
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,
|
|
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-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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);
|