@databrainhq/mcp-server 0.2.11 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -17
- package/dist/index.js +1266 -652
- 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)",
|
|
@@ -4681,30 +4800,58 @@ var columnSchema = z20.object({
|
|
|
4681
4800
|
"JSON"
|
|
4682
4801
|
]).nullable().optional().describe('Semantic column type. Use "Identifier" for IDs/foreign keys, "Number" for numeric values, "String" for text. Omit for date columns.')
|
|
4683
4802
|
});
|
|
4684
|
-
var
|
|
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 normalizeTablesForApi(tables) {
|
|
4815
|
+
if (!tables) return void 0;
|
|
4816
|
+
return tables.map((table) => {
|
|
4817
|
+
const normalized = { name: table.name };
|
|
4818
|
+
if (table.description !== void 0) normalized.description = table.description;
|
|
4819
|
+
if (table.synonyms !== void 0) normalized.synonyms = table.synonyms;
|
|
4820
|
+
if (table.miscellaneousInfo !== void 0) {
|
|
4821
|
+
normalized.miscellaneousInfo = table.miscellaneousInfo;
|
|
4822
|
+
}
|
|
4823
|
+
if (table.columns !== void 0) {
|
|
4824
|
+
normalized.columns = table.columns.map((column) => {
|
|
4825
|
+
const normalizedColumn = { name: column.name };
|
|
4826
|
+
if (column.description !== void 0) {
|
|
4827
|
+
normalizedColumn.description = column.description;
|
|
4828
|
+
}
|
|
4829
|
+
if (column.synonyms !== void 0) normalizedColumn.synonyms = column.synonyms;
|
|
4830
|
+
if (column.miscellaneousInfo !== void 0) {
|
|
4831
|
+
normalizedColumn.miscellaneousInfo = column.miscellaneousInfo;
|
|
4832
|
+
}
|
|
4833
|
+
if (column.columnType !== void 0 && column.columnType !== null) {
|
|
4834
|
+
normalizedColumn.columnType = column.columnType;
|
|
4835
|
+
}
|
|
4836
|
+
return normalizedColumn;
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4839
|
+
return normalized;
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4695
4842
|
function registerUpdateSemanticLayer(server, client) {
|
|
4696
4843
|
const services = createServiceBundle(client);
|
|
4697
4844
|
server.tool(
|
|
4698
4845
|
"update_semantic_layer",
|
|
4699
4846
|
"Populate or update the semantic layer for a datamart. Add table/column descriptions, synonyms, context. Required before ask_question works. Auto-generate descriptions from column names when empty.",
|
|
4700
4847
|
{
|
|
4701
|
-
datamartName:
|
|
4848
|
+
datamartName: z22.string().optional().describe(
|
|
4702
4849
|
"Name of the datamart to update. Omit to list available datamarts."
|
|
4703
4850
|
),
|
|
4704
|
-
tables:
|
|
4851
|
+
tables: z22.array(tableSchema2).optional().describe(
|
|
4705
4852
|
"Table and column definitions to update. Only include tables/columns you want to change."
|
|
4706
4853
|
),
|
|
4707
|
-
feedback:
|
|
4854
|
+
feedback: z22.string().optional().describe(
|
|
4708
4855
|
'General feedback or context about the datamart (e.g. "This datamart contains e-commerce data for the US market").'
|
|
4709
4856
|
)
|
|
4710
4857
|
},
|
|
@@ -4727,9 +4874,10 @@ function registerUpdateSemanticLayer(server, client) {
|
|
|
4727
4874
|
});
|
|
4728
4875
|
if (resolution.kind !== "resolved") return resolution.response;
|
|
4729
4876
|
const datamartName = resolution.name;
|
|
4877
|
+
const tables = normalizeTablesForApi(params.tables);
|
|
4730
4878
|
await services.queryApi.updateSemanticLayer({
|
|
4731
4879
|
datamartName,
|
|
4732
|
-
tables
|
|
4880
|
+
tables,
|
|
4733
4881
|
feedback: params.feedback
|
|
4734
4882
|
});
|
|
4735
4883
|
const updatedParts = [];
|
|
@@ -4761,18 +4909,128 @@ You can now:
|
|
|
4761
4909
|
);
|
|
4762
4910
|
}
|
|
4763
4911
|
|
|
4912
|
+
// src/tools/query/semantic-generation.ts
|
|
4913
|
+
import { z as z23 } from "zod";
|
|
4914
|
+
function datamartIdFrom(datamart) {
|
|
4915
|
+
return typeof datamart.id === "string" && datamart.id || typeof datamart._id === "string" && datamart._id || typeof datamart.datamartId === "string" && datamart.datamartId || null;
|
|
4916
|
+
}
|
|
4917
|
+
async function resolveDatamart(params) {
|
|
4918
|
+
const services = createServiceBundle(params.client);
|
|
4919
|
+
const datamarts = await services.datamartsApi.listDatamarts();
|
|
4920
|
+
const resolution = resolveResource({
|
|
4921
|
+
items: datamarts,
|
|
4922
|
+
name: params.datamartName,
|
|
4923
|
+
getName: (dm) => dm.name || dm.datamartName,
|
|
4924
|
+
formatItem: formatDatamart,
|
|
4925
|
+
resourceType: "datamart",
|
|
4926
|
+
toolName: params.toolName
|
|
4927
|
+
});
|
|
4928
|
+
if (resolution.kind !== "resolved") return { ok: false, response: resolution.response };
|
|
4929
|
+
const datamartId = datamartIdFrom(resolution.selected);
|
|
4930
|
+
if (!datamartId) {
|
|
4931
|
+
return {
|
|
4932
|
+
ok: false,
|
|
4933
|
+
response: toolError(
|
|
4934
|
+
`Datamart "${resolution.name}" resolved by name but no id/_id/datamartId was returned. Use list_datamarts to verify the API response includes IDs.`
|
|
4935
|
+
)
|
|
4936
|
+
};
|
|
4937
|
+
}
|
|
4938
|
+
return {
|
|
4939
|
+
ok: true,
|
|
4940
|
+
datamartName: resolution.name,
|
|
4941
|
+
datamartId
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
function registerStartSemanticLayerGeneration(server, client) {
|
|
4945
|
+
const services = createServiceBundle(client);
|
|
4946
|
+
server.tool(
|
|
4947
|
+
"start_semantic_layer_generation",
|
|
4948
|
+
"Start backend semantic-layer auto-generation for a datamart. Returns an async job id to poll.",
|
|
4949
|
+
{
|
|
4950
|
+
datamartName: z23.string().optional().describe("Datamart name. Omit to list available datamarts")
|
|
4951
|
+
},
|
|
4952
|
+
WRITE_CREATE,
|
|
4953
|
+
async (params) => {
|
|
4954
|
+
try {
|
|
4955
|
+
const resolved = await resolveDatamart({
|
|
4956
|
+
client,
|
|
4957
|
+
datamartName: params.datamartName,
|
|
4958
|
+
toolName: "start_semantic_layer_generation"
|
|
4959
|
+
});
|
|
4960
|
+
if (!resolved.ok) return resolved.response;
|
|
4961
|
+
const result = await services.queryApi.startSemanticLayerGeneration(
|
|
4962
|
+
resolved.datamartId
|
|
4963
|
+
);
|
|
4964
|
+
return toolSuccess(
|
|
4965
|
+
JSON.stringify(
|
|
4966
|
+
{
|
|
4967
|
+
datamartName: resolved.datamartName,
|
|
4968
|
+
datamartId: resolved.datamartId,
|
|
4969
|
+
...result,
|
|
4970
|
+
nextStep: "Call get_semantic_layer_generation_status with the same datamartName until status is complete or failed."
|
|
4971
|
+
},
|
|
4972
|
+
null,
|
|
4973
|
+
2
|
|
4974
|
+
)
|
|
4975
|
+
);
|
|
4976
|
+
} catch (err) {
|
|
4977
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
4978
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
);
|
|
4982
|
+
}
|
|
4983
|
+
function registerGetSemanticLayerGenerationStatus(server, client) {
|
|
4984
|
+
const services = createServiceBundle(client);
|
|
4985
|
+
server.tool(
|
|
4986
|
+
"get_semantic_layer_generation_status",
|
|
4987
|
+
"Get semantic-layer auto-generation job status for a datamart.",
|
|
4988
|
+
{
|
|
4989
|
+
datamartName: z23.string().optional().describe("Datamart name. Omit to list available datamarts")
|
|
4990
|
+
},
|
|
4991
|
+
READ_ONLY,
|
|
4992
|
+
async (params) => {
|
|
4993
|
+
try {
|
|
4994
|
+
const resolved = await resolveDatamart({
|
|
4995
|
+
client,
|
|
4996
|
+
datamartName: params.datamartName,
|
|
4997
|
+
toolName: "get_semantic_layer_generation_status"
|
|
4998
|
+
});
|
|
4999
|
+
if (!resolved.ok) return resolved.response;
|
|
5000
|
+
const result = await services.queryApi.getSemanticLayerGenerationStatus(
|
|
5001
|
+
resolved.datamartId
|
|
5002
|
+
);
|
|
5003
|
+
return toolSuccess(
|
|
5004
|
+
JSON.stringify(
|
|
5005
|
+
{
|
|
5006
|
+
datamartName: resolved.datamartName,
|
|
5007
|
+
datamartId: resolved.datamartId,
|
|
5008
|
+
...result
|
|
5009
|
+
},
|
|
5010
|
+
null,
|
|
5011
|
+
2
|
|
5012
|
+
)
|
|
5013
|
+
);
|
|
5014
|
+
} catch (err) {
|
|
5015
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5016
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5017
|
+
}
|
|
5018
|
+
}
|
|
5019
|
+
);
|
|
5020
|
+
}
|
|
5021
|
+
|
|
4764
5022
|
// src/tools/widgets/list-widgets.ts
|
|
4765
|
-
import { z as
|
|
5023
|
+
import { z as z24 } from "zod";
|
|
4766
5024
|
function registerListWidgets(server, client) {
|
|
4767
5025
|
const services = createServiceBundle(client);
|
|
4768
5026
|
server.tool(
|
|
4769
5027
|
"list_widgets",
|
|
4770
5028
|
"List widgets on a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous). Returns each widget's externalMetricId (uuid, used for update/remove), metricId (slug), name, published state, and creator identifier.",
|
|
4771
5029
|
{
|
|
4772
|
-
clientId:
|
|
4773
|
-
dashboardName:
|
|
4774
|
-
workspaceName:
|
|
4775
|
-
embedId:
|
|
5030
|
+
clientId: z24.string().describe("Client/tenant identifier for this dashboard"),
|
|
5031
|
+
dashboardName: z24.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
5032
|
+
workspaceName: z24.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
5033
|
+
embedId: z24.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier.")
|
|
4776
5034
|
},
|
|
4777
5035
|
READ_ONLY,
|
|
4778
5036
|
async (params) => {
|
|
@@ -4818,28 +5076,28 @@ function registerListWidgets(server, client) {
|
|
|
4818
5076
|
}
|
|
4819
5077
|
|
|
4820
5078
|
// src/tools/widgets/add-widget.ts
|
|
4821
|
-
import { z as
|
|
5079
|
+
import { z as z25 } from "zod";
|
|
4822
5080
|
function registerAddWidget(server, client) {
|
|
4823
5081
|
const services = createServiceBundle(client);
|
|
4824
5082
|
server.tool(
|
|
4825
5083
|
"add_widget",
|
|
4826
5084
|
"Add a widget to a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous). Cloning via `templateMetricId` is the recommended path \u2014 query, chart config, and datasource are all inherited. Data app and datasource auto-resolve; never ask the user for them.",
|
|
4827
5085
|
{
|
|
4828
|
-
clientId:
|
|
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:
|
|
5086
|
+
clientId: z25.string().describe("Client/tenant identifier (widget is created in their scope)"),
|
|
5087
|
+
dashboardName: z25.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
5088
|
+
workspaceName: z25.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
5089
|
+
embedId: z25.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
5090
|
+
dataAppName: z25.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
|
|
5091
|
+
templateMetricId: z25.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
|
|
5092
|
+
name: z25.string().optional().describe("Widget name (overrides template)"),
|
|
5093
|
+
metricId: z25.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
|
|
5094
|
+
query: z25.string().optional().describe("SQL query body (overrides template)"),
|
|
5095
|
+
chartOptions: z25.record(z25.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
|
|
5096
|
+
description: z25.string().optional(),
|
|
5097
|
+
companyIntegrationId: z25.string().optional().describe("Datasource UUID override. Auto-resolved from `embedId` or sole active datasource; leave blank unless the user explicitly picked one by ID."),
|
|
5098
|
+
integrationName: z25.string().optional().describe('Datasource display name (e.g. "Snowflake"). Use this when the user picks a datasource by name \u2014 the id is looked up automatically.'),
|
|
5099
|
+
isPublished: z25.boolean().optional().describe("Publish immediately (default true)"),
|
|
5100
|
+
body: z25.record(z25.unknown()).optional().describe("Raw createMetric body passthrough. If provided, used as-is (after defaults). Overrides all other params.")
|
|
4843
5101
|
},
|
|
4844
5102
|
WRITE_CREATE,
|
|
4845
5103
|
async (params) => {
|
|
@@ -4949,20 +5207,20 @@ Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust
|
|
|
4949
5207
|
}
|
|
4950
5208
|
|
|
4951
5209
|
// src/tools/widgets/update-widget.ts
|
|
4952
|
-
import { z as
|
|
5210
|
+
import { z as z26 } from "zod";
|
|
4953
5211
|
function registerUpdateWidget(server, client) {
|
|
4954
5212
|
const services = createServiceBundle(client);
|
|
4955
5213
|
server.tool(
|
|
4956
5214
|
"update_widget",
|
|
4957
5215
|
"Update a widget on a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous) + `externalMetricId` + `set`. Data app auto-resolves from the dashboard.",
|
|
4958
5216
|
{
|
|
4959
|
-
externalMetricId:
|
|
4960
|
-
clientId:
|
|
4961
|
-
dashboardName:
|
|
4962
|
-
workspaceName:
|
|
4963
|
-
embedId:
|
|
4964
|
-
dataAppName:
|
|
4965
|
-
set:
|
|
5217
|
+
externalMetricId: z26.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
|
|
5218
|
+
clientId: z26.string().describe("Client/tenant identifier"),
|
|
5219
|
+
dashboardName: z26.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
5220
|
+
workspaceName: z26.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
5221
|
+
embedId: z26.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
5222
|
+
dataAppName: z26.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank."),
|
|
5223
|
+
set: z26.record(z26.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
|
|
4966
5224
|
},
|
|
4967
5225
|
WRITE_IDEMPOTENT,
|
|
4968
5226
|
async (params) => {
|
|
@@ -5017,19 +5275,19 @@ Use \`list_widgets\` to verify.`
|
|
|
5017
5275
|
}
|
|
5018
5276
|
|
|
5019
5277
|
// src/tools/widgets/remove-widget.ts
|
|
5020
|
-
import { z as
|
|
5278
|
+
import { z as z27 } from "zod";
|
|
5021
5279
|
function registerRemoveWidget(server, client) {
|
|
5022
5280
|
const services = createServiceBundle(client);
|
|
5023
5281
|
server.tool(
|
|
5024
5282
|
"remove_widget",
|
|
5025
5283
|
"Remove a widget from a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous) + `externalMetricId`. Per-client soft delete \u2014 the widget stays on the template dashboard and on other customers' dashboards.",
|
|
5026
5284
|
{
|
|
5027
|
-
externalMetricId:
|
|
5028
|
-
clientId:
|
|
5029
|
-
dashboardName:
|
|
5030
|
-
workspaceName:
|
|
5031
|
-
embedId:
|
|
5032
|
-
dataAppName:
|
|
5285
|
+
externalMetricId: z27.string().describe("Internal uuid of the widget (externalMetrics.id)"),
|
|
5286
|
+
clientId: z27.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
|
|
5287
|
+
dashboardName: z27.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
5288
|
+
workspaceName: z27.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
5289
|
+
embedId: z27.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
5290
|
+
dataAppName: z27.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank.")
|
|
5033
5291
|
},
|
|
5034
5292
|
DESTRUCTIVE,
|
|
5035
5293
|
async (params) => {
|
|
@@ -5077,21 +5335,21 @@ Note: this is a per-client soft delete. The widget remains available for other c
|
|
|
5077
5335
|
}
|
|
5078
5336
|
|
|
5079
5337
|
// src/tools/widgets/generate-widget.ts
|
|
5080
|
-
import { z as
|
|
5338
|
+
import { z as z28 } from "zod";
|
|
5081
5339
|
function registerGenerateWidget(server, client) {
|
|
5082
5340
|
const services = createServiceBundle(client);
|
|
5083
5341
|
server.tool(
|
|
5084
5342
|
"generate_widget",
|
|
5085
5343
|
"AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft to pass to `add_widget`. Non-technical flow: `clientId` + `dashboardName` (+ `workspaceName` if ambiguous); data app and datasource auto-resolve. Never ask the user for data app, datasource, or integration.",
|
|
5086
5344
|
{
|
|
5087
|
-
clientId:
|
|
5088
|
-
userInputs:
|
|
5089
|
-
dashboardName:
|
|
5090
|
-
workspaceName:
|
|
5091
|
-
embedId:
|
|
5092
|
-
dataAppName:
|
|
5093
|
-
integrationId:
|
|
5094
|
-
integrationName:
|
|
5345
|
+
clientId: z28.string().describe("Client/tenant identifier"),
|
|
5346
|
+
userInputs: z28.record(z28.unknown()).describe('Prompt + context object. Typical shape: { prompt: "calls per day over last 30 days", schema: {...}, examples: [...] }. Backend decides which keys it honors.'),
|
|
5347
|
+
dashboardName: z28.string().optional().describe("Dashboard name (non-technical). Preferred input for CS/CX flows \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
5348
|
+
workspaceName: z28.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
5349
|
+
embedId: z28.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
5350
|
+
dataAppName: z28.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
|
|
5351
|
+
integrationId: z28.string().optional().describe("Technical override \u2014 the datasource UUID. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets."),
|
|
5352
|
+
integrationName: z28.string().optional().describe("Technical override \u2014 datasource display name. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets.")
|
|
5095
5353
|
},
|
|
5096
5354
|
READ_ONLY,
|
|
5097
5355
|
async (params) => {
|
|
@@ -5156,51 +5414,280 @@ function registerGenerateWidget(server, client) {
|
|
|
5156
5414
|
);
|
|
5157
5415
|
}
|
|
5158
5416
|
|
|
5159
|
-
// src/tools/
|
|
5160
|
-
import { z as
|
|
5161
|
-
|
|
5417
|
+
// src/tools/widgets/create-or-update-metric-from-query.ts
|
|
5418
|
+
import { z as z29 } from "zod";
|
|
5419
|
+
var modeSchema = z29.enum(["dry_run", "apply"]).default("dry_run");
|
|
5420
|
+
var conflictStrategySchema = z29.enum(["fail", "update", "create_new"]).default("fail");
|
|
5421
|
+
function makePlanText(plan) {
|
|
5422
|
+
return JSON.stringify(plan, null, 2);
|
|
5423
|
+
}
|
|
5424
|
+
function externalMetricIdFrom(widget) {
|
|
5425
|
+
return typeof widget.externalMetricId === "string" && widget.externalMetricId || typeof widget.id === "string" && widget.id || typeof widget._id === "string" && widget._id || null;
|
|
5426
|
+
}
|
|
5427
|
+
function createSuffix() {
|
|
5428
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
5429
|
+
}
|
|
5430
|
+
function registerCreateOrUpdateMetricFromQuery(server, client) {
|
|
5431
|
+
const services = createServiceBundle(client);
|
|
5162
5432
|
server.tool(
|
|
5163
|
-
"
|
|
5164
|
-
"
|
|
5433
|
+
"create_or_update_metric_from_query",
|
|
5434
|
+
"Create or update a production metric/widget from validated SQL. Defaults to dry-run; apply requires explicit confirmation.",
|
|
5165
5435
|
{
|
|
5166
|
-
|
|
5436
|
+
mode: modeSchema.describe("dry_run returns the plan only; apply executes after confirmation"),
|
|
5437
|
+
confirm: z29.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
|
|
5438
|
+
conflictStrategy: conflictStrategySchema.describe("What to do if metricId already exists on the target dashboard"),
|
|
5439
|
+
clientId: z29.string().describe("Client/tenant identifier for the dashboard widget scope"),
|
|
5440
|
+
workspaceName: z29.string().describe("Target workspace name"),
|
|
5441
|
+
dashboardName: z29.string().optional().describe("Dashboard name to attach to"),
|
|
5442
|
+
embedId: z29.string().optional().describe("Technical embed id alternative to dashboardName"),
|
|
5443
|
+
dataAppName: z29.string().optional().describe("Optional data app override"),
|
|
5444
|
+
name: z29.string().describe("Metric/widget display name"),
|
|
5445
|
+
metricId: z29.string().describe("Stable metric slug to create or update"),
|
|
5446
|
+
query: z29.string().describe("SQL query already validated against the warehouse"),
|
|
5447
|
+
description: z29.string().optional().describe("Metric description"),
|
|
5448
|
+
chartOptions: z29.record(z29.unknown()).optional().describe("Chart configuration JSON"),
|
|
5449
|
+
companyIntegrationId: z29.string().optional().describe("Datasource UUID override"),
|
|
5450
|
+
integrationName: z29.string().optional().describe("Datasource display name override"),
|
|
5451
|
+
isPublished: z29.boolean().optional().describe("Publish after apply. Defaults to false for production safety"),
|
|
5452
|
+
requestedBy: z29.string().optional().describe("Human operator/request context to store on the metric when supported")
|
|
5167
5453
|
},
|
|
5168
5454
|
WRITE_IDEMPOTENT,
|
|
5169
|
-
async (
|
|
5455
|
+
async (params) => {
|
|
5170
5456
|
try {
|
|
5171
|
-
const
|
|
5172
|
-
const
|
|
5173
|
-
|
|
5174
|
-
|
|
5457
|
+
const mode = params.mode || "dry_run";
|
|
5458
|
+
const conflictStrategy = params.conflictStrategy || "fail";
|
|
5459
|
+
if (!params.name.trim()) return toolError("`name` cannot be empty.");
|
|
5460
|
+
if (!params.metricId.trim()) return toolError("`metricId` cannot be empty.");
|
|
5461
|
+
if (!params.query.trim()) return toolError("`query` cannot be empty.");
|
|
5462
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
5463
|
+
embedId: params.embedId,
|
|
5464
|
+
dashboardName: params.dashboardName,
|
|
5465
|
+
workspaceName: params.workspaceName,
|
|
5466
|
+
required: true
|
|
5467
|
+
});
|
|
5468
|
+
if (!embed.ok) return toolError(embed.error);
|
|
5469
|
+
const embedId = embed.value.embedId;
|
|
5470
|
+
const ctx = await services.widgetService.resolveEmbedContext(embedId);
|
|
5471
|
+
if (!ctx.externalDashboardId) {
|
|
5472
|
+
return toolError(
|
|
5473
|
+
`Dashboard "${params.dashboardName || embedId}" has no externalDashboardId. Verify with get_embed_details before creating metrics.`
|
|
5474
|
+
);
|
|
5475
|
+
}
|
|
5476
|
+
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
5477
|
+
params.dataAppName,
|
|
5478
|
+
embedId
|
|
5175
5479
|
);
|
|
5176
|
-
|
|
5177
|
-
const
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5480
|
+
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
5481
|
+
const guestToken = await services.widgetService.mintGuestToken(
|
|
5482
|
+
params.clientId,
|
|
5483
|
+
dataAppName.value,
|
|
5484
|
+
[embedId]
|
|
5485
|
+
);
|
|
5486
|
+
const datasource = await services.widgetService.resolveDatasource(guestToken, {
|
|
5487
|
+
embedId,
|
|
5488
|
+
clientId: params.clientId,
|
|
5489
|
+
integrationId: params.companyIntegrationId,
|
|
5490
|
+
integrationName: params.integrationName
|
|
5491
|
+
});
|
|
5492
|
+
if (!datasource.ok) return toolError(datasource.error);
|
|
5493
|
+
const widgets = await services.widgetsApi.listWidgets(embedId, params.clientId);
|
|
5494
|
+
const existing = widgets.find((widget) => widget.metricId === params.metricId);
|
|
5495
|
+
const existingExternalMetricId = existing ? externalMetricIdFrom(existing) : null;
|
|
5496
|
+
if (existing && conflictStrategy === "fail") {
|
|
5497
|
+
return toolError(
|
|
5498
|
+
`MetricId "${params.metricId}" already exists on this dashboard. Retry with conflictStrategy="update" to update it or "create_new" to create a suffixed copy.`
|
|
5181
5499
|
);
|
|
5182
|
-
}
|
|
5183
|
-
|
|
5184
|
-
|
|
5500
|
+
}
|
|
5501
|
+
if (existing && conflictStrategy === "update" && !existingExternalMetricId) {
|
|
5502
|
+
return toolError(
|
|
5503
|
+
`MetricId "${params.metricId}" exists but no externalMetricId/id was returned, so it cannot be updated safely.`
|
|
5504
|
+
);
|
|
5505
|
+
}
|
|
5506
|
+
const finalMetricId = existing && conflictStrategy === "create_new" ? `${params.metricId}-${createSuffix()}` : params.metricId;
|
|
5507
|
+
const createBody = {
|
|
5508
|
+
name: params.name,
|
|
5509
|
+
metricId: finalMetricId,
|
|
5510
|
+
query: params.query,
|
|
5511
|
+
description: params.description || "",
|
|
5512
|
+
chartOptions: params.chartOptions || {},
|
|
5513
|
+
companyIntegrationId: datasource.value.integrationId,
|
|
5514
|
+
integrationName: datasource.value.integrationName,
|
|
5515
|
+
isPublished: params.isPublished ?? false,
|
|
5516
|
+
isCreatedByClient: false,
|
|
5517
|
+
createdBy: params.requestedBy,
|
|
5518
|
+
externalDashboardIds: [{ externalDashboardId: ctx.externalDashboardId }]
|
|
5519
|
+
};
|
|
5520
|
+
const updateSet = {
|
|
5521
|
+
name: params.name,
|
|
5522
|
+
query: params.query,
|
|
5523
|
+
description: params.description || "",
|
|
5524
|
+
chartOptions: params.chartOptions || {},
|
|
5525
|
+
companyIntegrationId: datasource.value.integrationId,
|
|
5526
|
+
integrationName: datasource.value.integrationName,
|
|
5527
|
+
isPublished: params.isPublished ?? false,
|
|
5528
|
+
createdBy: params.requestedBy
|
|
5529
|
+
};
|
|
5530
|
+
const operation = existing && conflictStrategy === "update" ? "update_metric" : "create_metric";
|
|
5531
|
+
const plan = {
|
|
5532
|
+
mode,
|
|
5533
|
+
operation,
|
|
5534
|
+
safety: productionSafetyNote(params.workspaceName),
|
|
5535
|
+
requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
|
|
5536
|
+
resolved: {
|
|
5537
|
+
workspaceName: params.workspaceName,
|
|
5538
|
+
dashboardName: embed.value.resolvedFrom?.dashboardName || params.dashboardName || null,
|
|
5539
|
+
embedId,
|
|
5540
|
+
dataAppName: dataAppName.value,
|
|
5541
|
+
externalDashboardId: ctx.externalDashboardId,
|
|
5542
|
+
clientId: params.clientId,
|
|
5543
|
+
datasource: datasource.value
|
|
5544
|
+
},
|
|
5545
|
+
conflict: existing ? {
|
|
5546
|
+
metricId: existing.metricId || params.metricId,
|
|
5547
|
+
externalMetricId: existingExternalMetricId,
|
|
5548
|
+
strategy: conflictStrategy
|
|
5549
|
+
} : null,
|
|
5550
|
+
metric: operation === "update_metric" ? { externalMetricId: existingExternalMetricId, set: updateSet } : createBody
|
|
5551
|
+
};
|
|
5552
|
+
if (mode === "dry_run") {
|
|
5553
|
+
return toolSuccess(
|
|
5554
|
+
makePlanText({
|
|
5555
|
+
...plan,
|
|
5556
|
+
status: "dry_run_only",
|
|
5557
|
+
nextStep: `Review this plan. To execute, call again with mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}".`
|
|
5558
|
+
})
|
|
5185
5559
|
);
|
|
5186
5560
|
}
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5561
|
+
const guardError = validateProductionApply({
|
|
5562
|
+
workspaceName: params.workspaceName,
|
|
5563
|
+
confirm: params.confirm
|
|
5564
|
+
});
|
|
5565
|
+
if (guardError) return toolError(guardError);
|
|
5566
|
+
if (operation === "update_metric") {
|
|
5567
|
+
const result2 = await services.widgetsApi.updateMetric(guestToken, {
|
|
5568
|
+
externalMetricId: existingExternalMetricId,
|
|
5569
|
+
set: updateSet
|
|
5570
|
+
});
|
|
5571
|
+
if (result2.error) {
|
|
5572
|
+
return toolError(`Metric update failed: ${stringify(result2.error)}`);
|
|
5573
|
+
}
|
|
5574
|
+
return toolSuccess(
|
|
5575
|
+
makePlanText({
|
|
5576
|
+
...plan,
|
|
5577
|
+
status: "applied",
|
|
5578
|
+
result: result2,
|
|
5579
|
+
verifyWith: "list_widgets"
|
|
5580
|
+
})
|
|
5581
|
+
);
|
|
5582
|
+
}
|
|
5583
|
+
const result = await services.widgetsApi.createMetric(guestToken, createBody);
|
|
5584
|
+
if (result.error) {
|
|
5585
|
+
return toolError(`Metric creation failed: ${stringify(result.error)}`);
|
|
5586
|
+
}
|
|
5587
|
+
return toolSuccess(
|
|
5588
|
+
makePlanText({
|
|
5589
|
+
...plan,
|
|
5590
|
+
status: "applied",
|
|
5591
|
+
result,
|
|
5592
|
+
verifyWith: "list_widgets"
|
|
5593
|
+
})
|
|
5594
|
+
);
|
|
5595
|
+
} catch (err) {
|
|
5596
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5597
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
);
|
|
5601
|
+
}
|
|
5602
|
+
|
|
5603
|
+
// src/tools/widgets/validate-metric-spec.ts
|
|
5604
|
+
import { z as z30 } from "zod";
|
|
5605
|
+
function registerValidateMetricSpec(server, client) {
|
|
5606
|
+
const services = createServiceBundle(client);
|
|
5607
|
+
server.tool(
|
|
5608
|
+
"validate_metric_spec",
|
|
5609
|
+
"Validate a production metric spec from SQL without saving it. Resolves dashboard, data app, datasource, conflicts, and required fields.",
|
|
5610
|
+
{
|
|
5611
|
+
clientId: z30.string().describe("Client/tenant identifier used to validate dashboard widget scope"),
|
|
5612
|
+
workspaceName: z30.string().describe("Target production-backed workspace name"),
|
|
5613
|
+
dashboardName: z30.string().optional().describe("Dashboard name to attach to"),
|
|
5614
|
+
embedId: z30.string().optional().describe("Technical embed id alternative to dashboardName"),
|
|
5615
|
+
dataAppName: z30.string().optional().describe("Optional data app override"),
|
|
5616
|
+
name: z30.string().describe("Metric/widget display name"),
|
|
5617
|
+
metricId: z30.string().describe("Stable metric slug to create or update"),
|
|
5618
|
+
query: z30.string().describe("SQL query already validated against the warehouse"),
|
|
5619
|
+
description: z30.string().optional().describe("Metric description"),
|
|
5620
|
+
chartOptions: z30.record(z30.unknown()).optional().describe("Chart configuration JSON"),
|
|
5621
|
+
companyIntegrationId: z30.string().optional().describe("Datasource UUID override"),
|
|
5622
|
+
integrationName: z30.string().optional().describe("Datasource display name override")
|
|
5623
|
+
},
|
|
5624
|
+
READ_ONLY,
|
|
5625
|
+
async (params) => {
|
|
5626
|
+
try {
|
|
5627
|
+
if (!params.name.trim()) return toolError("`name` cannot be empty.");
|
|
5628
|
+
if (!params.metricId.trim()) return toolError("`metricId` cannot be empty.");
|
|
5629
|
+
if (!params.query.trim()) return toolError("`query` cannot be empty.");
|
|
5630
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
5631
|
+
embedId: params.embedId,
|
|
5632
|
+
dashboardName: params.dashboardName,
|
|
5633
|
+
workspaceName: params.workspaceName,
|
|
5634
|
+
required: true
|
|
5635
|
+
});
|
|
5636
|
+
if (!embed.ok) return toolError(embed.error);
|
|
5637
|
+
const embedId = embed.value.embedId;
|
|
5638
|
+
const ctx = await services.widgetService.resolveEmbedContext(embedId);
|
|
5639
|
+
if (!ctx.externalDashboardId) {
|
|
5640
|
+
return toolError(
|
|
5641
|
+
`Dashboard "${params.dashboardName || embedId}" has no externalDashboardId. Verify with get_embed_details before creating metrics.`
|
|
5190
5642
|
);
|
|
5191
5643
|
}
|
|
5644
|
+
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
5645
|
+
params.dataAppName,
|
|
5646
|
+
embedId
|
|
5647
|
+
);
|
|
5648
|
+
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
5649
|
+
const guestToken = await services.widgetService.mintGuestToken(
|
|
5650
|
+
params.clientId,
|
|
5651
|
+
dataAppName.value,
|
|
5652
|
+
[embedId]
|
|
5653
|
+
);
|
|
5654
|
+
const datasource = await services.widgetService.resolveDatasource(guestToken, {
|
|
5655
|
+
embedId,
|
|
5656
|
+
clientId: params.clientId,
|
|
5657
|
+
integrationId: params.companyIntegrationId,
|
|
5658
|
+
integrationName: params.integrationName
|
|
5659
|
+
});
|
|
5660
|
+
if (!datasource.ok) return toolError(datasource.error);
|
|
5661
|
+
const widgets = await services.widgetsApi.listWidgets(embedId, params.clientId);
|
|
5662
|
+
const existing = widgets.find((widget) => widget.metricId === params.metricId);
|
|
5192
5663
|
return toolSuccess(
|
|
5193
5664
|
JSON.stringify(
|
|
5194
5665
|
{
|
|
5195
|
-
status: "
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5666
|
+
status: "valid_for_production_dry_run",
|
|
5667
|
+
safety: productionSafetyNote(params.workspaceName),
|
|
5668
|
+
resolved: {
|
|
5669
|
+
workspaceName: params.workspaceName,
|
|
5670
|
+
dashboardName: embed.value.resolvedFrom?.dashboardName || params.dashboardName || null,
|
|
5671
|
+
embedId,
|
|
5672
|
+
dataAppName: dataAppName.value,
|
|
5673
|
+
externalDashboardId: ctx.externalDashboardId,
|
|
5674
|
+
datasource
|
|
5675
|
+
},
|
|
5676
|
+
conflict: existing ? {
|
|
5677
|
+
strategyNeeded: true,
|
|
5678
|
+
metricId: existing.metricId,
|
|
5679
|
+
externalMetricId: existing.externalMetricId || existing.id || existing._id || null,
|
|
5680
|
+
name: existing.name || null
|
|
5681
|
+
} : null,
|
|
5682
|
+
proposedMetric: {
|
|
5683
|
+
name: params.name,
|
|
5684
|
+
metricId: params.metricId,
|
|
5685
|
+
query: params.query,
|
|
5686
|
+
description: params.description || "",
|
|
5687
|
+
chartOptions: params.chartOptions || {},
|
|
5688
|
+
isPublishedDefault: false
|
|
5689
|
+
},
|
|
5690
|
+
nextStep: `Call create_or_update_metric_from_query with mode="dry_run" first, then mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}" only after reviewing the plan.`
|
|
5204
5691
|
},
|
|
5205
5692
|
null,
|
|
5206
5693
|
2
|
|
@@ -5208,104 +5695,119 @@ function registerConnect(server, client) {
|
|
|
5208
5695
|
);
|
|
5209
5696
|
} catch (err) {
|
|
5210
5697
|
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5211
|
-
return toolError(`Unexpected error
|
|
5698
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5212
5699
|
}
|
|
5213
5700
|
}
|
|
5214
5701
|
);
|
|
5215
5702
|
}
|
|
5216
5703
|
|
|
5217
|
-
// src/tools/
|
|
5218
|
-
|
|
5704
|
+
// src/tools/reports/list-scheduled-reports.ts
|
|
5705
|
+
import { z as z31 } from "zod";
|
|
5706
|
+
function registerListScheduledReports(server, client) {
|
|
5707
|
+
const services = createServiceBundle(client);
|
|
5219
5708
|
server.tool(
|
|
5220
|
-
"
|
|
5221
|
-
"
|
|
5222
|
-
{
|
|
5709
|
+
"list_scheduled_reports",
|
|
5710
|
+
"List configured scheduled reports for a specific client/dashboard embed. Shows schedule metadata, not delivery success history.",
|
|
5711
|
+
{
|
|
5712
|
+
clientId: z31.string().describe("Client/tenant identifier. Required by the current backend API for non-empty results"),
|
|
5713
|
+
dashboardName: z31.string().optional().describe("Dashboard name to inspect"),
|
|
5714
|
+
workspaceName: z31.string().optional().describe("Workspace name if dashboardName is ambiguous"),
|
|
5715
|
+
embedId: z31.string().optional().describe("Technical embed id alternative to dashboardName"),
|
|
5716
|
+
userIdentifier: z31.string().optional().describe("Optional creator/user filter"),
|
|
5717
|
+
pageNumber: z31.number().int().positive().optional().describe("Page number when pagination is enabled"),
|
|
5718
|
+
isPagination: z31.boolean().optional().describe("Enable backend pagination")
|
|
5719
|
+
},
|
|
5223
5720
|
READ_ONLY,
|
|
5224
|
-
async () => {
|
|
5225
|
-
|
|
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
|
-
);
|
|
5721
|
+
async (params) => {
|
|
5722
|
+
try {
|
|
5723
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
5724
|
+
embedId: params.embedId,
|
|
5725
|
+
dashboardName: params.dashboardName,
|
|
5726
|
+
workspaceName: params.workspaceName,
|
|
5727
|
+
required: true
|
|
5728
|
+
});
|
|
5729
|
+
if (!embed.ok) return toolError(embed.error);
|
|
5730
|
+
const embedId = embed.value.embedId;
|
|
5731
|
+
const reports = await services.scheduledReportsApi.listByEmbed({
|
|
5732
|
+
embedId,
|
|
5733
|
+
clientId: params.clientId,
|
|
5734
|
+
userIdentifier: params.userIdentifier,
|
|
5735
|
+
pageNumber: params.pageNumber,
|
|
5736
|
+
isPagination: params.isPagination
|
|
5737
|
+
});
|
|
5253
5738
|
return toolSuccess(
|
|
5254
5739
|
JSON.stringify(
|
|
5255
5740
|
{
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5741
|
+
count: Array.isArray(reports) ? reports.length : 0,
|
|
5742
|
+
dashboardName: embed.value.resolvedFrom?.dashboardName || params.dashboardName || null,
|
|
5743
|
+
embedId,
|
|
5744
|
+
clientId: params.clientId,
|
|
5745
|
+
note: "This lists configured schedules only. Delivery success/failure history is not persisted in the current MCP/API surface yet.",
|
|
5746
|
+
reports: (Array.isArray(reports) ? reports : []).map((report) => ({
|
|
5747
|
+
id: report.id || null,
|
|
5748
|
+
subject: report.subject || null,
|
|
5749
|
+
nextScheduledAt: report.nextScheduledAt || null,
|
|
5750
|
+
recipientCount: Array.isArray(report.emails) ? report.emails.length : null,
|
|
5751
|
+
chartCount: Array.isArray(report.scheduleEmailReportCharts) ? report.scheduleEmailReportCharts.length : null,
|
|
5752
|
+
isDownloadFile: report.isDownloadFile ?? null,
|
|
5753
|
+
downloadFileFormat: report.downloadFileFormat || null,
|
|
5754
|
+
timeConfigurations: report.timeConfigurations || null
|
|
5755
|
+
}))
|
|
5263
5756
|
},
|
|
5264
5757
|
null,
|
|
5265
5758
|
2
|
|
5266
5759
|
)
|
|
5267
5760
|
);
|
|
5761
|
+
} catch (err) {
|
|
5762
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5763
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5268
5764
|
}
|
|
5269
|
-
return toolSuccess(
|
|
5270
|
-
JSON.stringify(
|
|
5271
|
-
{
|
|
5272
|
-
status: "not_connected",
|
|
5273
|
-
activeBearer: auth.source,
|
|
5274
|
-
note: "No operator bound to this MCP session. Calls fall back to the org-level service token (no per-user audit). Either run `connect <email>` to bootstrap interactively, or paste a Personal Access Token into your MCP config as DATABRAIN_PAT."
|
|
5275
|
-
},
|
|
5276
|
-
null,
|
|
5277
|
-
2
|
|
5278
|
-
)
|
|
5279
|
-
);
|
|
5280
5765
|
}
|
|
5281
5766
|
);
|
|
5282
5767
|
}
|
|
5283
5768
|
|
|
5284
|
-
// src/tools/auth/
|
|
5285
|
-
function
|
|
5769
|
+
// src/tools/auth/whoami.ts
|
|
5770
|
+
function registerWhoami(server, client) {
|
|
5286
5771
|
server.tool(
|
|
5287
|
-
"
|
|
5288
|
-
"
|
|
5772
|
+
"whoami",
|
|
5773
|
+
"Return the credential currently used for API calls. Use to confirm which auth mode this MCP session is operating under before mutating actions.",
|
|
5289
5774
|
{},
|
|
5290
|
-
|
|
5775
|
+
READ_ONLY,
|
|
5291
5776
|
async () => {
|
|
5292
|
-
const
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5777
|
+
const auth = client.getActiveAuth();
|
|
5778
|
+
if (auth.source === "service-token") {
|
|
5779
|
+
return toolSuccess(
|
|
5780
|
+
JSON.stringify(
|
|
5781
|
+
{
|
|
5782
|
+
status: "authenticated",
|
|
5783
|
+
activeBearer: "service-token",
|
|
5784
|
+
note: "Org-level service token (DATABRAIN_SERVICE_TOKEN). Per-user identity is NOT bound \u2014 mutations are audit-logged as actor=system. Per-user binding will be available once the OAuth login flow ships."
|
|
5785
|
+
},
|
|
5786
|
+
null,
|
|
5787
|
+
2
|
|
5788
|
+
)
|
|
5789
|
+
);
|
|
5300
5790
|
}
|
|
5301
|
-
if (
|
|
5302
|
-
|
|
5303
|
-
|
|
5791
|
+
if (auth.source === "api-token") {
|
|
5792
|
+
return toolSuccess(
|
|
5793
|
+
JSON.stringify(
|
|
5794
|
+
{
|
|
5795
|
+
status: "authenticated",
|
|
5796
|
+
activeBearer: "api-token",
|
|
5797
|
+
note: "Per-data-app API token. Limited to embedding operations on that data app. Org-level setup tools require DATABRAIN_SERVICE_TOKEN."
|
|
5798
|
+
},
|
|
5799
|
+
null,
|
|
5800
|
+
2
|
|
5801
|
+
)
|
|
5304
5802
|
);
|
|
5305
5803
|
}
|
|
5306
5804
|
return toolSuccess(
|
|
5307
5805
|
JSON.stringify(
|
|
5308
|
-
{
|
|
5806
|
+
{
|
|
5807
|
+
status: "not_authenticated",
|
|
5808
|
+
activeBearer: "none",
|
|
5809
|
+
note: "No usable credential is configured. Set DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN in the MCP config."
|
|
5810
|
+
},
|
|
5309
5811
|
null,
|
|
5310
5812
|
2
|
|
5311
5813
|
)
|
|
@@ -5316,13 +5818,13 @@ function registerDisconnect(server, client) {
|
|
|
5316
5818
|
|
|
5317
5819
|
// src/tools/registry.ts
|
|
5318
5820
|
function registerAllTools(server, client) {
|
|
5319
|
-
registerConnect(server, client);
|
|
5320
5821
|
registerWhoami(server, client);
|
|
5321
|
-
registerDisconnect(server, client);
|
|
5322
5822
|
registerListDatasources(server, client);
|
|
5323
5823
|
registerDiscoverDatasourceSchema(server, client);
|
|
5324
5824
|
registerListDatamarts(server, client);
|
|
5325
5825
|
registerListWorkspaces(server, client);
|
|
5826
|
+
registerSyncDatasource(server, client);
|
|
5827
|
+
registerCreateDatamart(server, client);
|
|
5326
5828
|
registerListDataApps(server, client);
|
|
5327
5829
|
registerCreateDataApp(server, client);
|
|
5328
5830
|
registerListApiTokens(server, client);
|
|
@@ -5340,11 +5842,16 @@ function registerAllTools(server, client) {
|
|
|
5340
5842
|
registerAskQuestion(server, client);
|
|
5341
5843
|
registerGetSemanticLayer(server, client);
|
|
5342
5844
|
registerUpdateSemanticLayer(server, client);
|
|
5845
|
+
registerStartSemanticLayerGeneration(server, client);
|
|
5846
|
+
registerGetSemanticLayerGenerationStatus(server, client);
|
|
5343
5847
|
registerListWidgets(server, client);
|
|
5848
|
+
registerValidateMetricSpec(server, client);
|
|
5849
|
+
registerCreateOrUpdateMetricFromQuery(server, client);
|
|
5344
5850
|
registerAddWidget(server, client);
|
|
5345
5851
|
registerUpdateWidget(server, client);
|
|
5346
5852
|
registerRemoveWidget(server, client);
|
|
5347
5853
|
registerGenerateWidget(server, client);
|
|
5854
|
+
registerListScheduledReports(server, client);
|
|
5348
5855
|
}
|
|
5349
5856
|
|
|
5350
5857
|
// src/resources/content/getting-started.ts
|
|
@@ -7707,7 +8214,7 @@ Then create an embed configuration with tenancy settings:
|
|
|
7707
8214
|
|
|
7708
8215
|
### Step 5: Generate Guest Tokens Per Tenant
|
|
7709
8216
|
|
|
7710
|
-
On your backend, when a user from
|
|
8217
|
+
On your backend, when a user from tenant A (org_id = "tenant_a") logs in:
|
|
7711
8218
|
|
|
7712
8219
|
\`\`\`typescript
|
|
7713
8220
|
// Server-side
|
|
@@ -7719,12 +8226,12 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
|
|
|
7719
8226
|
},
|
|
7720
8227
|
body: JSON.stringify({
|
|
7721
8228
|
embedId: 'embed-789',
|
|
7722
|
-
clientId: '
|
|
8229
|
+
clientId: 'tenant_a', // Maps to org_id values in the database
|
|
7723
8230
|
}),
|
|
7724
8231
|
});
|
|
7725
8232
|
\`\`\`
|
|
7726
8233
|
|
|
7727
|
-
When a user from
|
|
8234
|
+
When a user from tenant B (org_id = "tenant_b") logs in:
|
|
7728
8235
|
|
|
7729
8236
|
\`\`\`typescript
|
|
7730
8237
|
const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/create', {
|
|
@@ -7735,7 +8242,7 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
|
|
|
7735
8242
|
},
|
|
7736
8243
|
body: JSON.stringify({
|
|
7737
8244
|
embedId: 'embed-789',
|
|
7738
|
-
clientId: '
|
|
8245
|
+
clientId: 'tenant_b',
|
|
7739
8246
|
}),
|
|
7740
8247
|
});
|
|
7741
8248
|
\`\`\`
|
|
@@ -7743,20 +8250,20 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
|
|
|
7743
8250
|
### Step 6: Render
|
|
7744
8251
|
|
|
7745
8252
|
Both users see the same dashboard layout and metrics, but:
|
|
7746
|
-
-
|
|
7747
|
-
-
|
|
8253
|
+
- Tenant A sees only their projects, tasks, and team members
|
|
8254
|
+
- Tenant B sees only their projects, tasks, and team members
|
|
7748
8255
|
- Both see all task statuses (shared reference table)
|
|
7749
8256
|
|
|
7750
8257
|
### Result
|
|
7751
8258
|
|
|
7752
8259
|
\`\`\`
|
|
7753
|
-
|
|
7754
|
-
- 12 projects (all with org_id = '
|
|
8260
|
+
Tenant A user sees:
|
|
8261
|
+
- 12 projects (all with org_id = 'tenant_a')
|
|
7755
8262
|
- 87 tasks
|
|
7756
8263
|
- 15 team members
|
|
7757
8264
|
|
|
7758
|
-
|
|
7759
|
-
- 8 projects (all with org_id = '
|
|
8265
|
+
Tenant B user sees:
|
|
8266
|
+
- 8 projects (all with org_id = 'tenant_b')
|
|
7760
8267
|
- 45 tasks
|
|
7761
8268
|
- 9 team members
|
|
7762
8269
|
\`\`\`
|
|
@@ -8430,6 +8937,108 @@ Present the URL to the user and remind them: (a) only \`isPublished: true\` widg
|
|
|
8430
8937
|
|
|
8431
8938
|
After completion, suggest: \`brand-my-embed\` if they want to adjust theme, or \`manage-embeds\` if they want to tweak access settings / options.`;
|
|
8432
8939
|
|
|
8940
|
+
// src/prompts/sql-to-metric-migration.ts
|
|
8941
|
+
var sqlToMetricMigrationPrompt = `You are helping migrate validated warehouse SQL into Databrain metrics/widgets for a production-backed workspace.
|
|
8942
|
+
|
|
8943
|
+
This workflow is built for agent-assisted SQL migration: the user already has SQL converted and validated against their warehouse. Your job is to resolve the Databrain dashboard/data-app/datasource context, validate the metric spec, produce a dry-run plan, and only apply after explicit confirmation.
|
|
8944
|
+
|
|
8945
|
+
**Critical safety rules**
|
|
8946
|
+
- Default to dry-run. Never apply on the first pass.
|
|
8947
|
+
- Applying requires the exact confirmation string: \`APPLY_TO_PRODUCTION\`.
|
|
8948
|
+
- Service-token mode means writes are attributed as system until OAuth ships. State that clearly before apply.
|
|
8949
|
+
- Do not create datasources or workspaces in this workflow.
|
|
8950
|
+
- If a datamart is needed, use \`create_datamart\` in \`dry_run\` first; apply only after confirmation.
|
|
8951
|
+
- Keep \`isPublished: false\` unless the user explicitly asks to publish.
|
|
8952
|
+
|
|
8953
|
+
**Minimum inputs**
|
|
8954
|
+
- \`clientId\`
|
|
8955
|
+
- \`workspaceName\`
|
|
8956
|
+
- \`dashboardName\` or \`embedId\`
|
|
8957
|
+
- \`metricId\`
|
|
8958
|
+
- \`name\`
|
|
8959
|
+
- \`query\`
|
|
8960
|
+
- Optional: \`description\`, \`chartOptions\`, \`integrationName\`, \`companyIntegrationId\`
|
|
8961
|
+
|
|
8962
|
+
**Step 1 \u2014 Check context**
|
|
8963
|
+
Call:
|
|
8964
|
+
\`\`\`
|
|
8965
|
+
list_workspaces
|
|
8966
|
+
list_dashboards({ workspaceName })
|
|
8967
|
+
list_widgets({ clientId, dashboardName, workspaceName })
|
|
8968
|
+
\`\`\`
|
|
8969
|
+
Show the current target dashboard and existing widgets. If \`metricId\` already exists, tell the user they must choose \`conflictStrategy: "update"\` or \`"create_new"\`.
|
|
8970
|
+
|
|
8971
|
+
**Step 2 \u2014 Validate without saving**
|
|
8972
|
+
Call:
|
|
8973
|
+
\`\`\`
|
|
8974
|
+
validate_metric_spec({
|
|
8975
|
+
clientId,
|
|
8976
|
+
workspaceName,
|
|
8977
|
+
dashboardName,
|
|
8978
|
+
metricId,
|
|
8979
|
+
name,
|
|
8980
|
+
query,
|
|
8981
|
+
description,
|
|
8982
|
+
chartOptions
|
|
8983
|
+
})
|
|
8984
|
+
\`\`\`
|
|
8985
|
+
Present the resolved data app, datasource, embed id, dashboard id, and any conflict.
|
|
8986
|
+
|
|
8987
|
+
**Step 3 \u2014 Dry-run the write**
|
|
8988
|
+
Call:
|
|
8989
|
+
\`\`\`
|
|
8990
|
+
create_or_update_metric_from_query({
|
|
8991
|
+
mode: "dry_run",
|
|
8992
|
+
conflictStrategy,
|
|
8993
|
+
clientId,
|
|
8994
|
+
workspaceName,
|
|
8995
|
+
dashboardName,
|
|
8996
|
+
metricId,
|
|
8997
|
+
name,
|
|
8998
|
+
query,
|
|
8999
|
+
description,
|
|
9000
|
+
chartOptions,
|
|
9001
|
+
isPublished: false
|
|
9002
|
+
})
|
|
9003
|
+
\`\`\`
|
|
9004
|
+
Show the exact plan. Stop and ask the user to review it.
|
|
9005
|
+
|
|
9006
|
+
**Step 4 \u2014 Apply only after explicit confirmation**
|
|
9007
|
+
Only after the user approves, call:
|
|
9008
|
+
\`\`\`
|
|
9009
|
+
create_or_update_metric_from_query({
|
|
9010
|
+
mode: "apply",
|
|
9011
|
+
confirm: "APPLY_TO_PRODUCTION",
|
|
9012
|
+
conflictStrategy,
|
|
9013
|
+
clientId,
|
|
9014
|
+
workspaceName,
|
|
9015
|
+
dashboardName,
|
|
9016
|
+
metricId,
|
|
9017
|
+
name,
|
|
9018
|
+
query,
|
|
9019
|
+
description,
|
|
9020
|
+
chartOptions,
|
|
9021
|
+
isPublished: false,
|
|
9022
|
+
requestedBy: "<human/operator context>"
|
|
9023
|
+
})
|
|
9024
|
+
\`\`\`
|
|
9025
|
+
Never invent the confirmation. The user must provide explicit approval.
|
|
9026
|
+
|
|
9027
|
+
**Step 5 \u2014 Verify**
|
|
9028
|
+
Call:
|
|
9029
|
+
\`\`\`
|
|
9030
|
+
list_widgets({ clientId, dashboardName, workspaceName })
|
|
9031
|
+
get_demo_link({ clientId, dashboardName, workspaceName })
|
|
9032
|
+
\`\`\`
|
|
9033
|
+
Tell the user that unpublished widgets may not appear in the preview until published.
|
|
9034
|
+
|
|
9035
|
+
**Scheduled reports**
|
|
9036
|
+
If the user asks about schedule visibility, call:
|
|
9037
|
+
\`\`\`
|
|
9038
|
+
list_scheduled_reports({ clientId, dashboardName, workspaceName })
|
|
9039
|
+
\`\`\`
|
|
9040
|
+
Clarify that this lists configured schedules only. Delivery success/failure history is not yet persisted in the MCP/API surface.`;
|
|
9041
|
+
|
|
8433
9042
|
// src/prompts/registry.ts
|
|
8434
9043
|
var prompts = [
|
|
8435
9044
|
{
|
|
@@ -8461,6 +9070,11 @@ var prompts = [
|
|
|
8461
9070
|
name: "customize-customer-dashboard",
|
|
8462
9071
|
description: "Add, modify, or remove widgets on a specific customer's dashboard embed. Per-client scope \u2014 changes only affect that customer.",
|
|
8463
9072
|
content: customizeCustomerDashboardPrompt
|
|
9073
|
+
},
|
|
9074
|
+
{
|
|
9075
|
+
name: "sql-to-metric-migration",
|
|
9076
|
+
description: "Production-safe SQL-to-metric migration workflow: validate, dry-run, then apply with explicit confirmation.",
|
|
9077
|
+
content: sqlToMetricMigrationPrompt
|
|
8464
9078
|
}
|
|
8465
9079
|
];
|
|
8466
9080
|
function registerAllPrompts(server) {
|
|
@@ -8484,34 +9098,35 @@ function createServer(config) {
|
|
|
8484
9098
|
const client = new DatabrainClient(config);
|
|
8485
9099
|
const auth = client.getActiveAuth();
|
|
8486
9100
|
let operatorBlurb;
|
|
8487
|
-
if (auth.source === "
|
|
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
|
-
`;
|
|
9101
|
+
if (auth.source === "service-token") {
|
|
9102
|
+
operatorBlurb = "OPERATOR IDENTITY: service-token mode. Calls are authenticated at org level \u2014 actions in the audit log are tagged actor=system, not a specific user. Per-user identity binding will be added when the OAuth-based login flow ships. Do NOT ask the user for credentials or tokens.\n\n";
|
|
9103
|
+
} else if (auth.source === "api-token") {
|
|
9104
|
+
operatorBlurb = "OPERATOR IDENTITY: API-token-only mode. A per-data-app API token is configured, but DATABRAIN_SERVICE_TOKEN is not. Embedding operations can use the configured API token, but org-level setup tools require DATABRAIN_SERVICE_TOKEN in the MCP config.\n\n";
|
|
8494
9105
|
} else {
|
|
8495
|
-
operatorBlurb = "OPERATOR IDENTITY:
|
|
9106
|
+
operatorBlurb = "OPERATOR IDENTITY: no usable token is configured. The MCP server normally refuses this config at startup. Ask the user to add DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN to the MCP config before proceeding.\n\n";
|
|
8496
9107
|
}
|
|
9108
|
+
const serviceTokenStatus = config.serviceToken ? "This MCP server has DATABRAIN_SERVICE_TOKEN configured; never ask the user for it." : "DATABRAIN_SERVICE_TOKEN is not configured. Ask the user to add it to the MCP config only if org-level setup is needed.";
|
|
9109
|
+
const apiTokenStatus = client.hasApiToken() ? "This MCP server has a per-data-app API token configured." : "Created/rotated via create_api_token or rotate_api_token during setup when DATABRAIN_SERVICE_TOKEN is available.";
|
|
8497
9110
|
const server = new McpServer(
|
|
8498
9111
|
{
|
|
8499
9112
|
name: "databrain",
|
|
8500
|
-
version:
|
|
8501
|
-
description: "Databrain MCP Server \u2014 embed analytics dashboards and charts into your app.
|
|
9113
|
+
version: MCP_SERVER_VERSION,
|
|
9114
|
+
description: "Databrain MCP Server \u2014 embed analytics dashboards and charts into your app. Supports embed setup, semantic-layer maintenance, production dry-run/apply metric creation, theming, filters, permissions, and guest token generation."
|
|
8502
9115
|
},
|
|
8503
9116
|
{
|
|
8504
9117
|
instructions: operatorBlurb + `Databrain uses two token types \u2014 never confuse them:
|
|
8505
|
-
\u2022 SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, list_datamarts, list_workspaces, list_data_apps, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer
|
|
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)
|
|
9118
|
+
\u2022 SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, sync_datasource, list_datamarts, create_datamart, list_workspaces, list_data_apps, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status. ${serviceTokenStatus}
|
|
9119
|
+
\u2022 API TOKEN (per data app): scoped to one data app, used for embedding operations (list_embeds, get_embed_details, create_embed, update_embed, generate_guest_token), data-app scoped dashboard listing, widget CRUD/metric creation through guest-token APIs, and list_scheduled_reports. ${apiTokenStatus}
|
|
8507
9120
|
|
|
8508
|
-
|
|
9121
|
+
4 core use cases \u2014 match the user's scenario to the right prompt:
|
|
8509
9122
|
1. Embed existing dashboard \u2192 prompt: embed-existing-dashboard (list_data_apps \u2192 create_api_token \u2192 list_dashboards \u2192 create_embed \u2192 generate_guest_token \u2192 generate_embed_code)
|
|
8510
9123
|
2. Blank dashboard per client \u2192 prompt: embed-blank-dashboard (list_data_apps \u2192 list_datamarts \u2192 create_embed with clientId per client \u2192 generate_guest_token \u2192 generate_embed_code)
|
|
8511
9124
|
3. Explore & query data \u2192 prompt: query-data (list_datamarts \u2192 get_semantic_layer \u2192 if empty, auto-generate descriptions with update_semantic_layer or use populate-semantic-layer prompt \u2192 ask_question)
|
|
9125
|
+
4. SQL-to-metric migration \u2192 prompt: sql-to-metric-migration (validate_metric_spec \u2192 create_or_update_metric_from_query dry_run \u2192 apply only with explicit production confirmation)
|
|
9126
|
+
|
|
9127
|
+
Production safety: OAuth may not be available yet. In service-token mode, writes are actor=system. For create_datamart and create_or_update_metric_from_query, dry_run is the default. Apply requires confirm="APPLY_TO_PRODUCTION" after the user reviews the plan. Do not apply from a vague request.
|
|
8512
9128
|
|
|
8513
|
-
Infrastructure
|
|
8514
|
-
However, the **semantic layer** is populated and maintained entirely via MCP tools (get_semantic_layer, update_semantic_layer). When the semantic layer is empty, YOU (the assistant) should auto-generate descriptions from table/column names and push them with update_semantic_layer \u2014 do NOT send the user to the UI for this.
|
|
9129
|
+
Infrastructure creation is intentionally narrow in MCP: datasource and workspace creation still happen outside this workflow; sync_datasource and dry-run/apply create_datamart are available. The semantic layer is populated and maintained via MCP tools (get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status). When the semantic layer is empty, use these tools \u2014 do NOT send the user to the UI for semantic metadata.
|
|
8515
9130
|
|
|
8516
9131
|
After embed setup, customize with: brand-my-embed prompt for theming. Use update_embed (body passthrough) for filters, localization, and permissions.
|
|
8517
9132
|
Use generate_guest_token for frontend auth and generate_embed_code for framework snippets.
|
|
@@ -8535,7 +9150,6 @@ Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, fi
|
|
|
8535
9150
|
}
|
|
8536
9151
|
|
|
8537
9152
|
// src/index.ts
|
|
8538
|
-
var VERSION = "0.2.0";
|
|
8539
9153
|
function banner(msg) {
|
|
8540
9154
|
process.stderr.write(`[databrain] ${msg}
|
|
8541
9155
|
`);
|
|
@@ -8546,7 +9160,7 @@ async function main() {
|
|
|
8546
9160
|
const server = createServer(config);
|
|
8547
9161
|
const transport = new StdioServerTransport();
|
|
8548
9162
|
await server.connect(transport);
|
|
8549
|
-
banner(`v${
|
|
9163
|
+
banner(`v${MCP_SERVER_VERSION} ready`);
|
|
8550
9164
|
} catch (error) {
|
|
8551
9165
|
logger.error("Failed to start server", error);
|
|
8552
9166
|
process.exit(1);
|