@databrainhq/mcp-server 0.2.10 → 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 +76 -17
- package/dist/index.js +1439 -412
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@ import { z, ZodError } from "zod";
|
|
|
8
8
|
var configSchema = z.object({
|
|
9
9
|
serviceToken: z.string().min(1).optional(),
|
|
10
10
|
apiToken: z.string().min(1).optional(),
|
|
11
|
-
apiUrl: z.string().url().default("https://api.usedatabrain.com")
|
|
11
|
+
apiUrl: z.string().url().default("https://api.usedatabrain.com"),
|
|
12
|
+
demoDomain: z.string().url().default("https://demo.usedatabrain.com")
|
|
12
13
|
}).refine((c) => c.serviceToken || c.apiToken, {
|
|
13
14
|
message: "At least one of DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN must be set"
|
|
14
15
|
});
|
|
@@ -19,8 +20,10 @@ function formatConfigError(err) {
|
|
|
19
20
|
lines.push(` - ${issue}`);
|
|
20
21
|
}
|
|
21
22
|
lines.push("");
|
|
22
|
-
lines.push("
|
|
23
|
+
lines.push(" Set DATABRAIN_SERVICE_TOKEN (Settings > Service Tokens) \u2014 org-level token for setup and management.");
|
|
23
24
|
lines.push(' Then add to your MCP config: { "env": { "DATABRAIN_SERVICE_TOKEN": "..." } }');
|
|
25
|
+
lines.push("");
|
|
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.");
|
|
24
27
|
return lines.join("\n");
|
|
25
28
|
}
|
|
26
29
|
function loadConfig() {
|
|
@@ -28,7 +31,8 @@ function loadConfig() {
|
|
|
28
31
|
return configSchema.parse({
|
|
29
32
|
serviceToken: process.env.DATABRAIN_SERVICE_TOKEN || void 0,
|
|
30
33
|
apiToken: process.env.DATABRAIN_API_TOKEN || void 0,
|
|
31
|
-
apiUrl: process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com"
|
|
34
|
+
apiUrl: process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com",
|
|
35
|
+
demoDomain: process.env.DATABRAIN_DEMO_DOMAIN || "https://demo.usedatabrain.com"
|
|
32
36
|
});
|
|
33
37
|
} catch (err) {
|
|
34
38
|
if (err instanceof ZodError) {
|
|
@@ -78,13 +82,39 @@ var DatabrainApiError = class extends Error {
|
|
|
78
82
|
this.name = "DatabrainApiError";
|
|
79
83
|
}
|
|
80
84
|
toToolResponse() {
|
|
85
|
+
const parts = [`Error: ${this.message}`];
|
|
86
|
+
const detailDump = this.formatDetails();
|
|
87
|
+
if (detailDump) parts.push(detailDump);
|
|
88
|
+
const hint = this.hint();
|
|
89
|
+
if (hint) parts.push(hint);
|
|
81
90
|
return {
|
|
82
|
-
content: [{ type: "text", text:
|
|
83
|
-
|
|
84
|
-
${this.hint()}` }],
|
|
91
|
+
content: [{ type: "text", text: parts.join("\n\n") }],
|
|
85
92
|
isError: true
|
|
86
93
|
};
|
|
87
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Surface API-returned details for 4xx errors so the LLM can recover or
|
|
97
|
+
* report the real reason (validation messages, field names, etc.). Skips
|
|
98
|
+
* fields already collapsed into `message`/`code`, and skips 5xx where the
|
|
99
|
+
* payload usually isn't actionable.
|
|
100
|
+
*/
|
|
101
|
+
formatDetails() {
|
|
102
|
+
if (this.status < 400 || this.status >= 500) return "";
|
|
103
|
+
if (!this.details || typeof this.details !== "object") return "";
|
|
104
|
+
const skipKeys = /* @__PURE__ */ new Set(["message", "detail", "error", "code"]);
|
|
105
|
+
const extra = {};
|
|
106
|
+
for (const [k, v] of Object.entries(this.details)) {
|
|
107
|
+
if (skipKeys.has(k)) continue;
|
|
108
|
+
if (v === null || v === void 0) continue;
|
|
109
|
+
extra[k] = v;
|
|
110
|
+
}
|
|
111
|
+
if (Object.keys(extra).length === 0) return "";
|
|
112
|
+
try {
|
|
113
|
+
return `API details: ${JSON.stringify(extra)}`;
|
|
114
|
+
} catch {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
88
118
|
hint() {
|
|
89
119
|
if (this.status === 401 || this.status === 403) {
|
|
90
120
|
if (this.code === "INVALID_SERVICE_TOKEN" || this.code === "MISSING_SERVICE_TOKEN" || this.message.toLowerCase().includes("service token")) {
|
|
@@ -154,8 +184,10 @@ var logger = {
|
|
|
154
184
|
error: (msg, data) => log("error", msg, data)
|
|
155
185
|
};
|
|
156
186
|
|
|
187
|
+
// src/version.ts
|
|
188
|
+
var MCP_SERVER_VERSION = "0.2.10";
|
|
189
|
+
|
|
157
190
|
// src/client/databrain-client.ts
|
|
158
|
-
var MCP_SERVER_VERSION = "0.2.0";
|
|
159
191
|
var MCP_USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION}`;
|
|
160
192
|
var MAX_RETRIES = 3;
|
|
161
193
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -188,12 +220,27 @@ var DatabrainClient = class {
|
|
|
188
220
|
getApiUrl() {
|
|
189
221
|
return this.config.apiUrl;
|
|
190
222
|
}
|
|
223
|
+
getDemoDomain() {
|
|
224
|
+
return this.config.demoDomain;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Describes which credential is currently used as the bearer for outgoing
|
|
228
|
+
* API calls. Used by `whoami` so the LLM can give the user a truthful answer.
|
|
229
|
+
*/
|
|
230
|
+
getActiveAuth() {
|
|
231
|
+
if (this.config.serviceToken) return { source: "service-token" };
|
|
232
|
+
if (this.runtimeApiToken || this.config.apiToken) return { source: "api-token" };
|
|
233
|
+
return { source: "none" };
|
|
234
|
+
}
|
|
191
235
|
getToken(auth) {
|
|
192
236
|
if (auth === "none") return void 0;
|
|
193
237
|
if (auth === "service") return this.config.serviceToken;
|
|
194
238
|
return this.runtimeApiToken || this.config.apiToken;
|
|
195
239
|
}
|
|
196
240
|
async request(options) {
|
|
241
|
+
return this.requestInternal(options);
|
|
242
|
+
}
|
|
243
|
+
async requestInternal(options) {
|
|
197
244
|
if (options.invalidateCache) {
|
|
198
245
|
for (const prefix of options.invalidateCache) {
|
|
199
246
|
this.cache.invalidateByPrefix(prefix);
|
|
@@ -596,6 +643,69 @@ var QueryApi = class {
|
|
|
596
643
|
throw err;
|
|
597
644
|
}
|
|
598
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
|
+
}
|
|
599
709
|
};
|
|
600
710
|
|
|
601
711
|
// src/api/widgets-api.ts
|
|
@@ -735,19 +845,19 @@ var DEFAULT_ACCESS_SETTINGS = {
|
|
|
735
845
|
function isNonEmptyString2(value) {
|
|
736
846
|
return typeof value === "string" && value.trim().length > 0;
|
|
737
847
|
}
|
|
738
|
-
function
|
|
848
|
+
function asRecord2(value) {
|
|
739
849
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
740
850
|
return value;
|
|
741
851
|
}
|
|
742
852
|
function extractAccessSettings(embed) {
|
|
743
|
-
const metadata =
|
|
744
|
-
const metadataConfig =
|
|
745
|
-
const options =
|
|
853
|
+
const metadata = asRecord2(embed.embedMetadata);
|
|
854
|
+
const metadataConfig = asRecord2(metadata?.embedConfig);
|
|
855
|
+
const options = asRecord2(embed.options);
|
|
746
856
|
const candidates = [
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
857
|
+
asRecord2(embed.accessSettings),
|
|
858
|
+
asRecord2(metadata?.accessSettings),
|
|
859
|
+
asRecord2(metadataConfig?.accessSettings),
|
|
860
|
+
asRecord2(options?.accessSettings)
|
|
751
861
|
].filter((c) => !!c);
|
|
752
862
|
for (const candidate of candidates) {
|
|
753
863
|
if (Object.keys(candidate).length > 0) return candidate;
|
|
@@ -755,8 +865,8 @@ function extractAccessSettings(embed) {
|
|
|
755
865
|
return null;
|
|
756
866
|
}
|
|
757
867
|
function extractDatamartName(embed, existingAccess) {
|
|
758
|
-
const metadata =
|
|
759
|
-
const metadataConfig =
|
|
868
|
+
const metadata = asRecord2(embed.embedMetadata);
|
|
869
|
+
const metadataConfig = asRecord2(metadata?.embedConfig);
|
|
760
870
|
const values = [
|
|
761
871
|
existingAccess?.datamartName,
|
|
762
872
|
existingAccess?.dataMartName,
|
|
@@ -793,7 +903,7 @@ var EmbedService = class {
|
|
|
793
903
|
return this.embedsApi.listEmbeds(dataAppName);
|
|
794
904
|
}
|
|
795
905
|
async createEmbedStrict(body) {
|
|
796
|
-
const accessSettings =
|
|
906
|
+
const accessSettings = asRecord2(body.accessSettings);
|
|
797
907
|
if (!accessSettings || Object.keys(accessSettings).length === 0) {
|
|
798
908
|
throw new Error(
|
|
799
909
|
"Embed creation requires `accessSettings` for future update compatibility."
|
|
@@ -881,8 +991,8 @@ Run \`update_embed\` with explicit \`accessSettings.datamartName\`, or recreate
|
|
|
881
991
|
}
|
|
882
992
|
async updateEmbed(embedId, updatePatch) {
|
|
883
993
|
const body = await this.buildUpdateBase(embedId);
|
|
884
|
-
const baseAccessSettings =
|
|
885
|
-
const patchAccessSettings =
|
|
994
|
+
const baseAccessSettings = asRecord2(body.accessSettings);
|
|
995
|
+
const patchAccessSettings = asRecord2(updatePatch.accessSettings);
|
|
886
996
|
for (const [key, value] of Object.entries(updatePatch)) {
|
|
887
997
|
if (key === "accessSettings") continue;
|
|
888
998
|
if (value !== void 0) body[key] = value;
|
|
@@ -942,7 +1052,7 @@ var GuestTokenService = class {
|
|
|
942
1052
|
|
|
943
1053
|
// src/services/widget-service.ts
|
|
944
1054
|
var isNonEmptyString3 = (v) => typeof v === "string" && v.trim().length > 0;
|
|
945
|
-
var
|
|
1055
|
+
var asRecord3 = (v) => {
|
|
946
1056
|
if (!v || typeof v !== "object" || Array.isArray(v)) return null;
|
|
947
1057
|
return v;
|
|
948
1058
|
};
|
|
@@ -1263,8 +1373,8 @@ Do not guess between the two \u2014 wait for the user to choose.`
|
|
|
1263
1373
|
if (!exact) {
|
|
1264
1374
|
throw new Error(`Embed "${embedId}" not found. Run \`list_embeds\` to verify.`);
|
|
1265
1375
|
}
|
|
1266
|
-
const metadata =
|
|
1267
|
-
const externalDashboard =
|
|
1376
|
+
const metadata = asRecord3(exact.embedMetadata);
|
|
1377
|
+
const externalDashboard = asRecord3(exact.externalDashboard);
|
|
1268
1378
|
const dataAppId = isNonEmptyString3(exact.dataAppId) ? exact.dataAppId : null;
|
|
1269
1379
|
const externalDashboardId = isNonEmptyString3(exact.externalDashboardId) && exact.externalDashboardId || (externalDashboard && isNonEmptyString3(externalDashboard.externalDashboardId) ? externalDashboard.externalDashboardId : null) || (metadata && isNonEmptyString3(metadata.externalDashboardId) ? metadata.externalDashboardId : null);
|
|
1270
1380
|
const dashboardId = isNonEmptyString3(exact.dashboardId) && exact.dashboardId || (metadata && isNonEmptyString3(metadata.dashboardId) ? metadata.dashboardId : null);
|
|
@@ -1314,7 +1424,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
|
|
|
1314
1424
|
guestToken,
|
|
1315
1425
|
templateMetricId
|
|
1316
1426
|
);
|
|
1317
|
-
const metric =
|
|
1427
|
+
const metric = asRecord3(response.externalMetric);
|
|
1318
1428
|
if (!metric) {
|
|
1319
1429
|
throw new Error(
|
|
1320
1430
|
`Template metric "${templateMetricId}" not found or could not be loaded. Response keys: ${Object.keys(
|
|
@@ -1330,7 +1440,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
|
|
|
1330
1440
|
query: metric.query || void 0,
|
|
1331
1441
|
metricQuery: metric.metricQuery || void 0,
|
|
1332
1442
|
description: metric.description || "",
|
|
1333
|
-
chartOptions:
|
|
1443
|
+
chartOptions: asRecord3(metric.chartOptions) || {},
|
|
1334
1444
|
companyId: metric.companyId || void 0,
|
|
1335
1445
|
companyIntegrationId: metric.companyIntegrationId || void 0,
|
|
1336
1446
|
integrationName: metric.integrationName || void 0,
|
|
@@ -1371,6 +1481,7 @@ function createServiceBundle(client) {
|
|
|
1371
1481
|
const datasourcesApi = new DatasourcesApi(client);
|
|
1372
1482
|
const guestTokenApi = new GuestTokenApi(client);
|
|
1373
1483
|
const queryApi = new QueryApi(client);
|
|
1484
|
+
const scheduledReportsApi = new ScheduledReportsApi(client);
|
|
1374
1485
|
const widgetsApi = new WidgetsApi(client);
|
|
1375
1486
|
const workspacesApi = new WorkspacesApi(client);
|
|
1376
1487
|
const discoveryService = new DiscoveryService();
|
|
@@ -1383,6 +1494,7 @@ function createServiceBundle(client) {
|
|
|
1383
1494
|
datasourcesApi,
|
|
1384
1495
|
workspacesApi,
|
|
1385
1496
|
queryApi,
|
|
1497
|
+
scheduledReportsApi,
|
|
1386
1498
|
widgetsApi,
|
|
1387
1499
|
embedService: new EmbedService(embedsApi, datamartsApi),
|
|
1388
1500
|
discoveryService,
|
|
@@ -1990,68 +2102,223 @@ function registerListWorkspaces(server, client) {
|
|
|
1990
2102
|
);
|
|
1991
2103
|
}
|
|
1992
2104
|
|
|
1993
|
-
// src/tools/infrastructure/
|
|
2105
|
+
// src/tools/infrastructure/create-datamart.ts
|
|
1994
2106
|
import { z as z9 } from "zod";
|
|
1995
|
-
|
|
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);
|
|
1996
2163
|
server.tool(
|
|
1997
|
-
"
|
|
1998
|
-
"
|
|
2164
|
+
"create_datamart",
|
|
2165
|
+
"Create a production datamart from selected tables and relationships. Defaults to dry-run; apply requires explicit confirmation.",
|
|
1999
2166
|
{
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
),
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
)
|
|
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")
|
|
2006
2182
|
},
|
|
2007
|
-
|
|
2008
|
-
async (
|
|
2183
|
+
WRITE_CREATE,
|
|
2184
|
+
async (params) => {
|
|
2009
2185
|
try {
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
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"
|
|
2017
2196
|
});
|
|
2018
|
-
if (
|
|
2197
|
+
if (resolution.kind !== "resolved") return resolution.response;
|
|
2198
|
+
if (!params.tableList || params.tableList.length === 0) {
|
|
2019
2199
|
return toolError(
|
|
2020
|
-
"
|
|
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
|
+
)
|
|
2021
2223
|
);
|
|
2022
2224
|
}
|
|
2023
|
-
const
|
|
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") {
|
|
2024
2240
|
return toolSuccess(
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
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
|
+
)
|
|
2030
2252
|
);
|
|
2031
2253
|
}
|
|
2254
|
+
const guardError = validateProductionApply({ confirm: params.confirm });
|
|
2255
|
+
if (guardError) return toolError(guardError);
|
|
2032
2256
|
const result = await client.request({
|
|
2033
|
-
method: "
|
|
2034
|
-
path: "/api/v2/
|
|
2035
|
-
body:
|
|
2036
|
-
name: workspaceName,
|
|
2037
|
-
connectionType: "DATAMART",
|
|
2038
|
-
datamartName
|
|
2039
|
-
},
|
|
2257
|
+
method: "POST",
|
|
2258
|
+
path: "/api/v2/data-app/datamarts",
|
|
2259
|
+
body: params.body,
|
|
2040
2260
|
auth: "service",
|
|
2041
|
-
invalidateCache: ["
|
|
2261
|
+
invalidateCache: ["list_datamarts"]
|
|
2042
2262
|
});
|
|
2043
2263
|
if (result.error) {
|
|
2044
|
-
return toolError(
|
|
2045
|
-
|
|
2046
|
-
|
|
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.`);
|
|
2047
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";
|
|
2048
2317
|
return toolSuccess(
|
|
2049
|
-
`
|
|
2318
|
+
`Datasource "${selected.name}" sync triggered (ID: ${datasourceId}).
|
|
2319
|
+
Status: ${message}
|
|
2050
2320
|
|
|
2051
|
-
|
|
2052
|
-
- Build dashboards in the Databrain UI using this workspace
|
|
2053
|
-
- Use \`list_dashboards\` to find dashboards in this workspace
|
|
2054
|
-
- Use \`create_embed\` to embed dashboards into your app`
|
|
2321
|
+
The schema metadata will be refreshed. You can now use \`discover_datasource_schema\` or \`create_datamart\` to select tables.`
|
|
2055
2322
|
);
|
|
2056
2323
|
} catch (err) {
|
|
2057
2324
|
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
@@ -2062,7 +2329,7 @@ Next steps:
|
|
|
2062
2329
|
}
|
|
2063
2330
|
|
|
2064
2331
|
// src/tools/embed/create-embed.ts
|
|
2065
|
-
import { z as
|
|
2332
|
+
import { z as z11 } from "zod";
|
|
2066
2333
|
var DEFAULT_ACCESS_SETTINGS2 = {
|
|
2067
2334
|
isAllowMetricCreation: false,
|
|
2068
2335
|
isAllowMetricDeletion: false,
|
|
@@ -2075,18 +2342,18 @@ var DEFAULT_ACCESS_SETTINGS2 = {
|
|
|
2075
2342
|
isAllowCreateDashboardView: false,
|
|
2076
2343
|
metricCreationMode: "DRAG_DROP"
|
|
2077
2344
|
};
|
|
2078
|
-
var accessSettingsSchema =
|
|
2079
|
-
datamartName:
|
|
2080
|
-
isAllowMetricCreation:
|
|
2081
|
-
metricCreationMode:
|
|
2082
|
-
isAllowMetricDeletion:
|
|
2083
|
-
isAllowMetricUpdate:
|
|
2084
|
-
isAllowMetricLayoutChange:
|
|
2085
|
-
isAllowManageMetrics:
|
|
2086
|
-
isAllowUnderlyingData:
|
|
2087
|
-
isAllowAiPilot:
|
|
2088
|
-
isAllowEmailReports:
|
|
2089
|
-
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")
|
|
2090
2357
|
}).describe("Embed access settings. Only datamartName is required \u2014 all flags default to false.");
|
|
2091
2358
|
function registerCreateEmbed(server, client) {
|
|
2092
2359
|
const services = createServiceBundle(client);
|
|
@@ -2094,23 +2361,23 @@ function registerCreateEmbed(server, client) {
|
|
|
2094
2361
|
"create_embed",
|
|
2095
2362
|
"Create an embed. Provide dashboardId, workspaceName, accessSettings.datamartName. Add clientId for multi-tenant per-client dashboards. All isAllow* flags default to false.",
|
|
2096
2363
|
{
|
|
2097
|
-
body:
|
|
2364
|
+
body: z11.record(z11.unknown()).optional().describe(
|
|
2098
2365
|
"Raw API body passthrough. If provided, all other fields are ignored."
|
|
2099
2366
|
),
|
|
2100
|
-
dashboardId:
|
|
2101
|
-
workspaceName:
|
|
2102
|
-
embedType:
|
|
2103
|
-
metricId:
|
|
2104
|
-
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"),
|
|
2105
2372
|
accessSettings: accessSettingsSchema.optional(),
|
|
2106
2373
|
// Multi-tenant (dashboard embed) params
|
|
2107
|
-
clientId:
|
|
2374
|
+
clientId: z11.string().optional().describe(
|
|
2108
2375
|
"Client identifier for multi-tenant isolation. When provided, creates a dashboard embed (each client gets their own dashboard copy)."
|
|
2109
2376
|
),
|
|
2110
|
-
templateDashboardId:
|
|
2111
|
-
isImportMetrics:
|
|
2112
|
-
metricImportMode:
|
|
2113
|
-
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")
|
|
2114
2381
|
},
|
|
2115
2382
|
WRITE_CREATE,
|
|
2116
2383
|
async (params) => {
|
|
@@ -2212,14 +2479,14 @@ Next: run \`get_embed_details\` to verify the embed configuration.`
|
|
|
2212
2479
|
}
|
|
2213
2480
|
|
|
2214
2481
|
// src/tools/embed/list-embeds.ts
|
|
2215
|
-
import { z as
|
|
2482
|
+
import { z as z12 } from "zod";
|
|
2216
2483
|
function registerListEmbeds(server, client) {
|
|
2217
2484
|
const services = createServiceBundle(client);
|
|
2218
2485
|
server.tool(
|
|
2219
2486
|
"list_embeds",
|
|
2220
|
-
"List embed configurations. Requires an API token (use create_api_token first).",
|
|
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.",
|
|
2221
2488
|
{
|
|
2222
|
-
dataAppName:
|
|
2489
|
+
dataAppName: z12.string().optional().describe("Filter by data app name")
|
|
2223
2490
|
},
|
|
2224
2491
|
READ_ONLY,
|
|
2225
2492
|
async (params) => {
|
|
@@ -2230,19 +2497,28 @@ function registerListEmbeds(server, client) {
|
|
|
2230
2497
|
"No embeds found. Create an embed in the Databrain UI, then run `list_embeds` again."
|
|
2231
2498
|
);
|
|
2232
2499
|
}
|
|
2500
|
+
const embeds = result.map((e) => {
|
|
2501
|
+
const n = services.discoveryService.normalizeEmbed(e);
|
|
2502
|
+
const clientId = typeof e.clientId === "string" && e.clientId.trim().length > 0 ? e.clientId : null;
|
|
2503
|
+
return {
|
|
2504
|
+
embedId: n.embedId || null,
|
|
2505
|
+
internalId: !n.embedId && n.internalId ? n.internalId : void 0,
|
|
2506
|
+
name: n.name,
|
|
2507
|
+
type: n.embedType || "dashboard",
|
|
2508
|
+
dashboardId: n.dashboardId || null,
|
|
2509
|
+
dataApp: n.dataAppName || null,
|
|
2510
|
+
clientId
|
|
2511
|
+
};
|
|
2512
|
+
});
|
|
2513
|
+
const distinctClientIds = Array.from(
|
|
2514
|
+
new Set(
|
|
2515
|
+
embeds.map((e) => e.clientId).filter((id) => typeof id === "string" && id.length > 0)
|
|
2516
|
+
)
|
|
2517
|
+
).sort();
|
|
2233
2518
|
return toolSuccess(JSON.stringify({
|
|
2234
|
-
count:
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
return {
|
|
2238
|
-
embedId: n.embedId || null,
|
|
2239
|
-
internalId: !n.embedId && n.internalId ? n.internalId : void 0,
|
|
2240
|
-
name: n.name,
|
|
2241
|
-
type: n.embedType || "dashboard",
|
|
2242
|
-
dashboardId: n.dashboardId || null,
|
|
2243
|
-
dataApp: n.dataAppName || null
|
|
2244
|
-
};
|
|
2245
|
-
})
|
|
2519
|
+
count: embeds.length,
|
|
2520
|
+
distinctClientIds,
|
|
2521
|
+
embeds
|
|
2246
2522
|
}));
|
|
2247
2523
|
} catch (err) {
|
|
2248
2524
|
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
@@ -2253,14 +2529,14 @@ function registerListEmbeds(server, client) {
|
|
|
2253
2529
|
}
|
|
2254
2530
|
|
|
2255
2531
|
// src/tools/embed/get-embed-details.ts
|
|
2256
|
-
import { z as
|
|
2532
|
+
import { z as z13 } from "zod";
|
|
2257
2533
|
function registerGetEmbedDetails(server, client) {
|
|
2258
2534
|
const services = createServiceBundle(client);
|
|
2259
2535
|
server.tool(
|
|
2260
2536
|
"get_embed_details",
|
|
2261
2537
|
"Get full embed configuration including access settings and theme.",
|
|
2262
2538
|
{
|
|
2263
|
-
embedId:
|
|
2539
|
+
embedId: z13.string().describe("ID of the embed")
|
|
2264
2540
|
},
|
|
2265
2541
|
READ_ONLY,
|
|
2266
2542
|
async (params) => {
|
|
@@ -2276,7 +2552,7 @@ function registerGetEmbedDetails(server, client) {
|
|
|
2276
2552
|
}
|
|
2277
2553
|
|
|
2278
2554
|
// src/tools/embed/update-embed.ts
|
|
2279
|
-
import { z as
|
|
2555
|
+
import { z as z14 } from "zod";
|
|
2280
2556
|
|
|
2281
2557
|
// src/tools/embed/helpers.ts
|
|
2282
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.";
|
|
@@ -2314,15 +2590,15 @@ function registerUpdateEmbed(server, client) {
|
|
|
2314
2590
|
"update_embed",
|
|
2315
2591
|
"Update embed access settings, permissions, theme, or name.",
|
|
2316
2592
|
{
|
|
2317
|
-
embedId:
|
|
2318
|
-
body:
|
|
2593
|
+
embedId: z14.string().describe("ID of the embed to update"),
|
|
2594
|
+
body: z14.record(z14.unknown()).optional().describe(
|
|
2319
2595
|
"Raw API body for PUT /api/v2/data-app/embeds. If provided, it is sent directly (passthrough mode)."
|
|
2320
2596
|
),
|
|
2321
|
-
name:
|
|
2322
|
-
accessSettings:
|
|
2597
|
+
name: z14.string().optional().describe("New name for the embed"),
|
|
2598
|
+
accessSettings: z14.record(z14.unknown()).optional().describe(
|
|
2323
2599
|
"Partial update of access settings. See `get_embed_details` for current settings."
|
|
2324
2600
|
),
|
|
2325
|
-
theme:
|
|
2601
|
+
theme: z14.record(z14.unknown()).optional().describe(
|
|
2326
2602
|
"Theme configuration object. See databrain://theme-reference for the full schema."
|
|
2327
2603
|
)
|
|
2328
2604
|
},
|
|
@@ -2368,7 +2644,7 @@ function registerUpdateEmbed(server, client) {
|
|
|
2368
2644
|
}
|
|
2369
2645
|
|
|
2370
2646
|
// src/tools/embed/customize-theme.ts
|
|
2371
|
-
import { z as
|
|
2647
|
+
import { z as z15 } from "zod";
|
|
2372
2648
|
var THEME_PRESETS = {
|
|
2373
2649
|
light: {
|
|
2374
2650
|
general: {
|
|
@@ -2502,7 +2778,7 @@ var ACCESS_PRESETS = {
|
|
|
2502
2778
|
metricCreationMode: "CHAT"
|
|
2503
2779
|
}
|
|
2504
2780
|
};
|
|
2505
|
-
var fontFamilyEnum =
|
|
2781
|
+
var fontFamilyEnum = z15.enum([
|
|
2506
2782
|
"Inter",
|
|
2507
2783
|
"Roboto",
|
|
2508
2784
|
"Open Sans",
|
|
@@ -2514,17 +2790,17 @@ var fontFamilyEnum = z14.enum([
|
|
|
2514
2790
|
"Nunito",
|
|
2515
2791
|
"system-ui"
|
|
2516
2792
|
]).describe("Font family");
|
|
2517
|
-
var tooltipFontSchema =
|
|
2518
|
-
fontFamily:
|
|
2519
|
-
fontSize:
|
|
2520
|
-
fontWeight:
|
|
2521
|
-
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()
|
|
2522
2798
|
}).optional();
|
|
2523
|
-
var fontConfigInputSchema =
|
|
2524
|
-
fontSize:
|
|
2525
|
-
fontFamily:
|
|
2526
|
-
fontWeight:
|
|
2527
|
-
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()
|
|
2528
2804
|
}).optional();
|
|
2529
2805
|
function mapToFrontendFontConfig(input) {
|
|
2530
2806
|
if (!input) return void 0;
|
|
@@ -2535,23 +2811,23 @@ function mapToFrontendFontConfig(input) {
|
|
|
2535
2811
|
...input.color && { color: input.color }
|
|
2536
2812
|
};
|
|
2537
2813
|
}
|
|
2538
|
-
var axisSchema =
|
|
2539
|
-
hideAxisLines:
|
|
2540
|
-
hideSplitLines:
|
|
2541
|
-
hideAxisLabels:
|
|
2542
|
-
hideAxisTicks:
|
|
2543
|
-
axisName:
|
|
2544
|
-
axisNameOffset:
|
|
2545
|
-
axisLabelMargin:
|
|
2546
|
-
fontSize:
|
|
2547
|
-
fontFamily:
|
|
2548
|
-
fontWeight:
|
|
2549
|
-
color:
|
|
2550
|
-
axisColor:
|
|
2551
|
-
axisNameFontConfig:
|
|
2552
|
-
fontFamily:
|
|
2553
|
-
fontSize:
|
|
2554
|
-
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()
|
|
2555
2831
|
}).optional().describe("Axis title font configuration")
|
|
2556
2832
|
}).optional();
|
|
2557
2833
|
function buildAdminTheme(p, presetBase) {
|
|
@@ -2610,37 +2886,37 @@ function buildAdminTheme(p, presetBase) {
|
|
|
2610
2886
|
function buildComponentTheme(p) {
|
|
2611
2887
|
const componentTheme = {};
|
|
2612
2888
|
if (p.buttonPrimaryColor || p.buttonPrimaryTextColor || p.buttonSecondaryColor || p.buttonSecondaryTextColor) {
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
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;
|
|
2619
2895
|
}
|
|
2620
2896
|
if (p.checkboxCheckedColor || p.checkboxUncheckedColor) {
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2897
|
+
const checkbox = {};
|
|
2898
|
+
if (p.checkboxCheckedColor) checkbox.checked = p.checkboxCheckedColor;
|
|
2899
|
+
if (p.checkboxUncheckedColor) checkbox.unChecked = p.checkboxUncheckedColor;
|
|
2900
|
+
componentTheme.checkbox = checkbox;
|
|
2625
2901
|
}
|
|
2626
2902
|
if (p.switchEnabledColor || p.switchDisabledColor) {
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2903
|
+
const switchTheme = {};
|
|
2904
|
+
if (p.switchEnabledColor) switchTheme.enabled = p.switchEnabledColor;
|
|
2905
|
+
if (p.switchDisabledColor) switchTheme.disabled = p.switchDisabledColor;
|
|
2906
|
+
componentTheme.switch = switchTheme;
|
|
2631
2907
|
}
|
|
2632
2908
|
if (p.breadcrumbFontFamily || p.breadcrumbFontColor || p.breadcrumbActiveColor) {
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
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;
|
|
2638
2914
|
}
|
|
2639
2915
|
if (p.filterBadgeColor || p.filterBadgeTextColor) {
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2916
|
+
const multiSelectFilterDropdown = {};
|
|
2917
|
+
if (p.filterBadgeColor) multiSelectFilterDropdown.badgeColor = p.filterBadgeColor;
|
|
2918
|
+
if (p.filterBadgeTextColor) multiSelectFilterDropdown.badgeTextColor = p.filterBadgeTextColor;
|
|
2919
|
+
componentTheme.multiSelectFilterDropdown = multiSelectFilterDropdown;
|
|
2644
2920
|
}
|
|
2645
2921
|
if (p.datePickerColor) componentTheme.datePickerColor = p.datePickerColor;
|
|
2646
2922
|
const breakpoint = {};
|
|
@@ -2707,94 +2983,94 @@ function registerCustomizeTheme(server, client) {
|
|
|
2707
2983
|
"customize_embed_theme",
|
|
2708
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("/")}).`,
|
|
2709
2985
|
{
|
|
2710
|
-
embedId:
|
|
2711
|
-
body:
|
|
2986
|
+
embedId: z15.string().describe("ID of the embed to theme"),
|
|
2987
|
+
body: z15.record(z15.unknown()).optional().describe(
|
|
2712
2988
|
"Raw API body for PUT /api/v2/data-app/embeds. If provided, sent directly and all convenience fields are ignored."
|
|
2713
2989
|
),
|
|
2714
2990
|
// --- Presets ---
|
|
2715
|
-
themePreset:
|
|
2716
|
-
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"),
|
|
2717
2993
|
// --- General ---
|
|
2718
2994
|
fontFamily: fontFamilyEnum.optional().describe("Global font family"),
|
|
2719
|
-
datePickerFormat:
|
|
2995
|
+
datePickerFormat: z15.string().optional().describe("Date picker display format"),
|
|
2720
2996
|
// --- Colors ---
|
|
2721
|
-
primaryColor:
|
|
2722
|
-
backgroundColor:
|
|
2723
|
-
textColor:
|
|
2724
|
-
ctaColor:
|
|
2725
|
-
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)"),
|
|
2726
3002
|
// --- Dashboard ---
|
|
2727
|
-
dashboardBackgroundColor:
|
|
2728
|
-
selectBoxSize:
|
|
2729
|
-
selectBoxVariant:
|
|
2730
|
-
selectBoxBorderRadius:
|
|
2731
|
-
selectBoxTextColor:
|
|
2732
|
-
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)"),
|
|
2733
3009
|
// --- Card Title ---
|
|
2734
|
-
cardTitleFontSize:
|
|
2735
|
-
cardTitleFontWeight:
|
|
2736
|
-
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)"),
|
|
2737
3013
|
// --- Card Description ---
|
|
2738
|
-
cardDescriptionFontSize:
|
|
2739
|
-
cardDescriptionFontWeight:
|
|
2740
|
-
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)"),
|
|
2741
3017
|
// --- Card Customization ---
|
|
2742
|
-
cardPadding:
|
|
2743
|
-
cardBorderRadius:
|
|
2744
|
-
cardShadow:
|
|
2745
|
-
disableShadowOnHover:
|
|
2746
|
-
disableCardStroke:
|
|
2747
|
-
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)"),
|
|
2748
3024
|
// --- Chart Colors (admin theme level) ---
|
|
2749
|
-
chartFontFamily:
|
|
2750
|
-
chartFontColor:
|
|
2751
|
-
chartColors:
|
|
2752
|
-
chartPaletteName:
|
|
2753
|
-
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"),
|
|
2754
3030
|
// --- Chart Tooltip (admin theme level) ---
|
|
2755
3031
|
tooltipHeader: tooltipFontSchema.describe("Tooltip header font config"),
|
|
2756
3032
|
tooltipLabel: tooltipFontSchema.describe("Tooltip label font config"),
|
|
2757
3033
|
tooltipValue: tooltipFontSchema.describe("Tooltip value font config"),
|
|
2758
3034
|
// --- Component Theme ---
|
|
2759
|
-
buttonPrimaryColor:
|
|
2760
|
-
buttonPrimaryTextColor:
|
|
2761
|
-
buttonSecondaryColor:
|
|
2762
|
-
buttonSecondaryTextColor:
|
|
2763
|
-
checkboxCheckedColor:
|
|
2764
|
-
checkboxUncheckedColor:
|
|
2765
|
-
switchEnabledColor:
|
|
2766
|
-
switchDisabledColor:
|
|
2767
|
-
breadcrumbFontFamily:
|
|
2768
|
-
breadcrumbFontColor:
|
|
2769
|
-
breadcrumbActiveColor:
|
|
2770
|
-
filterBadgeColor:
|
|
2771
|
-
filterBadgeTextColor:
|
|
2772
|
-
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)"),
|
|
2773
3049
|
// --- Responsive Breakpoints ---
|
|
2774
|
-
breakpointXl:
|
|
2775
|
-
breakpointLg:
|
|
2776
|
-
breakpointMd:
|
|
2777
|
-
breakpointSm:
|
|
2778
|
-
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"),
|
|
2779
3055
|
// --- Metric Layout Columns ---
|
|
2780
|
-
layoutColsXl:
|
|
2781
|
-
layoutColsLg:
|
|
2782
|
-
layoutColsMd:
|
|
2783
|
-
layoutColsSm:
|
|
2784
|
-
layoutColsXs:
|
|
2785
|
-
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"),
|
|
2786
3062
|
// --- Chart Appearance (detailed) ---
|
|
2787
3063
|
tooltipHeaderStyle: fontConfigInputSchema.describe("Tooltip header: fontSize, fontFamily, fontWeight, color"),
|
|
2788
3064
|
tooltipLabelStyle: fontConfigInputSchema.describe("Tooltip label: fontSize, fontFamily, fontWeight, color"),
|
|
2789
3065
|
tooltipValueStyle: fontConfigInputSchema.describe("Tooltip value: fontSize, fontFamily, fontWeight, color"),
|
|
2790
|
-
labelPosition:
|
|
2791
|
-
radialLabelPosition:
|
|
2792
|
-
marginTop:
|
|
2793
|
-
marginLeft:
|
|
2794
|
-
marginRight:
|
|
2795
|
-
marginBottom:
|
|
2796
|
-
legendShow:
|
|
2797
|
-
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([
|
|
2798
3074
|
"top-left",
|
|
2799
3075
|
"top-center",
|
|
2800
3076
|
"top-right",
|
|
@@ -2804,17 +3080,17 @@ function registerCustomizeTheme(server, client) {
|
|
|
2804
3080
|
"bottom-center",
|
|
2805
3081
|
"bottom-right"
|
|
2806
3082
|
]).optional().describe("Legend position"),
|
|
2807
|
-
legendLayout:
|
|
2808
|
-
legendShape:
|
|
2809
|
-
legendFontSize:
|
|
2810
|
-
legendFontWeight:
|
|
2811
|
-
legendFontFamily:
|
|
2812
|
-
legendColor:
|
|
2813
|
-
legendTruncateLength:
|
|
2814
|
-
disableLegendScrolling:
|
|
2815
|
-
enableVariableLegendPosition:
|
|
2816
|
-
legendTop:
|
|
2817
|
-
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"),
|
|
2818
3094
|
verticalAxis: axisSchema.describe("Vertical (Y) axis configuration"),
|
|
2819
3095
|
horizontalAxis: axisSchema.describe("Horizontal (X) axis configuration")
|
|
2820
3096
|
},
|
|
@@ -2888,71 +3164,141 @@ Use \`get_embed_details\` to view the full current theme configuration.` + GUEST
|
|
|
2888
3164
|
);
|
|
2889
3165
|
}
|
|
2890
3166
|
|
|
3167
|
+
// src/tools/embed/get-demo-link.ts
|
|
3168
|
+
import { z as z16 } from "zod";
|
|
3169
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
3170
|
+
var DEFAULT_EXPIRY_HOURS = 24;
|
|
3171
|
+
var MAX_EXPIRY_HOURS = 24 * 30;
|
|
3172
|
+
function registerGetDemoLink(server, client) {
|
|
3173
|
+
const services = createServiceBundle(client);
|
|
3174
|
+
server.tool(
|
|
3175
|
+
"get_demo_link",
|
|
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.`,
|
|
3177
|
+
{
|
|
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).`)
|
|
3183
|
+
},
|
|
3184
|
+
WRITE_CREATE,
|
|
3185
|
+
async (params) => {
|
|
3186
|
+
try {
|
|
3187
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
3188
|
+
embedId: params.embedId,
|
|
3189
|
+
dashboardName: params.dashboardName,
|
|
3190
|
+
workspaceName: params.workspaceName,
|
|
3191
|
+
required: true
|
|
3192
|
+
});
|
|
3193
|
+
if (!embed.ok) return toolError(embed.error);
|
|
3194
|
+
const embedId = embed.value.embedId;
|
|
3195
|
+
const dashboardName = embed.value.resolvedFrom?.dashboardName || null;
|
|
3196
|
+
const ctx = await services.widgetService.resolveEmbedContext(embedId);
|
|
3197
|
+
if (!ctx.dataAppName) {
|
|
3198
|
+
return toolError(
|
|
3199
|
+
`Could not resolve the data app for embed "${embedId}". The demo link needs a data app to mint a guest token against. Run \`list_data_apps\` to confirm one exists, or pass \`embedId\` for an embed with metadata.dataAppName populated.`
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
const dashboardSlug = ctx.externalDashboardId || ctx.dashboardId;
|
|
3203
|
+
if (!dashboardSlug) {
|
|
3204
|
+
return toolError(
|
|
3205
|
+
`Embed "${embedId}" has no dashboardId/externalDashboardId \u2014 the demo URL requires one to load the dashboard. This is unexpected; verify the embed in the Databrain UI.`
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3208
|
+
const expiryHours = params.expiryHours ?? DEFAULT_EXPIRY_HOURS;
|
|
3209
|
+
const { token } = await services.guestTokenService.generateToken({
|
|
3210
|
+
clientId: params.clientId,
|
|
3211
|
+
dataAppName: ctx.dataAppName,
|
|
3212
|
+
allowedEmbeds: [embedId],
|
|
3213
|
+
expiryTime: expiryHours * HOUR_MS
|
|
3214
|
+
});
|
|
3215
|
+
const baseUrl = client.getDemoDomain().replace(/\/+$/, "");
|
|
3216
|
+
const demoUrl = `${baseUrl}/?token=${encodeURIComponent(token)}&dashboardId=${encodeURIComponent(dashboardSlug)}`;
|
|
3217
|
+
return toolSuccess(
|
|
3218
|
+
JSON.stringify({
|
|
3219
|
+
demoUrl,
|
|
3220
|
+
clientId: params.clientId,
|
|
3221
|
+
dashboardName,
|
|
3222
|
+
embedId,
|
|
3223
|
+
dashboardId: dashboardSlug,
|
|
3224
|
+
dataAppName: ctx.dataAppName,
|
|
3225
|
+
expiresInHours: expiryHours,
|
|
3226
|
+
note: `Open this URL in a browser to preview the dashboard exactly as the end-customer sees it. This matches the UI's "Share Demo Link" flow. Override the host with DATABRAIN_DEMO_DOMAIN for self-hosted / custom preview domains.`
|
|
3227
|
+
})
|
|
3228
|
+
);
|
|
3229
|
+
} catch (err) {
|
|
3230
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
3231
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
3236
|
+
|
|
2891
3237
|
// src/tools/frontend/generate-guest-token.ts
|
|
2892
|
-
import { z as
|
|
3238
|
+
import { z as z17 } from "zod";
|
|
2893
3239
|
function registerGenerateGuestToken(server, client) {
|
|
2894
3240
|
const services = createServiceBundle(client);
|
|
2895
3241
|
server.tool(
|
|
2896
3242
|
"generate_guest_token",
|
|
2897
3243
|
"Generate a guest token for frontend embedding. Requires clientId. Omit dataAppName to auto-resolve.",
|
|
2898
3244
|
{
|
|
2899
|
-
clientId:
|
|
2900
|
-
dataAppName:
|
|
2901
|
-
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(
|
|
2902
3248
|
"Raw API body for POST /api/v2/guest-token/create. If provided, sent directly and convenience fields are ignored."
|
|
2903
3249
|
),
|
|
2904
|
-
params:
|
|
2905
|
-
allowedEmbeds:
|
|
2906
|
-
rlsSettings:
|
|
2907
|
-
metricId:
|
|
2908
|
-
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")
|
|
2909
3255
|
})).optional().describe("Legacy alias used in docs/examples; forwarded as params.rlsSettings"),
|
|
2910
|
-
appFilters:
|
|
2911
|
-
metricId:
|
|
2912
|
-
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")
|
|
2913
3259
|
})).optional().describe("Metric-level app filters"),
|
|
2914
|
-
dashboardAppFilters:
|
|
2915
|
-
dashboardId:
|
|
2916
|
-
values:
|
|
2917
|
-
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")
|
|
2918
3264
|
})).optional().describe("Dashboard-level app filters"),
|
|
2919
|
-
hideDashboardFilters:
|
|
2920
|
-
userIdentifier:
|
|
2921
|
-
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"')
|
|
2922
3268
|
}).optional().describe("Advanced token params"),
|
|
2923
3269
|
// Legacy convenience fields (auto-mapped into params for backward compatibility)
|
|
2924
|
-
timezone:
|
|
2925
|
-
allowedEmbeds:
|
|
2926
|
-
rlsSettings:
|
|
2927
|
-
metricId:
|
|
2928
|
-
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")
|
|
2929
3275
|
})).optional().describe("Legacy: moved under params.rlsSettings"),
|
|
2930
|
-
appFilters:
|
|
2931
|
-
metricId:
|
|
2932
|
-
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")
|
|
2933
3279
|
})).optional().describe("Legacy: moved under params.appFilters"),
|
|
2934
|
-
dashboardAppFilters:
|
|
2935
|
-
dashboardId:
|
|
2936
|
-
values:
|
|
2937
|
-
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")
|
|
2938
3284
|
})).optional().describe("Legacy: moved under params.dashboardAppFilters"),
|
|
2939
|
-
permissions:
|
|
2940
|
-
isEnableArchiveMetrics:
|
|
2941
|
-
isEnableManageMetrics:
|
|
2942
|
-
isEnableCreateDashboardView:
|
|
2943
|
-
isEnableMetricUpdation:
|
|
2944
|
-
isEnableCustomizeLayout:
|
|
2945
|
-
isEnableUnderlyingData:
|
|
2946
|
-
isEnableDownloadMetrics:
|
|
2947
|
-
isShowSideBar:
|
|
2948
|
-
isShowDashboardName:
|
|
2949
|
-
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()
|
|
2950
3296
|
}).optional().describe("Runtime permission overrides (merge with and override embed-level settings)"),
|
|
2951
|
-
theme:
|
|
2952
|
-
hideDashboardFilters:
|
|
2953
|
-
userIdentifier:
|
|
2954
|
-
expiryTime:
|
|
2955
|
-
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")
|
|
2956
3302
|
},
|
|
2957
3303
|
WRITE_CREATE,
|
|
2958
3304
|
async (params) => {
|
|
@@ -3005,7 +3351,7 @@ Next step: Use \`generate_embed_code\` to get copy-paste-ready frontend code.`
|
|
|
3005
3351
|
}
|
|
3006
3352
|
|
|
3007
3353
|
// src/tools/frontend/generate-embed-code.ts
|
|
3008
|
-
import { z as
|
|
3354
|
+
import { z as z18 } from "zod";
|
|
3009
3355
|
|
|
3010
3356
|
// src/templates/react.ts
|
|
3011
3357
|
function kebabCase(str) {
|
|
@@ -3114,7 +3460,7 @@ export default function DatabrainEmbed() {
|
|
|
3114
3460
|
### Environment Variables
|
|
3115
3461
|
Set the following environment variables on your backend server:
|
|
3116
3462
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3117
|
-
- \`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)
|
|
3118
3464
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3119
3465
|
|
|
3120
3466
|
### Security
|
|
@@ -3247,7 +3593,7 @@ export default function DatabrainEmbed() {
|
|
|
3247
3593
|
### Environment Variables
|
|
3248
3594
|
Add the following to your \`.env.local\` file:
|
|
3249
3595
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3250
|
-
- \`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)
|
|
3251
3597
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3252
3598
|
|
|
3253
3599
|
### Security
|
|
@@ -3379,7 +3725,7 @@ onMounted(async () => {
|
|
|
3379
3725
|
### Environment Variables
|
|
3380
3726
|
Set the following environment variables on your backend server:
|
|
3381
3727
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3382
|
-
- \`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)
|
|
3383
3729
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3384
3730
|
|
|
3385
3731
|
### Security
|
|
@@ -3529,7 +3875,7 @@ export class DatabrainEmbedComponent implements OnInit {
|
|
|
3529
3875
|
### Environment Variables
|
|
3530
3876
|
Set the following environment variables on your backend server:
|
|
3531
3877
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3532
|
-
- \`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)
|
|
3533
3879
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3534
3880
|
|
|
3535
3881
|
### Security
|
|
@@ -3671,7 +4017,7 @@ app.listen(3001, () => {
|
|
|
3671
4017
|
### Environment Variables
|
|
3672
4018
|
Set the following environment variables on your backend server:
|
|
3673
4019
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3674
|
-
- \`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)
|
|
3675
4021
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3676
4022
|
|
|
3677
4023
|
### Security
|
|
@@ -3803,7 +4149,7 @@ export default function DatabrainEmbed() {
|
|
|
3803
4149
|
### Environment Variables
|
|
3804
4150
|
Set the following environment variables on your backend server:
|
|
3805
4151
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3806
|
-
- \`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)
|
|
3807
4153
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3808
4154
|
|
|
3809
4155
|
### Security
|
|
@@ -3950,7 +4296,7 @@ app.listen(3001, () => {
|
|
|
3950
4296
|
### Environment Variables
|
|
3951
4297
|
Set the following environment variables on your backend server:
|
|
3952
4298
|
- \`DATABRAIN_API_TOKEN\` - Your Databrain API token
|
|
3953
|
-
- \`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)
|
|
3954
4300
|
- \`DATABRAIN_DATA_APP_NAME\` - Default data app name for guest token generation
|
|
3955
4301
|
|
|
3956
4302
|
### Security
|
|
@@ -4001,22 +4347,22 @@ function registerGenerateEmbedCode(server, _client) {
|
|
|
4001
4347
|
"generate_embed_code",
|
|
4002
4348
|
"Generate framework-specific embed code (React, Next.js, Vue, Angular, Svelte, SolidJS, vanilla HTML).",
|
|
4003
4349
|
{
|
|
4004
|
-
embedId:
|
|
4005
|
-
framework:
|
|
4006
|
-
embedType:
|
|
4007
|
-
options:
|
|
4008
|
-
enableDownloadCsv:
|
|
4009
|
-
enableEmailCsv:
|
|
4010
|
-
enableDownloadAllMetrics:
|
|
4011
|
-
enableDownloadAllPdf:
|
|
4012
|
-
disableFullscreen:
|
|
4013
|
-
isHideTablePreview:
|
|
4014
|
-
isHideChartSettings:
|
|
4015
|
-
isStickyDashboardFilters:
|
|
4016
|
-
enableMultiMetricFilters:
|
|
4017
|
-
themeName:
|
|
4018
|
-
customTheme:
|
|
4019
|
-
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()
|
|
4020
4366
|
}).optional()
|
|
4021
4367
|
},
|
|
4022
4368
|
READ_ONLY,
|
|
@@ -4056,17 +4402,17 @@ ${generated.instructions}`;
|
|
|
4056
4402
|
}
|
|
4057
4403
|
|
|
4058
4404
|
// src/tools/analytics/list-dashboards.ts
|
|
4059
|
-
import { z as
|
|
4405
|
+
import { z as z19 } from "zod";
|
|
4060
4406
|
function registerListDashboards(server, client) {
|
|
4061
4407
|
const services = createServiceBundle(client);
|
|
4062
4408
|
server.tool(
|
|
4063
4409
|
"list_dashboards",
|
|
4064
4410
|
"List dashboards globally or for a specific data app.",
|
|
4065
4411
|
{
|
|
4066
|
-
scope:
|
|
4412
|
+
scope: z19.enum(["auto", "global", "data_app"]).optional().describe(
|
|
4067
4413
|
"Dashboard listing scope. auto: use data_app when dataAppName + API token are available, otherwise global."
|
|
4068
4414
|
),
|
|
4069
|
-
dataAppName:
|
|
4415
|
+
dataAppName: z19.string().optional().describe("Data app name for data_app scope. Ignored for global scope.")
|
|
4070
4416
|
},
|
|
4071
4417
|
READ_ONLY,
|
|
4072
4418
|
async (params) => {
|
|
@@ -4101,7 +4447,7 @@ function registerListDashboards(server, client) {
|
|
|
4101
4447
|
}
|
|
4102
4448
|
|
|
4103
4449
|
// src/tools/query/ask-question.ts
|
|
4104
|
-
import { z as
|
|
4450
|
+
import { z as z20 } from "zod";
|
|
4105
4451
|
var MAX_DISPLAY_ROWS = 50;
|
|
4106
4452
|
function formatDataTable(data, columns) {
|
|
4107
4453
|
if (!Array.isArray(data) || data.length === 0) return "_No data returned._";
|
|
@@ -4131,16 +4477,16 @@ function registerAskQuestion(server, client) {
|
|
|
4131
4477
|
"ask_question",
|
|
4132
4478
|
"Ask a natural language question \u2014 converts to SQL, returns data + chart suggestion. Requires datamartName with a populated semantic layer.",
|
|
4133
4479
|
{
|
|
4134
|
-
question:
|
|
4480
|
+
question: z20.string().describe(
|
|
4135
4481
|
'The natural language question to ask (e.g. "What was total revenue last month?")'
|
|
4136
4482
|
),
|
|
4137
|
-
datamartName:
|
|
4483
|
+
datamartName: z20.string().optional().describe(
|
|
4138
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."
|
|
4139
4485
|
),
|
|
4140
|
-
sessionId:
|
|
4486
|
+
sessionId: z20.string().optional().describe(
|
|
4141
4487
|
"Session ID from a previous ask_question response, for follow-up questions."
|
|
4142
4488
|
),
|
|
4143
|
-
timezone:
|
|
4489
|
+
timezone: z20.string().optional().describe('IANA timezone (e.g. "America/New_York"). Defaults to UTC.')
|
|
4144
4490
|
},
|
|
4145
4491
|
WRITE_CREATE,
|
|
4146
4492
|
async (params) => {
|
|
@@ -4154,7 +4500,13 @@ function registerAskQuestion(server, client) {
|
|
|
4154
4500
|
);
|
|
4155
4501
|
}
|
|
4156
4502
|
if (datamarts.length === 1) {
|
|
4157
|
-
|
|
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;
|
|
4158
4510
|
} else {
|
|
4159
4511
|
const list = datamarts.map(formatDatamart).join("\n");
|
|
4160
4512
|
return toolSuccess(
|
|
@@ -4166,6 +4518,11 @@ Call \`ask_question\` again with the \`datamartName\` parameter.`
|
|
|
4166
4518
|
);
|
|
4167
4519
|
}
|
|
4168
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
|
+
}
|
|
4169
4526
|
try {
|
|
4170
4527
|
const semanticLayer = await services.queryApi.getSemanticLayer(
|
|
4171
4528
|
datamartName
|
|
@@ -4305,14 +4662,14 @@ To fix this:
|
|
|
4305
4662
|
}
|
|
4306
4663
|
|
|
4307
4664
|
// src/tools/query/get-semantic-layer.ts
|
|
4308
|
-
import { z as
|
|
4665
|
+
import { z as z21 } from "zod";
|
|
4309
4666
|
function registerGetSemanticLayer(server, client) {
|
|
4310
4667
|
const services = createServiceBundle(client);
|
|
4311
4668
|
server.tool(
|
|
4312
4669
|
"get_semantic_layer",
|
|
4313
4670
|
"Get semantic layer metadata for a datamart: table/column descriptions, synonyms, completion score. Omit datamartName to auto-resolve.",
|
|
4314
4671
|
{
|
|
4315
|
-
datamartName:
|
|
4672
|
+
datamartName: z21.string().optional().describe(
|
|
4316
4673
|
"Name of the datamart to inspect. Omit to list available datamarts."
|
|
4317
4674
|
)
|
|
4318
4675
|
},
|
|
@@ -4422,15 +4779,15 @@ ${result.feedback}`);
|
|
|
4422
4779
|
}
|
|
4423
4780
|
|
|
4424
4781
|
// src/tools/query/update-semantic-layer.ts
|
|
4425
|
-
import { z as
|
|
4426
|
-
var
|
|
4427
|
-
name:
|
|
4428
|
-
description:
|
|
4429
|
-
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(
|
|
4430
4787
|
'Alternative names users might use for this column (e.g. ["revenue", "sales amount"])'
|
|
4431
4788
|
),
|
|
4432
|
-
miscellaneousInfo:
|
|
4433
|
-
columnType:
|
|
4789
|
+
miscellaneousInfo: z22.string().optional().describe("Additional context such as valid values, units, or caveats"),
|
|
4790
|
+
columnType: z22.enum([
|
|
4434
4791
|
"String",
|
|
4435
4792
|
"Long String",
|
|
4436
4793
|
"String (Custom)",
|
|
@@ -4443,30 +4800,58 @@ var columnSchema = z20.object({
|
|
|
4443
4800
|
"JSON"
|
|
4444
4801
|
]).nullable().optional().describe('Semantic column type. Use "Identifier" for IDs/foreign keys, "Number" for numeric values, "String" for text. Omit for date columns.')
|
|
4445
4802
|
});
|
|
4446
|
-
var
|
|
4447
|
-
name:
|
|
4448
|
-
description:
|
|
4803
|
+
var tableSchema2 = z22.object({
|
|
4804
|
+
name: z22.string().describe("Table name (must exist in the datamart)"),
|
|
4805
|
+
description: z22.string().optional().describe(
|
|
4449
4806
|
"Business-friendly description of what this table represents"
|
|
4450
4807
|
),
|
|
4451
|
-
synonyms:
|
|
4452
|
-
miscellaneousInfo:
|
|
4808
|
+
synonyms: z22.array(z22.string()).optional().describe("Alternative names for this table"),
|
|
4809
|
+
miscellaneousInfo: z22.string().optional().describe(
|
|
4453
4810
|
"Additional context such as how the table relates to other tables"
|
|
4454
4811
|
),
|
|
4455
|
-
columns:
|
|
4812
|
+
columns: z22.array(columnSchema2).optional().describe("Column-level semantic definitions to update")
|
|
4456
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
|
+
}
|
|
4457
4842
|
function registerUpdateSemanticLayer(server, client) {
|
|
4458
4843
|
const services = createServiceBundle(client);
|
|
4459
4844
|
server.tool(
|
|
4460
4845
|
"update_semantic_layer",
|
|
4461
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.",
|
|
4462
4847
|
{
|
|
4463
|
-
datamartName:
|
|
4848
|
+
datamartName: z22.string().optional().describe(
|
|
4464
4849
|
"Name of the datamart to update. Omit to list available datamarts."
|
|
4465
4850
|
),
|
|
4466
|
-
tables:
|
|
4851
|
+
tables: z22.array(tableSchema2).optional().describe(
|
|
4467
4852
|
"Table and column definitions to update. Only include tables/columns you want to change."
|
|
4468
4853
|
),
|
|
4469
|
-
feedback:
|
|
4854
|
+
feedback: z22.string().optional().describe(
|
|
4470
4855
|
'General feedback or context about the datamart (e.g. "This datamart contains e-commerce data for the US market").'
|
|
4471
4856
|
)
|
|
4472
4857
|
},
|
|
@@ -4489,9 +4874,10 @@ function registerUpdateSemanticLayer(server, client) {
|
|
|
4489
4874
|
});
|
|
4490
4875
|
if (resolution.kind !== "resolved") return resolution.response;
|
|
4491
4876
|
const datamartName = resolution.name;
|
|
4877
|
+
const tables = normalizeTablesForApi(params.tables);
|
|
4492
4878
|
await services.queryApi.updateSemanticLayer({
|
|
4493
4879
|
datamartName,
|
|
4494
|
-
tables
|
|
4880
|
+
tables,
|
|
4495
4881
|
feedback: params.feedback
|
|
4496
4882
|
});
|
|
4497
4883
|
const updatedParts = [];
|
|
@@ -4523,18 +4909,128 @@ You can now:
|
|
|
4523
4909
|
);
|
|
4524
4910
|
}
|
|
4525
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
|
+
|
|
4526
5022
|
// src/tools/widgets/list-widgets.ts
|
|
4527
|
-
import { z as
|
|
5023
|
+
import { z as z24 } from "zod";
|
|
4528
5024
|
function registerListWidgets(server, client) {
|
|
4529
5025
|
const services = createServiceBundle(client);
|
|
4530
5026
|
server.tool(
|
|
4531
5027
|
"list_widgets",
|
|
4532
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.",
|
|
4533
5029
|
{
|
|
4534
|
-
clientId:
|
|
4535
|
-
dashboardName:
|
|
4536
|
-
workspaceName:
|
|
4537
|
-
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.")
|
|
4538
5034
|
},
|
|
4539
5035
|
READ_ONLY,
|
|
4540
5036
|
async (params) => {
|
|
@@ -4580,28 +5076,28 @@ function registerListWidgets(server, client) {
|
|
|
4580
5076
|
}
|
|
4581
5077
|
|
|
4582
5078
|
// src/tools/widgets/add-widget.ts
|
|
4583
|
-
import { z as
|
|
5079
|
+
import { z as z25 } from "zod";
|
|
4584
5080
|
function registerAddWidget(server, client) {
|
|
4585
5081
|
const services = createServiceBundle(client);
|
|
4586
5082
|
server.tool(
|
|
4587
5083
|
"add_widget",
|
|
4588
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.",
|
|
4589
5085
|
{
|
|
4590
|
-
clientId:
|
|
4591
|
-
dashboardName:
|
|
4592
|
-
workspaceName:
|
|
4593
|
-
embedId:
|
|
4594
|
-
dataAppName:
|
|
4595
|
-
templateMetricId:
|
|
4596
|
-
name:
|
|
4597
|
-
metricId:
|
|
4598
|
-
query:
|
|
4599
|
-
chartOptions:
|
|
4600
|
-
description:
|
|
4601
|
-
companyIntegrationId:
|
|
4602
|
-
integrationName:
|
|
4603
|
-
isPublished:
|
|
4604
|
-
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.")
|
|
4605
5101
|
},
|
|
4606
5102
|
WRITE_CREATE,
|
|
4607
5103
|
async (params) => {
|
|
@@ -4711,20 +5207,20 @@ Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust
|
|
|
4711
5207
|
}
|
|
4712
5208
|
|
|
4713
5209
|
// src/tools/widgets/update-widget.ts
|
|
4714
|
-
import { z as
|
|
5210
|
+
import { z as z26 } from "zod";
|
|
4715
5211
|
function registerUpdateWidget(server, client) {
|
|
4716
5212
|
const services = createServiceBundle(client);
|
|
4717
5213
|
server.tool(
|
|
4718
5214
|
"update_widget",
|
|
4719
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.",
|
|
4720
5216
|
{
|
|
4721
|
-
externalMetricId:
|
|
4722
|
-
clientId:
|
|
4723
|
-
dashboardName:
|
|
4724
|
-
workspaceName:
|
|
4725
|
-
embedId:
|
|
4726
|
-
dataAppName:
|
|
4727
|
-
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.")
|
|
4728
5224
|
},
|
|
4729
5225
|
WRITE_IDEMPOTENT,
|
|
4730
5226
|
async (params) => {
|
|
@@ -4779,19 +5275,19 @@ Use \`list_widgets\` to verify.`
|
|
|
4779
5275
|
}
|
|
4780
5276
|
|
|
4781
5277
|
// src/tools/widgets/remove-widget.ts
|
|
4782
|
-
import { z as
|
|
5278
|
+
import { z as z27 } from "zod";
|
|
4783
5279
|
function registerRemoveWidget(server, client) {
|
|
4784
5280
|
const services = createServiceBundle(client);
|
|
4785
5281
|
server.tool(
|
|
4786
5282
|
"remove_widget",
|
|
4787
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.",
|
|
4788
5284
|
{
|
|
4789
|
-
externalMetricId:
|
|
4790
|
-
clientId:
|
|
4791
|
-
dashboardName:
|
|
4792
|
-
workspaceName:
|
|
4793
|
-
embedId:
|
|
4794
|
-
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.")
|
|
4795
5291
|
},
|
|
4796
5292
|
DESTRUCTIVE,
|
|
4797
5293
|
async (params) => {
|
|
@@ -4839,21 +5335,21 @@ Note: this is a per-client soft delete. The widget remains available for other c
|
|
|
4839
5335
|
}
|
|
4840
5336
|
|
|
4841
5337
|
// src/tools/widgets/generate-widget.ts
|
|
4842
|
-
import { z as
|
|
5338
|
+
import { z as z28 } from "zod";
|
|
4843
5339
|
function registerGenerateWidget(server, client) {
|
|
4844
5340
|
const services = createServiceBundle(client);
|
|
4845
5341
|
server.tool(
|
|
4846
5342
|
"generate_widget",
|
|
4847
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.",
|
|
4848
5344
|
{
|
|
4849
|
-
clientId:
|
|
4850
|
-
userInputs:
|
|
4851
|
-
dashboardName:
|
|
4852
|
-
workspaceName:
|
|
4853
|
-
embedId:
|
|
4854
|
-
dataAppName:
|
|
4855
|
-
integrationId:
|
|
4856
|
-
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.")
|
|
4857
5353
|
},
|
|
4858
5354
|
READ_ONLY,
|
|
4859
5355
|
async (params) => {
|
|
@@ -4918,13 +5414,417 @@ function registerGenerateWidget(server, client) {
|
|
|
4918
5414
|
);
|
|
4919
5415
|
}
|
|
4920
5416
|
|
|
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);
|
|
5432
|
+
server.tool(
|
|
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.",
|
|
5435
|
+
{
|
|
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")
|
|
5453
|
+
},
|
|
5454
|
+
WRITE_IDEMPOTENT,
|
|
5455
|
+
async (params) => {
|
|
5456
|
+
try {
|
|
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
|
|
5479
|
+
);
|
|
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.`
|
|
5499
|
+
);
|
|
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
|
+
})
|
|
5559
|
+
);
|
|
5560
|
+
}
|
|
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.`
|
|
5642
|
+
);
|
|
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);
|
|
5663
|
+
return toolSuccess(
|
|
5664
|
+
JSON.stringify(
|
|
5665
|
+
{
|
|
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.`
|
|
5691
|
+
},
|
|
5692
|
+
null,
|
|
5693
|
+
2
|
|
5694
|
+
)
|
|
5695
|
+
);
|
|
5696
|
+
} catch (err) {
|
|
5697
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5698
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
);
|
|
5702
|
+
}
|
|
5703
|
+
|
|
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);
|
|
5708
|
+
server.tool(
|
|
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
|
+
},
|
|
5720
|
+
READ_ONLY,
|
|
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
|
+
});
|
|
5738
|
+
return toolSuccess(
|
|
5739
|
+
JSON.stringify(
|
|
5740
|
+
{
|
|
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
|
+
}))
|
|
5756
|
+
},
|
|
5757
|
+
null,
|
|
5758
|
+
2
|
|
5759
|
+
)
|
|
5760
|
+
);
|
|
5761
|
+
} catch (err) {
|
|
5762
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5763
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
);
|
|
5767
|
+
}
|
|
5768
|
+
|
|
5769
|
+
// src/tools/auth/whoami.ts
|
|
5770
|
+
function registerWhoami(server, client) {
|
|
5771
|
+
server.tool(
|
|
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.",
|
|
5774
|
+
{},
|
|
5775
|
+
READ_ONLY,
|
|
5776
|
+
async () => {
|
|
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
|
+
);
|
|
5790
|
+
}
|
|
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
|
+
)
|
|
5802
|
+
);
|
|
5803
|
+
}
|
|
5804
|
+
return toolSuccess(
|
|
5805
|
+
JSON.stringify(
|
|
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
|
+
},
|
|
5811
|
+
null,
|
|
5812
|
+
2
|
|
5813
|
+
)
|
|
5814
|
+
);
|
|
5815
|
+
}
|
|
5816
|
+
);
|
|
5817
|
+
}
|
|
5818
|
+
|
|
4921
5819
|
// src/tools/registry.ts
|
|
4922
5820
|
function registerAllTools(server, client) {
|
|
5821
|
+
registerWhoami(server, client);
|
|
4923
5822
|
registerListDatasources(server, client);
|
|
4924
5823
|
registerDiscoverDatasourceSchema(server, client);
|
|
4925
5824
|
registerListDatamarts(server, client);
|
|
4926
5825
|
registerListWorkspaces(server, client);
|
|
4927
|
-
|
|
5826
|
+
registerSyncDatasource(server, client);
|
|
5827
|
+
registerCreateDatamart(server, client);
|
|
4928
5828
|
registerListDataApps(server, client);
|
|
4929
5829
|
registerCreateDataApp(server, client);
|
|
4930
5830
|
registerListApiTokens(server, client);
|
|
@@ -4935,17 +5835,23 @@ function registerAllTools(server, client) {
|
|
|
4935
5835
|
registerGetEmbedDetails(server, client);
|
|
4936
5836
|
registerUpdateEmbed(server, client);
|
|
4937
5837
|
registerCustomizeTheme(server, client);
|
|
5838
|
+
registerGetDemoLink(server, client);
|
|
4938
5839
|
registerGenerateGuestToken(server, client);
|
|
4939
5840
|
registerGenerateEmbedCode(server, client);
|
|
4940
5841
|
registerListDashboards(server, client);
|
|
4941
5842
|
registerAskQuestion(server, client);
|
|
4942
5843
|
registerGetSemanticLayer(server, client);
|
|
4943
5844
|
registerUpdateSemanticLayer(server, client);
|
|
5845
|
+
registerStartSemanticLayerGeneration(server, client);
|
|
5846
|
+
registerGetSemanticLayerGenerationStatus(server, client);
|
|
4944
5847
|
registerListWidgets(server, client);
|
|
5848
|
+
registerValidateMetricSpec(server, client);
|
|
5849
|
+
registerCreateOrUpdateMetricFromQuery(server, client);
|
|
4945
5850
|
registerAddWidget(server, client);
|
|
4946
5851
|
registerUpdateWidget(server, client);
|
|
4947
5852
|
registerRemoveWidget(server, client);
|
|
4948
5853
|
registerGenerateWidget(server, client);
|
|
5854
|
+
registerListScheduledReports(server, client);
|
|
4949
5855
|
}
|
|
4950
5856
|
|
|
4951
5857
|
// src/resources/content/getting-started.ts
|
|
@@ -7308,7 +8214,7 @@ Then create an embed configuration with tenancy settings:
|
|
|
7308
8214
|
|
|
7309
8215
|
### Step 5: Generate Guest Tokens Per Tenant
|
|
7310
8216
|
|
|
7311
|
-
On your backend, when a user from
|
|
8217
|
+
On your backend, when a user from tenant A (org_id = "tenant_a") logs in:
|
|
7312
8218
|
|
|
7313
8219
|
\`\`\`typescript
|
|
7314
8220
|
// Server-side
|
|
@@ -7320,12 +8226,12 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
|
|
|
7320
8226
|
},
|
|
7321
8227
|
body: JSON.stringify({
|
|
7322
8228
|
embedId: 'embed-789',
|
|
7323
|
-
clientId: '
|
|
8229
|
+
clientId: 'tenant_a', // Maps to org_id values in the database
|
|
7324
8230
|
}),
|
|
7325
8231
|
});
|
|
7326
8232
|
\`\`\`
|
|
7327
8233
|
|
|
7328
|
-
When a user from
|
|
8234
|
+
When a user from tenant B (org_id = "tenant_b") logs in:
|
|
7329
8235
|
|
|
7330
8236
|
\`\`\`typescript
|
|
7331
8237
|
const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/create', {
|
|
@@ -7336,7 +8242,7 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
|
|
|
7336
8242
|
},
|
|
7337
8243
|
body: JSON.stringify({
|
|
7338
8244
|
embedId: 'embed-789',
|
|
7339
|
-
clientId: '
|
|
8245
|
+
clientId: 'tenant_b',
|
|
7340
8246
|
}),
|
|
7341
8247
|
});
|
|
7342
8248
|
\`\`\`
|
|
@@ -7344,20 +8250,20 @@ const guestToken = await fetch('https://api.usedatabrain.com/api/v2/guest-token/
|
|
|
7344
8250
|
### Step 6: Render
|
|
7345
8251
|
|
|
7346
8252
|
Both users see the same dashboard layout and metrics, but:
|
|
7347
|
-
-
|
|
7348
|
-
-
|
|
8253
|
+
- Tenant A sees only their projects, tasks, and team members
|
|
8254
|
+
- Tenant B sees only their projects, tasks, and team members
|
|
7349
8255
|
- Both see all task statuses (shared reference table)
|
|
7350
8256
|
|
|
7351
8257
|
### Result
|
|
7352
8258
|
|
|
7353
8259
|
\`\`\`
|
|
7354
|
-
|
|
7355
|
-
- 12 projects (all with org_id = '
|
|
8260
|
+
Tenant A user sees:
|
|
8261
|
+
- 12 projects (all with org_id = 'tenant_a')
|
|
7356
8262
|
- 87 tasks
|
|
7357
8263
|
- 15 team members
|
|
7358
8264
|
|
|
7359
|
-
|
|
7360
|
-
- 8 projects (all with org_id = '
|
|
8265
|
+
Tenant B user sees:
|
|
8266
|
+
- 8 projects (all with org_id = 'tenant_b')
|
|
7361
8267
|
- 45 tasks
|
|
7362
8268
|
- 9 team members
|
|
7363
8269
|
\`\`\`
|
|
@@ -8018,8 +8924,10 @@ Wait for explicit confirmation before calling \`remove_widget({ externalMetricId
|
|
|
8018
8924
|
**Step 4 \u2014 Verify**
|
|
8019
8925
|
Call \`list_widgets({ clientId, dashboardName, workspaceName? })\` again. Present the updated list and confirm the changes match what the user asked for.
|
|
8020
8926
|
|
|
8021
|
-
**Step 5 \u2014
|
|
8022
|
-
|
|
8927
|
+
**Step 5 \u2014 Hand off the demo link**
|
|
8928
|
+
Always close with a demo link so the user can verify visually. Call \`get_demo_link({ clientId, dashboardName, workspaceName? })\` \u2014 it returns the same URL the Databrain UI's "Share Demo Link" button generates: a hosted preview page that loads the dashboard exactly as that \`clientId\` will see it. Default expiry is 24h; pass \`expiryHours\` to override.
|
|
8929
|
+
|
|
8930
|
+
Present the URL to the user and remind them: (a) only \`isPublished: true\` widgets appear in the preview, so flag any unpublished widgets from the Step 4 \`list_widgets\` output; (b) if they already have the embed wired up in their own app, the cached frontend guest token there must be refreshed before changes appear.
|
|
8023
8931
|
|
|
8024
8932
|
**Important rules:**
|
|
8025
8933
|
- STOP after each step. Present results and wait for user input before proceeding.
|
|
@@ -8029,6 +8937,108 @@ If the user wants to see the changes in their running app, regenerate their gues
|
|
|
8029
8937
|
|
|
8030
8938
|
After completion, suggest: \`brand-my-embed\` if they want to adjust theme, or \`manage-embeds\` if they want to tweak access settings / options.`;
|
|
8031
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
|
+
|
|
8032
9042
|
// src/prompts/registry.ts
|
|
8033
9043
|
var prompts = [
|
|
8034
9044
|
{
|
|
@@ -8060,6 +9070,11 @@ var prompts = [
|
|
|
8060
9070
|
name: "customize-customer-dashboard",
|
|
8061
9071
|
description: "Add, modify, or remove widgets on a specific customer's dashboard embed. Per-client scope \u2014 changes only affect that customer.",
|
|
8062
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
|
|
8063
9078
|
}
|
|
8064
9079
|
];
|
|
8065
9080
|
function registerAllPrompts(server) {
|
|
@@ -8080,24 +9095,38 @@ function registerAllPrompts(server) {
|
|
|
8080
9095
|
|
|
8081
9096
|
// src/server.ts
|
|
8082
9097
|
function createServer(config) {
|
|
9098
|
+
const client = new DatabrainClient(config);
|
|
9099
|
+
const auth = client.getActiveAuth();
|
|
9100
|
+
let operatorBlurb;
|
|
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";
|
|
9105
|
+
} else {
|
|
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";
|
|
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.";
|
|
8083
9110
|
const server = new McpServer(
|
|
8084
9111
|
{
|
|
8085
9112
|
name: "databrain",
|
|
8086
|
-
version:
|
|
8087
|
-
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."
|
|
8088
9115
|
},
|
|
8089
9116
|
{
|
|
8090
|
-
instructions: `Databrain uses two token types \u2014 never confuse them:
|
|
8091
|
-
\u2022 SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, list_datamarts,
|
|
8092
|
-
\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)
|
|
9117
|
+
instructions: operatorBlurb + `Databrain uses two token types \u2014 never confuse them:
|
|
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}
|
|
8093
9120
|
|
|
8094
|
-
|
|
9121
|
+
4 core use cases \u2014 match the user's scenario to the right prompt:
|
|
8095
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)
|
|
8096
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)
|
|
8097
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.
|
|
8098
9128
|
|
|
8099
|
-
Infrastructure
|
|
8100
|
-
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.
|
|
8101
9130
|
|
|
8102
9131
|
After embed setup, customize with: brand-my-embed prompt for theming. Use update_embed (body passthrough) for filters, localization, and permissions.
|
|
8103
9132
|
Use generate_guest_token for frontend auth and generate_embed_code for framework snippets.
|
|
@@ -8114,7 +9143,6 @@ IMPORTANT \u2014 Be interactive. Present results from each tool call and ask the
|
|
|
8114
9143
|
Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, filters, permissions, datasource, datamart, workspace, metric creation.`
|
|
8115
9144
|
}
|
|
8116
9145
|
);
|
|
8117
|
-
const client = new DatabrainClient(config);
|
|
8118
9146
|
registerAllTools(server, client);
|
|
8119
9147
|
registerAllResources(server);
|
|
8120
9148
|
registerAllPrompts(server);
|
|
@@ -8122,7 +9150,6 @@ Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, fi
|
|
|
8122
9150
|
}
|
|
8123
9151
|
|
|
8124
9152
|
// src/index.ts
|
|
8125
|
-
var VERSION = "0.2.0";
|
|
8126
9153
|
function banner(msg) {
|
|
8127
9154
|
process.stderr.write(`[databrain] ${msg}
|
|
8128
9155
|
`);
|
|
@@ -8133,7 +9160,7 @@ async function main() {
|
|
|
8133
9160
|
const server = createServer(config);
|
|
8134
9161
|
const transport = new StdioServerTransport();
|
|
8135
9162
|
await server.connect(transport);
|
|
8136
|
-
banner(`v${
|
|
9163
|
+
banner(`v${MCP_SERVER_VERSION} ready`);
|
|
8137
9164
|
} catch (error) {
|
|
8138
9165
|
logger.error("Failed to start server", error);
|
|
8139
9166
|
process.exit(1);
|