@databrainhq/mcp-server 0.2.11 → 0.2.13
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 +69 -29
- package/dist/index.js +1454 -678
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,11 +8,10 @@ import { z, ZodError } from "zod";
|
|
|
8
8
|
var configSchema = z.object({
|
|
9
9
|
serviceToken: z.string().min(1).optional(),
|
|
10
10
|
apiToken: z.string().min(1).optional(),
|
|
11
|
-
pat: z.string().min(1).optional(),
|
|
12
11
|
apiUrl: z.string().url().default("https://api.usedatabrain.com"),
|
|
13
12
|
demoDomain: z.string().url().default("https://demo.usedatabrain.com")
|
|
14
|
-
}).refine((c) => c.serviceToken || c.apiToken
|
|
15
|
-
message: "At least one of
|
|
13
|
+
}).refine((c) => c.serviceToken || c.apiToken, {
|
|
14
|
+
message: "At least one of DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN must be set"
|
|
16
15
|
});
|
|
17
16
|
function formatConfigError(err) {
|
|
18
17
|
const issues = err.issues.map((i) => i.message);
|
|
@@ -21,10 +20,8 @@ function formatConfigError(err) {
|
|
|
21
20
|
lines.push(` - ${issue}`);
|
|
22
21
|
}
|
|
23
22
|
lines.push("");
|
|
24
|
-
lines.push("
|
|
25
|
-
lines.push(
|
|
26
|
-
lines.push(" \u2022 OR set DATABRAIN_SERVICE_TOKEN (Settings > Service Tokens) \u2014 org-level, no per-user audit; the `connect` tool can mint a PAT on top.");
|
|
27
|
-
lines.push(' Then add to your MCP config: { "env": { "DATABRAIN_PAT": "..." } }');
|
|
23
|
+
lines.push(" Set DATABRAIN_SERVICE_TOKEN (Settings > Service Tokens) \u2014 org-level token for setup and management.");
|
|
24
|
+
lines.push(' Then add to your MCP config: { "env": { "DATABRAIN_SERVICE_TOKEN": "..." } }');
|
|
28
25
|
lines.push("");
|
|
29
26
|
lines.push(" Self-hosted? Also set DATABRAIN_API_URL to your instance's API origin (e.g. https://api.your-databrain.example.com). Tokens minted on cloud do not work against a self-hosted instance and vice versa.");
|
|
30
27
|
return lines.join("\n");
|
|
@@ -34,7 +31,6 @@ function loadConfig() {
|
|
|
34
31
|
return configSchema.parse({
|
|
35
32
|
serviceToken: process.env.DATABRAIN_SERVICE_TOKEN || void 0,
|
|
36
33
|
apiToken: process.env.DATABRAIN_API_TOKEN || void 0,
|
|
37
|
-
pat: process.env.DATABRAIN_PAT || void 0,
|
|
38
34
|
apiUrl: process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com",
|
|
39
35
|
demoDomain: process.env.DATABRAIN_DEMO_DOMAIN || "https://demo.usedatabrain.com"
|
|
40
36
|
});
|
|
@@ -188,124 +184,10 @@ var logger = {
|
|
|
188
184
|
error: (msg, data) => log("error", msg, data)
|
|
189
185
|
};
|
|
190
186
|
|
|
191
|
-
// src/
|
|
192
|
-
|
|
193
|
-
import { homedir } from "os";
|
|
194
|
-
import { dirname, join } from "path";
|
|
195
|
-
function defaultStorePath() {
|
|
196
|
-
const override = process.env.DATABRAIN_CREDENTIAL_PATH;
|
|
197
|
-
if (override) return override;
|
|
198
|
-
return join(homedir(), ".databrain", "mcp", "credentials.json");
|
|
199
|
-
}
|
|
200
|
-
function readStore(path) {
|
|
201
|
-
if (!existsSync(path)) return {};
|
|
202
|
-
try {
|
|
203
|
-
const raw = readFileSync(path, "utf8");
|
|
204
|
-
if (!raw.trim()) return {};
|
|
205
|
-
const parsed = JSON.parse(raw);
|
|
206
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
207
|
-
return parsed;
|
|
208
|
-
}
|
|
209
|
-
return {};
|
|
210
|
-
} catch (err) {
|
|
211
|
-
logger.warn("Credential store unreadable, starting fresh", {
|
|
212
|
-
error: err.message
|
|
213
|
-
});
|
|
214
|
-
return {};
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
function writeStore(path, store) {
|
|
218
|
-
const dir = dirname(path);
|
|
219
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
220
|
-
const tmp = `${path}.tmp.${process.pid}`;
|
|
221
|
-
writeFileSync(tmp, JSON.stringify(store, null, 2), { mode: 384 });
|
|
222
|
-
renameSync(tmp, path);
|
|
223
|
-
try {
|
|
224
|
-
chmodSync(path, 384);
|
|
225
|
-
} catch {
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
var CredentialStore = class {
|
|
229
|
-
constructor(path = defaultStorePath()) {
|
|
230
|
-
this.path = path;
|
|
231
|
-
}
|
|
232
|
-
get(baseUrl) {
|
|
233
|
-
const store = readStore(this.path);
|
|
234
|
-
const cred = store[normalizeUrl(baseUrl)];
|
|
235
|
-
return cred ?? null;
|
|
236
|
-
}
|
|
237
|
-
set(baseUrl, cred) {
|
|
238
|
-
const store = readStore(this.path);
|
|
239
|
-
store[normalizeUrl(baseUrl)] = cred;
|
|
240
|
-
writeStore(this.path, store);
|
|
241
|
-
}
|
|
242
|
-
clear(baseUrl) {
|
|
243
|
-
const store = readStore(this.path);
|
|
244
|
-
const key = normalizeUrl(baseUrl);
|
|
245
|
-
if (!(key in store)) return false;
|
|
246
|
-
delete store[key];
|
|
247
|
-
if (Object.keys(store).length === 0 && existsSync(this.path)) {
|
|
248
|
-
unlinkSync(this.path);
|
|
249
|
-
} else {
|
|
250
|
-
writeStore(this.path, store);
|
|
251
|
-
}
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
getPath() {
|
|
255
|
-
return this.path;
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
function normalizeUrl(url) {
|
|
259
|
-
return url.replace(/\/+$/, "");
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// src/auth/operator.ts
|
|
263
|
-
var OPERATOR_PAT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
264
|
-
var storeSingleton;
|
|
265
|
-
function getCredentialStore() {
|
|
266
|
-
if (!storeSingleton) storeSingleton = new CredentialStore();
|
|
267
|
-
return storeSingleton;
|
|
268
|
-
}
|
|
269
|
-
function loadCachedOperator(baseUrl) {
|
|
270
|
-
let cred;
|
|
271
|
-
try {
|
|
272
|
-
cred = getCredentialStore().get(baseUrl);
|
|
273
|
-
} catch (err) {
|
|
274
|
-
logger.warn("Credential store unreadable, treating as empty", {
|
|
275
|
-
error: err.message
|
|
276
|
-
});
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
if (!cred) return null;
|
|
280
|
-
if (cred.expiresAt && cred.expiresAt < Date.now()) return null;
|
|
281
|
-
return cred;
|
|
282
|
-
}
|
|
283
|
-
function persistOperator(baseUrl, response) {
|
|
284
|
-
const cred = {
|
|
285
|
-
pat: response.pat,
|
|
286
|
-
email: response.email,
|
|
287
|
-
userId: response.userId,
|
|
288
|
-
role: response.role,
|
|
289
|
-
workspaces: response.workspaces,
|
|
290
|
-
expiresAt: response.expiresAt
|
|
291
|
-
};
|
|
292
|
-
try {
|
|
293
|
-
getCredentialStore().set(baseUrl, cred);
|
|
294
|
-
return { cred, persisted: true };
|
|
295
|
-
} catch (err) {
|
|
296
|
-
logger.warn(
|
|
297
|
-
"Could not write operator credential to disk \u2014 using in-memory only for this session",
|
|
298
|
-
{ error: err.message }
|
|
299
|
-
);
|
|
300
|
-
return { cred, persisted: false };
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
function clearOperator(baseUrl) {
|
|
304
|
-
return getCredentialStore().clear(baseUrl);
|
|
305
|
-
}
|
|
187
|
+
// src/version.ts
|
|
188
|
+
var MCP_SERVER_VERSION = "0.2.10";
|
|
306
189
|
|
|
307
190
|
// src/client/databrain-client.ts
|
|
308
|
-
var MCP_SERVER_VERSION = "0.2.0";
|
|
309
191
|
var MCP_USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION}`;
|
|
310
192
|
var MAX_RETRIES = 3;
|
|
311
193
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -325,8 +207,6 @@ var DatabrainClient = class {
|
|
|
325
207
|
}
|
|
326
208
|
cache = new TtlCache();
|
|
327
209
|
runtimeApiToken;
|
|
328
|
-
operator = null;
|
|
329
|
-
operatorLoaded = false;
|
|
330
210
|
hasApiToken() {
|
|
331
211
|
return !!(this.runtimeApiToken || this.config.apiToken);
|
|
332
212
|
}
|
|
@@ -343,85 +223,22 @@ var DatabrainClient = class {
|
|
|
343
223
|
getDemoDomain() {
|
|
344
224
|
return this.config.demoDomain;
|
|
345
225
|
}
|
|
346
|
-
getOperator() {
|
|
347
|
-
if (!this.operatorLoaded) {
|
|
348
|
-
this.operator = loadCachedOperator(this.config.apiUrl);
|
|
349
|
-
this.operatorLoaded = true;
|
|
350
|
-
}
|
|
351
|
-
return this.operator;
|
|
352
|
-
}
|
|
353
|
-
setOperator(cred) {
|
|
354
|
-
this.operator = cred;
|
|
355
|
-
this.operatorLoaded = true;
|
|
356
|
-
this.cache.invalidateByPrefix("");
|
|
357
|
-
}
|
|
358
|
-
clearOperator() {
|
|
359
|
-
this.operator = null;
|
|
360
|
-
this.operatorLoaded = true;
|
|
361
|
-
this.cache.invalidateByPrefix("");
|
|
362
|
-
return clearOperator(this.config.apiUrl);
|
|
363
|
-
}
|
|
364
|
-
async bootstrapOperator(email) {
|
|
365
|
-
if (!this.config.serviceToken) {
|
|
366
|
-
throw new DatabrainApiError(
|
|
367
|
-
401,
|
|
368
|
-
"MISSING_SERVICE_TOKEN",
|
|
369
|
-
"Cannot connect \u2014 DATABRAIN_SERVICE_TOKEN is not configured. Set it in your MCP server config first."
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
const response = await this.request({
|
|
373
|
-
method: "POST",
|
|
374
|
-
path: "/api/mcp/bootstrap",
|
|
375
|
-
body: { email },
|
|
376
|
-
auth: "service",
|
|
377
|
-
// Bootstrap uses the service token explicitly — not the operator PAT.
|
|
378
|
-
token: this.config.serviceToken
|
|
379
|
-
});
|
|
380
|
-
const result = persistOperator(this.config.apiUrl, response);
|
|
381
|
-
this.setOperator(result.cred);
|
|
382
|
-
return result;
|
|
383
|
-
}
|
|
384
226
|
/**
|
|
385
227
|
* Describes which credential is currently used as the bearer for outgoing
|
|
386
228
|
* API calls. Used by `whoami` so the LLM can give the user a truthful answer.
|
|
387
229
|
*/
|
|
388
230
|
getActiveAuth() {
|
|
389
|
-
if (this.config.pat) return { source: "env-pat" };
|
|
390
|
-
const op = this.getOperator();
|
|
391
|
-
if (op?.pat) return { source: "cached-pat", operator: op };
|
|
392
231
|
if (this.config.serviceToken) return { source: "service-token" };
|
|
393
232
|
if (this.runtimeApiToken || this.config.apiToken) return { source: "api-token" };
|
|
394
233
|
return { source: "none" };
|
|
395
234
|
}
|
|
396
235
|
getToken(auth) {
|
|
397
236
|
if (auth === "none") return void 0;
|
|
398
|
-
if (this.config.pat) return this.config.pat;
|
|
399
|
-
const op = this.getOperator();
|
|
400
|
-
if (op?.pat) return op.pat;
|
|
401
237
|
if (auth === "service") return this.config.serviceToken;
|
|
402
238
|
return this.runtimeApiToken || this.config.apiToken;
|
|
403
239
|
}
|
|
404
240
|
async request(options) {
|
|
405
|
-
|
|
406
|
-
return await this.requestInternal(options);
|
|
407
|
-
} catch (err) {
|
|
408
|
-
if (err instanceof DatabrainApiError && err.code === "PAT_EXPIRED" && !options.token && !this.config.pat && options.path !== "/api/mcp/bootstrap") {
|
|
409
|
-
const op = this.getOperator();
|
|
410
|
-
if (op?.email) {
|
|
411
|
-
logger.info("Operator PAT expired \u2014 silently re-bootstrapping");
|
|
412
|
-
try {
|
|
413
|
-
await this.bootstrapOperator(op.email);
|
|
414
|
-
} catch (bootstrapErr) {
|
|
415
|
-
logger.warn("Silent re-bootstrap failed", {
|
|
416
|
-
error: bootstrapErr.message
|
|
417
|
-
});
|
|
418
|
-
throw err;
|
|
419
|
-
}
|
|
420
|
-
return this.requestInternal(options);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
throw err;
|
|
424
|
-
}
|
|
241
|
+
return this.requestInternal(options);
|
|
425
242
|
}
|
|
426
243
|
async requestInternal(options) {
|
|
427
244
|
if (options.invalidateCache) {
|
|
@@ -826,6 +643,69 @@ var QueryApi = class {
|
|
|
826
643
|
throw err;
|
|
827
644
|
}
|
|
828
645
|
}
|
|
646
|
+
async startSemanticLayerGeneration(datamartId) {
|
|
647
|
+
return this.client.request({
|
|
648
|
+
method: "POST",
|
|
649
|
+
path: `/api/schema/datamart/${encodeURIComponent(datamartId)}/semantic-layer/auto-generate`,
|
|
650
|
+
auth: "service",
|
|
651
|
+
invalidateCache: ["semantic_layer_"]
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
async getSemanticLayerGenerationStatus(datamartId) {
|
|
655
|
+
return this.client.request({
|
|
656
|
+
method: "GET",
|
|
657
|
+
path: `/api/schema/datamart/${encodeURIComponent(datamartId)}/semantic-layer/generation-status`,
|
|
658
|
+
auth: "service"
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// src/api/scheduled-reports-api.ts
|
|
664
|
+
function asRecord(value) {
|
|
665
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
666
|
+
return value;
|
|
667
|
+
}
|
|
668
|
+
function normalizeRecords(value) {
|
|
669
|
+
if (Array.isArray(value)) {
|
|
670
|
+
return value.filter((item) => asRecord(item) !== null);
|
|
671
|
+
}
|
|
672
|
+
const record = asRecord(value);
|
|
673
|
+
if (!record) return [];
|
|
674
|
+
const candidates = [
|
|
675
|
+
record.scheduleEmailReports,
|
|
676
|
+
record.reports,
|
|
677
|
+
record.items,
|
|
678
|
+
record.data
|
|
679
|
+
];
|
|
680
|
+
for (const candidate of candidates) {
|
|
681
|
+
if (Array.isArray(candidate)) {
|
|
682
|
+
return candidate.filter((item) => asRecord(item) !== null);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
var ScheduledReportsApi = class {
|
|
688
|
+
constructor(client) {
|
|
689
|
+
this.client = client;
|
|
690
|
+
}
|
|
691
|
+
async listByEmbed(params) {
|
|
692
|
+
const query = {
|
|
693
|
+
embedId: params.embedId,
|
|
694
|
+
clientId: params.clientId
|
|
695
|
+
};
|
|
696
|
+
if (params.userIdentifier) query.userIdentifier = params.userIdentifier;
|
|
697
|
+
if (params.pageNumber !== void 0) query.pageNumber = String(params.pageNumber);
|
|
698
|
+
if (params.isPagination !== void 0) {
|
|
699
|
+
query.isPagination = params.isPagination ? "true" : "false";
|
|
700
|
+
}
|
|
701
|
+
const response = await this.client.request({
|
|
702
|
+
method: "GET",
|
|
703
|
+
path: "/api/v2/data-app/embeds/reports",
|
|
704
|
+
auth: "api",
|
|
705
|
+
query
|
|
706
|
+
});
|
|
707
|
+
return normalizeRecords(response);
|
|
708
|
+
}
|
|
829
709
|
};
|
|
830
710
|
|
|
831
711
|
// src/api/widgets-api.ts
|
|
@@ -965,19 +845,19 @@ var DEFAULT_ACCESS_SETTINGS = {
|
|
|
965
845
|
function isNonEmptyString2(value) {
|
|
966
846
|
return typeof value === "string" && value.trim().length > 0;
|
|
967
847
|
}
|
|
968
|
-
function
|
|
848
|
+
function asRecord2(value) {
|
|
969
849
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
970
850
|
return value;
|
|
971
851
|
}
|
|
972
852
|
function extractAccessSettings(embed) {
|
|
973
|
-
const metadata =
|
|
974
|
-
const metadataConfig =
|
|
975
|
-
const options =
|
|
853
|
+
const metadata = asRecord2(embed.embedMetadata);
|
|
854
|
+
const metadataConfig = asRecord2(metadata?.embedConfig);
|
|
855
|
+
const options = asRecord2(embed.options);
|
|
976
856
|
const candidates = [
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
857
|
+
asRecord2(embed.accessSettings),
|
|
858
|
+
asRecord2(metadata?.accessSettings),
|
|
859
|
+
asRecord2(metadataConfig?.accessSettings),
|
|
860
|
+
asRecord2(options?.accessSettings)
|
|
981
861
|
].filter((c) => !!c);
|
|
982
862
|
for (const candidate of candidates) {
|
|
983
863
|
if (Object.keys(candidate).length > 0) return candidate;
|
|
@@ -985,8 +865,8 @@ function extractAccessSettings(embed) {
|
|
|
985
865
|
return null;
|
|
986
866
|
}
|
|
987
867
|
function extractDatamartName(embed, existingAccess) {
|
|
988
|
-
const metadata =
|
|
989
|
-
const metadataConfig =
|
|
868
|
+
const metadata = asRecord2(embed.embedMetadata);
|
|
869
|
+
const metadataConfig = asRecord2(metadata?.embedConfig);
|
|
990
870
|
const values = [
|
|
991
871
|
existingAccess?.datamartName,
|
|
992
872
|
existingAccess?.dataMartName,
|
|
@@ -1023,7 +903,7 @@ var EmbedService = class {
|
|
|
1023
903
|
return this.embedsApi.listEmbeds(dataAppName);
|
|
1024
904
|
}
|
|
1025
905
|
async createEmbedStrict(body) {
|
|
1026
|
-
const accessSettings =
|
|
906
|
+
const accessSettings = asRecord2(body.accessSettings);
|
|
1027
907
|
if (!accessSettings || Object.keys(accessSettings).length === 0) {
|
|
1028
908
|
throw new Error(
|
|
1029
909
|
"Embed creation requires `accessSettings` for future update compatibility."
|
|
@@ -1111,8 +991,8 @@ Run \`update_embed\` with explicit \`accessSettings.datamartName\`, or recreate
|
|
|
1111
991
|
}
|
|
1112
992
|
async updateEmbed(embedId, updatePatch) {
|
|
1113
993
|
const body = await this.buildUpdateBase(embedId);
|
|
1114
|
-
const baseAccessSettings =
|
|
1115
|
-
const patchAccessSettings =
|
|
994
|
+
const baseAccessSettings = asRecord2(body.accessSettings);
|
|
995
|
+
const patchAccessSettings = asRecord2(updatePatch.accessSettings);
|
|
1116
996
|
for (const [key, value] of Object.entries(updatePatch)) {
|
|
1117
997
|
if (key === "accessSettings") continue;
|
|
1118
998
|
if (value !== void 0) body[key] = value;
|
|
@@ -1172,7 +1052,7 @@ var GuestTokenService = class {
|
|
|
1172
1052
|
|
|
1173
1053
|
// src/services/widget-service.ts
|
|
1174
1054
|
var isNonEmptyString3 = (v) => typeof v === "string" && v.trim().length > 0;
|
|
1175
|
-
var
|
|
1055
|
+
var asRecord3 = (v) => {
|
|
1176
1056
|
if (!v || typeof v !== "object" || Array.isArray(v)) return null;
|
|
1177
1057
|
return v;
|
|
1178
1058
|
};
|
|
@@ -1493,8 +1373,8 @@ Do not guess between the two \u2014 wait for the user to choose.`
|
|
|
1493
1373
|
if (!exact) {
|
|
1494
1374
|
throw new Error(`Embed "${embedId}" not found. Run \`list_embeds\` to verify.`);
|
|
1495
1375
|
}
|
|
1496
|
-
const metadata =
|
|
1497
|
-
const externalDashboard =
|
|
1376
|
+
const metadata = asRecord3(exact.embedMetadata);
|
|
1377
|
+
const externalDashboard = asRecord3(exact.externalDashboard);
|
|
1498
1378
|
const dataAppId = isNonEmptyString3(exact.dataAppId) ? exact.dataAppId : null;
|
|
1499
1379
|
const externalDashboardId = isNonEmptyString3(exact.externalDashboardId) && exact.externalDashboardId || (externalDashboard && isNonEmptyString3(externalDashboard.externalDashboardId) ? externalDashboard.externalDashboardId : null) || (metadata && isNonEmptyString3(metadata.externalDashboardId) ? metadata.externalDashboardId : null);
|
|
1500
1380
|
const dashboardId = isNonEmptyString3(exact.dashboardId) && exact.dashboardId || (metadata && isNonEmptyString3(metadata.dashboardId) ? metadata.dashboardId : null);
|
|
@@ -1544,7 +1424,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
|
|
|
1544
1424
|
guestToken,
|
|
1545
1425
|
templateMetricId
|
|
1546
1426
|
);
|
|
1547
|
-
const metric =
|
|
1427
|
+
const metric = asRecord3(response.externalMetric);
|
|
1548
1428
|
if (!metric) {
|
|
1549
1429
|
throw new Error(
|
|
1550
1430
|
`Template metric "${templateMetricId}" not found or could not be loaded. Response keys: ${Object.keys(
|
|
@@ -1560,7 +1440,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
|
|
|
1560
1440
|
query: metric.query || void 0,
|
|
1561
1441
|
metricQuery: metric.metricQuery || void 0,
|
|
1562
1442
|
description: metric.description || "",
|
|
1563
|
-
chartOptions:
|
|
1443
|
+
chartOptions: asRecord3(metric.chartOptions) || {},
|
|
1564
1444
|
companyId: metric.companyId || void 0,
|
|
1565
1445
|
companyIntegrationId: metric.companyIntegrationId || void 0,
|
|
1566
1446
|
integrationName: metric.integrationName || void 0,
|
|
@@ -1601,6 +1481,7 @@ function createServiceBundle(client) {
|
|
|
1601
1481
|
const datasourcesApi = new DatasourcesApi(client);
|
|
1602
1482
|
const guestTokenApi = new GuestTokenApi(client);
|
|
1603
1483
|
const queryApi = new QueryApi(client);
|
|
1484
|
+
const scheduledReportsApi = new ScheduledReportsApi(client);
|
|
1604
1485
|
const widgetsApi = new WidgetsApi(client);
|
|
1605
1486
|
const workspacesApi = new WorkspacesApi(client);
|
|
1606
1487
|
const discoveryService = new DiscoveryService();
|
|
@@ -1613,6 +1494,7 @@ function createServiceBundle(client) {
|
|
|
1613
1494
|
datasourcesApi,
|
|
1614
1495
|
workspacesApi,
|
|
1615
1496
|
queryApi,
|
|
1497
|
+
scheduledReportsApi,
|
|
1616
1498
|
widgetsApi,
|
|
1617
1499
|
embedService: new EmbedService(embedsApi, datamartsApi),
|
|
1618
1500
|
discoveryService,
|
|
@@ -2220,8 +2102,234 @@ function registerListWorkspaces(server, client) {
|
|
|
2220
2102
|
);
|
|
2221
2103
|
}
|
|
2222
2104
|
|
|
2223
|
-
// src/tools/
|
|
2105
|
+
// src/tools/infrastructure/create-datamart.ts
|
|
2224
2106
|
import { z as z9 } from "zod";
|
|
2107
|
+
|
|
2108
|
+
// src/tools/widgets/production-guards.ts
|
|
2109
|
+
var APPLY_TO_PRODUCTION_CONFIRMATION = "APPLY_TO_PRODUCTION";
|
|
2110
|
+
function validateProductionApply(params) {
|
|
2111
|
+
if (params.confirm !== APPLY_TO_PRODUCTION_CONFIRMATION) {
|
|
2112
|
+
return `Applying to production requires confirm: "${APPLY_TO_PRODUCTION_CONFIRMATION}". Run dry-run first, review the plan, then retry with that confirmation string.`;
|
|
2113
|
+
}
|
|
2114
|
+
return null;
|
|
2115
|
+
}
|
|
2116
|
+
function productionSafetyNote(workspaceName) {
|
|
2117
|
+
return `This workspace is production-backed. Dry-run is the default; apply requires explicit production confirmation. Resolved workspace: "${workspaceName.trim()}".`;
|
|
2118
|
+
}
|
|
2119
|
+
function genericProductionSafetyNote() {
|
|
2120
|
+
return "This operation targets production-backed Databrain resources. Dry-run is the default; apply requires explicit production confirmation.";
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// src/tools/infrastructure/create-datamart.ts
|
|
2124
|
+
var columnSchema = z9.object({
|
|
2125
|
+
name: z9.string(),
|
|
2126
|
+
alias: z9.string().optional(),
|
|
2127
|
+
label: z9.string().optional(),
|
|
2128
|
+
isHide: z9.boolean().optional(),
|
|
2129
|
+
isCustomColumn: z9.boolean().optional(),
|
|
2130
|
+
sql: z9.string().optional()
|
|
2131
|
+
});
|
|
2132
|
+
var tableSchema = z9.object({
|
|
2133
|
+
name: z9.string(),
|
|
2134
|
+
schemaName: z9.string().optional(),
|
|
2135
|
+
clientColumn: z9.string().optional(),
|
|
2136
|
+
label: z9.string().optional(),
|
|
2137
|
+
columns: z9.array(columnSchema)
|
|
2138
|
+
});
|
|
2139
|
+
var relationshipSchema = z9.object({
|
|
2140
|
+
parentTableName: z9.string(),
|
|
2141
|
+
parentColumnName: z9.string(),
|
|
2142
|
+
childTableName: z9.string(),
|
|
2143
|
+
childColumnName: z9.string(),
|
|
2144
|
+
cardinality: z9.enum(["OneToOne", "OneToMany", "ManyToOne", "ManyToMany"]),
|
|
2145
|
+
join: z9.enum(["INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN"]),
|
|
2146
|
+
relationshipName: z9.string()
|
|
2147
|
+
});
|
|
2148
|
+
function datamartSuccessMessage(name, result) {
|
|
2149
|
+
return `Datamart "${name}" created successfully (ID: ${result.id || result._id || "UNKNOWN"}).
|
|
2150
|
+
|
|
2151
|
+
**To query data**, complete the semantic layer first:
|
|
2152
|
+
1. \`get_semantic_layer\` with \`datamartName: "${name}"\` \u2014 check if table descriptions exist
|
|
2153
|
+
2. \`update_semantic_layer\` \u2014 add table/column descriptions and synonyms so the AI can generate accurate SQL
|
|
2154
|
+
3. \`ask_question\` with \`datamartName: "${name}"\` \u2014 ask natural language questions
|
|
2155
|
+
|
|
2156
|
+
**To embed dashboards** (optional):
|
|
2157
|
+
1. Connect this datamart to a workspace outside MCP
|
|
2158
|
+
2. Create a dashboard in the Databrain UI
|
|
2159
|
+
3. Use \`create_embed\` to embed the dashboard`;
|
|
2160
|
+
}
|
|
2161
|
+
function registerCreateDatamart(server, client) {
|
|
2162
|
+
const services = createServiceBundle(client);
|
|
2163
|
+
server.tool(
|
|
2164
|
+
"create_datamart",
|
|
2165
|
+
"Create a production datamart from selected tables and relationships. Defaults to dry-run; apply requires explicit confirmation.",
|
|
2166
|
+
{
|
|
2167
|
+
mode: z9.enum(["dry_run", "apply"]).default("dry_run").describe("dry_run returns the plan only; apply creates the datamart"),
|
|
2168
|
+
confirm: z9.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
|
|
2169
|
+
name: z9.string().describe("Unique datamart name"),
|
|
2170
|
+
datasourceName: z9.string().optional().describe("Name of the datasource to use. Omit to list available datasources."),
|
|
2171
|
+
tableList: z9.array(tableSchema).optional().describe("Array of tables with their columns"),
|
|
2172
|
+
relationships: z9.array(relationshipSchema).optional().describe("Join definitions between tables"),
|
|
2173
|
+
tenancySettings: z9.object({
|
|
2174
|
+
tenancyLevel: z9.enum(["TABLE", "DATABASE"]).optional(),
|
|
2175
|
+
clientColumnType: z9.string().optional(),
|
|
2176
|
+
schemaName: z9.string().optional(),
|
|
2177
|
+
tableName: z9.string().optional(),
|
|
2178
|
+
tableClientNameColumn: z9.string().optional(),
|
|
2179
|
+
tablePrimaryKeyColumn: z9.string().optional()
|
|
2180
|
+
}).optional().describe("Multi-tenant configuration"),
|
|
2181
|
+
body: z9.record(z9.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/datamarts")
|
|
2182
|
+
},
|
|
2183
|
+
WRITE_CREATE,
|
|
2184
|
+
async (params) => {
|
|
2185
|
+
try {
|
|
2186
|
+
const mode = params.mode || "dry_run";
|
|
2187
|
+
if (!params.body) {
|
|
2188
|
+
const datasources = await services.datasourcesApi.listDatasources();
|
|
2189
|
+
const resolution = resolveResource({
|
|
2190
|
+
items: datasources,
|
|
2191
|
+
name: params.datasourceName,
|
|
2192
|
+
getName: (ds) => ds.name,
|
|
2193
|
+
formatItem: formatDatasource,
|
|
2194
|
+
resourceType: "datasource",
|
|
2195
|
+
toolName: "create_datamart"
|
|
2196
|
+
});
|
|
2197
|
+
if (resolution.kind !== "resolved") return resolution.response;
|
|
2198
|
+
if (!params.tableList || params.tableList.length === 0) {
|
|
2199
|
+
return toolError(
|
|
2200
|
+
"tableList is required. Use `discover_datasource_schema` to see available tables first."
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
const body = {
|
|
2204
|
+
name: params.name,
|
|
2205
|
+
datasourceName: resolution.name,
|
|
2206
|
+
tableList: params.tableList,
|
|
2207
|
+
relationships: params.relationships,
|
|
2208
|
+
tenancySettings: params.tenancySettings
|
|
2209
|
+
};
|
|
2210
|
+
if (mode === "dry_run") {
|
|
2211
|
+
return toolSuccess(
|
|
2212
|
+
JSON.stringify(
|
|
2213
|
+
{
|
|
2214
|
+
status: "dry_run_only",
|
|
2215
|
+
safety: genericProductionSafetyNote(),
|
|
2216
|
+
requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
|
|
2217
|
+
body,
|
|
2218
|
+
nextStep: `Review this plan. To execute, call again with mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}".`
|
|
2219
|
+
},
|
|
2220
|
+
null,
|
|
2221
|
+
2
|
|
2222
|
+
)
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
const guardError2 = validateProductionApply({ confirm: params.confirm });
|
|
2226
|
+
if (guardError2) return toolError(guardError2);
|
|
2227
|
+
const result2 = await client.request({
|
|
2228
|
+
method: "POST",
|
|
2229
|
+
path: "/api/v2/data-app/datamarts",
|
|
2230
|
+
body,
|
|
2231
|
+
auth: "service",
|
|
2232
|
+
invalidateCache: ["list_datamarts"]
|
|
2233
|
+
});
|
|
2234
|
+
if (result2.error) {
|
|
2235
|
+
return toolError(`Datamart creation failed: ${stringify(result2.error)}`);
|
|
2236
|
+
}
|
|
2237
|
+
return toolSuccess(datamartSuccessMessage(params.name, result2));
|
|
2238
|
+
}
|
|
2239
|
+
if (mode === "dry_run") {
|
|
2240
|
+
return toolSuccess(
|
|
2241
|
+
JSON.stringify(
|
|
2242
|
+
{
|
|
2243
|
+
status: "dry_run_only",
|
|
2244
|
+
safety: genericProductionSafetyNote(),
|
|
2245
|
+
requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
|
|
2246
|
+
body: params.body,
|
|
2247
|
+
nextStep: `Review this raw datamart body. To execute, call again with mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}".`
|
|
2248
|
+
},
|
|
2249
|
+
null,
|
|
2250
|
+
2
|
|
2251
|
+
)
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
const guardError = validateProductionApply({ confirm: params.confirm });
|
|
2255
|
+
if (guardError) return toolError(guardError);
|
|
2256
|
+
const result = await client.request({
|
|
2257
|
+
method: "POST",
|
|
2258
|
+
path: "/api/v2/data-app/datamarts",
|
|
2259
|
+
body: params.body,
|
|
2260
|
+
auth: "service",
|
|
2261
|
+
invalidateCache: ["list_datamarts"]
|
|
2262
|
+
});
|
|
2263
|
+
if (result.error) {
|
|
2264
|
+
return toolError(`Datamart creation failed: ${stringify(result.error)}`);
|
|
2265
|
+
}
|
|
2266
|
+
return toolSuccess(datamartSuccessMessage(params.name, result));
|
|
2267
|
+
} catch (err) {
|
|
2268
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
2269
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// src/tools/infrastructure/sync-datasource.ts
|
|
2276
|
+
import { z as z10 } from "zod";
|
|
2277
|
+
function registerSyncDatasource(server, client) {
|
|
2278
|
+
const datasourcesApi = new DatasourcesApi(client);
|
|
2279
|
+
server.tool(
|
|
2280
|
+
"sync_datasource",
|
|
2281
|
+
"Trigger a datasource schema sync to refresh table and column metadata. If datasourceName is omitted and there are multiple datasources, returns the list so the user can choose.",
|
|
2282
|
+
{
|
|
2283
|
+
datasourceName: z10.string().optional().describe("Name of the datasource to sync. Omit to list available datasources.")
|
|
2284
|
+
},
|
|
2285
|
+
WRITE_IDEMPOTENT,
|
|
2286
|
+
async (params) => {
|
|
2287
|
+
try {
|
|
2288
|
+
const datasources = await client.request({
|
|
2289
|
+
method: "GET",
|
|
2290
|
+
path: "/api/v2/datasource/",
|
|
2291
|
+
auth: "service",
|
|
2292
|
+
cacheKey: "list_datasources",
|
|
2293
|
+
cacheTtlMs: 10 * 60 * 1e3
|
|
2294
|
+
});
|
|
2295
|
+
const resolution = resolveResource({
|
|
2296
|
+
items: datasources,
|
|
2297
|
+
name: params.datasourceName,
|
|
2298
|
+
getName: (ds) => ds.name,
|
|
2299
|
+
formatItem: formatDatasource,
|
|
2300
|
+
resourceType: "datasource",
|
|
2301
|
+
toolName: "sync_datasource"
|
|
2302
|
+
});
|
|
2303
|
+
if (resolution.kind !== "resolved") return resolution.response;
|
|
2304
|
+
const { selected } = resolution;
|
|
2305
|
+
const datasourceId = selected._id || selected.id || (selected.name ? await datasourcesApi.resolveIdByName(selected.name) : null);
|
|
2306
|
+
if (!datasourceId) {
|
|
2307
|
+
return toolError(`Datasource "${selected.name}" found but has no ID. This is unexpected.`);
|
|
2308
|
+
}
|
|
2309
|
+
const result = await client.request({
|
|
2310
|
+
method: "POST",
|
|
2311
|
+
path: "/api/v2/datasource/sync",
|
|
2312
|
+
body: { input: { datasourceId } },
|
|
2313
|
+
auth: "service",
|
|
2314
|
+
invalidateCache: [`datasource_schema_${selected.name}`]
|
|
2315
|
+
});
|
|
2316
|
+
const message = result?.data?.message || "Sync completed";
|
|
2317
|
+
return toolSuccess(
|
|
2318
|
+
`Datasource "${selected.name}" sync triggered (ID: ${datasourceId}).
|
|
2319
|
+
Status: ${message}
|
|
2320
|
+
|
|
2321
|
+
The schema metadata will be refreshed. You can now use \`discover_datasource_schema\` or \`create_datamart\` to select tables.`
|
|
2322
|
+
);
|
|
2323
|
+
} catch (err) {
|
|
2324
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
2325
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// src/tools/embed/create-embed.ts
|
|
2332
|
+
import { z as z11 } from "zod";
|
|
2225
2333
|
var DEFAULT_ACCESS_SETTINGS2 = {
|
|
2226
2334
|
isAllowMetricCreation: false,
|
|
2227
2335
|
isAllowMetricDeletion: false,
|
|
@@ -2234,18 +2342,18 @@ var DEFAULT_ACCESS_SETTINGS2 = {
|
|
|
2234
2342
|
isAllowCreateDashboardView: false,
|
|
2235
2343
|
metricCreationMode: "DRAG_DROP"
|
|
2236
2344
|
};
|
|
2237
|
-
var accessSettingsSchema =
|
|
2238
|
-
datamartName:
|
|
2239
|
-
isAllowMetricCreation:
|
|
2240
|
-
metricCreationMode:
|
|
2241
|
-
isAllowMetricDeletion:
|
|
2242
|
-
isAllowMetricUpdate:
|
|
2243
|
-
isAllowMetricLayoutChange:
|
|
2244
|
-
isAllowManageMetrics:
|
|
2245
|
-
isAllowUnderlyingData:
|
|
2246
|
-
isAllowAiPilot:
|
|
2247
|
-
isAllowEmailReports:
|
|
2248
|
-
isAllowCreateDashboardView:
|
|
2345
|
+
var accessSettingsSchema = z11.object({
|
|
2346
|
+
datamartName: z11.string().describe("Name of the datamart (required)"),
|
|
2347
|
+
isAllowMetricCreation: z11.boolean().optional().describe("Allow end-users to create metrics"),
|
|
2348
|
+
metricCreationMode: z11.enum(["DRAG_DROP", "CHAT"]).optional().describe("Metric creation mode"),
|
|
2349
|
+
isAllowMetricDeletion: z11.boolean().optional().describe("Allow metric deletion"),
|
|
2350
|
+
isAllowMetricUpdate: z11.boolean().optional().describe("Allow metric editing"),
|
|
2351
|
+
isAllowMetricLayoutChange: z11.boolean().optional().describe("Allow layout changes"),
|
|
2352
|
+
isAllowManageMetrics: z11.boolean().optional().describe("Show manage metrics UI"),
|
|
2353
|
+
isAllowUnderlyingData: z11.boolean().optional().describe("Allow viewing underlying data"),
|
|
2354
|
+
isAllowAiPilot: z11.boolean().optional().describe("Enable AI pilot features"),
|
|
2355
|
+
isAllowEmailReports: z11.boolean().optional().describe("Allow email report scheduling"),
|
|
2356
|
+
isAllowCreateDashboardView: z11.boolean().optional().describe("Allow creating dashboard views")
|
|
2249
2357
|
}).describe("Embed access settings. Only datamartName is required \u2014 all flags default to false.");
|
|
2250
2358
|
function registerCreateEmbed(server, client) {
|
|
2251
2359
|
const services = createServiceBundle(client);
|
|
@@ -2253,23 +2361,23 @@ function registerCreateEmbed(server, client) {
|
|
|
2253
2361
|
"create_embed",
|
|
2254
2362
|
"Create an embed. Provide dashboardId, workspaceName, accessSettings.datamartName. Add clientId for multi-tenant per-client dashboards. All isAllow* flags default to false.",
|
|
2255
2363
|
{
|
|
2256
|
-
body:
|
|
2364
|
+
body: z11.record(z11.unknown()).optional().describe(
|
|
2257
2365
|
"Raw API body passthrough. If provided, all other fields are ignored."
|
|
2258
2366
|
),
|
|
2259
|
-
dashboardId:
|
|
2260
|
-
workspaceName:
|
|
2261
|
-
embedType:
|
|
2262
|
-
metricId:
|
|
2263
|
-
name:
|
|
2367
|
+
dashboardId: z11.string().optional().describe("Dashboard identifier for embed creation"),
|
|
2368
|
+
workspaceName: z11.string().optional().describe("Workspace name. Omit to auto-resolve if only one exists."),
|
|
2369
|
+
embedType: z11.enum(["dashboard", "metric"]).optional().describe("Embed type"),
|
|
2370
|
+
metricId: z11.string().optional().describe("Required for metric embed type"),
|
|
2371
|
+
name: z11.string().optional().describe("Human-readable embed config name"),
|
|
2264
2372
|
accessSettings: accessSettingsSchema.optional(),
|
|
2265
2373
|
// Multi-tenant (dashboard embed) params
|
|
2266
|
-
clientId:
|
|
2374
|
+
clientId: z11.string().optional().describe(
|
|
2267
2375
|
"Client identifier for multi-tenant isolation. When provided, creates a dashboard embed (each client gets their own dashboard copy)."
|
|
2268
2376
|
),
|
|
2269
|
-
templateDashboardId:
|
|
2270
|
-
isImportMetrics:
|
|
2271
|
-
metricImportMode:
|
|
2272
|
-
isRenameDashboard:
|
|
2377
|
+
templateDashboardId: z11.string().optional().describe("Template dashboard ID to clone metrics from"),
|
|
2378
|
+
isImportMetrics: z11.boolean().optional().describe("Import metrics from template dashboard"),
|
|
2379
|
+
metricImportMode: z11.enum(["COPY", "LINK"]).optional().describe("COPY duplicates metrics, LINK shares them"),
|
|
2380
|
+
isRenameDashboard: z11.boolean().optional().describe("Rename the dashboard for this client")
|
|
2273
2381
|
},
|
|
2274
2382
|
WRITE_CREATE,
|
|
2275
2383
|
async (params) => {
|
|
@@ -2371,14 +2479,14 @@ Next: run \`get_embed_details\` to verify the embed configuration.`
|
|
|
2371
2479
|
}
|
|
2372
2480
|
|
|
2373
2481
|
// src/tools/embed/list-embeds.ts
|
|
2374
|
-
import { z as
|
|
2482
|
+
import { z as z12 } from "zod";
|
|
2375
2483
|
function registerListEmbeds(server, client) {
|
|
2376
2484
|
const services = createServiceBundle(client);
|
|
2377
2485
|
server.tool(
|
|
2378
2486
|
"list_embeds",
|
|
2379
2487
|
"List embed configurations. Requires an API token (use create_api_token first). Response includes `distinctClientIds` \u2014 a deduped list of end-customer client IDs that have multi-tenant dashboard embeds, useful for discovering which customers exist when only the workspace operator persona knows them.",
|
|
2380
2488
|
{
|
|
2381
|
-
dataAppName:
|
|
2489
|
+
dataAppName: z12.string().optional().describe("Filter by data app name")
|
|
2382
2490
|
},
|
|
2383
2491
|
READ_ONLY,
|
|
2384
2492
|
async (params) => {
|
|
@@ -2421,14 +2529,14 @@ function registerListEmbeds(server, client) {
|
|
|
2421
2529
|
}
|
|
2422
2530
|
|
|
2423
2531
|
// src/tools/embed/get-embed-details.ts
|
|
2424
|
-
import { z as
|
|
2532
|
+
import { z as z13 } from "zod";
|
|
2425
2533
|
function registerGetEmbedDetails(server, client) {
|
|
2426
2534
|
const services = createServiceBundle(client);
|
|
2427
2535
|
server.tool(
|
|
2428
2536
|
"get_embed_details",
|
|
2429
2537
|
"Get full embed configuration including access settings and theme.",
|
|
2430
2538
|
{
|
|
2431
|
-
embedId:
|
|
2539
|
+
embedId: z13.string().describe("ID of the embed")
|
|
2432
2540
|
},
|
|
2433
2541
|
READ_ONLY,
|
|
2434
2542
|
async (params) => {
|
|
@@ -2444,7 +2552,7 @@ function registerGetEmbedDetails(server, client) {
|
|
|
2444
2552
|
}
|
|
2445
2553
|
|
|
2446
2554
|
// src/tools/embed/update-embed.ts
|
|
2447
|
-
import { z as
|
|
2555
|
+
import { z as z14 } from "zod";
|
|
2448
2556
|
|
|
2449
2557
|
// src/tools/embed/helpers.ts
|
|
2450
2558
|
var GUEST_TOKEN_REMINDER = "\n\n\u26A0\uFE0F **Guest tokens must be regenerated** for these changes to take effect. Existing guest tokens still carry the old settings. Call `generate_guest_token` to create a new token with the updated configuration.";
|
|
@@ -2482,15 +2590,15 @@ function registerUpdateEmbed(server, client) {
|
|
|
2482
2590
|
"update_embed",
|
|
2483
2591
|
"Update embed access settings, permissions, theme, or name.",
|
|
2484
2592
|
{
|
|
2485
|
-
embedId:
|
|
2486
|
-
body:
|
|
2593
|
+
embedId: z14.string().describe("ID of the embed to update"),
|
|
2594
|
+
body: z14.record(z14.unknown()).optional().describe(
|
|
2487
2595
|
"Raw API body for PUT /api/v2/data-app/embeds. If provided, it is sent directly (passthrough mode)."
|
|
2488
2596
|
),
|
|
2489
|
-
name:
|
|
2490
|
-
accessSettings:
|
|
2597
|
+
name: z14.string().optional().describe("New name for the embed"),
|
|
2598
|
+
accessSettings: z14.record(z14.unknown()).optional().describe(
|
|
2491
2599
|
"Partial update of access settings. See `get_embed_details` for current settings."
|
|
2492
2600
|
),
|
|
2493
|
-
theme:
|
|
2601
|
+
theme: z14.record(z14.unknown()).optional().describe(
|
|
2494
2602
|
"Theme configuration object. See databrain://theme-reference for the full schema."
|
|
2495
2603
|
)
|
|
2496
2604
|
},
|
|
@@ -2536,7 +2644,7 @@ function registerUpdateEmbed(server, client) {
|
|
|
2536
2644
|
}
|
|
2537
2645
|
|
|
2538
2646
|
// src/tools/embed/customize-theme.ts
|
|
2539
|
-
import { z as
|
|
2647
|
+
import { z as z15 } from "zod";
|
|
2540
2648
|
var THEME_PRESETS = {
|
|
2541
2649
|
light: {
|
|
2542
2650
|
general: {
|
|
@@ -2670,7 +2778,7 @@ var ACCESS_PRESETS = {
|
|
|
2670
2778
|
metricCreationMode: "CHAT"
|
|
2671
2779
|
}
|
|
2672
2780
|
};
|
|
2673
|
-
var fontFamilyEnum =
|
|
2781
|
+
var fontFamilyEnum = z15.enum([
|
|
2674
2782
|
"Inter",
|
|
2675
2783
|
"Roboto",
|
|
2676
2784
|
"Open Sans",
|
|
@@ -2682,17 +2790,17 @@ var fontFamilyEnum = z13.enum([
|
|
|
2682
2790
|
"Nunito",
|
|
2683
2791
|
"system-ui"
|
|
2684
2792
|
]).describe("Font family");
|
|
2685
|
-
var tooltipFontSchema =
|
|
2686
|
-
fontFamily:
|
|
2687
|
-
fontSize:
|
|
2688
|
-
fontWeight:
|
|
2689
|
-
fontColor:
|
|
2793
|
+
var tooltipFontSchema = z15.object({
|
|
2794
|
+
fontFamily: z15.string().optional(),
|
|
2795
|
+
fontSize: z15.number().optional(),
|
|
2796
|
+
fontWeight: z15.number().optional(),
|
|
2797
|
+
fontColor: z15.string().optional()
|
|
2690
2798
|
}).optional();
|
|
2691
|
-
var fontConfigInputSchema =
|
|
2692
|
-
fontSize:
|
|
2693
|
-
fontFamily:
|
|
2694
|
-
fontWeight:
|
|
2695
|
-
color:
|
|
2799
|
+
var fontConfigInputSchema = z15.object({
|
|
2800
|
+
fontSize: z15.number().optional(),
|
|
2801
|
+
fontFamily: z15.string().optional(),
|
|
2802
|
+
fontWeight: z15.number().optional(),
|
|
2803
|
+
color: z15.string().optional()
|
|
2696
2804
|
}).optional();
|
|
2697
2805
|
function mapToFrontendFontConfig(input) {
|
|
2698
2806
|
if (!input) return void 0;
|
|
@@ -2703,23 +2811,23 @@ function mapToFrontendFontConfig(input) {
|
|
|
2703
2811
|
...input.color && { color: input.color }
|
|
2704
2812
|
};
|
|
2705
2813
|
}
|
|
2706
|
-
var axisSchema =
|
|
2707
|
-
hideAxisLines:
|
|
2708
|
-
hideSplitLines:
|
|
2709
|
-
hideAxisLabels:
|
|
2710
|
-
hideAxisTicks:
|
|
2711
|
-
axisName:
|
|
2712
|
-
axisNameOffset:
|
|
2713
|
-
axisLabelMargin:
|
|
2714
|
-
fontSize:
|
|
2715
|
-
fontFamily:
|
|
2716
|
-
fontWeight:
|
|
2717
|
-
color:
|
|
2718
|
-
axisColor:
|
|
2719
|
-
axisNameFontConfig:
|
|
2720
|
-
fontFamily:
|
|
2721
|
-
fontSize:
|
|
2722
|
-
fontWeight:
|
|
2814
|
+
var axisSchema = z15.object({
|
|
2815
|
+
hideAxisLines: z15.boolean().optional().describe("Hide axis lines"),
|
|
2816
|
+
hideSplitLines: z15.boolean().optional().describe("Hide grid/split lines"),
|
|
2817
|
+
hideAxisLabels: z15.boolean().optional().describe("Hide axis labels"),
|
|
2818
|
+
hideAxisTicks: z15.boolean().optional().describe("Hide axis tick marks"),
|
|
2819
|
+
axisName: z15.string().optional().describe("Axis title text"),
|
|
2820
|
+
axisNameOffset: z15.number().optional().describe("Axis title offset from axis"),
|
|
2821
|
+
axisLabelMargin: z15.number().optional().describe("Margin between axis labels and chart area in pixels"),
|
|
2822
|
+
fontSize: z15.number().optional().describe("Axis label font size"),
|
|
2823
|
+
fontFamily: z15.string().optional().describe("Axis label font family"),
|
|
2824
|
+
fontWeight: z15.number().optional().describe("Axis label font weight"),
|
|
2825
|
+
color: z15.string().optional().describe("Axis label color (hex)"),
|
|
2826
|
+
axisColor: z15.string().optional().describe("Axis line color (hex)"),
|
|
2827
|
+
axisNameFontConfig: z15.object({
|
|
2828
|
+
fontFamily: z15.string().optional(),
|
|
2829
|
+
fontSize: z15.number().optional(),
|
|
2830
|
+
fontWeight: z15.number().optional()
|
|
2723
2831
|
}).optional().describe("Axis title font configuration")
|
|
2724
2832
|
}).optional();
|
|
2725
2833
|
function buildAdminTheme(p, presetBase) {
|
|
@@ -2778,37 +2886,37 @@ function buildAdminTheme(p, presetBase) {
|
|
|
2778
2886
|
function buildComponentTheme(p) {
|
|
2779
2887
|
const componentTheme = {};
|
|
2780
2888
|
if (p.buttonPrimaryColor || p.buttonPrimaryTextColor || p.buttonSecondaryColor || p.buttonSecondaryTextColor) {
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2889
|
+
const button = {};
|
|
2890
|
+
if (p.buttonPrimaryColor) button.primary = p.buttonPrimaryColor;
|
|
2891
|
+
if (p.buttonPrimaryTextColor) button.primaryText = p.buttonPrimaryTextColor;
|
|
2892
|
+
if (p.buttonSecondaryColor) button.secondary = p.buttonSecondaryColor;
|
|
2893
|
+
if (p.buttonSecondaryTextColor) button.secondaryText = p.buttonSecondaryTextColor;
|
|
2894
|
+
componentTheme.button = button;
|
|
2787
2895
|
}
|
|
2788
2896
|
if (p.checkboxCheckedColor || p.checkboxUncheckedColor) {
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2897
|
+
const checkbox = {};
|
|
2898
|
+
if (p.checkboxCheckedColor) checkbox.checked = p.checkboxCheckedColor;
|
|
2899
|
+
if (p.checkboxUncheckedColor) checkbox.unChecked = p.checkboxUncheckedColor;
|
|
2900
|
+
componentTheme.checkbox = checkbox;
|
|
2793
2901
|
}
|
|
2794
2902
|
if (p.switchEnabledColor || p.switchDisabledColor) {
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2903
|
+
const switchTheme = {};
|
|
2904
|
+
if (p.switchEnabledColor) switchTheme.enabled = p.switchEnabledColor;
|
|
2905
|
+
if (p.switchDisabledColor) switchTheme.disabled = p.switchDisabledColor;
|
|
2906
|
+
componentTheme.switch = switchTheme;
|
|
2799
2907
|
}
|
|
2800
2908
|
if (p.breadcrumbFontFamily || p.breadcrumbFontColor || p.breadcrumbActiveColor) {
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2909
|
+
const drillBreadCrumbs = {};
|
|
2910
|
+
if (p.breadcrumbFontFamily) drillBreadCrumbs.fontFamily = p.breadcrumbFontFamily;
|
|
2911
|
+
if (p.breadcrumbFontColor) drillBreadCrumbs.fontColor = p.breadcrumbFontColor;
|
|
2912
|
+
if (p.breadcrumbActiveColor) drillBreadCrumbs.activeColor = p.breadcrumbActiveColor;
|
|
2913
|
+
componentTheme.drillBreadCrumbs = drillBreadCrumbs;
|
|
2806
2914
|
}
|
|
2807
2915
|
if (p.filterBadgeColor || p.filterBadgeTextColor) {
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2916
|
+
const multiSelectFilterDropdown = {};
|
|
2917
|
+
if (p.filterBadgeColor) multiSelectFilterDropdown.badgeColor = p.filterBadgeColor;
|
|
2918
|
+
if (p.filterBadgeTextColor) multiSelectFilterDropdown.badgeTextColor = p.filterBadgeTextColor;
|
|
2919
|
+
componentTheme.multiSelectFilterDropdown = multiSelectFilterDropdown;
|
|
2812
2920
|
}
|
|
2813
2921
|
if (p.datePickerColor) componentTheme.datePickerColor = p.datePickerColor;
|
|
2814
2922
|
const breakpoint = {};
|
|
@@ -2875,94 +2983,94 @@ function registerCustomizeTheme(server, client) {
|
|
|
2875
2983
|
"customize_embed_theme",
|
|
2876
2984
|
`Theme an embed: presets (${Object.keys(THEME_PRESETS).join("/")}), colors, fonts, cards, chart appearance, responsive layout, or access presets (${Object.keys(ACCESS_PRESETS).join("/")}).`,
|
|
2877
2985
|
{
|
|
2878
|
-
embedId:
|
|
2879
|
-
body:
|
|
2986
|
+
embedId: z15.string().describe("ID of the embed to theme"),
|
|
2987
|
+
body: z15.record(z15.unknown()).optional().describe(
|
|
2880
2988
|
"Raw API body for PUT /api/v2/data-app/embeds. If provided, sent directly and all convenience fields are ignored."
|
|
2881
2989
|
),
|
|
2882
2990
|
// --- Presets ---
|
|
2883
|
-
themePreset:
|
|
2884
|
-
accessPreset:
|
|
2991
|
+
themePreset: z15.enum(["light", "dark", "corporate", "minimal"]).optional().describe("Apply a theme preset as the base. Granular params below override preset values."),
|
|
2992
|
+
accessPreset: z15.enum(["view-only", "power-user", "export-only", "ai-enabled"]).optional().describe("Apply an access settings preset"),
|
|
2885
2993
|
// --- General ---
|
|
2886
2994
|
fontFamily: fontFamilyEnum.optional().describe("Global font family"),
|
|
2887
|
-
datePickerFormat:
|
|
2995
|
+
datePickerFormat: z15.string().optional().describe("Date picker display format"),
|
|
2888
2996
|
// --- Colors ---
|
|
2889
|
-
primaryColor:
|
|
2890
|
-
backgroundColor:
|
|
2891
|
-
textColor:
|
|
2892
|
-
ctaColor:
|
|
2893
|
-
ctaTextColor:
|
|
2997
|
+
primaryColor: z15.string().optional().describe("Primary accent color (hex)"),
|
|
2998
|
+
backgroundColor: z15.string().optional().describe("Overall background color (hex)"),
|
|
2999
|
+
textColor: z15.string().optional().describe("Default text color (hex)"),
|
|
3000
|
+
ctaColor: z15.string().optional().describe("Call-to-action button color (hex)"),
|
|
3001
|
+
ctaTextColor: z15.string().optional().describe("CTA button text color (hex)"),
|
|
2894
3002
|
// --- Dashboard ---
|
|
2895
|
-
dashboardBackgroundColor:
|
|
2896
|
-
selectBoxSize:
|
|
2897
|
-
selectBoxVariant:
|
|
2898
|
-
selectBoxBorderRadius:
|
|
2899
|
-
selectBoxTextColor:
|
|
2900
|
-
metricCardColor:
|
|
3003
|
+
dashboardBackgroundColor: z15.string().optional().describe("Dashboard container background (hex)"),
|
|
3004
|
+
selectBoxSize: z15.enum(["small", "medium", "large"]).optional().describe("Filter select box size"),
|
|
3005
|
+
selectBoxVariant: z15.enum(["floating", "outlined", "standard"]).optional().describe("Filter select box style"),
|
|
3006
|
+
selectBoxBorderRadius: z15.string().optional().describe('Select box border radius (e.g. "20px")'),
|
|
3007
|
+
selectBoxTextColor: z15.string().optional().describe("Select box text color (hex)"),
|
|
3008
|
+
metricCardColor: z15.string().optional().describe("Metric card background color (hex)"),
|
|
2901
3009
|
// --- Card Title ---
|
|
2902
|
-
cardTitleFontSize:
|
|
2903
|
-
cardTitleFontWeight:
|
|
2904
|
-
cardTitleColor:
|
|
3010
|
+
cardTitleFontSize: z15.string().optional().describe('Card title font size (e.g. "16px")'),
|
|
3011
|
+
cardTitleFontWeight: z15.string().optional().describe('Card title font weight (e.g. "600")'),
|
|
3012
|
+
cardTitleColor: z15.string().optional().describe("Card title text color (hex)"),
|
|
2905
3013
|
// --- Card Description ---
|
|
2906
|
-
cardDescriptionFontSize:
|
|
2907
|
-
cardDescriptionFontWeight:
|
|
2908
|
-
cardDescriptionColor:
|
|
3014
|
+
cardDescriptionFontSize: z15.string().optional().describe("Card description font size"),
|
|
3015
|
+
cardDescriptionFontWeight: z15.string().optional().describe("Card description font weight"),
|
|
3016
|
+
cardDescriptionColor: z15.string().optional().describe("Card description text color (hex)"),
|
|
2909
3017
|
// --- Card Customization ---
|
|
2910
|
-
cardPadding:
|
|
2911
|
-
cardBorderRadius:
|
|
2912
|
-
cardShadow:
|
|
2913
|
-
disableShadowOnHover:
|
|
2914
|
-
disableCardStroke:
|
|
2915
|
-
cardStrokeColor:
|
|
3018
|
+
cardPadding: z15.string().optional().describe("Card internal padding"),
|
|
3019
|
+
cardBorderRadius: z15.string().optional().describe("Card border radius"),
|
|
3020
|
+
cardShadow: z15.string().optional().describe("Card box-shadow CSS value"),
|
|
3021
|
+
disableShadowOnHover: z15.boolean().optional().describe("Disable shadow on card hover"),
|
|
3022
|
+
disableCardStroke: z15.boolean().optional().describe("Remove card border stroke"),
|
|
3023
|
+
cardStrokeColor: z15.string().optional().describe("Card border stroke color (hex)"),
|
|
2916
3024
|
// --- Chart Colors (admin theme level) ---
|
|
2917
|
-
chartFontFamily:
|
|
2918
|
-
chartFontColor:
|
|
2919
|
-
chartColors:
|
|
2920
|
-
chartPaletteName:
|
|
2921
|
-
chartPaletteColors:
|
|
3025
|
+
chartFontFamily: z15.string().optional().describe("Font family for chart text"),
|
|
3026
|
+
chartFontColor: z15.string().optional().describe("Font color for chart text (hex)"),
|
|
3027
|
+
chartColors: z15.array(z15.string()).optional().describe("Array of chart series colors (hex)"),
|
|
3028
|
+
chartPaletteName: z15.string().optional().describe("Name for a custom palette"),
|
|
3029
|
+
chartPaletteColors: z15.array(z15.string()).optional().describe("9 colors for a custom palette"),
|
|
2922
3030
|
// --- Chart Tooltip (admin theme level) ---
|
|
2923
3031
|
tooltipHeader: tooltipFontSchema.describe("Tooltip header font config"),
|
|
2924
3032
|
tooltipLabel: tooltipFontSchema.describe("Tooltip label font config"),
|
|
2925
3033
|
tooltipValue: tooltipFontSchema.describe("Tooltip value font config"),
|
|
2926
3034
|
// --- Component Theme ---
|
|
2927
|
-
buttonPrimaryColor:
|
|
2928
|
-
buttonPrimaryTextColor:
|
|
2929
|
-
buttonSecondaryColor:
|
|
2930
|
-
buttonSecondaryTextColor:
|
|
2931
|
-
checkboxCheckedColor:
|
|
2932
|
-
checkboxUncheckedColor:
|
|
2933
|
-
switchEnabledColor:
|
|
2934
|
-
switchDisabledColor:
|
|
2935
|
-
breadcrumbFontFamily:
|
|
2936
|
-
breadcrumbFontColor:
|
|
2937
|
-
breadcrumbActiveColor:
|
|
2938
|
-
filterBadgeColor:
|
|
2939
|
-
filterBadgeTextColor:
|
|
2940
|
-
datePickerColor:
|
|
3035
|
+
buttonPrimaryColor: z15.string().optional().describe("Primary button background (hex)"),
|
|
3036
|
+
buttonPrimaryTextColor: z15.string().optional().describe("Primary button text (hex)"),
|
|
3037
|
+
buttonSecondaryColor: z15.string().optional().describe("Secondary button background (hex)"),
|
|
3038
|
+
buttonSecondaryTextColor: z15.string().optional().describe("Secondary button text (hex)"),
|
|
3039
|
+
checkboxCheckedColor: z15.string().optional().describe("Checked checkbox color (hex)"),
|
|
3040
|
+
checkboxUncheckedColor: z15.string().optional().describe("Unchecked checkbox color (hex)"),
|
|
3041
|
+
switchEnabledColor: z15.string().optional().describe("Enabled switch color (hex)"),
|
|
3042
|
+
switchDisabledColor: z15.string().optional().describe("Disabled switch color (hex)"),
|
|
3043
|
+
breadcrumbFontFamily: z15.string().optional().describe("Drill breadcrumb font family"),
|
|
3044
|
+
breadcrumbFontColor: z15.string().optional().describe("Drill breadcrumb font color (hex)"),
|
|
3045
|
+
breadcrumbActiveColor: z15.string().optional().describe("Active breadcrumb color (hex)"),
|
|
3046
|
+
filterBadgeColor: z15.string().optional().describe("Multi-select filter badge color (hex)"),
|
|
3047
|
+
filterBadgeTextColor: z15.string().optional().describe("Filter badge text color (hex)"),
|
|
3048
|
+
datePickerColor: z15.string().optional().describe("Date picker accent color (hex)"),
|
|
2941
3049
|
// --- Responsive Breakpoints ---
|
|
2942
|
-
breakpointXl:
|
|
2943
|
-
breakpointLg:
|
|
2944
|
-
breakpointMd:
|
|
2945
|
-
breakpointSm:
|
|
2946
|
-
breakpointXs:
|
|
3050
|
+
breakpointXl: z15.number().optional().describe("XL breakpoint width in px"),
|
|
3051
|
+
breakpointLg: z15.number().optional().describe("LG breakpoint width in px"),
|
|
3052
|
+
breakpointMd: z15.number().optional().describe("MD breakpoint width in px"),
|
|
3053
|
+
breakpointSm: z15.number().optional().describe("SM breakpoint width in px"),
|
|
3054
|
+
breakpointXs: z15.number().optional().describe("XS breakpoint width in px"),
|
|
2947
3055
|
// --- Metric Layout Columns ---
|
|
2948
|
-
layoutColsXl:
|
|
2949
|
-
layoutColsLg:
|
|
2950
|
-
layoutColsMd:
|
|
2951
|
-
layoutColsSm:
|
|
2952
|
-
layoutColsXs:
|
|
2953
|
-
layoutColsXxs:
|
|
3056
|
+
layoutColsXl: z15.number().optional().describe("Grid columns at XL breakpoint"),
|
|
3057
|
+
layoutColsLg: z15.number().optional().describe("Grid columns at LG breakpoint"),
|
|
3058
|
+
layoutColsMd: z15.number().optional().describe("Grid columns at MD breakpoint"),
|
|
3059
|
+
layoutColsSm: z15.number().optional().describe("Grid columns at SM breakpoint"),
|
|
3060
|
+
layoutColsXs: z15.number().optional().describe("Grid columns at XS breakpoint"),
|
|
3061
|
+
layoutColsXxs: z15.number().optional().describe("Grid columns at XXS breakpoint"),
|
|
2954
3062
|
// --- Chart Appearance (detailed) ---
|
|
2955
3063
|
tooltipHeaderStyle: fontConfigInputSchema.describe("Tooltip header: fontSize, fontFamily, fontWeight, color"),
|
|
2956
3064
|
tooltipLabelStyle: fontConfigInputSchema.describe("Tooltip label: fontSize, fontFamily, fontWeight, color"),
|
|
2957
3065
|
tooltipValueStyle: fontConfigInputSchema.describe("Tooltip value: fontSize, fontFamily, fontWeight, color"),
|
|
2958
|
-
labelPosition:
|
|
2959
|
-
radialLabelPosition:
|
|
2960
|
-
marginTop:
|
|
2961
|
-
marginLeft:
|
|
2962
|
-
marginRight:
|
|
2963
|
-
marginBottom:
|
|
2964
|
-
legendShow:
|
|
2965
|
-
legendPosition:
|
|
3066
|
+
labelPosition: z15.enum(["hidden", "top", "left", "right", "bottom", "inside"]).optional().describe("Data label position on charts"),
|
|
3067
|
+
radialLabelPosition: z15.enum(["outside", "inside"]).optional().describe("Label position for radial/pie charts"),
|
|
3068
|
+
marginTop: z15.number().optional().describe("Chart top margin in pixels"),
|
|
3069
|
+
marginLeft: z15.number().optional().describe("Chart left margin in pixels"),
|
|
3070
|
+
marginRight: z15.number().optional().describe("Chart right margin in pixels"),
|
|
3071
|
+
marginBottom: z15.number().optional().describe("Chart bottom margin in pixels"),
|
|
3072
|
+
legendShow: z15.boolean().optional().describe("Show or hide chart legend"),
|
|
3073
|
+
legendPosition: z15.enum([
|
|
2966
3074
|
"top-left",
|
|
2967
3075
|
"top-center",
|
|
2968
3076
|
"top-right",
|
|
@@ -2972,17 +3080,17 @@ function registerCustomizeTheme(server, client) {
|
|
|
2972
3080
|
"bottom-center",
|
|
2973
3081
|
"bottom-right"
|
|
2974
3082
|
]).optional().describe("Legend position"),
|
|
2975
|
-
legendLayout:
|
|
2976
|
-
legendShape:
|
|
2977
|
-
legendFontSize:
|
|
2978
|
-
legendFontWeight:
|
|
2979
|
-
legendFontFamily:
|
|
2980
|
-
legendColor:
|
|
2981
|
-
legendTruncateLength:
|
|
2982
|
-
disableLegendScrolling:
|
|
2983
|
-
enableVariableLegendPosition:
|
|
2984
|
-
legendTop:
|
|
2985
|
-
legendLeft:
|
|
3083
|
+
legendLayout: z15.enum(["horizontal", "vertical"]).optional().describe("Legend layout direction"),
|
|
3084
|
+
legendShape: z15.enum(["circle", "rect", "roundRect", "triangle", "diamond", "arrow", "none"]).optional().describe("Legend icon shape"),
|
|
3085
|
+
legendFontSize: z15.number().optional().describe("Legend font size"),
|
|
3086
|
+
legendFontWeight: z15.number().optional().describe("Legend font weight"),
|
|
3087
|
+
legendFontFamily: z15.string().optional().describe("Legend font family"),
|
|
3088
|
+
legendColor: z15.string().optional().describe("Legend text color (hex)"),
|
|
3089
|
+
legendTruncateLength: z15.number().optional().describe("Max characters before truncating legend"),
|
|
3090
|
+
disableLegendScrolling: z15.boolean().optional().describe("Disable scrolling in legend"),
|
|
3091
|
+
enableVariableLegendPosition: z15.boolean().optional().describe("Allow user to drag legend"),
|
|
3092
|
+
legendTop: z15.number().optional().describe("Custom legend top offset"),
|
|
3093
|
+
legendLeft: z15.number().optional().describe("Custom legend left offset"),
|
|
2986
3094
|
verticalAxis: axisSchema.describe("Vertical (Y) axis configuration"),
|
|
2987
3095
|
horizontalAxis: axisSchema.describe("Horizontal (X) axis configuration")
|
|
2988
3096
|
},
|
|
@@ -3057,7 +3165,7 @@ Use \`get_embed_details\` to view the full current theme configuration.` + GUEST
|
|
|
3057
3165
|
}
|
|
3058
3166
|
|
|
3059
3167
|
// src/tools/embed/get-demo-link.ts
|
|
3060
|
-
import { z as
|
|
3168
|
+
import { z as z16 } from "zod";
|
|
3061
3169
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
3062
3170
|
var DEFAULT_EXPIRY_HOURS = 24;
|
|
3063
3171
|
var MAX_EXPIRY_HOURS = 24 * 30;
|
|
@@ -3067,13 +3175,13 @@ function registerGetDemoLink(server, client) {
|
|
|
3067
3175
|
"get_demo_link",
|
|
3068
3176
|
`Get a shareable demo URL for a dashboard embed scoped to a specific clientId \u2014 same link the UI's "Share Demo Link" generates. Loads the dashboard as that end-customer sees it. Use to verify ops-built widgets without a test app.`,
|
|
3069
3177
|
{
|
|
3070
|
-
clientId:
|
|
3071
|
-
dashboardName:
|
|
3072
|
-
workspaceName:
|
|
3073
|
-
embedId:
|
|
3074
|
-
expiryHours:
|
|
3178
|
+
clientId: z16.string().describe("End-customer identifier to scope the preview to."),
|
|
3179
|
+
dashboardName: z16.string().optional().describe("Dashboard name (non-technical). Preferred \u2014 pass with optional `workspaceName`."),
|
|
3180
|
+
workspaceName: z16.string().optional().describe("Workspace name. Only needed if `dashboardName` is ambiguous."),
|
|
3181
|
+
embedId: z16.string().optional().describe("Technical alternative to `dashboardName`."),
|
|
3182
|
+
expiryHours: z16.number().min(1).max(MAX_EXPIRY_HOURS).optional().describe(`Token TTL in hours. Defaults to ${DEFAULT_EXPIRY_HOURS}. Max ${MAX_EXPIRY_HOURS} (30 days).`)
|
|
3075
3183
|
},
|
|
3076
|
-
|
|
3184
|
+
WRITE_CREATE,
|
|
3077
3185
|
async (params) => {
|
|
3078
3186
|
try {
|
|
3079
3187
|
const embed = await services.widgetService.resolveEmbedId({
|
|
@@ -3127,70 +3235,70 @@ function registerGetDemoLink(server, client) {
|
|
|
3127
3235
|
}
|
|
3128
3236
|
|
|
3129
3237
|
// src/tools/frontend/generate-guest-token.ts
|
|
3130
|
-
import { z as
|
|
3238
|
+
import { z as z17 } from "zod";
|
|
3131
3239
|
function registerGenerateGuestToken(server, client) {
|
|
3132
3240
|
const services = createServiceBundle(client);
|
|
3133
3241
|
server.tool(
|
|
3134
3242
|
"generate_guest_token",
|
|
3135
3243
|
"Generate a guest token for frontend embedding. Requires clientId. Omit dataAppName to auto-resolve.",
|
|
3136
3244
|
{
|
|
3137
|
-
clientId:
|
|
3138
|
-
dataAppName:
|
|
3139
|
-
body:
|
|
3245
|
+
clientId: z17.string().describe('Unique end-user identifier for multi-tenant filtering. Use "None" if no tenancy.'),
|
|
3246
|
+
dataAppName: z17.string().optional().describe("Name of the data app. Omit to list available data apps."),
|
|
3247
|
+
body: z17.record(z17.unknown()).optional().describe(
|
|
3140
3248
|
"Raw API body for POST /api/v2/guest-token/create. If provided, sent directly and convenience fields are ignored."
|
|
3141
3249
|
),
|
|
3142
|
-
params:
|
|
3143
|
-
allowedEmbeds:
|
|
3144
|
-
rlsSettings:
|
|
3145
|
-
metricId:
|
|
3146
|
-
values:
|
|
3250
|
+
params: z17.object({
|
|
3251
|
+
allowedEmbeds: z17.array(z17.string()).optional().describe("Restrict token to specific embed IDs"),
|
|
3252
|
+
rlsSettings: z17.array(z17.object({
|
|
3253
|
+
metricId: z17.string().describe("Metric ID"),
|
|
3254
|
+
values: z17.record(z17.unknown()).describe("RLS values by column name")
|
|
3147
3255
|
})).optional().describe("Legacy alias used in docs/examples; forwarded as params.rlsSettings"),
|
|
3148
|
-
appFilters:
|
|
3149
|
-
metricId:
|
|
3150
|
-
values:
|
|
3256
|
+
appFilters: z17.array(z17.object({
|
|
3257
|
+
metricId: z17.string().describe("Metric ID"),
|
|
3258
|
+
values: z17.record(z17.unknown()).describe("Filter values by column name")
|
|
3151
3259
|
})).optional().describe("Metric-level app filters"),
|
|
3152
|
-
dashboardAppFilters:
|
|
3153
|
-
dashboardId:
|
|
3154
|
-
values:
|
|
3155
|
-
isShowOnUrl:
|
|
3260
|
+
dashboardAppFilters: z17.array(z17.object({
|
|
3261
|
+
dashboardId: z17.string().describe("Dashboard ID"),
|
|
3262
|
+
values: z17.record(z17.unknown()).describe("Filter values by dashboard filter name"),
|
|
3263
|
+
isShowOnUrl: z17.boolean().optional().describe("Whether filters are shown in URL params")
|
|
3156
3264
|
})).optional().describe("Dashboard-level app filters"),
|
|
3157
|
-
hideDashboardFilters:
|
|
3158
|
-
userIdentifier:
|
|
3159
|
-
timezone:
|
|
3265
|
+
hideDashboardFilters: z17.array(z17.string()).optional().describe("Dashboard filter names to hide"),
|
|
3266
|
+
userIdentifier: z17.string().optional().describe("End-user identifier for private metrics support"),
|
|
3267
|
+
timezone: z17.string().optional().describe('IANA timezone, e.g. "America/New_York"')
|
|
3160
3268
|
}).optional().describe("Advanced token params"),
|
|
3161
3269
|
// Legacy convenience fields (auto-mapped into params for backward compatibility)
|
|
3162
|
-
timezone:
|
|
3163
|
-
allowedEmbeds:
|
|
3164
|
-
rlsSettings:
|
|
3165
|
-
metricId:
|
|
3166
|
-
values:
|
|
3270
|
+
timezone: z17.string().optional().describe("Legacy: moved under params.timezone"),
|
|
3271
|
+
allowedEmbeds: z17.array(z17.string()).optional().describe("Legacy: moved under params.allowedEmbeds"),
|
|
3272
|
+
rlsSettings: z17.array(z17.object({
|
|
3273
|
+
metricId: z17.string().describe("Metric ID"),
|
|
3274
|
+
values: z17.record(z17.unknown()).describe("RLS values by column name")
|
|
3167
3275
|
})).optional().describe("Legacy: moved under params.rlsSettings"),
|
|
3168
|
-
appFilters:
|
|
3169
|
-
metricId:
|
|
3170
|
-
values:
|
|
3276
|
+
appFilters: z17.array(z17.object({
|
|
3277
|
+
metricId: z17.string().describe("Metric ID"),
|
|
3278
|
+
values: z17.record(z17.unknown()).describe("Filter values by column name")
|
|
3171
3279
|
})).optional().describe("Legacy: moved under params.appFilters"),
|
|
3172
|
-
dashboardAppFilters:
|
|
3173
|
-
dashboardId:
|
|
3174
|
-
values:
|
|
3175
|
-
isShowOnUrl:
|
|
3280
|
+
dashboardAppFilters: z17.array(z17.object({
|
|
3281
|
+
dashboardId: z17.string().describe("Dashboard ID to apply filters to"),
|
|
3282
|
+
values: z17.record(z17.unknown()).describe("Filter values for this dashboard"),
|
|
3283
|
+
isShowOnUrl: z17.boolean().optional().describe("Whether filters are shown in URL params")
|
|
3176
3284
|
})).optional().describe("Legacy: moved under params.dashboardAppFilters"),
|
|
3177
|
-
permissions:
|
|
3178
|
-
isEnableArchiveMetrics:
|
|
3179
|
-
isEnableManageMetrics:
|
|
3180
|
-
isEnableCreateDashboardView:
|
|
3181
|
-
isEnableMetricUpdation:
|
|
3182
|
-
isEnableCustomizeLayout:
|
|
3183
|
-
isEnableUnderlyingData:
|
|
3184
|
-
isEnableDownloadMetrics:
|
|
3185
|
-
isShowSideBar:
|
|
3186
|
-
isShowDashboardName:
|
|
3187
|
-
isDisableMetricCreation:
|
|
3285
|
+
permissions: z17.object({
|
|
3286
|
+
isEnableArchiveMetrics: z17.boolean().optional(),
|
|
3287
|
+
isEnableManageMetrics: z17.boolean().optional(),
|
|
3288
|
+
isEnableCreateDashboardView: z17.boolean().optional(),
|
|
3289
|
+
isEnableMetricUpdation: z17.boolean().optional(),
|
|
3290
|
+
isEnableCustomizeLayout: z17.boolean().optional(),
|
|
3291
|
+
isEnableUnderlyingData: z17.boolean().optional(),
|
|
3292
|
+
isEnableDownloadMetrics: z17.boolean().optional(),
|
|
3293
|
+
isShowSideBar: z17.boolean().optional(),
|
|
3294
|
+
isShowDashboardName: z17.boolean().optional(),
|
|
3295
|
+
isDisableMetricCreation: z17.boolean().optional()
|
|
3188
3296
|
}).optional().describe("Runtime permission overrides (merge with and override embed-level settings)"),
|
|
3189
|
-
theme:
|
|
3190
|
-
hideDashboardFilters:
|
|
3191
|
-
userIdentifier:
|
|
3192
|
-
expiryTime:
|
|
3193
|
-
datasourceName:
|
|
3297
|
+
theme: z17.record(z17.unknown()).optional().describe("Runtime theme overrides (overrides embed-level theme)"),
|
|
3298
|
+
hideDashboardFilters: z17.array(z17.string()).optional().describe("Legacy: moved under params.hideDashboardFilters"),
|
|
3299
|
+
userIdentifier: z17.string().optional().describe("Legacy: moved under params.userIdentifier"),
|
|
3300
|
+
expiryTime: z17.number().optional().describe("Token TTL in milliseconds"),
|
|
3301
|
+
datasourceName: z17.string().optional().describe("Datasource name for multi-datasource routing")
|
|
3194
3302
|
},
|
|
3195
3303
|
WRITE_CREATE,
|
|
3196
3304
|
async (params) => {
|
|
@@ -3243,7 +3351,7 @@ Next step: Use \`generate_embed_code\` to get copy-paste-ready frontend code.`
|
|
|
3243
3351
|
}
|
|
3244
3352
|
|
|
3245
3353
|
// src/tools/frontend/generate-embed-code.ts
|
|
3246
|
-
import { z as
|
|
3354
|
+
import { z as z18 } from "zod";
|
|
3247
3355
|
|
|
3248
3356
|
// src/templates/react.ts
|
|
3249
3357
|
function kebabCase(str) {
|
|
@@ -3352,7 +3460,7 @@ export default function DatabrainEmbed() {
|
|
|
3352
3460
|
### Environment Variables
|
|
3353
3461
|
Set the following environment variables on your backend server:
|
|
3354
3462
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3355
|
-
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://
|
|
3463
|
+
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
|
|
3356
3464
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3357
3465
|
|
|
3358
3466
|
### Security
|
|
@@ -3485,7 +3593,7 @@ export default function DatabrainEmbed() {
|
|
|
3485
3593
|
### Environment Variables
|
|
3486
3594
|
Add the following to your \`.env.local\` file:
|
|
3487
3595
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3488
|
-
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://
|
|
3596
|
+
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
|
|
3489
3597
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3490
3598
|
|
|
3491
3599
|
### Security
|
|
@@ -3617,7 +3725,7 @@ onMounted(async () => {
|
|
|
3617
3725
|
### Environment Variables
|
|
3618
3726
|
Set the following environment variables on your backend server:
|
|
3619
3727
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3620
|
-
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://
|
|
3728
|
+
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
|
|
3621
3729
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3622
3730
|
|
|
3623
3731
|
### Security
|
|
@@ -3767,7 +3875,7 @@ export class DatabrainEmbedComponent implements OnInit {
|
|
|
3767
3875
|
### Environment Variables
|
|
3768
3876
|
Set the following environment variables on your backend server:
|
|
3769
3877
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3770
|
-
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://
|
|
3878
|
+
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
|
|
3771
3879
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3772
3880
|
|
|
3773
3881
|
### Security
|
|
@@ -3909,7 +4017,7 @@ app.listen(3001, () => {
|
|
|
3909
4017
|
### Environment Variables
|
|
3910
4018
|
Set the following environment variables on your backend server:
|
|
3911
4019
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3912
|
-
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://
|
|
4020
|
+
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
|
|
3913
4021
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3914
4022
|
|
|
3915
4023
|
### Security
|
|
@@ -4041,7 +4149,7 @@ export default function DatabrainEmbed() {
|
|
|
4041
4149
|
### Environment Variables
|
|
4042
4150
|
Set the following environment variables on your backend server:
|
|
4043
4151
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
4044
|
-
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://
|
|
4152
|
+
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
|
|
4045
4153
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
4046
4154
|
|
|
4047
4155
|
### Security
|
|
@@ -4188,7 +4296,7 @@ app.listen(3001, () => {
|
|
|
4188
4296
|
### Environment Variables
|
|
4189
4297
|
Set the following environment variables on your backend server:
|
|
4190
4298
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
4191
|
-
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://
|
|
4299
|
+
- \`DATABRAIN_API_URL\` - Your Databrain API base URL (e.g. https://api.usedatabrain.com)
|
|
4192
4300
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
4193
4301
|
|
|
4194
4302
|
### Security
|
|
@@ -4239,22 +4347,22 @@ function registerGenerateEmbedCode(server, _client) {
|
|
|
4239
4347
|
"generate_embed_code",
|
|
4240
4348
|
"Generate framework-specific embed code (React, Next.js, Vue, Angular, Svelte, SolidJS, vanilla HTML).",
|
|
4241
4349
|
{
|
|
4242
|
-
embedId:
|
|
4243
|
-
framework:
|
|
4244
|
-
embedType:
|
|
4245
|
-
options:
|
|
4246
|
-
enableDownloadCsv:
|
|
4247
|
-
enableEmailCsv:
|
|
4248
|
-
enableDownloadAllMetrics:
|
|
4249
|
-
enableDownloadAllPdf:
|
|
4250
|
-
disableFullscreen:
|
|
4251
|
-
isHideTablePreview:
|
|
4252
|
-
isHideChartSettings:
|
|
4253
|
-
isStickyDashboardFilters:
|
|
4254
|
-
enableMultiMetricFilters:
|
|
4255
|
-
themeName:
|
|
4256
|
-
customTheme:
|
|
4257
|
-
optionsIcon:
|
|
4350
|
+
embedId: z18.string().describe("The embed ID to generate code for"),
|
|
4351
|
+
framework: z18.enum(FRAMEWORKS).describe("Frontend framework"),
|
|
4352
|
+
embedType: z18.enum(["dashboard", "metric"]).optional().default("dashboard"),
|
|
4353
|
+
options: z18.object({
|
|
4354
|
+
enableDownloadCsv: z18.boolean().optional(),
|
|
4355
|
+
enableEmailCsv: z18.boolean().optional(),
|
|
4356
|
+
enableDownloadAllMetrics: z18.boolean().optional(),
|
|
4357
|
+
enableDownloadAllPdf: z18.boolean().optional(),
|
|
4358
|
+
disableFullscreen: z18.boolean().optional(),
|
|
4359
|
+
isHideTablePreview: z18.boolean().optional(),
|
|
4360
|
+
isHideChartSettings: z18.boolean().optional(),
|
|
4361
|
+
isStickyDashboardFilters: z18.boolean().optional(),
|
|
4362
|
+
enableMultiMetricFilters: z18.boolean().optional(),
|
|
4363
|
+
themeName: z18.string().optional(),
|
|
4364
|
+
customTheme: z18.record(z18.unknown()).optional(),
|
|
4365
|
+
optionsIcon: z18.enum(["kebab-menu-vertical", "download"]).optional()
|
|
4258
4366
|
}).optional()
|
|
4259
4367
|
},
|
|
4260
4368
|
READ_ONLY,
|
|
@@ -4294,17 +4402,17 @@ ${generated.instructions}`;
|
|
|
4294
4402
|
}
|
|
4295
4403
|
|
|
4296
4404
|
// src/tools/analytics/list-dashboards.ts
|
|
4297
|
-
import { z as
|
|
4405
|
+
import { z as z19 } from "zod";
|
|
4298
4406
|
function registerListDashboards(server, client) {
|
|
4299
4407
|
const services = createServiceBundle(client);
|
|
4300
4408
|
server.tool(
|
|
4301
4409
|
"list_dashboards",
|
|
4302
4410
|
"List dashboards globally or for a specific data app.",
|
|
4303
4411
|
{
|
|
4304
|
-
scope:
|
|
4412
|
+
scope: z19.enum(["auto", "global", "data_app"]).optional().describe(
|
|
4305
4413
|
"Dashboard listing scope. auto: use data_app when dataAppName + API token are available, otherwise global."
|
|
4306
4414
|
),
|
|
4307
|
-
dataAppName:
|
|
4415
|
+
dataAppName: z19.string().optional().describe("Data app name for data_app scope. Ignored for global scope.")
|
|
4308
4416
|
},
|
|
4309
4417
|
READ_ONLY,
|
|
4310
4418
|
async (params) => {
|
|
@@ -4339,7 +4447,7 @@ function registerListDashboards(server, client) {
|
|
|
4339
4447
|
}
|
|
4340
4448
|
|
|
4341
4449
|
// src/tools/query/ask-question.ts
|
|
4342
|
-
import { z as
|
|
4450
|
+
import { z as z20 } from "zod";
|
|
4343
4451
|
var MAX_DISPLAY_ROWS = 50;
|
|
4344
4452
|
function formatDataTable(data, columns) {
|
|
4345
4453
|
if (!Array.isArray(data) || data.length === 0) return "_No data returned._";
|
|
@@ -4369,16 +4477,16 @@ function registerAskQuestion(server, client) {
|
|
|
4369
4477
|
"ask_question",
|
|
4370
4478
|
"Ask a natural language question \u2014 converts to SQL, returns data + chart suggestion. Requires datamartName with a populated semantic layer.",
|
|
4371
4479
|
{
|
|
4372
|
-
question:
|
|
4480
|
+
question: z20.string().describe(
|
|
4373
4481
|
'The natural language question to ask (e.g. "What was total revenue last month?")'
|
|
4374
4482
|
),
|
|
4375
|
-
datamartName:
|
|
4483
|
+
datamartName: z20.string().optional().describe(
|
|
4376
4484
|
"Name of the datamart to query. Use `list_datamarts` to find available names. REQUIRED unless exactly one datamart exists (auto-resolved). Do NOT pass a workspace name here."
|
|
4377
4485
|
),
|
|
4378
|
-
sessionId:
|
|
4486
|
+
sessionId: z20.string().optional().describe(
|
|
4379
4487
|
"Session ID from a previous ask_question response, for follow-up questions."
|
|
4380
4488
|
),
|
|
4381
|
-
timezone:
|
|
4489
|
+
timezone: z20.string().optional().describe('IANA timezone (e.g. "America/New_York"). Defaults to UTC.')
|
|
4382
4490
|
},
|
|
4383
4491
|
WRITE_CREATE,
|
|
4384
4492
|
async (params) => {
|
|
@@ -4392,7 +4500,13 @@ function registerAskQuestion(server, client) {
|
|
|
4392
4500
|
);
|
|
4393
4501
|
}
|
|
4394
4502
|
if (datamarts.length === 1) {
|
|
4395
|
-
|
|
4503
|
+
const resolvedName = datamarts[0].name || datamarts[0].datamartName;
|
|
4504
|
+
if (!resolvedName) {
|
|
4505
|
+
return toolError(
|
|
4506
|
+
"Found one datamart, but the API did not return a usable name. Use `list_datamarts` to inspect the response and pass `datamartName` explicitly."
|
|
4507
|
+
);
|
|
4508
|
+
}
|
|
4509
|
+
datamartName = resolvedName;
|
|
4396
4510
|
} else {
|
|
4397
4511
|
const list = datamarts.map(formatDatamart).join("\n");
|
|
4398
4512
|
return toolSuccess(
|
|
@@ -4404,6 +4518,11 @@ Call \`ask_question\` again with the \`datamartName\` parameter.`
|
|
|
4404
4518
|
);
|
|
4405
4519
|
}
|
|
4406
4520
|
}
|
|
4521
|
+
if (!datamartName) {
|
|
4522
|
+
return toolError(
|
|
4523
|
+
"Could not resolve a datamart name. Use `list_datamarts`, then call `ask_question` again with `datamartName`."
|
|
4524
|
+
);
|
|
4525
|
+
}
|
|
4407
4526
|
try {
|
|
4408
4527
|
const semanticLayer = await services.queryApi.getSemanticLayer(
|
|
4409
4528
|
datamartName
|
|
@@ -4543,14 +4662,14 @@ To fix this:
|
|
|
4543
4662
|
}
|
|
4544
4663
|
|
|
4545
4664
|
// src/tools/query/get-semantic-layer.ts
|
|
4546
|
-
import { z as
|
|
4665
|
+
import { z as z21 } from "zod";
|
|
4547
4666
|
function registerGetSemanticLayer(server, client) {
|
|
4548
4667
|
const services = createServiceBundle(client);
|
|
4549
4668
|
server.tool(
|
|
4550
4669
|
"get_semantic_layer",
|
|
4551
4670
|
"Get semantic layer metadata for a datamart: table/column descriptions, synonyms, completion score. Omit datamartName to auto-resolve.",
|
|
4552
4671
|
{
|
|
4553
|
-
datamartName:
|
|
4672
|
+
datamartName: z21.string().optional().describe(
|
|
4554
4673
|
"Name of the datamart to inspect. Omit to list available datamarts."
|
|
4555
4674
|
)
|
|
4556
4675
|
},
|
|
@@ -4660,15 +4779,15 @@ ${result.feedback}`);
|
|
|
4660
4779
|
}
|
|
4661
4780
|
|
|
4662
4781
|
// src/tools/query/update-semantic-layer.ts
|
|
4663
|
-
import { z as
|
|
4664
|
-
var
|
|
4665
|
-
name:
|
|
4666
|
-
description:
|
|
4667
|
-
synonyms:
|
|
4782
|
+
import { z as z22 } from "zod";
|
|
4783
|
+
var columnSchema2 = z22.object({
|
|
4784
|
+
name: z22.string().describe("Column name (must exist in the datamart)"),
|
|
4785
|
+
description: z22.string().optional().describe("Business-friendly description of what this column represents"),
|
|
4786
|
+
synonyms: z22.array(z22.string()).optional().describe(
|
|
4668
4787
|
'Alternative names users might use for this column (e.g. ["revenue", "sales amount"])'
|
|
4669
4788
|
),
|
|
4670
|
-
miscellaneousInfo:
|
|
4671
|
-
columnType:
|
|
4789
|
+
miscellaneousInfo: z22.string().optional().describe("Additional context such as valid values, units, or caveats"),
|
|
4790
|
+
columnType: z22.enum([
|
|
4672
4791
|
"String",
|
|
4673
4792
|
"Long String",
|
|
4674
4793
|
"String (Custom)",
|
|
@@ -4679,32 +4798,91 @@ var columnSchema = z20.object({
|
|
|
4679
4798
|
"Identifier",
|
|
4680
4799
|
"Number",
|
|
4681
4800
|
"JSON"
|
|
4682
|
-
]).nullable().optional().describe('Semantic column type. Use "Identifier" for IDs/foreign keys, "Number" for numeric values, "String" for text. Omit for date columns.')
|
|
4801
|
+
]).nullable().optional().describe('Semantic column type. Use "Identifier" for IDs/foreign keys, "Number" for numeric values, "String" for text. Omit for date and boolean columns.')
|
|
4683
4802
|
});
|
|
4684
|
-
var
|
|
4685
|
-
name:
|
|
4686
|
-
description:
|
|
4803
|
+
var tableSchema2 = z22.object({
|
|
4804
|
+
name: z22.string().describe("Table name (must exist in the datamart)"),
|
|
4805
|
+
description: z22.string().optional().describe(
|
|
4687
4806
|
"Business-friendly description of what this table represents"
|
|
4688
4807
|
),
|
|
4689
|
-
synonyms:
|
|
4690
|
-
miscellaneousInfo:
|
|
4808
|
+
synonyms: z22.array(z22.string()).optional().describe("Alternative names for this table"),
|
|
4809
|
+
miscellaneousInfo: z22.string().optional().describe(
|
|
4691
4810
|
"Additional context such as how the table relates to other tables"
|
|
4692
4811
|
),
|
|
4693
|
-
columns:
|
|
4812
|
+
columns: z22.array(columnSchema2).optional().describe("Column-level semantic definitions to update")
|
|
4694
4813
|
});
|
|
4814
|
+
function columnKey(tableName, columnName) {
|
|
4815
|
+
return `${tableName.toLowerCase()}\0${columnName.toLowerCase()}`;
|
|
4816
|
+
}
|
|
4817
|
+
function buildColumnDatatypeMap(semanticLayer) {
|
|
4818
|
+
const datatypes = /* @__PURE__ */ new Map();
|
|
4819
|
+
for (const table of semanticLayer.tables ?? []) {
|
|
4820
|
+
for (const column of table.columns ?? []) {
|
|
4821
|
+
if (column.datatype) {
|
|
4822
|
+
datatypes.set(columnKey(table.name, column.name), column.datatype);
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
return datatypes;
|
|
4827
|
+
}
|
|
4828
|
+
function hasExplicitColumnType(tables) {
|
|
4829
|
+
return tables?.some(
|
|
4830
|
+
(table) => table.columns?.some(
|
|
4831
|
+
(column) => column.columnType !== void 0 && column.columnType !== null
|
|
4832
|
+
)
|
|
4833
|
+
) ?? false;
|
|
4834
|
+
}
|
|
4835
|
+
function shouldOmitColumnTypeForDatatype(datatype) {
|
|
4836
|
+
if (!datatype) return false;
|
|
4837
|
+
const normalized = datatype.trim().toLowerCase();
|
|
4838
|
+
return normalized === "boolean" || normalized === "bool" || normalized === "date" || normalized.startsWith("date(") || normalized.startsWith("datetime") || normalized.startsWith("timestamp") || normalized === "time" || normalized.startsWith("time(") || normalized.startsWith("time ");
|
|
4839
|
+
}
|
|
4840
|
+
function normalizeTablesForApi(tables, datatypesByColumn) {
|
|
4841
|
+
if (!tables) return void 0;
|
|
4842
|
+
return tables.map((table) => {
|
|
4843
|
+
const normalized = { name: table.name };
|
|
4844
|
+
if (table.description !== void 0) normalized.description = table.description;
|
|
4845
|
+
if (table.synonyms !== void 0) normalized.synonyms = table.synonyms;
|
|
4846
|
+
if (table.miscellaneousInfo !== void 0) {
|
|
4847
|
+
normalized.miscellaneousInfo = table.miscellaneousInfo;
|
|
4848
|
+
}
|
|
4849
|
+
if (table.columns !== void 0) {
|
|
4850
|
+
normalized.columns = table.columns.map((column) => {
|
|
4851
|
+
const normalizedColumn = { name: column.name };
|
|
4852
|
+
if (column.description !== void 0) {
|
|
4853
|
+
normalizedColumn.description = column.description;
|
|
4854
|
+
}
|
|
4855
|
+
if (column.synonyms !== void 0) normalizedColumn.synonyms = column.synonyms;
|
|
4856
|
+
if (column.miscellaneousInfo !== void 0) {
|
|
4857
|
+
normalizedColumn.miscellaneousInfo = column.miscellaneousInfo;
|
|
4858
|
+
}
|
|
4859
|
+
if (column.columnType !== void 0 && column.columnType !== null) {
|
|
4860
|
+
const datatype = datatypesByColumn?.get(
|
|
4861
|
+
columnKey(table.name, column.name)
|
|
4862
|
+
);
|
|
4863
|
+
if (!shouldOmitColumnTypeForDatatype(datatype)) {
|
|
4864
|
+
normalizedColumn.columnType = column.columnType;
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
return normalizedColumn;
|
|
4868
|
+
});
|
|
4869
|
+
}
|
|
4870
|
+
return normalized;
|
|
4871
|
+
});
|
|
4872
|
+
}
|
|
4695
4873
|
function registerUpdateSemanticLayer(server, client) {
|
|
4696
4874
|
const services = createServiceBundle(client);
|
|
4697
4875
|
server.tool(
|
|
4698
4876
|
"update_semantic_layer",
|
|
4699
4877
|
"Populate or update the semantic layer for a datamart. Add table/column descriptions, synonyms, context. Required before ask_question works. Auto-generate descriptions from column names when empty.",
|
|
4700
4878
|
{
|
|
4701
|
-
datamartName:
|
|
4879
|
+
datamartName: z22.string().optional().describe(
|
|
4702
4880
|
"Name of the datamart to update. Omit to list available datamarts."
|
|
4703
4881
|
),
|
|
4704
|
-
tables:
|
|
4882
|
+
tables: z22.array(tableSchema2).optional().describe(
|
|
4705
4883
|
"Table and column definitions to update. Only include tables/columns you want to change."
|
|
4706
4884
|
),
|
|
4707
|
-
feedback:
|
|
4885
|
+
feedback: z22.string().optional().describe(
|
|
4708
4886
|
'General feedback or context about the datamart (e.g. "This datamart contains e-commerce data for the US market").'
|
|
4709
4887
|
)
|
|
4710
4888
|
},
|
|
@@ -4727,9 +4905,13 @@ function registerUpdateSemanticLayer(server, client) {
|
|
|
4727
4905
|
});
|
|
4728
4906
|
if (resolution.kind !== "resolved") return resolution.response;
|
|
4729
4907
|
const datamartName = resolution.name;
|
|
4908
|
+
const datatypesByColumn = hasExplicitColumnType(params.tables) ? buildColumnDatatypeMap(
|
|
4909
|
+
await services.queryApi.getSemanticLayer(datamartName)
|
|
4910
|
+
) : void 0;
|
|
4911
|
+
const tables = normalizeTablesForApi(params.tables, datatypesByColumn);
|
|
4730
4912
|
await services.queryApi.updateSemanticLayer({
|
|
4731
4913
|
datamartName,
|
|
4732
|
-
tables
|
|
4914
|
+
tables,
|
|
4733
4915
|
feedback: params.feedback
|
|
4734
4916
|
});
|
|
4735
4917
|
const updatedParts = [];
|
|
@@ -4761,35 +4943,273 @@ You can now:
|
|
|
4761
4943
|
);
|
|
4762
4944
|
}
|
|
4763
4945
|
|
|
4764
|
-
// src/tools/
|
|
4765
|
-
import { z as
|
|
4766
|
-
function
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4946
|
+
// src/tools/query/semantic-generation.ts
|
|
4947
|
+
import { z as z23 } from "zod";
|
|
4948
|
+
function nonEmptyString(value) {
|
|
4949
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
4950
|
+
}
|
|
4951
|
+
function datamartIdFrom(datamart) {
|
|
4952
|
+
return nonEmptyString(datamart.id) || nonEmptyString(datamart._id) || nonEmptyString(datamart.datamartId) || nonEmptyString(datamart.dataMartId) || null;
|
|
4953
|
+
}
|
|
4954
|
+
function nestedDatamartFromWorkspace(workspace) {
|
|
4955
|
+
const nested = workspace.datamart;
|
|
4956
|
+
return nested && typeof nested === "object" && !Array.isArray(nested) ? nested : null;
|
|
4957
|
+
}
|
|
4958
|
+
function workspaceDatamartIdFrom(workspace) {
|
|
4959
|
+
const nested = nestedDatamartFromWorkspace(workspace);
|
|
4960
|
+
return nonEmptyString(workspace.dataMartId) || nonEmptyString(workspace.datamartId) || (nested ? datamartIdFrom(nested) : null);
|
|
4961
|
+
}
|
|
4962
|
+
function workspaceDatamartNameFrom(workspace) {
|
|
4963
|
+
const nested = nestedDatamartFromWorkspace(workspace);
|
|
4964
|
+
return (nested ? nonEmptyString(nested.name) || nonEmptyString(nested.datamartName) : null) || nonEmptyString(workspace.datamartName) || null;
|
|
4965
|
+
}
|
|
4966
|
+
async function resolveDatamartFromWorkspace(params) {
|
|
4967
|
+
const services = createServiceBundle(params.client);
|
|
4968
|
+
const workspaces = await services.workspacesApi.listWorkspaces();
|
|
4969
|
+
const resolution = resolveResource({
|
|
4970
|
+
items: workspaces,
|
|
4971
|
+
name: params.workspaceName,
|
|
4972
|
+
getName: (ws) => ws.name,
|
|
4973
|
+
formatItem: formatWorkspace,
|
|
4974
|
+
resourceType: "workspace",
|
|
4975
|
+
toolName: params.toolName
|
|
4976
|
+
});
|
|
4977
|
+
if (resolution.kind !== "resolved") return { ok: false, response: resolution.response };
|
|
4978
|
+
const selected = resolution.selected;
|
|
4979
|
+
const datamartId = workspaceDatamartIdFrom(selected);
|
|
4980
|
+
const workspaceDatamartName = workspaceDatamartNameFrom(selected);
|
|
4981
|
+
if (!datamartId) {
|
|
4982
|
+
return {
|
|
4983
|
+
ok: false,
|
|
4984
|
+
response: toolError(
|
|
4985
|
+
`Workspace "${resolution.name}" does not expose a linked dataMartId/datamart.id. Pass datamartId directly or choose a workspace linked to a datamart.`
|
|
4986
|
+
)
|
|
4987
|
+
};
|
|
4988
|
+
}
|
|
4989
|
+
if (params.datamartName && workspaceDatamartName && workspaceDatamartName.toLowerCase() !== params.datamartName.toLowerCase()) {
|
|
4990
|
+
return {
|
|
4991
|
+
ok: false,
|
|
4992
|
+
response: toolError(
|
|
4993
|
+
`Workspace "${resolution.name}" is linked to datamart "${workspaceDatamartName}", not "${params.datamartName}". Use the linked datamart name or choose another workspace.`
|
|
4994
|
+
)
|
|
4995
|
+
};
|
|
4996
|
+
}
|
|
4997
|
+
return {
|
|
4998
|
+
ok: true,
|
|
4999
|
+
datamartName: workspaceDatamartName || params.datamartName || datamartId,
|
|
5000
|
+
datamartId,
|
|
5001
|
+
workspaceName: resolution.name
|
|
5002
|
+
};
|
|
5003
|
+
}
|
|
5004
|
+
async function findDatamartIdInLinkedWorkspaces(params) {
|
|
5005
|
+
const services = createServiceBundle(params.client);
|
|
5006
|
+
const workspaces = await services.workspacesApi.listWorkspaces();
|
|
5007
|
+
const matches = workspaces.map((workspace) => {
|
|
5008
|
+
const record = workspace;
|
|
5009
|
+
return {
|
|
5010
|
+
name: nonEmptyString(record.name),
|
|
5011
|
+
datamartName: workspaceDatamartNameFrom(record),
|
|
5012
|
+
datamartId: workspaceDatamartIdFrom(record)
|
|
5013
|
+
};
|
|
5014
|
+
}).filter(
|
|
5015
|
+
(workspace) => workspace.datamartName?.toLowerCase() === params.datamartName.toLowerCase() && workspace.datamartId
|
|
5016
|
+
);
|
|
5017
|
+
const uniqueIds = [...new Set(matches.map((workspace) => workspace.datamartId))];
|
|
5018
|
+
if (uniqueIds.length === 1 && uniqueIds[0]) {
|
|
5019
|
+
return {
|
|
5020
|
+
kind: "resolved",
|
|
5021
|
+
datamartId: uniqueIds[0],
|
|
5022
|
+
workspaceName: matches.find((workspace) => workspace.datamartId === uniqueIds[0])?.name || void 0
|
|
5023
|
+
};
|
|
5024
|
+
}
|
|
5025
|
+
if (uniqueIds.length > 1) {
|
|
5026
|
+
return {
|
|
5027
|
+
kind: "ambiguous",
|
|
5028
|
+
workspaceNames: matches.map((workspace) => workspace.name).filter((name) => !!name)
|
|
5029
|
+
};
|
|
5030
|
+
}
|
|
5031
|
+
return { kind: "none" };
|
|
5032
|
+
}
|
|
5033
|
+
async function resolveDatamart(params) {
|
|
5034
|
+
if (params.datamartId) {
|
|
5035
|
+
return {
|
|
5036
|
+
ok: true,
|
|
5037
|
+
datamartName: params.datamartName || params.datamartId,
|
|
5038
|
+
datamartId: params.datamartId,
|
|
5039
|
+
workspaceName: params.workspaceName
|
|
5040
|
+
};
|
|
5041
|
+
}
|
|
5042
|
+
if (params.workspaceName) {
|
|
5043
|
+
return resolveDatamartFromWorkspace({
|
|
5044
|
+
client: params.client,
|
|
5045
|
+
workspaceName: params.workspaceName,
|
|
5046
|
+
datamartName: params.datamartName,
|
|
5047
|
+
toolName: params.toolName
|
|
5048
|
+
});
|
|
5049
|
+
}
|
|
5050
|
+
const services = createServiceBundle(params.client);
|
|
5051
|
+
const datamarts = await services.datamartsApi.listDatamarts();
|
|
5052
|
+
const resolution = resolveResource({
|
|
5053
|
+
items: datamarts,
|
|
5054
|
+
name: params.datamartName,
|
|
5055
|
+
getName: (dm) => dm.name || dm.datamartName,
|
|
5056
|
+
formatItem: formatDatamart,
|
|
5057
|
+
resourceType: "datamart",
|
|
5058
|
+
toolName: params.toolName
|
|
5059
|
+
});
|
|
5060
|
+
if (resolution.kind !== "resolved") return { ok: false, response: resolution.response };
|
|
5061
|
+
const datamartId = datamartIdFrom(resolution.selected);
|
|
5062
|
+
if (!datamartId) {
|
|
5063
|
+
const workspaceMatch = await findDatamartIdInLinkedWorkspaces({
|
|
5064
|
+
client: params.client,
|
|
5065
|
+
datamartName: resolution.name
|
|
5066
|
+
});
|
|
5067
|
+
if (workspaceMatch.kind === "resolved") {
|
|
5068
|
+
return {
|
|
5069
|
+
ok: true,
|
|
5070
|
+
datamartName: resolution.name,
|
|
5071
|
+
datamartId: workspaceMatch.datamartId,
|
|
5072
|
+
workspaceName: workspaceMatch.workspaceName
|
|
5073
|
+
};
|
|
5074
|
+
}
|
|
5075
|
+
if (workspaceMatch.kind === "ambiguous") {
|
|
5076
|
+
return {
|
|
5077
|
+
ok: false,
|
|
5078
|
+
response: toolError(
|
|
5079
|
+
`Datamart "${resolution.name}" is linked from multiple workspaces with different IDs. Call ${params.toolName} again with workspaceName. Matching workspaces: ${workspaceMatch.workspaceNames.join(", ")}`
|
|
5080
|
+
)
|
|
5081
|
+
};
|
|
5082
|
+
}
|
|
5083
|
+
return {
|
|
5084
|
+
ok: false,
|
|
5085
|
+
response: toolError(
|
|
5086
|
+
`Datamart "${resolution.name}" resolved by name but no id/_id/datamartId/dataMartId was returned and no linked workspace exposed its ID. Pass workspaceName for a workspace linked to this datamart, pass datamartId directly, or update the datamart list API to include IDs.`
|
|
5087
|
+
)
|
|
5088
|
+
};
|
|
5089
|
+
}
|
|
5090
|
+
return {
|
|
5091
|
+
ok: true,
|
|
5092
|
+
datamartName: resolution.name,
|
|
5093
|
+
datamartId
|
|
5094
|
+
};
|
|
5095
|
+
}
|
|
5096
|
+
function registerStartSemanticLayerGeneration(server, client) {
|
|
5097
|
+
const services = createServiceBundle(client);
|
|
5098
|
+
server.tool(
|
|
5099
|
+
"start_semantic_layer_generation",
|
|
5100
|
+
"Start backend semantic-layer auto-generation for a datamart. Returns an async job id to poll.",
|
|
5101
|
+
{
|
|
5102
|
+
datamartName: z23.string().optional().describe("Datamart name. Omit to list available datamarts"),
|
|
5103
|
+
workspaceName: z23.string().optional().describe("Workspace linked to the datamart. Use when datamartName resolution needs a dataMartId bridge."),
|
|
5104
|
+
datamartId: z23.string().optional().describe("Datamart UUID. Use only when already known.")
|
|
5105
|
+
},
|
|
5106
|
+
WRITE_CREATE,
|
|
5107
|
+
async (params) => {
|
|
5108
|
+
try {
|
|
5109
|
+
const resolved = await resolveDatamart({
|
|
5110
|
+
client,
|
|
5111
|
+
datamartId: params.datamartId,
|
|
5112
|
+
datamartName: params.datamartName,
|
|
5113
|
+
workspaceName: params.workspaceName,
|
|
5114
|
+
toolName: "start_semantic_layer_generation"
|
|
5115
|
+
});
|
|
5116
|
+
if (!resolved.ok) return resolved.response;
|
|
5117
|
+
const result = await services.queryApi.startSemanticLayerGeneration(
|
|
5118
|
+
resolved.datamartId
|
|
5119
|
+
);
|
|
5120
|
+
return toolSuccess(
|
|
5121
|
+
JSON.stringify(
|
|
5122
|
+
{
|
|
5123
|
+
datamartName: resolved.datamartName,
|
|
5124
|
+
datamartId: resolved.datamartId,
|
|
5125
|
+
workspaceName: resolved.workspaceName,
|
|
5126
|
+
...result,
|
|
5127
|
+
nextStep: "Call get_semantic_layer_generation_status with the same datamartName or workspaceName until status is complete or failed."
|
|
5128
|
+
},
|
|
5129
|
+
null,
|
|
5130
|
+
2
|
|
5131
|
+
)
|
|
5132
|
+
);
|
|
5133
|
+
} catch (err) {
|
|
5134
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5135
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
);
|
|
5139
|
+
}
|
|
5140
|
+
function registerGetSemanticLayerGenerationStatus(server, client) {
|
|
5141
|
+
const services = createServiceBundle(client);
|
|
5142
|
+
server.tool(
|
|
5143
|
+
"get_semantic_layer_generation_status",
|
|
5144
|
+
"Get semantic-layer auto-generation job status for a datamart.",
|
|
5145
|
+
{
|
|
5146
|
+
datamartName: z23.string().optional().describe("Datamart name. Omit to list available datamarts"),
|
|
5147
|
+
workspaceName: z23.string().optional().describe("Workspace linked to the datamart. Use when datamartName resolution needs a dataMartId bridge."),
|
|
5148
|
+
datamartId: z23.string().optional().describe("Datamart UUID. Use only when already known.")
|
|
5149
|
+
},
|
|
5150
|
+
READ_ONLY,
|
|
5151
|
+
async (params) => {
|
|
5152
|
+
try {
|
|
5153
|
+
const resolved = await resolveDatamart({
|
|
5154
|
+
client,
|
|
5155
|
+
datamartId: params.datamartId,
|
|
5156
|
+
datamartName: params.datamartName,
|
|
5157
|
+
workspaceName: params.workspaceName,
|
|
5158
|
+
toolName: "get_semantic_layer_generation_status"
|
|
5159
|
+
});
|
|
5160
|
+
if (!resolved.ok) return resolved.response;
|
|
5161
|
+
const result = await services.queryApi.getSemanticLayerGenerationStatus(
|
|
5162
|
+
resolved.datamartId
|
|
5163
|
+
);
|
|
5164
|
+
return toolSuccess(
|
|
5165
|
+
JSON.stringify(
|
|
5166
|
+
{
|
|
5167
|
+
datamartName: resolved.datamartName,
|
|
5168
|
+
datamartId: resolved.datamartId,
|
|
5169
|
+
workspaceName: resolved.workspaceName,
|
|
5170
|
+
...result
|
|
5171
|
+
},
|
|
5172
|
+
null,
|
|
5173
|
+
2
|
|
5174
|
+
)
|
|
5175
|
+
);
|
|
5176
|
+
} catch (err) {
|
|
5177
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5178
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
);
|
|
5182
|
+
}
|
|
5183
|
+
|
|
5184
|
+
// src/tools/widgets/list-widgets.ts
|
|
5185
|
+
import { z as z24 } from "zod";
|
|
5186
|
+
function registerListWidgets(server, client) {
|
|
5187
|
+
const services = createServiceBundle(client);
|
|
5188
|
+
server.tool(
|
|
5189
|
+
"list_widgets",
|
|
5190
|
+
"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.",
|
|
5191
|
+
{
|
|
5192
|
+
clientId: z24.string().describe("Client/tenant identifier for this dashboard"),
|
|
5193
|
+
dashboardName: z24.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
5194
|
+
workspaceName: z24.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
5195
|
+
embedId: z24.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier.")
|
|
5196
|
+
},
|
|
5197
|
+
READ_ONLY,
|
|
5198
|
+
async (params) => {
|
|
5199
|
+
try {
|
|
5200
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
5201
|
+
embedId: params.embedId,
|
|
5202
|
+
dashboardName: params.dashboardName,
|
|
5203
|
+
workspaceName: params.workspaceName,
|
|
5204
|
+
required: true
|
|
5205
|
+
});
|
|
5206
|
+
if (!embed.ok) return toolError(embed.error);
|
|
5207
|
+
const embedId = embed.value.embedId;
|
|
5208
|
+
const widgets = await services.widgetsApi.listWidgets(
|
|
5209
|
+
embedId,
|
|
5210
|
+
params.clientId
|
|
5211
|
+
);
|
|
5212
|
+
if (!Array.isArray(widgets) || widgets.length === 0) {
|
|
4793
5213
|
return toolSuccess(
|
|
4794
5214
|
`No widgets found on dashboard "${embed.value.resolvedFrom?.dashboardName || embedId}" for client "${params.clientId}".`
|
|
4795
5215
|
);
|
|
@@ -4818,28 +5238,28 @@ function registerListWidgets(server, client) {
|
|
|
4818
5238
|
}
|
|
4819
5239
|
|
|
4820
5240
|
// src/tools/widgets/add-widget.ts
|
|
4821
|
-
import { z as
|
|
5241
|
+
import { z as z25 } from "zod";
|
|
4822
5242
|
function registerAddWidget(server, client) {
|
|
4823
5243
|
const services = createServiceBundle(client);
|
|
4824
5244
|
server.tool(
|
|
4825
5245
|
"add_widget",
|
|
4826
5246
|
"Add a widget to a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous). Cloning via `templateMetricId` is the recommended path \u2014 query, chart config, and datasource are all inherited. Data app and datasource auto-resolve; never ask the user for them.",
|
|
4827
5247
|
{
|
|
4828
|
-
clientId:
|
|
4829
|
-
dashboardName:
|
|
4830
|
-
workspaceName:
|
|
4831
|
-
embedId:
|
|
4832
|
-
dataAppName:
|
|
4833
|
-
templateMetricId:
|
|
4834
|
-
name:
|
|
4835
|
-
metricId:
|
|
4836
|
-
query:
|
|
4837
|
-
chartOptions:
|
|
4838
|
-
description:
|
|
4839
|
-
companyIntegrationId:
|
|
4840
|
-
integrationName:
|
|
4841
|
-
isPublished:
|
|
4842
|
-
body:
|
|
5248
|
+
clientId: z25.string().describe("Client/tenant identifier (widget is created in their scope)"),
|
|
5249
|
+
dashboardName: z25.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
5250
|
+
workspaceName: z25.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
5251
|
+
embedId: z25.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
5252
|
+
dataAppName: z25.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
|
|
5253
|
+
templateMetricId: z25.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
|
|
5254
|
+
name: z25.string().optional().describe("Widget name (overrides template)"),
|
|
5255
|
+
metricId: z25.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
|
|
5256
|
+
query: z25.string().optional().describe("SQL query body (overrides template)"),
|
|
5257
|
+
chartOptions: z25.record(z25.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
|
|
5258
|
+
description: z25.string().optional(),
|
|
5259
|
+
companyIntegrationId: z25.string().optional().describe("Datasource UUID override. Auto-resolved from `embedId` or sole active datasource; leave blank unless the user explicitly picked one by ID."),
|
|
5260
|
+
integrationName: z25.string().optional().describe('Datasource display name (e.g. "Snowflake"). Use this when the user picks a datasource by name \u2014 the id is looked up automatically.'),
|
|
5261
|
+
isPublished: z25.boolean().optional().describe("Publish immediately (default true)"),
|
|
5262
|
+
body: z25.record(z25.unknown()).optional().describe("Raw createMetric body passthrough. If provided, used as-is (after defaults). Overrides all other params.")
|
|
4843
5263
|
},
|
|
4844
5264
|
WRITE_CREATE,
|
|
4845
5265
|
async (params) => {
|
|
@@ -4949,20 +5369,20 @@ Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust
|
|
|
4949
5369
|
}
|
|
4950
5370
|
|
|
4951
5371
|
// src/tools/widgets/update-widget.ts
|
|
4952
|
-
import { z as
|
|
5372
|
+
import { z as z26 } from "zod";
|
|
4953
5373
|
function registerUpdateWidget(server, client) {
|
|
4954
5374
|
const services = createServiceBundle(client);
|
|
4955
5375
|
server.tool(
|
|
4956
5376
|
"update_widget",
|
|
4957
5377
|
"Update a widget on a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous) + `externalMetricId` + `set`. Data app auto-resolves from the dashboard.",
|
|
4958
5378
|
{
|
|
4959
|
-
externalMetricId:
|
|
4960
|
-
clientId:
|
|
4961
|
-
dashboardName:
|
|
4962
|
-
workspaceName:
|
|
4963
|
-
embedId:
|
|
4964
|
-
dataAppName:
|
|
4965
|
-
set:
|
|
5379
|
+
externalMetricId: z26.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
|
|
5380
|
+
clientId: z26.string().describe("Client/tenant identifier"),
|
|
5381
|
+
dashboardName: z26.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
5382
|
+
workspaceName: z26.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
5383
|
+
embedId: z26.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
5384
|
+
dataAppName: z26.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank."),
|
|
5385
|
+
set: z26.record(z26.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
|
|
4966
5386
|
},
|
|
4967
5387
|
WRITE_IDEMPOTENT,
|
|
4968
5388
|
async (params) => {
|
|
@@ -5017,19 +5437,19 @@ Use \`list_widgets\` to verify.`
|
|
|
5017
5437
|
}
|
|
5018
5438
|
|
|
5019
5439
|
// src/tools/widgets/remove-widget.ts
|
|
5020
|
-
import { z as
|
|
5440
|
+
import { z as z27 } from "zod";
|
|
5021
5441
|
function registerRemoveWidget(server, client) {
|
|
5022
5442
|
const services = createServiceBundle(client);
|
|
5023
5443
|
server.tool(
|
|
5024
5444
|
"remove_widget",
|
|
5025
5445
|
"Remove a widget from a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous) + `externalMetricId`. Per-client soft delete \u2014 the widget stays on the template dashboard and on other customers' dashboards.",
|
|
5026
5446
|
{
|
|
5027
|
-
externalMetricId:
|
|
5028
|
-
clientId:
|
|
5029
|
-
dashboardName:
|
|
5030
|
-
workspaceName:
|
|
5031
|
-
embedId:
|
|
5032
|
-
dataAppName:
|
|
5447
|
+
externalMetricId: z27.string().describe("Internal uuid of the widget (externalMetrics.id)"),
|
|
5448
|
+
clientId: z27.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
|
|
5449
|
+
dashboardName: z27.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
5450
|
+
workspaceName: z27.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
5451
|
+
embedId: z27.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
5452
|
+
dataAppName: z27.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank.")
|
|
5033
5453
|
},
|
|
5034
5454
|
DESTRUCTIVE,
|
|
5035
5455
|
async (params) => {
|
|
@@ -5077,21 +5497,21 @@ Note: this is a per-client soft delete. The widget remains available for other c
|
|
|
5077
5497
|
}
|
|
5078
5498
|
|
|
5079
5499
|
// src/tools/widgets/generate-widget.ts
|
|
5080
|
-
import { z as
|
|
5500
|
+
import { z as z28 } from "zod";
|
|
5081
5501
|
function registerGenerateWidget(server, client) {
|
|
5082
5502
|
const services = createServiceBundle(client);
|
|
5083
5503
|
server.tool(
|
|
5084
5504
|
"generate_widget",
|
|
5085
5505
|
"AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft to pass to `add_widget`. Non-technical flow: `clientId` + `dashboardName` (+ `workspaceName` if ambiguous); data app and datasource auto-resolve. Never ask the user for data app, datasource, or integration.",
|
|
5086
5506
|
{
|
|
5087
|
-
clientId:
|
|
5088
|
-
userInputs:
|
|
5089
|
-
dashboardName:
|
|
5090
|
-
workspaceName:
|
|
5091
|
-
embedId:
|
|
5092
|
-
dataAppName:
|
|
5093
|
-
integrationId:
|
|
5094
|
-
integrationName:
|
|
5507
|
+
clientId: z28.string().describe("Client/tenant identifier"),
|
|
5508
|
+
userInputs: z28.record(z28.unknown()).describe('Prompt + context object. Typical shape: { prompt: "calls per day over last 30 days", schema: {...}, examples: [...] }. Backend decides which keys it honors.'),
|
|
5509
|
+
dashboardName: z28.string().optional().describe("Dashboard name (non-technical). Preferred input for CS/CX flows \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
5510
|
+
workspaceName: z28.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
5511
|
+
embedId: z28.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
5512
|
+
dataAppName: z28.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
|
|
5513
|
+
integrationId: z28.string().optional().describe("Technical override \u2014 the datasource UUID. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets."),
|
|
5514
|
+
integrationName: z28.string().optional().describe("Technical override \u2014 datasource display name. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets.")
|
|
5095
5515
|
},
|
|
5096
5516
|
READ_ONLY,
|
|
5097
5517
|
async (params) => {
|
|
@@ -5156,51 +5576,280 @@ function registerGenerateWidget(server, client) {
|
|
|
5156
5576
|
);
|
|
5157
5577
|
}
|
|
5158
5578
|
|
|
5159
|
-
// src/tools/
|
|
5160
|
-
import { z as
|
|
5161
|
-
|
|
5579
|
+
// src/tools/widgets/create-or-update-metric-from-query.ts
|
|
5580
|
+
import { z as z29 } from "zod";
|
|
5581
|
+
var modeSchema = z29.enum(["dry_run", "apply"]).default("dry_run");
|
|
5582
|
+
var conflictStrategySchema = z29.enum(["fail", "update", "create_new"]).default("fail");
|
|
5583
|
+
function makePlanText(plan) {
|
|
5584
|
+
return JSON.stringify(plan, null, 2);
|
|
5585
|
+
}
|
|
5586
|
+
function externalMetricIdFrom(widget) {
|
|
5587
|
+
return typeof widget.externalMetricId === "string" && widget.externalMetricId || typeof widget.id === "string" && widget.id || typeof widget._id === "string" && widget._id || null;
|
|
5588
|
+
}
|
|
5589
|
+
function createSuffix() {
|
|
5590
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
5591
|
+
}
|
|
5592
|
+
function registerCreateOrUpdateMetricFromQuery(server, client) {
|
|
5593
|
+
const services = createServiceBundle(client);
|
|
5162
5594
|
server.tool(
|
|
5163
|
-
"
|
|
5164
|
-
"
|
|
5595
|
+
"create_or_update_metric_from_query",
|
|
5596
|
+
"Create or update a production metric/widget from validated SQL. Defaults to dry-run; apply requires explicit confirmation.",
|
|
5165
5597
|
{
|
|
5166
|
-
|
|
5598
|
+
mode: modeSchema.describe("dry_run returns the plan only; apply executes after confirmation"),
|
|
5599
|
+
confirm: z29.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
|
|
5600
|
+
conflictStrategy: conflictStrategySchema.describe("What to do if metricId already exists on the target dashboard"),
|
|
5601
|
+
clientId: z29.string().describe("Client/tenant identifier for the dashboard widget scope"),
|
|
5602
|
+
workspaceName: z29.string().describe("Target workspace name"),
|
|
5603
|
+
dashboardName: z29.string().optional().describe("Dashboard name to attach to"),
|
|
5604
|
+
embedId: z29.string().optional().describe("Technical embed id alternative to dashboardName"),
|
|
5605
|
+
dataAppName: z29.string().optional().describe("Optional data app override"),
|
|
5606
|
+
name: z29.string().describe("Metric/widget display name"),
|
|
5607
|
+
metricId: z29.string().describe("Stable metric slug to create or update"),
|
|
5608
|
+
query: z29.string().describe("SQL query already validated against the warehouse"),
|
|
5609
|
+
description: z29.string().optional().describe("Metric description"),
|
|
5610
|
+
chartOptions: z29.record(z29.unknown()).optional().describe("Chart configuration JSON"),
|
|
5611
|
+
companyIntegrationId: z29.string().optional().describe("Datasource UUID override"),
|
|
5612
|
+
integrationName: z29.string().optional().describe("Datasource display name override"),
|
|
5613
|
+
isPublished: z29.boolean().optional().describe("Publish after apply. Defaults to false for production safety"),
|
|
5614
|
+
requestedBy: z29.string().optional().describe("Human operator/request context to store on the metric when supported")
|
|
5167
5615
|
},
|
|
5168
5616
|
WRITE_IDEMPOTENT,
|
|
5169
|
-
async (
|
|
5617
|
+
async (params) => {
|
|
5170
5618
|
try {
|
|
5171
|
-
const
|
|
5172
|
-
const
|
|
5173
|
-
|
|
5174
|
-
|
|
5619
|
+
const mode = params.mode || "dry_run";
|
|
5620
|
+
const conflictStrategy = params.conflictStrategy || "fail";
|
|
5621
|
+
if (!params.name.trim()) return toolError("`name` cannot be empty.");
|
|
5622
|
+
if (!params.metricId.trim()) return toolError("`metricId` cannot be empty.");
|
|
5623
|
+
if (!params.query.trim()) return toolError("`query` cannot be empty.");
|
|
5624
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
5625
|
+
embedId: params.embedId,
|
|
5626
|
+
dashboardName: params.dashboardName,
|
|
5627
|
+
workspaceName: params.workspaceName,
|
|
5628
|
+
required: true
|
|
5629
|
+
});
|
|
5630
|
+
if (!embed.ok) return toolError(embed.error);
|
|
5631
|
+
const embedId = embed.value.embedId;
|
|
5632
|
+
const ctx = await services.widgetService.resolveEmbedContext(embedId);
|
|
5633
|
+
if (!ctx.externalDashboardId) {
|
|
5634
|
+
return toolError(
|
|
5635
|
+
`Dashboard "${params.dashboardName || embedId}" has no externalDashboardId. Verify with get_embed_details before creating metrics.`
|
|
5636
|
+
);
|
|
5637
|
+
}
|
|
5638
|
+
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
5639
|
+
params.dataAppName,
|
|
5640
|
+
embedId
|
|
5175
5641
|
);
|
|
5176
|
-
|
|
5177
|
-
const
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5642
|
+
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
5643
|
+
const guestToken = await services.widgetService.mintGuestToken(
|
|
5644
|
+
params.clientId,
|
|
5645
|
+
dataAppName.value,
|
|
5646
|
+
[embedId]
|
|
5647
|
+
);
|
|
5648
|
+
const datasource = await services.widgetService.resolveDatasource(guestToken, {
|
|
5649
|
+
embedId,
|
|
5650
|
+
clientId: params.clientId,
|
|
5651
|
+
integrationId: params.companyIntegrationId,
|
|
5652
|
+
integrationName: params.integrationName
|
|
5653
|
+
});
|
|
5654
|
+
if (!datasource.ok) return toolError(datasource.error);
|
|
5655
|
+
const widgets = await services.widgetsApi.listWidgets(embedId, params.clientId);
|
|
5656
|
+
const existing = widgets.find((widget) => widget.metricId === params.metricId);
|
|
5657
|
+
const existingExternalMetricId = existing ? externalMetricIdFrom(existing) : null;
|
|
5658
|
+
if (existing && conflictStrategy === "fail") {
|
|
5659
|
+
return toolError(
|
|
5660
|
+
`MetricId "${params.metricId}" already exists on this dashboard. Retry with conflictStrategy="update" to update it or "create_new" to create a suffixed copy.`
|
|
5181
5661
|
);
|
|
5182
|
-
}
|
|
5183
|
-
|
|
5184
|
-
|
|
5662
|
+
}
|
|
5663
|
+
if (existing && conflictStrategy === "update" && !existingExternalMetricId) {
|
|
5664
|
+
return toolError(
|
|
5665
|
+
`MetricId "${params.metricId}" exists but no externalMetricId/id was returned, so it cannot be updated safely.`
|
|
5666
|
+
);
|
|
5667
|
+
}
|
|
5668
|
+
const finalMetricId = existing && conflictStrategy === "create_new" ? `${params.metricId}-${createSuffix()}` : params.metricId;
|
|
5669
|
+
const createBody = {
|
|
5670
|
+
name: params.name,
|
|
5671
|
+
metricId: finalMetricId,
|
|
5672
|
+
query: params.query,
|
|
5673
|
+
description: params.description || "",
|
|
5674
|
+
chartOptions: params.chartOptions || {},
|
|
5675
|
+
companyIntegrationId: datasource.value.integrationId,
|
|
5676
|
+
integrationName: datasource.value.integrationName,
|
|
5677
|
+
isPublished: params.isPublished ?? false,
|
|
5678
|
+
isCreatedByClient: false,
|
|
5679
|
+
createdBy: params.requestedBy,
|
|
5680
|
+
externalDashboardIds: [{ externalDashboardId: ctx.externalDashboardId }]
|
|
5681
|
+
};
|
|
5682
|
+
const updateSet = {
|
|
5683
|
+
name: params.name,
|
|
5684
|
+
query: params.query,
|
|
5685
|
+
description: params.description || "",
|
|
5686
|
+
chartOptions: params.chartOptions || {},
|
|
5687
|
+
companyIntegrationId: datasource.value.integrationId,
|
|
5688
|
+
integrationName: datasource.value.integrationName,
|
|
5689
|
+
isPublished: params.isPublished ?? false,
|
|
5690
|
+
createdBy: params.requestedBy
|
|
5691
|
+
};
|
|
5692
|
+
const operation = existing && conflictStrategy === "update" ? "update_metric" : "create_metric";
|
|
5693
|
+
const plan = {
|
|
5694
|
+
mode,
|
|
5695
|
+
operation,
|
|
5696
|
+
safety: productionSafetyNote(params.workspaceName),
|
|
5697
|
+
requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
|
|
5698
|
+
resolved: {
|
|
5699
|
+
workspaceName: params.workspaceName,
|
|
5700
|
+
dashboardName: embed.value.resolvedFrom?.dashboardName || params.dashboardName || null,
|
|
5701
|
+
embedId,
|
|
5702
|
+
dataAppName: dataAppName.value,
|
|
5703
|
+
externalDashboardId: ctx.externalDashboardId,
|
|
5704
|
+
clientId: params.clientId,
|
|
5705
|
+
datasource: datasource.value
|
|
5706
|
+
},
|
|
5707
|
+
conflict: existing ? {
|
|
5708
|
+
metricId: existing.metricId || params.metricId,
|
|
5709
|
+
externalMetricId: existingExternalMetricId,
|
|
5710
|
+
strategy: conflictStrategy
|
|
5711
|
+
} : null,
|
|
5712
|
+
metric: operation === "update_metric" ? { externalMetricId: existingExternalMetricId, set: updateSet } : createBody
|
|
5713
|
+
};
|
|
5714
|
+
if (mode === "dry_run") {
|
|
5715
|
+
return toolSuccess(
|
|
5716
|
+
makePlanText({
|
|
5717
|
+
...plan,
|
|
5718
|
+
status: "dry_run_only",
|
|
5719
|
+
nextStep: `Review this plan. To execute, call again with mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}".`
|
|
5720
|
+
})
|
|
5721
|
+
);
|
|
5722
|
+
}
|
|
5723
|
+
const guardError = validateProductionApply({
|
|
5724
|
+
workspaceName: params.workspaceName,
|
|
5725
|
+
confirm: params.confirm
|
|
5726
|
+
});
|
|
5727
|
+
if (guardError) return toolError(guardError);
|
|
5728
|
+
if (operation === "update_metric") {
|
|
5729
|
+
const result2 = await services.widgetsApi.updateMetric(guestToken, {
|
|
5730
|
+
externalMetricId: existingExternalMetricId,
|
|
5731
|
+
set: updateSet
|
|
5732
|
+
});
|
|
5733
|
+
if (result2.error) {
|
|
5734
|
+
return toolError(`Metric update failed: ${stringify(result2.error)}`);
|
|
5735
|
+
}
|
|
5736
|
+
return toolSuccess(
|
|
5737
|
+
makePlanText({
|
|
5738
|
+
...plan,
|
|
5739
|
+
status: "applied",
|
|
5740
|
+
result: result2,
|
|
5741
|
+
verifyWith: "list_widgets"
|
|
5742
|
+
})
|
|
5185
5743
|
);
|
|
5186
5744
|
}
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5745
|
+
const result = await services.widgetsApi.createMetric(guestToken, createBody);
|
|
5746
|
+
if (result.error) {
|
|
5747
|
+
return toolError(`Metric creation failed: ${stringify(result.error)}`);
|
|
5748
|
+
}
|
|
5749
|
+
return toolSuccess(
|
|
5750
|
+
makePlanText({
|
|
5751
|
+
...plan,
|
|
5752
|
+
status: "applied",
|
|
5753
|
+
result,
|
|
5754
|
+
verifyWith: "list_widgets"
|
|
5755
|
+
})
|
|
5756
|
+
);
|
|
5757
|
+
} catch (err) {
|
|
5758
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5759
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
);
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
// src/tools/widgets/validate-metric-spec.ts
|
|
5766
|
+
import { z as z30 } from "zod";
|
|
5767
|
+
function registerValidateMetricSpec(server, client) {
|
|
5768
|
+
const services = createServiceBundle(client);
|
|
5769
|
+
server.tool(
|
|
5770
|
+
"validate_metric_spec",
|
|
5771
|
+
"Validate a production metric spec from SQL without saving it. Resolves dashboard, data app, datasource, conflicts, and required fields.",
|
|
5772
|
+
{
|
|
5773
|
+
clientId: z30.string().describe("Client/tenant identifier used to validate dashboard widget scope"),
|
|
5774
|
+
workspaceName: z30.string().describe("Target production-backed workspace name"),
|
|
5775
|
+
dashboardName: z30.string().optional().describe("Dashboard name to attach to"),
|
|
5776
|
+
embedId: z30.string().optional().describe("Technical embed id alternative to dashboardName"),
|
|
5777
|
+
dataAppName: z30.string().optional().describe("Optional data app override"),
|
|
5778
|
+
name: z30.string().describe("Metric/widget display name"),
|
|
5779
|
+
metricId: z30.string().describe("Stable metric slug to create or update"),
|
|
5780
|
+
query: z30.string().describe("SQL query already validated against the warehouse"),
|
|
5781
|
+
description: z30.string().optional().describe("Metric description"),
|
|
5782
|
+
chartOptions: z30.record(z30.unknown()).optional().describe("Chart configuration JSON"),
|
|
5783
|
+
companyIntegrationId: z30.string().optional().describe("Datasource UUID override"),
|
|
5784
|
+
integrationName: z30.string().optional().describe("Datasource display name override")
|
|
5785
|
+
},
|
|
5786
|
+
READ_ONLY,
|
|
5787
|
+
async (params) => {
|
|
5788
|
+
try {
|
|
5789
|
+
if (!params.name.trim()) return toolError("`name` cannot be empty.");
|
|
5790
|
+
if (!params.metricId.trim()) return toolError("`metricId` cannot be empty.");
|
|
5791
|
+
if (!params.query.trim()) return toolError("`query` cannot be empty.");
|
|
5792
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
5793
|
+
embedId: params.embedId,
|
|
5794
|
+
dashboardName: params.dashboardName,
|
|
5795
|
+
workspaceName: params.workspaceName,
|
|
5796
|
+
required: true
|
|
5797
|
+
});
|
|
5798
|
+
if (!embed.ok) return toolError(embed.error);
|
|
5799
|
+
const embedId = embed.value.embedId;
|
|
5800
|
+
const ctx = await services.widgetService.resolveEmbedContext(embedId);
|
|
5801
|
+
if (!ctx.externalDashboardId) {
|
|
5802
|
+
return toolError(
|
|
5803
|
+
`Dashboard "${params.dashboardName || embedId}" has no externalDashboardId. Verify with get_embed_details before creating metrics.`
|
|
5190
5804
|
);
|
|
5191
5805
|
}
|
|
5806
|
+
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
5807
|
+
params.dataAppName,
|
|
5808
|
+
embedId
|
|
5809
|
+
);
|
|
5810
|
+
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
5811
|
+
const guestToken = await services.widgetService.mintGuestToken(
|
|
5812
|
+
params.clientId,
|
|
5813
|
+
dataAppName.value,
|
|
5814
|
+
[embedId]
|
|
5815
|
+
);
|
|
5816
|
+
const datasource = await services.widgetService.resolveDatasource(guestToken, {
|
|
5817
|
+
embedId,
|
|
5818
|
+
clientId: params.clientId,
|
|
5819
|
+
integrationId: params.companyIntegrationId,
|
|
5820
|
+
integrationName: params.integrationName
|
|
5821
|
+
});
|
|
5822
|
+
if (!datasource.ok) return toolError(datasource.error);
|
|
5823
|
+
const widgets = await services.widgetsApi.listWidgets(embedId, params.clientId);
|
|
5824
|
+
const existing = widgets.find((widget) => widget.metricId === params.metricId);
|
|
5192
5825
|
return toolSuccess(
|
|
5193
5826
|
JSON.stringify(
|
|
5194
5827
|
{
|
|
5195
|
-
status: "
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5828
|
+
status: "valid_for_production_dry_run",
|
|
5829
|
+
safety: productionSafetyNote(params.workspaceName),
|
|
5830
|
+
resolved: {
|
|
5831
|
+
workspaceName: params.workspaceName,
|
|
5832
|
+
dashboardName: embed.value.resolvedFrom?.dashboardName || params.dashboardName || null,
|
|
5833
|
+
embedId,
|
|
5834
|
+
dataAppName: dataAppName.value,
|
|
5835
|
+
externalDashboardId: ctx.externalDashboardId,
|
|
5836
|
+
datasource
|
|
5837
|
+
},
|
|
5838
|
+
conflict: existing ? {
|
|
5839
|
+
strategyNeeded: true,
|
|
5840
|
+
metricId: existing.metricId,
|
|
5841
|
+
externalMetricId: existing.externalMetricId || existing.id || existing._id || null,
|
|
5842
|
+
name: existing.name || null
|
|
5843
|
+
} : null,
|
|
5844
|
+
proposedMetric: {
|
|
5845
|
+
name: params.name,
|
|
5846
|
+
metricId: params.metricId,
|
|
5847
|
+
query: params.query,
|
|
5848
|
+
description: params.description || "",
|
|
5849
|
+
chartOptions: params.chartOptions || {},
|
|
5850
|
+
isPublishedDefault: false
|
|
5851
|
+
},
|
|
5852
|
+
nextStep: `Call create_or_update_metric_from_query with mode="dry_run" first, then mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}" only after reviewing the plan.`
|
|
5204
5853
|
},
|
|
5205
5854
|
null,
|
|
5206
5855
|
2
|
|
@@ -5208,104 +5857,119 @@ function registerConnect(server, client) {
|
|
|
5208
5857
|
);
|
|
5209
5858
|
} catch (err) {
|
|
5210
5859
|
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5211
|
-
return toolError(`Unexpected error
|
|
5860
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5212
5861
|
}
|
|
5213
5862
|
}
|
|
5214
5863
|
);
|
|
5215
5864
|
}
|
|
5216
5865
|
|
|
5217
|
-
// src/tools/
|
|
5218
|
-
|
|
5866
|
+
// src/tools/reports/list-scheduled-reports.ts
|
|
5867
|
+
import { z as z31 } from "zod";
|
|
5868
|
+
function registerListScheduledReports(server, client) {
|
|
5869
|
+
const services = createServiceBundle(client);
|
|
5219
5870
|
server.tool(
|
|
5220
|
-
"
|
|
5221
|
-
"
|
|
5222
|
-
{
|
|
5871
|
+
"list_scheduled_reports",
|
|
5872
|
+
"List configured scheduled reports for a specific client/dashboard embed. Shows schedule metadata, not delivery success history.",
|
|
5873
|
+
{
|
|
5874
|
+
clientId: z31.string().describe("Client/tenant identifier. Required by the current backend API for non-empty results"),
|
|
5875
|
+
dashboardName: z31.string().optional().describe("Dashboard name to inspect"),
|
|
5876
|
+
workspaceName: z31.string().optional().describe("Workspace name if dashboardName is ambiguous"),
|
|
5877
|
+
embedId: z31.string().optional().describe("Technical embed id alternative to dashboardName"),
|
|
5878
|
+
userIdentifier: z31.string().optional().describe("Optional creator/user filter"),
|
|
5879
|
+
pageNumber: z31.number().int().positive().optional().describe("Page number when pagination is enabled"),
|
|
5880
|
+
isPagination: z31.boolean().optional().describe("Enable backend pagination")
|
|
5881
|
+
},
|
|
5223
5882
|
READ_ONLY,
|
|
5224
|
-
async () => {
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
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
|
-
);
|
|
5883
|
+
async (params) => {
|
|
5884
|
+
try {
|
|
5885
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
5886
|
+
embedId: params.embedId,
|
|
5887
|
+
dashboardName: params.dashboardName,
|
|
5888
|
+
workspaceName: params.workspaceName,
|
|
5889
|
+
required: true
|
|
5890
|
+
});
|
|
5891
|
+
if (!embed.ok) return toolError(embed.error);
|
|
5892
|
+
const embedId = embed.value.embedId;
|
|
5893
|
+
const reports = await services.scheduledReportsApi.listByEmbed({
|
|
5894
|
+
embedId,
|
|
5895
|
+
clientId: params.clientId,
|
|
5896
|
+
userIdentifier: params.userIdentifier,
|
|
5897
|
+
pageNumber: params.pageNumber,
|
|
5898
|
+
isPagination: params.isPagination
|
|
5899
|
+
});
|
|
5253
5900
|
return toolSuccess(
|
|
5254
5901
|
JSON.stringify(
|
|
5255
5902
|
{
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5903
|
+
count: Array.isArray(reports) ? reports.length : 0,
|
|
5904
|
+
dashboardName: embed.value.resolvedFrom?.dashboardName || params.dashboardName || null,
|
|
5905
|
+
embedId,
|
|
5906
|
+
clientId: params.clientId,
|
|
5907
|
+
note: "This lists configured schedules only. Delivery success/failure history is not persisted in the current MCP/API surface yet.",
|
|
5908
|
+
reports: (Array.isArray(reports) ? reports : []).map((report) => ({
|
|
5909
|
+
id: report.id || null,
|
|
5910
|
+
subject: report.subject || null,
|
|
5911
|
+
nextScheduledAt: report.nextScheduledAt || null,
|
|
5912
|
+
recipientCount: Array.isArray(report.emails) ? report.emails.length : null,
|
|
5913
|
+
chartCount: Array.isArray(report.scheduleEmailReportCharts) ? report.scheduleEmailReportCharts.length : null,
|
|
5914
|
+
isDownloadFile: report.isDownloadFile ?? null,
|
|
5915
|
+
downloadFileFormat: report.downloadFileFormat || null,
|
|
5916
|
+
timeConfigurations: report.timeConfigurations || null
|
|
5917
|
+
}))
|
|
5263
5918
|
},
|
|
5264
5919
|
null,
|
|
5265
5920
|
2
|
|
5266
5921
|
)
|
|
5267
5922
|
);
|
|
5923
|
+
} catch (err) {
|
|
5924
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5925
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5268
5926
|
}
|
|
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
5927
|
}
|
|
5281
5928
|
);
|
|
5282
5929
|
}
|
|
5283
5930
|
|
|
5284
|
-
// src/tools/auth/
|
|
5285
|
-
function
|
|
5931
|
+
// src/tools/auth/whoami.ts
|
|
5932
|
+
function registerWhoami(server, client) {
|
|
5286
5933
|
server.tool(
|
|
5287
|
-
"
|
|
5288
|
-
"
|
|
5934
|
+
"whoami",
|
|
5935
|
+
"Return the credential currently used for API calls. Use to confirm which auth mode this MCP session is operating under before mutating actions.",
|
|
5289
5936
|
{},
|
|
5290
|
-
|
|
5937
|
+
READ_ONLY,
|
|
5291
5938
|
async () => {
|
|
5292
|
-
const
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5939
|
+
const auth = client.getActiveAuth();
|
|
5940
|
+
if (auth.source === "service-token") {
|
|
5941
|
+
return toolSuccess(
|
|
5942
|
+
JSON.stringify(
|
|
5943
|
+
{
|
|
5944
|
+
status: "authenticated",
|
|
5945
|
+
activeBearer: "service-token",
|
|
5946
|
+
note: "Org-level service token (DATABRAIN_SERVICE_TOKEN). Per-user identity is NOT bound \u2014 mutations are audit-logged as actor=system. Per-user binding will be available once the OAuth login flow ships."
|
|
5947
|
+
},
|
|
5948
|
+
null,
|
|
5949
|
+
2
|
|
5950
|
+
)
|
|
5951
|
+
);
|
|
5300
5952
|
}
|
|
5301
|
-
if (
|
|
5302
|
-
|
|
5303
|
-
|
|
5953
|
+
if (auth.source === "api-token") {
|
|
5954
|
+
return toolSuccess(
|
|
5955
|
+
JSON.stringify(
|
|
5956
|
+
{
|
|
5957
|
+
status: "authenticated",
|
|
5958
|
+
activeBearer: "api-token",
|
|
5959
|
+
note: "Per-data-app API token. Limited to embedding operations on that data app. Org-level setup tools require DATABRAIN_SERVICE_TOKEN."
|
|
5960
|
+
},
|
|
5961
|
+
null,
|
|
5962
|
+
2
|
|
5963
|
+
)
|
|
5304
5964
|
);
|
|
5305
5965
|
}
|
|
5306
5966
|
return toolSuccess(
|
|
5307
5967
|
JSON.stringify(
|
|
5308
|
-
{
|
|
5968
|
+
{
|
|
5969
|
+
status: "not_authenticated",
|
|
5970
|
+
activeBearer: "none",
|
|
5971
|
+
note: "No usable credential is configured. Set DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN in the MCP config."
|
|
5972
|
+
},
|
|
5309
5973
|
null,
|
|
5310
5974
|
2
|
|
5311
5975
|
)
|
|
@@ -5316,13 +5980,13 @@ function registerDisconnect(server, client) {
|
|
|
5316
5980
|
|
|
5317
5981
|
// src/tools/registry.ts
|
|
5318
5982
|
function registerAllTools(server, client) {
|
|
5319
|
-
registerConnect(server, client);
|
|
5320
5983
|
registerWhoami(server, client);
|
|
5321
|
-
registerDisconnect(server, client);
|
|
5322
5984
|
registerListDatasources(server, client);
|
|
5323
5985
|
registerDiscoverDatasourceSchema(server, client);
|
|
5324
5986
|
registerListDatamarts(server, client);
|
|
5325
5987
|
registerListWorkspaces(server, client);
|
|
5988
|
+
registerSyncDatasource(server, client);
|
|
5989
|
+
registerCreateDatamart(server, client);
|
|
5326
5990
|
registerListDataApps(server, client);
|
|
5327
5991
|
registerCreateDataApp(server, client);
|
|
5328
5992
|
registerListApiTokens(server, client);
|
|
@@ -5340,11 +6004,16 @@ function registerAllTools(server, client) {
|
|
|
5340
6004
|
registerAskQuestion(server, client);
|
|
5341
6005
|
registerGetSemanticLayer(server, client);
|
|
5342
6006
|
registerUpdateSemanticLayer(server, client);
|
|
6007
|
+
registerStartSemanticLayerGeneration(server, client);
|
|
6008
|
+
registerGetSemanticLayerGenerationStatus(server, client);
|
|
5343
6009
|
registerListWidgets(server, client);
|
|
6010
|
+
registerValidateMetricSpec(server, client);
|
|
6011
|
+
registerCreateOrUpdateMetricFromQuery(server, client);
|
|
5344
6012
|
registerAddWidget(server, client);
|
|
5345
6013
|
registerUpdateWidget(server, client);
|
|
5346
6014
|
registerRemoveWidget(server, client);
|
|
5347
6015
|
registerGenerateWidget(server, client);
|
|
6016
|
+
registerListScheduledReports(server, client);
|
|
5348
6017
|
}
|
|
5349
6018
|
|
|
5350
6019
|
// src/resources/content/getting-started.ts
|
|
@@ -7707,7 +8376,7 @@ Then create an embed configuration with tenancy settings:
|
|
|
7707
8376
|
|
|
7708
8377
|
### Step 5: Generate Guest Tokens Per Tenant
|
|
7709
8378
|
|
|
7710
|
-
On your backend, when a user from
|
|
8379
|
+
On your backend, when a user from tenant A (org_id = "tenant_a") logs in:
|
|
7711
8380
|
|
|
7712
8381
|
\`\`\`typescript
|
|
7713
8382
|
// Server-side
|
|
@@ -7719,12 +8388,12 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
|
|
|
7719
8388
|
},
|
|
7720
8389
|
body: JSON.stringify({
|
|
7721
8390
|
embedId: 'embed-789',
|
|
7722
|
-
clientId: '
|
|
8391
|
+
clientId: 'tenant_a', // Maps to org_id values in the database
|
|
7723
8392
|
}),
|
|
7724
8393
|
});
|
|
7725
8394
|
\`\`\`
|
|
7726
8395
|
|
|
7727
|
-
When a user from
|
|
8396
|
+
When a user from tenant B (org_id = "tenant_b") logs in:
|
|
7728
8397
|
|
|
7729
8398
|
\`\`\`typescript
|
|
7730
8399
|
const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/create', {
|
|
@@ -7735,7 +8404,7 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
|
|
|
7735
8404
|
},
|
|
7736
8405
|
body: JSON.stringify({
|
|
7737
8406
|
embedId: 'embed-789',
|
|
7738
|
-
clientId: '
|
|
8407
|
+
clientId: 'tenant_b',
|
|
7739
8408
|
}),
|
|
7740
8409
|
});
|
|
7741
8410
|
\`\`\`
|
|
@@ -7743,20 +8412,20 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
|
|
|
7743
8412
|
### Step 6: Render
|
|
7744
8413
|
|
|
7745
8414
|
Both users see the same dashboard layout and metrics, but:
|
|
7746
|
-
-
|
|
7747
|
-
-
|
|
8415
|
+
- Tenant A sees only their projects, tasks, and team members
|
|
8416
|
+
- Tenant B sees only their projects, tasks, and team members
|
|
7748
8417
|
- Both see all task statuses (shared reference table)
|
|
7749
8418
|
|
|
7750
8419
|
### Result
|
|
7751
8420
|
|
|
7752
8421
|
\`\`\`
|
|
7753
|
-
|
|
7754
|
-
- 12 projects (all with org_id = '
|
|
8422
|
+
Tenant A user sees:
|
|
8423
|
+
- 12 projects (all with org_id = 'tenant_a')
|
|
7755
8424
|
- 87 tasks
|
|
7756
8425
|
- 15 team members
|
|
7757
8426
|
|
|
7758
|
-
|
|
7759
|
-
- 8 projects (all with org_id = '
|
|
8427
|
+
Tenant B user sees:
|
|
8428
|
+
- 8 projects (all with org_id = 'tenant_b')
|
|
7760
8429
|
- 45 tasks
|
|
7761
8430
|
- 9 team members
|
|
7762
8431
|
\`\`\`
|
|
@@ -8312,7 +8981,7 @@ Guidelines for generating descriptions:
|
|
|
8312
8981
|
- **Table descriptions:** Explain what the table contains in 1-2 sentences. Example: \`orders\` \u2192 "Contains customer purchase orders with line items, totals, and fulfillment status."
|
|
8313
8982
|
- **Column descriptions:** Explain the business meaning. Example: \`total_amount\` \u2192 "Total order value in the customer's local currency, including tax and shipping."
|
|
8314
8983
|
- **Synonyms:** Add common alternative names users might say. Example: \`total_amount\` \u2192 ["revenue", "order total", "sales amount"]
|
|
8315
|
-
- **Column types:**
|
|
8984
|
+
- **Column types:** Only use supported semantic types: "String", "Long String", "ENUM", "Mapper", "Range", "Expression", "Identifier", "Number", or "JSON". Omit \`columnType\` for date and boolean database columns.
|
|
8316
8985
|
|
|
8317
8986
|
Ask the user if they want to review and refine the descriptions before applying, or if you should go ahead and apply them directly.
|
|
8318
8987
|
|
|
@@ -8430,6 +9099,108 @@ Present the URL to the user and remind them: (a) only \`isPublished: true\` widg
|
|
|
8430
9099
|
|
|
8431
9100
|
After completion, suggest: \`brand-my-embed\` if they want to adjust theme, or \`manage-embeds\` if they want to tweak access settings / options.`;
|
|
8432
9101
|
|
|
9102
|
+
// src/prompts/sql-to-metric-migration.ts
|
|
9103
|
+
var sqlToMetricMigrationPrompt = `You are helping migrate validated warehouse SQL into Databrain metrics/widgets for a production-backed workspace.
|
|
9104
|
+
|
|
9105
|
+
This workflow is built for agent-assisted SQL migration: the user already has SQL converted and validated against their warehouse. Your job is to resolve the Databrain dashboard/data-app/datasource context, validate the metric spec, produce a dry-run plan, and only apply after explicit confirmation.
|
|
9106
|
+
|
|
9107
|
+
**Critical safety rules**
|
|
9108
|
+
- Default to dry-run. Never apply on the first pass.
|
|
9109
|
+
- Applying requires the exact confirmation string: \`APPLY_TO_PRODUCTION\`.
|
|
9110
|
+
- Service-token mode means writes are attributed as system until OAuth ships. State that clearly before apply.
|
|
9111
|
+
- Do not create datasources or workspaces in this workflow.
|
|
9112
|
+
- If a datamart is needed, use \`create_datamart\` in \`dry_run\` first; apply only after confirmation.
|
|
9113
|
+
- Keep \`isPublished: false\` unless the user explicitly asks to publish.
|
|
9114
|
+
|
|
9115
|
+
**Minimum inputs**
|
|
9116
|
+
- \`clientId\`
|
|
9117
|
+
- \`workspaceName\`
|
|
9118
|
+
- \`dashboardName\` or \`embedId\`
|
|
9119
|
+
- \`metricId\`
|
|
9120
|
+
- \`name\`
|
|
9121
|
+
- \`query\`
|
|
9122
|
+
- Optional: \`description\`, \`chartOptions\`, \`integrationName\`, \`companyIntegrationId\`
|
|
9123
|
+
|
|
9124
|
+
**Step 1 \u2014 Check context**
|
|
9125
|
+
Call:
|
|
9126
|
+
\`\`\`
|
|
9127
|
+
list_workspaces
|
|
9128
|
+
list_dashboards({ workspaceName })
|
|
9129
|
+
list_widgets({ clientId, dashboardName, workspaceName })
|
|
9130
|
+
\`\`\`
|
|
9131
|
+
Show the current target dashboard and existing widgets. If \`metricId\` already exists, tell the user they must choose \`conflictStrategy: "update"\` or \`"create_new"\`.
|
|
9132
|
+
|
|
9133
|
+
**Step 2 \u2014 Validate without saving**
|
|
9134
|
+
Call:
|
|
9135
|
+
\`\`\`
|
|
9136
|
+
validate_metric_spec({
|
|
9137
|
+
clientId,
|
|
9138
|
+
workspaceName,
|
|
9139
|
+
dashboardName,
|
|
9140
|
+
metricId,
|
|
9141
|
+
name,
|
|
9142
|
+
query,
|
|
9143
|
+
description,
|
|
9144
|
+
chartOptions
|
|
9145
|
+
})
|
|
9146
|
+
\`\`\`
|
|
9147
|
+
Present the resolved data app, datasource, embed id, dashboard id, and any conflict.
|
|
9148
|
+
|
|
9149
|
+
**Step 3 \u2014 Dry-run the write**
|
|
9150
|
+
Call:
|
|
9151
|
+
\`\`\`
|
|
9152
|
+
create_or_update_metric_from_query({
|
|
9153
|
+
mode: "dry_run",
|
|
9154
|
+
conflictStrategy,
|
|
9155
|
+
clientId,
|
|
9156
|
+
workspaceName,
|
|
9157
|
+
dashboardName,
|
|
9158
|
+
metricId,
|
|
9159
|
+
name,
|
|
9160
|
+
query,
|
|
9161
|
+
description,
|
|
9162
|
+
chartOptions,
|
|
9163
|
+
isPublished: false
|
|
9164
|
+
})
|
|
9165
|
+
\`\`\`
|
|
9166
|
+
Show the exact plan. Stop and ask the user to review it.
|
|
9167
|
+
|
|
9168
|
+
**Step 4 \u2014 Apply only after explicit confirmation**
|
|
9169
|
+
Only after the user approves, call:
|
|
9170
|
+
\`\`\`
|
|
9171
|
+
create_or_update_metric_from_query({
|
|
9172
|
+
mode: "apply",
|
|
9173
|
+
confirm: "APPLY_TO_PRODUCTION",
|
|
9174
|
+
conflictStrategy,
|
|
9175
|
+
clientId,
|
|
9176
|
+
workspaceName,
|
|
9177
|
+
dashboardName,
|
|
9178
|
+
metricId,
|
|
9179
|
+
name,
|
|
9180
|
+
query,
|
|
9181
|
+
description,
|
|
9182
|
+
chartOptions,
|
|
9183
|
+
isPublished: false,
|
|
9184
|
+
requestedBy: "<human/operator context>"
|
|
9185
|
+
})
|
|
9186
|
+
\`\`\`
|
|
9187
|
+
Never invent the confirmation. The user must provide explicit approval.
|
|
9188
|
+
|
|
9189
|
+
**Step 5 \u2014 Verify**
|
|
9190
|
+
Call:
|
|
9191
|
+
\`\`\`
|
|
9192
|
+
list_widgets({ clientId, dashboardName, workspaceName })
|
|
9193
|
+
get_demo_link({ clientId, dashboardName, workspaceName })
|
|
9194
|
+
\`\`\`
|
|
9195
|
+
Tell the user that unpublished widgets may not appear in the preview until published.
|
|
9196
|
+
|
|
9197
|
+
**Scheduled reports**
|
|
9198
|
+
If the user asks about schedule visibility, call:
|
|
9199
|
+
\`\`\`
|
|
9200
|
+
list_scheduled_reports({ clientId, dashboardName, workspaceName })
|
|
9201
|
+
\`\`\`
|
|
9202
|
+
Clarify that this lists configured schedules only. Delivery success/failure history is not yet persisted in the MCP/API surface.`;
|
|
9203
|
+
|
|
8433
9204
|
// src/prompts/registry.ts
|
|
8434
9205
|
var prompts = [
|
|
8435
9206
|
{
|
|
@@ -8461,6 +9232,11 @@ var prompts = [
|
|
|
8461
9232
|
name: "customize-customer-dashboard",
|
|
8462
9233
|
description: "Add, modify, or remove widgets on a specific customer's dashboard embed. Per-client scope \u2014 changes only affect that customer.",
|
|
8463
9234
|
content: customizeCustomerDashboardPrompt
|
|
9235
|
+
},
|
|
9236
|
+
{
|
|
9237
|
+
name: "sql-to-metric-migration",
|
|
9238
|
+
description: "Production-safe SQL-to-metric migration workflow: validate, dry-run, then apply with explicit confirmation.",
|
|
9239
|
+
content: sqlToMetricMigrationPrompt
|
|
8464
9240
|
}
|
|
8465
9241
|
];
|
|
8466
9242
|
function registerAllPrompts(server) {
|
|
@@ -8484,34 +9260,35 @@ function createServer(config) {
|
|
|
8484
9260
|
const client = new DatabrainClient(config);
|
|
8485
9261
|
const auth = client.getActiveAuth();
|
|
8486
9262
|
let operatorBlurb;
|
|
8487
|
-
if (auth.source === "
|
|
8488
|
-
operatorBlurb = "OPERATOR IDENTITY:
|
|
8489
|
-
} else if (auth.source === "
|
|
8490
|
-
|
|
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
|
-
`;
|
|
9263
|
+
if (auth.source === "service-token") {
|
|
9264
|
+
operatorBlurb = "OPERATOR IDENTITY: service-token mode. Calls are authenticated at org level \u2014 actions in the audit log are tagged actor=system, not a specific user. Per-user identity binding will be added when the OAuth-based login flow ships. Do NOT ask the user for credentials or tokens.\n\n";
|
|
9265
|
+
} else if (auth.source === "api-token") {
|
|
9266
|
+
operatorBlurb = "OPERATOR IDENTITY: API-token-only mode. A per-data-app API token is configured, but DATABRAIN_SERVICE_TOKEN is not. Embedding operations can use the configured API token, but org-level setup tools require DATABRAIN_SERVICE_TOKEN in the MCP config.\n\n";
|
|
8494
9267
|
} else {
|
|
8495
|
-
operatorBlurb = "OPERATOR IDENTITY:
|
|
9268
|
+
operatorBlurb = "OPERATOR IDENTITY: no usable token is configured. The MCP server normally refuses this config at startup. Ask the user to add DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN to the MCP config before proceeding.\n\n";
|
|
8496
9269
|
}
|
|
9270
|
+
const serviceTokenStatus = config.serviceToken ? "This MCP server has DATABRAIN_SERVICE_TOKEN configured; never ask the user for it." : "DATABRAIN_SERVICE_TOKEN is not configured. Ask the user to add it to the MCP config only if org-level setup is needed.";
|
|
9271
|
+
const apiTokenStatus = client.hasApiToken() ? "This MCP server has a per-data-app API token configured." : "Created/rotated via create_api_token or rotate_api_token during setup when DATABRAIN_SERVICE_TOKEN is available.";
|
|
8497
9272
|
const server = new McpServer(
|
|
8498
9273
|
{
|
|
8499
9274
|
name: "databrain",
|
|
8500
|
-
version:
|
|
8501
|
-
description: "Databrain MCP Server \u2014 embed analytics dashboards and charts into your app.
|
|
9275
|
+
version: MCP_SERVER_VERSION,
|
|
9276
|
+
description: "Databrain MCP Server \u2014 embed analytics dashboards and charts into your app. Supports embed setup, semantic-layer maintenance, production dry-run/apply metric creation, theming, filters, permissions, and guest token generation."
|
|
8502
9277
|
},
|
|
8503
9278
|
{
|
|
8504
9279
|
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
|
|
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)
|
|
9280
|
+
\u2022 SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, sync_datasource, list_datamarts, create_datamart, list_workspaces, list_data_apps, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status. ${serviceTokenStatus}
|
|
9281
|
+
\u2022 API TOKEN (per data app): scoped to one data app, used for embedding operations (list_embeds, get_embed_details, create_embed, update_embed, generate_guest_token), data-app scoped dashboard listing, widget CRUD/metric creation through guest-token APIs, and list_scheduled_reports. ${apiTokenStatus}
|
|
8507
9282
|
|
|
8508
|
-
|
|
9283
|
+
4 core use cases \u2014 match the user's scenario to the right prompt:
|
|
8509
9284
|
1. Embed existing dashboard \u2192 prompt: embed-existing-dashboard (list_data_apps \u2192 create_api_token \u2192 list_dashboards \u2192 create_embed \u2192 generate_guest_token \u2192 generate_embed_code)
|
|
8510
9285
|
2. Blank dashboard per client \u2192 prompt: embed-blank-dashboard (list_data_apps \u2192 list_datamarts \u2192 create_embed with clientId per client \u2192 generate_guest_token \u2192 generate_embed_code)
|
|
8511
9286
|
3. Explore & query data \u2192 prompt: query-data (list_datamarts \u2192 get_semantic_layer \u2192 if empty, auto-generate descriptions with update_semantic_layer or use populate-semantic-layer prompt \u2192 ask_question)
|
|
9287
|
+
4. SQL-to-metric migration \u2192 prompt: sql-to-metric-migration (validate_metric_spec \u2192 create_or_update_metric_from_query dry_run \u2192 apply only with explicit production confirmation)
|
|
9288
|
+
|
|
9289
|
+
Production safety: OAuth may not be available yet. In service-token mode, writes are actor=system. For create_datamart and create_or_update_metric_from_query, dry_run is the default. Apply requires confirm="APPLY_TO_PRODUCTION" after the user reviews the plan. Do not apply from a vague request.
|
|
8512
9290
|
|
|
8513
|
-
Infrastructure
|
|
8514
|
-
However, the **semantic layer** is populated and maintained entirely via MCP tools (get_semantic_layer, update_semantic_layer). When the semantic layer is empty, YOU (the assistant) should auto-generate descriptions from table/column names and push them with update_semantic_layer \u2014 do NOT send the user to the UI for this.
|
|
9291
|
+
Infrastructure creation is intentionally narrow in MCP: datasource and workspace creation still happen outside this workflow; sync_datasource and dry-run/apply create_datamart are available. The semantic layer is populated and maintained via MCP tools (get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status). When the semantic layer is empty, use these tools \u2014 do NOT send the user to the UI for semantic metadata.
|
|
8515
9292
|
|
|
8516
9293
|
After embed setup, customize with: brand-my-embed prompt for theming. Use update_embed (body passthrough) for filters, localization, and permissions.
|
|
8517
9294
|
Use generate_guest_token for frontend auth and generate_embed_code for framework snippets.
|
|
@@ -8535,7 +9312,6 @@ Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, fi
|
|
|
8535
9312
|
}
|
|
8536
9313
|
|
|
8537
9314
|
// src/index.ts
|
|
8538
|
-
var VERSION = "0.2.0";
|
|
8539
9315
|
function banner(msg) {
|
|
8540
9316
|
process.stderr.write(`[databrain] ${msg}
|
|
8541
9317
|
`);
|
|
@@ -8546,7 +9322,7 @@ async function main() {
|
|
|
8546
9322
|
const server = createServer(config);
|
|
8547
9323
|
const transport = new StdioServerTransport();
|
|
8548
9324
|
await server.connect(transport);
|
|
8549
|
-
banner(`v${
|
|
9325
|
+
banner(`v${MCP_SERVER_VERSION} ready`);
|
|
8550
9326
|
} catch (error) {
|
|
8551
9327
|
logger.error("Failed to start server", error);
|
|
8552
9328
|
process.exit(1);
|