@databrainhq/mcp-server 0.2.4 → 0.2.6
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 +21 -3
- package/dist/index.js +705 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -206,8 +206,8 @@ var DatabrainClient = class {
|
|
|
206
206
|
return cached;
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
-
const token = this.getToken(options.auth);
|
|
210
|
-
if (options.auth !== "none" && !token) {
|
|
209
|
+
const token = options.token ?? this.getToken(options.auth);
|
|
210
|
+
if (options.auth !== "none" && !options.token && !token) {
|
|
211
211
|
if (options.auth === "service") {
|
|
212
212
|
throw new DatabrainApiError(
|
|
213
213
|
401,
|
|
@@ -236,7 +236,12 @@ var DatabrainClient = class {
|
|
|
236
236
|
"X-Databrain-Client": MCP_USER_AGENT
|
|
237
237
|
};
|
|
238
238
|
if (token) {
|
|
239
|
-
|
|
239
|
+
if (options.auth === "none") {
|
|
240
|
+
headers["x-plugin-token"] = token;
|
|
241
|
+
headers["Origin"] = "https://mcp.usedatabrain.com";
|
|
242
|
+
} else {
|
|
243
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
244
|
+
}
|
|
240
245
|
}
|
|
241
246
|
const response = await fetch(url, {
|
|
242
247
|
method: options.method,
|
|
@@ -585,6 +590,68 @@ var QueryApi = class {
|
|
|
585
590
|
}
|
|
586
591
|
};
|
|
587
592
|
|
|
593
|
+
// src/api/widgets-api.ts
|
|
594
|
+
var WidgetsApi = class {
|
|
595
|
+
constructor(client) {
|
|
596
|
+
this.client = client;
|
|
597
|
+
}
|
|
598
|
+
async listWidgets(embedId, clientId) {
|
|
599
|
+
return this.client.request({
|
|
600
|
+
method: "GET",
|
|
601
|
+
path: "/api/v2/data-app/metrics",
|
|
602
|
+
auth: "api",
|
|
603
|
+
query: { embedId, clientId }
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
async createMetric(guestToken, body) {
|
|
607
|
+
return this.client.request({
|
|
608
|
+
method: "POST",
|
|
609
|
+
path: "/api/v2/dashboard/createMetric",
|
|
610
|
+
auth: "none",
|
|
611
|
+
token: guestToken,
|
|
612
|
+
body
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async updateMetric(guestToken, body) {
|
|
616
|
+
return this.client.request({
|
|
617
|
+
method: "POST",
|
|
618
|
+
path: "/api/v2/dashboard/updateMetric",
|
|
619
|
+
auth: "none",
|
|
620
|
+
token: guestToken,
|
|
621
|
+
body
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
async markArchived(guestToken, body) {
|
|
625
|
+
return this.client.request({
|
|
626
|
+
method: "POST",
|
|
627
|
+
path: "/api/v2/dashboard/markArchived",
|
|
628
|
+
auth: "none",
|
|
629
|
+
token: guestToken,
|
|
630
|
+
body
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
async generateMetric(guestToken, body) {
|
|
634
|
+
return this.client.request({
|
|
635
|
+
method: "POST",
|
|
636
|
+
path: "/api/v2/dashboard/generateMetric",
|
|
637
|
+
auth: "none",
|
|
638
|
+
token: guestToken,
|
|
639
|
+
body,
|
|
640
|
+
timeoutMs: 12e4,
|
|
641
|
+
noRetryOnTimeout: true
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
async fetchEmbeddedMetric(guestToken, metricId) {
|
|
645
|
+
return this.client.request({
|
|
646
|
+
method: "GET",
|
|
647
|
+
path: "/api/v2/dashboard/embeddedMetric",
|
|
648
|
+
auth: "none",
|
|
649
|
+
token: guestToken,
|
|
650
|
+
query: { metricId }
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
588
655
|
// src/api/workspaces-api.ts
|
|
589
656
|
var WorkspacesApi = class {
|
|
590
657
|
constructor(client) {
|
|
@@ -863,6 +930,126 @@ var GuestTokenService = class {
|
|
|
863
930
|
}
|
|
864
931
|
};
|
|
865
932
|
|
|
933
|
+
// src/services/widget-service.ts
|
|
934
|
+
var isNonEmptyString3 = (v) => typeof v === "string" && v.trim().length > 0;
|
|
935
|
+
var asRecord2 = (v) => {
|
|
936
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return null;
|
|
937
|
+
return v;
|
|
938
|
+
};
|
|
939
|
+
var WidgetService = class {
|
|
940
|
+
constructor(widgetsApi, embedsApi, dataAppsApi, guestTokenService, discoveryService) {
|
|
941
|
+
this.widgetsApi = widgetsApi;
|
|
942
|
+
this.embedsApi = embedsApi;
|
|
943
|
+
this.dataAppsApi = dataAppsApi;
|
|
944
|
+
this.guestTokenService = guestTokenService;
|
|
945
|
+
this.discoveryService = discoveryService;
|
|
946
|
+
}
|
|
947
|
+
async resolveEmbedContext(embedId) {
|
|
948
|
+
const response = await this.embedsApi.getByEmbedId(embedId);
|
|
949
|
+
const records = Array.isArray(response) ? response : response ? [response] : [];
|
|
950
|
+
const exact = records.find((r) => r && r.embedId === embedId) || records[0];
|
|
951
|
+
if (!exact) {
|
|
952
|
+
throw new Error(`Embed "${embedId}" not found. Run \`list_embeds\` to verify.`);
|
|
953
|
+
}
|
|
954
|
+
const metadata = asRecord2(exact.embedMetadata);
|
|
955
|
+
const externalDashboard = asRecord2(exact.externalDashboard);
|
|
956
|
+
const dataAppId = isNonEmptyString3(exact.dataAppId) ? exact.dataAppId : null;
|
|
957
|
+
const externalDashboardId = isNonEmptyString3(exact.externalDashboardId) && exact.externalDashboardId || (externalDashboard && isNonEmptyString3(externalDashboard.externalDashboardId) ? externalDashboard.externalDashboardId : null) || (metadata && isNonEmptyString3(metadata.externalDashboardId) ? metadata.externalDashboardId : null);
|
|
958
|
+
const dashboardId = isNonEmptyString3(exact.dashboardId) && exact.dashboardId || (metadata && isNonEmptyString3(metadata.dashboardId) ? metadata.dashboardId : null);
|
|
959
|
+
let dataAppName = metadata && isNonEmptyString3(metadata.dataAppName) ? metadata.dataAppName : null;
|
|
960
|
+
if (!dataAppName && dataAppId) {
|
|
961
|
+
try {
|
|
962
|
+
const apps = await this.dataAppsApi.listDataApps();
|
|
963
|
+
const match = apps.find((a) => {
|
|
964
|
+
const n = this.discoveryService.normalizeDataApp(a);
|
|
965
|
+
return n.dataAppId === dataAppId;
|
|
966
|
+
});
|
|
967
|
+
if (match) {
|
|
968
|
+
dataAppName = this.discoveryService.normalizeDataApp(match).name;
|
|
969
|
+
}
|
|
970
|
+
} catch {
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
embedId,
|
|
975
|
+
dataAppId,
|
|
976
|
+
dataAppName,
|
|
977
|
+
externalDashboardId,
|
|
978
|
+
dashboardId
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
async mintGuestToken(clientId, dataAppName, allowedEmbeds) {
|
|
982
|
+
const { token } = await this.guestTokenService.generateToken({
|
|
983
|
+
clientId,
|
|
984
|
+
dataAppName,
|
|
985
|
+
...allowedEmbeds && allowedEmbeds.length > 0 ? { allowedEmbeds } : {},
|
|
986
|
+
permissions: {
|
|
987
|
+
isDisableMetricCreation: false,
|
|
988
|
+
isEnableManageMetrics: true,
|
|
989
|
+
isEnableMetricUpdation: true,
|
|
990
|
+
isEnableArchiveMetrics: true
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
return token;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Build a create body by cloning a template metric. Fetches the full metric
|
|
997
|
+
* via the embeddedMetric endpoint and copies structural fields, then lets the
|
|
998
|
+
* caller override `name`/`metricId`/etc.
|
|
999
|
+
*/
|
|
1000
|
+
async buildCloneBody(guestToken, templateMetricId, overrides, externalDashboardId) {
|
|
1001
|
+
const response = await this.widgetsApi.fetchEmbeddedMetric(
|
|
1002
|
+
guestToken,
|
|
1003
|
+
templateMetricId
|
|
1004
|
+
);
|
|
1005
|
+
const metric = asRecord2(response.externalMetric);
|
|
1006
|
+
if (!metric) {
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
`Template metric "${templateMetricId}" not found or could not be loaded. Response keys: ${Object.keys(
|
|
1009
|
+
response || {}
|
|
1010
|
+
).join(", ")}. Error: ${JSON.stringify(response?.error ?? null)}`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
const clone = {
|
|
1014
|
+
// Required identity — caller must override or we derive.
|
|
1015
|
+
name: overrides.name || (isNonEmptyString3(metric.name) ? `${metric.name} (copy)` : "Widget"),
|
|
1016
|
+
metricId: overrides.metricId || `${isNonEmptyString3(metric.metricId) && metric.metricId || "widget"}-${Date.now()}`,
|
|
1017
|
+
// Structural fields copied from template.
|
|
1018
|
+
query: metric.query || void 0,
|
|
1019
|
+
metricQuery: metric.metricQuery || void 0,
|
|
1020
|
+
description: metric.description || "",
|
|
1021
|
+
chartOptions: asRecord2(metric.chartOptions) || {},
|
|
1022
|
+
companyId: metric.companyId || void 0,
|
|
1023
|
+
companyIntegrationId: metric.companyIntegrationId || void 0,
|
|
1024
|
+
integrationName: metric.integrationName || void 0,
|
|
1025
|
+
outputColumns: metric.outputColumns || void 0,
|
|
1026
|
+
inputFields: metric.inputFields,
|
|
1027
|
+
isLive: metric.isLive ?? void 0,
|
|
1028
|
+
timeGrain: metric.timeGrain || void 0,
|
|
1029
|
+
selectedGroupBy: metric.selectedGroupBy,
|
|
1030
|
+
isEnableGroupBy: metric.isEnableGroupBy ?? void 0,
|
|
1031
|
+
datasetMetricSettings: metric.datasetMetricSettings,
|
|
1032
|
+
groupBy: metric.groupBy,
|
|
1033
|
+
drillDownSettings: metric.drillDownSettings,
|
|
1034
|
+
limit: metric.limit ?? void 0,
|
|
1035
|
+
// Dashboard attachment — always for the target embed's dashboard.
|
|
1036
|
+
externalDashboardIds: [{ externalDashboardId }],
|
|
1037
|
+
// Defaults that shouldn't be inherited from the template.
|
|
1038
|
+
isCreatedByClient: false,
|
|
1039
|
+
isPublished: true
|
|
1040
|
+
};
|
|
1041
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
1042
|
+
if (value !== void 0) {
|
|
1043
|
+
clone[key] = value;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (!clone.externalDashboardIds || clone.externalDashboardIds.length === 0) {
|
|
1047
|
+
clone.externalDashboardIds = [{ externalDashboardId }];
|
|
1048
|
+
}
|
|
1049
|
+
return clone;
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
|
|
866
1053
|
// src/services/factory.ts
|
|
867
1054
|
function createServiceBundle(client) {
|
|
868
1055
|
const embedsApi = new EmbedsApi(client);
|
|
@@ -872,7 +1059,10 @@ function createServiceBundle(client) {
|
|
|
872
1059
|
const datasourcesApi = new DatasourcesApi(client);
|
|
873
1060
|
const guestTokenApi = new GuestTokenApi(client);
|
|
874
1061
|
const queryApi = new QueryApi(client);
|
|
1062
|
+
const widgetsApi = new WidgetsApi(client);
|
|
875
1063
|
const workspacesApi = new WorkspacesApi(client);
|
|
1064
|
+
const discoveryService = new DiscoveryService();
|
|
1065
|
+
const guestTokenService = new GuestTokenService(guestTokenApi);
|
|
876
1066
|
return {
|
|
877
1067
|
embedsApi,
|
|
878
1068
|
dashboardsApi,
|
|
@@ -881,9 +1071,17 @@ function createServiceBundle(client) {
|
|
|
881
1071
|
datasourcesApi,
|
|
882
1072
|
workspacesApi,
|
|
883
1073
|
queryApi,
|
|
1074
|
+
widgetsApi,
|
|
884
1075
|
embedService: new EmbedService(embedsApi, datamartsApi),
|
|
885
|
-
discoveryService
|
|
886
|
-
guestTokenService
|
|
1076
|
+
discoveryService,
|
|
1077
|
+
guestTokenService,
|
|
1078
|
+
widgetService: new WidgetService(
|
|
1079
|
+
widgetsApi,
|
|
1080
|
+
embedsApi,
|
|
1081
|
+
dataAppsApi,
|
|
1082
|
+
guestTokenService,
|
|
1083
|
+
discoveryService
|
|
1084
|
+
)
|
|
887
1085
|
};
|
|
888
1086
|
}
|
|
889
1087
|
|
|
@@ -1281,6 +1479,7 @@ function registerListDatasources(server, client) {
|
|
|
1281
1479
|
return toolSuccess(JSON.stringify({
|
|
1282
1480
|
count: active.length,
|
|
1283
1481
|
datasources: active.map((ds) => ({
|
|
1482
|
+
id: ds.id,
|
|
1284
1483
|
name: ds.name,
|
|
1285
1484
|
type: ds.dbName || ds.datasourceType || "unknown"
|
|
1286
1485
|
})),
|
|
@@ -4012,6 +4211,424 @@ You can now:
|
|
|
4012
4211
|
);
|
|
4013
4212
|
}
|
|
4014
4213
|
|
|
4214
|
+
// src/tools/widgets/list-widgets.ts
|
|
4215
|
+
import { z as z21 } from "zod";
|
|
4216
|
+
function registerListWidgets(server, client) {
|
|
4217
|
+
const services = createServiceBundle(client);
|
|
4218
|
+
server.tool(
|
|
4219
|
+
"list_widgets",
|
|
4220
|
+
"List widgets (metrics) on a customer dashboard embed. Returns each widget's metricId, name, published state, and creator identifier.",
|
|
4221
|
+
{
|
|
4222
|
+
embedId: z21.string().describe("Embed ID (customer dashboard instantiation)"),
|
|
4223
|
+
clientId: z21.string().describe("Client/tenant identifier for this embed")
|
|
4224
|
+
},
|
|
4225
|
+
READ_ONLY,
|
|
4226
|
+
async (params) => {
|
|
4227
|
+
try {
|
|
4228
|
+
const widgets = await services.widgetsApi.listWidgets(
|
|
4229
|
+
params.embedId,
|
|
4230
|
+
params.clientId
|
|
4231
|
+
);
|
|
4232
|
+
if (!Array.isArray(widgets) || widgets.length === 0) {
|
|
4233
|
+
return toolSuccess(
|
|
4234
|
+
`No widgets found on embed "${params.embedId}" for client "${params.clientId}".`
|
|
4235
|
+
);
|
|
4236
|
+
}
|
|
4237
|
+
return toolSuccess(
|
|
4238
|
+
JSON.stringify({
|
|
4239
|
+
count: widgets.length,
|
|
4240
|
+
embedId: params.embedId,
|
|
4241
|
+
clientId: params.clientId,
|
|
4242
|
+
widgets: widgets.map((w) => ({
|
|
4243
|
+
metricId: w.metricId || null,
|
|
4244
|
+
name: w.name || null,
|
|
4245
|
+
isPublished: w.isPublished ?? null,
|
|
4246
|
+
createdByUser: w.createdByUser || null
|
|
4247
|
+
}))
|
|
4248
|
+
})
|
|
4249
|
+
);
|
|
4250
|
+
} catch (err) {
|
|
4251
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
4252
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
);
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
// src/tools/widgets/add-widget.ts
|
|
4259
|
+
import { z as z22 } from "zod";
|
|
4260
|
+
function registerAddWidget(server, client) {
|
|
4261
|
+
const services = createServiceBundle(client);
|
|
4262
|
+
server.tool(
|
|
4263
|
+
"add_widget",
|
|
4264
|
+
"Add a widget (metric) to a customer dashboard embed. Either clone a template via `templateMetricId` (recommended \u2014 inherits query, chartOptions, datasource), or supply a full spec (name + query + chartOptions + companyIntegrationId). Mints a guest token for the clientId internally.",
|
|
4265
|
+
{
|
|
4266
|
+
embedId: z22.string().describe("Embed ID the widget will be added to"),
|
|
4267
|
+
clientId: z22.string().describe("Client/tenant identifier (widget is created in their scope)"),
|
|
4268
|
+
dataAppName: z22.string().optional().describe("Data app name (required for guest token). Auto-resolved from embedId if omitted."),
|
|
4269
|
+
templateMetricId: z22.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
|
|
4270
|
+
name: z22.string().optional().describe("Widget name (overrides template)"),
|
|
4271
|
+
metricId: z22.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
|
|
4272
|
+
query: z22.string().optional().describe("SQL query body (overrides template)"),
|
|
4273
|
+
chartOptions: z22.record(z22.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
|
|
4274
|
+
description: z22.string().optional(),
|
|
4275
|
+
companyIntegrationId: z22.string().optional().describe("Datasource ID. Required when not cloning a template."),
|
|
4276
|
+
integrationName: z22.string().optional(),
|
|
4277
|
+
isPublished: z22.boolean().optional().describe("Publish immediately (default true)"),
|
|
4278
|
+
body: z22.record(z22.unknown()).optional().describe("Raw createMetric body passthrough. If provided, used as-is (after defaults). Overrides all other params.")
|
|
4279
|
+
},
|
|
4280
|
+
WRITE_CREATE,
|
|
4281
|
+
async (params) => {
|
|
4282
|
+
try {
|
|
4283
|
+
const ctx = await services.widgetService.resolveEmbedContext(params.embedId);
|
|
4284
|
+
const dataAppName = params.dataAppName || ctx.dataAppName;
|
|
4285
|
+
if (!dataAppName) {
|
|
4286
|
+
return toolError(
|
|
4287
|
+
`Could not resolve dataAppName for embed "${params.embedId}". Pass \`dataAppName\` explicitly or verify the embed exists via \`list_embeds\`.`
|
|
4288
|
+
);
|
|
4289
|
+
}
|
|
4290
|
+
if (!ctx.externalDashboardId) {
|
|
4291
|
+
return toolError(
|
|
4292
|
+
`Embed "${params.embedId}" has no externalDashboardId \u2014 cannot attach a widget. Verify via \`get_embed_details\`.`
|
|
4293
|
+
);
|
|
4294
|
+
}
|
|
4295
|
+
const guestToken = await services.widgetService.mintGuestToken(
|
|
4296
|
+
params.clientId,
|
|
4297
|
+
dataAppName,
|
|
4298
|
+
[params.embedId]
|
|
4299
|
+
);
|
|
4300
|
+
let createBody;
|
|
4301
|
+
if (params.body) {
|
|
4302
|
+
createBody = { ...params.body };
|
|
4303
|
+
if (!createBody.externalDashboardIds || createBody.externalDashboardIds.length === 0) {
|
|
4304
|
+
createBody.externalDashboardIds = [
|
|
4305
|
+
{ externalDashboardId: ctx.externalDashboardId }
|
|
4306
|
+
];
|
|
4307
|
+
}
|
|
4308
|
+
} else if (params.templateMetricId) {
|
|
4309
|
+
const overrides = {};
|
|
4310
|
+
if (params.name !== void 0) overrides.name = params.name;
|
|
4311
|
+
if (params.metricId !== void 0) overrides.metricId = params.metricId;
|
|
4312
|
+
if (params.query !== void 0) overrides.query = params.query;
|
|
4313
|
+
if (params.chartOptions !== void 0) overrides.chartOptions = params.chartOptions;
|
|
4314
|
+
if (params.description !== void 0) overrides.description = params.description;
|
|
4315
|
+
if (params.companyIntegrationId !== void 0) {
|
|
4316
|
+
overrides.companyIntegrationId = params.companyIntegrationId;
|
|
4317
|
+
}
|
|
4318
|
+
if (params.integrationName !== void 0) overrides.integrationName = params.integrationName;
|
|
4319
|
+
if (params.isPublished !== void 0) overrides.isPublished = params.isPublished;
|
|
4320
|
+
createBody = await services.widgetService.buildCloneBody(
|
|
4321
|
+
guestToken,
|
|
4322
|
+
params.templateMetricId,
|
|
4323
|
+
overrides,
|
|
4324
|
+
ctx.externalDashboardId
|
|
4325
|
+
);
|
|
4326
|
+
} else {
|
|
4327
|
+
if (!params.name) return toolError("`name` is required when not cloning a template.");
|
|
4328
|
+
if (!params.query) return toolError("`query` is required when not cloning a template.");
|
|
4329
|
+
if (!params.companyIntegrationId) {
|
|
4330
|
+
return toolError(
|
|
4331
|
+
"`companyIntegrationId` is required when not cloning a template. Use `list_datasources` to find the datasource ID, or clone from an existing widget via `templateMetricId`."
|
|
4332
|
+
);
|
|
4333
|
+
}
|
|
4334
|
+
createBody = {
|
|
4335
|
+
name: params.name,
|
|
4336
|
+
metricId: params.metricId || `widget-${Date.now()}`,
|
|
4337
|
+
query: params.query,
|
|
4338
|
+
chartOptions: params.chartOptions || {},
|
|
4339
|
+
description: params.description || "",
|
|
4340
|
+
companyIntegrationId: params.companyIntegrationId,
|
|
4341
|
+
integrationName: params.integrationName,
|
|
4342
|
+
isPublished: params.isPublished ?? true,
|
|
4343
|
+
isCreatedByClient: false,
|
|
4344
|
+
externalDashboardIds: [{ externalDashboardId: ctx.externalDashboardId }]
|
|
4345
|
+
};
|
|
4346
|
+
}
|
|
4347
|
+
const result = await services.widgetsApi.createMetric(guestToken, createBody);
|
|
4348
|
+
if (result.error) {
|
|
4349
|
+
return toolError(
|
|
4350
|
+
`Widget creation failed: ${stringify(result.error)}
|
|
4351
|
+
|
|
4352
|
+
Request body sent:
|
|
4353
|
+
${JSON.stringify(createBody, null, 2).slice(0, 3e3)}`
|
|
4354
|
+
);
|
|
4355
|
+
}
|
|
4356
|
+
const inserted = result.insert_externalMetrics_one;
|
|
4357
|
+
const newId = inserted?.id || inserted?.metricId || "UNKNOWN_ID";
|
|
4358
|
+
const newMetricId = inserted?.metricId || createBody.metricId;
|
|
4359
|
+
return toolSuccess(
|
|
4360
|
+
`Widget added successfully.
|
|
4361
|
+
|
|
4362
|
+
- externalMetricId: \`${newId}\`
|
|
4363
|
+
- metricId: \`${newMetricId}\`
|
|
4364
|
+
- name: \`${createBody.name}\`
|
|
4365
|
+
- embedId: \`${params.embedId}\`
|
|
4366
|
+
- clientId: \`${params.clientId}\`
|
|
4367
|
+
|
|
4368
|
+
Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust.`
|
|
4369
|
+
);
|
|
4370
|
+
} catch (err) {
|
|
4371
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
4372
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
);
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
// src/tools/widgets/update-widget.ts
|
|
4379
|
+
import { z as z23 } from "zod";
|
|
4380
|
+
function registerUpdateWidget(server, client) {
|
|
4381
|
+
const services = createServiceBundle(client);
|
|
4382
|
+
server.tool(
|
|
4383
|
+
"update_widget",
|
|
4384
|
+
"Update a widget on a customer dashboard embed. Applies a partial patch to the externalMetrics row (name, query, chartOptions, isPublished, etc.). Mints a guest token internally.",
|
|
4385
|
+
{
|
|
4386
|
+
externalMetricId: z23.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
|
|
4387
|
+
clientId: z23.string().describe("Client/tenant identifier"),
|
|
4388
|
+
dataAppName: z23.string().describe("Data app name the widget lives in (required to mint guest token)"),
|
|
4389
|
+
embedId: z23.string().optional().describe("Optional \u2014 if provided, the guest token is scoped to just this embed."),
|
|
4390
|
+
set: z23.record(z23.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
|
|
4391
|
+
},
|
|
4392
|
+
WRITE_IDEMPOTENT,
|
|
4393
|
+
async (params) => {
|
|
4394
|
+
try {
|
|
4395
|
+
if (!params.set || Object.keys(params.set).length === 0) {
|
|
4396
|
+
return toolError("`set` must contain at least one field to update.");
|
|
4397
|
+
}
|
|
4398
|
+
const guestToken = await services.widgetService.mintGuestToken(
|
|
4399
|
+
params.clientId,
|
|
4400
|
+
params.dataAppName,
|
|
4401
|
+
params.embedId ? [params.embedId] : void 0
|
|
4402
|
+
);
|
|
4403
|
+
const result = await services.widgetsApi.updateMetric(guestToken, {
|
|
4404
|
+
externalMetricId: params.externalMetricId,
|
|
4405
|
+
set: params.set
|
|
4406
|
+
});
|
|
4407
|
+
if (result.error) {
|
|
4408
|
+
return toolError(
|
|
4409
|
+
`Widget update failed: ${stringify(result.error)}`
|
|
4410
|
+
);
|
|
4411
|
+
}
|
|
4412
|
+
const updated = result.update_externalMetrics_by_pk;
|
|
4413
|
+
const id = updated?.id || params.externalMetricId;
|
|
4414
|
+
const metricId = updated?.metricId || null;
|
|
4415
|
+
return toolSuccess(
|
|
4416
|
+
`Widget updated successfully.
|
|
4417
|
+
|
|
4418
|
+
- externalMetricId: \`${id}\`
|
|
4419
|
+
` + (metricId ? `- metricId: \`${metricId}\`
|
|
4420
|
+
` : "") + `- fields changed: ${Object.keys(params.set).join(", ")}
|
|
4421
|
+
|
|
4422
|
+
Use \`list_widgets\` to verify.`
|
|
4423
|
+
);
|
|
4424
|
+
} catch (err) {
|
|
4425
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
4426
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
);
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
// src/tools/widgets/remove-widget.ts
|
|
4433
|
+
import { z as z24 } from "zod";
|
|
4434
|
+
function registerRemoveWidget(server, client) {
|
|
4435
|
+
const services = createServiceBundle(client);
|
|
4436
|
+
server.tool(
|
|
4437
|
+
"remove_widget",
|
|
4438
|
+
"Remove a widget from a customer dashboard embed. This is a per-client soft delete (writes to clientDeletedMetrics) \u2014 the widget stays on the template dashboard and on other customers' embeds.",
|
|
4439
|
+
{
|
|
4440
|
+
externalMetricId: z24.string().describe("Internal uuid of the widget (externalMetrics.id)"),
|
|
4441
|
+
clientId: z24.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
|
|
4442
|
+
dataAppName: z24.string().describe("Data app name the widget lives in (required to mint guest token)"),
|
|
4443
|
+
embedId: z24.string().optional().describe("Optional \u2014 scopes the guest token to this embed.")
|
|
4444
|
+
},
|
|
4445
|
+
DESTRUCTIVE,
|
|
4446
|
+
async (params) => {
|
|
4447
|
+
try {
|
|
4448
|
+
const guestToken = await services.widgetService.mintGuestToken(
|
|
4449
|
+
params.clientId,
|
|
4450
|
+
params.dataAppName,
|
|
4451
|
+
params.embedId ? [params.embedId] : void 0
|
|
4452
|
+
);
|
|
4453
|
+
const result = await services.widgetsApi.markArchived(guestToken, {
|
|
4454
|
+
id: params.externalMetricId,
|
|
4455
|
+
clientId: params.clientId
|
|
4456
|
+
});
|
|
4457
|
+
if (result.error) {
|
|
4458
|
+
return toolError(
|
|
4459
|
+
`Widget removal failed: ${stringify(result.error)}`
|
|
4460
|
+
);
|
|
4461
|
+
}
|
|
4462
|
+
return toolSuccess(
|
|
4463
|
+
`Widget removed for client "${params.clientId}".
|
|
4464
|
+
|
|
4465
|
+
- externalMetricId: \`${params.externalMetricId}\`
|
|
4466
|
+
- clientId: \`${params.clientId}\`
|
|
4467
|
+
|
|
4468
|
+
Note: this is a per-client soft delete. The widget remains available for other clients and on the source dashboard.`
|
|
4469
|
+
);
|
|
4470
|
+
} catch (err) {
|
|
4471
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
4472
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
);
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
// src/tools/widgets/generate-widget.ts
|
|
4479
|
+
import { z as z25 } from "zod";
|
|
4480
|
+
function registerGenerateWidget(server, client) {
|
|
4481
|
+
const services = createServiceBundle(client);
|
|
4482
|
+
server.tool(
|
|
4483
|
+
"generate_widget",
|
|
4484
|
+
"AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft the caller can review and pass to `add_widget`. Data app and datasource auto-resolve from `embedId` (preferred) or from the sole workspace defaults. `dataAppName`/`integrationId`/`integrationName` are technical overrides.",
|
|
4485
|
+
{
|
|
4486
|
+
clientId: z25.string().describe("Client/tenant identifier"),
|
|
4487
|
+
userInputs: z25.record(z25.unknown()).describe('Prompt + context object. Typical shape: { prompt: "calls per day over last 30 days", schema: {...}, examples: [...] }. Backend decides which keys it honors.'),
|
|
4488
|
+
embedId: z25.string().optional().describe("Optional. When provided, data app and datasource are inherited from the embed's existing widgets \u2014 the preferred path for CS/CX flows."),
|
|
4489
|
+
dataAppName: z25.string().optional().describe("Optional override. Auto-resolved from embedId, or from the sole data app when there is only one."),
|
|
4490
|
+
integrationId: z25.string().optional().describe("Optional datasource override (technical users only). Auto-resolved from the embed's widgets, or from the sole datasource when there is only one."),
|
|
4491
|
+
integrationName: z25.string().optional().describe("Optional datasource name override. Auto-resolved alongside integrationId.")
|
|
4492
|
+
},
|
|
4493
|
+
READ_ONLY,
|
|
4494
|
+
async (params) => {
|
|
4495
|
+
try {
|
|
4496
|
+
const dataAppName = await resolveDataAppName(
|
|
4497
|
+
services,
|
|
4498
|
+
params.dataAppName,
|
|
4499
|
+
params.embedId
|
|
4500
|
+
);
|
|
4501
|
+
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
4502
|
+
const guestToken = await services.widgetService.mintGuestToken(
|
|
4503
|
+
params.clientId,
|
|
4504
|
+
dataAppName.value,
|
|
4505
|
+
params.embedId ? [params.embedId] : void 0
|
|
4506
|
+
);
|
|
4507
|
+
const datasource = await resolveDatasource(services, guestToken, {
|
|
4508
|
+
embedId: params.embedId,
|
|
4509
|
+
clientId: params.clientId,
|
|
4510
|
+
integrationId: params.integrationId,
|
|
4511
|
+
integrationName: params.integrationName
|
|
4512
|
+
});
|
|
4513
|
+
if (!datasource.ok) return toolError(datasource.error);
|
|
4514
|
+
const result = await services.widgetsApi.generateMetric(guestToken, {
|
|
4515
|
+
integrationId: datasource.value.integrationId,
|
|
4516
|
+
integrationName: datasource.value.integrationName,
|
|
4517
|
+
userInputs: params.userInputs
|
|
4518
|
+
});
|
|
4519
|
+
const resultRec = result;
|
|
4520
|
+
if (resultRec.error) {
|
|
4521
|
+
return toolError(
|
|
4522
|
+
`Widget generation failed: ${stringify(resultRec.error)}`
|
|
4523
|
+
);
|
|
4524
|
+
}
|
|
4525
|
+
return toolSuccess(
|
|
4526
|
+
JSON.stringify({
|
|
4527
|
+
message: "Draft generated. Review the `query` and `result` fields below, then pass to `add_widget` (using raw `body` passthrough) to persist.",
|
|
4528
|
+
resolved: {
|
|
4529
|
+
dataAppName: dataAppName.value,
|
|
4530
|
+
integrationName: datasource.value.integrationName
|
|
4531
|
+
},
|
|
4532
|
+
draft: {
|
|
4533
|
+
query: resultRec.query ?? null,
|
|
4534
|
+
result: resultRec.result ?? null,
|
|
4535
|
+
metaData: resultRec.metaData ?? null,
|
|
4536
|
+
timeTaken: resultRec.timeTaken ?? null,
|
|
4537
|
+
error: resultRec.error ?? null
|
|
4538
|
+
}
|
|
4539
|
+
})
|
|
4540
|
+
);
|
|
4541
|
+
} catch (err) {
|
|
4542
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
4543
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
);
|
|
4547
|
+
}
|
|
4548
|
+
async function resolveDataAppName(services, provided, embedId) {
|
|
4549
|
+
if (provided) return { ok: true, value: provided };
|
|
4550
|
+
if (embedId) {
|
|
4551
|
+
const ctx = await services.widgetService.resolveEmbedContext(embedId);
|
|
4552
|
+
if (ctx.dataAppName) return { ok: true, value: ctx.dataAppName };
|
|
4553
|
+
}
|
|
4554
|
+
const apps = await services.dataAppsApi.listDataApps();
|
|
4555
|
+
const normalized = apps.map((a) => services.discoveryService.normalizeDataApp(a)).filter((a) => a.name && a.name !== "Unnamed Data App");
|
|
4556
|
+
if (normalized.length === 1) return { ok: true, value: normalized[0].name };
|
|
4557
|
+
if (normalized.length === 0) {
|
|
4558
|
+
return {
|
|
4559
|
+
ok: false,
|
|
4560
|
+
error: "No data apps found in this workspace. Create one in the Databrain UI first."
|
|
4561
|
+
};
|
|
4562
|
+
}
|
|
4563
|
+
return {
|
|
4564
|
+
ok: false,
|
|
4565
|
+
error: `Multiple data apps exist (${normalized.map((a) => `"${a.name}"`).join(", ")}). Pass \`dataAppName\` explicitly, or pass \`embedId\` to auto-resolve.`
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
async function resolveDatasource(services, guestToken, params) {
|
|
4569
|
+
if (params.integrationId && params.integrationName) {
|
|
4570
|
+
return {
|
|
4571
|
+
ok: true,
|
|
4572
|
+
value: {
|
|
4573
|
+
integrationId: params.integrationId,
|
|
4574
|
+
integrationName: params.integrationName
|
|
4575
|
+
}
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
if (params.embedId) {
|
|
4579
|
+
const fromEmbed = await resolveDatasourceFromEmbed(
|
|
4580
|
+
services,
|
|
4581
|
+
guestToken,
|
|
4582
|
+
params.embedId,
|
|
4583
|
+
params.clientId
|
|
4584
|
+
);
|
|
4585
|
+
if (fromEmbed) {
|
|
4586
|
+
return {
|
|
4587
|
+
ok: true,
|
|
4588
|
+
value: {
|
|
4589
|
+
integrationId: params.integrationId || fromEmbed.integrationId,
|
|
4590
|
+
integrationName: params.integrationName || fromEmbed.integrationName
|
|
4591
|
+
}
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
const datasources = await services.datasourcesApi.listDatasources();
|
|
4596
|
+
const named = datasources.filter(
|
|
4597
|
+
(ds) => typeof ds.name === "string" && ds.name.trim().length > 0
|
|
4598
|
+
);
|
|
4599
|
+
if (named.length === 1) {
|
|
4600
|
+
const ds = named[0];
|
|
4601
|
+
const id = ds._id || ds.id || "";
|
|
4602
|
+
const name = ds.name;
|
|
4603
|
+
if (id && name) {
|
|
4604
|
+
return { ok: true, value: { integrationId: id, integrationName: name } };
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
if (named.length === 0) {
|
|
4608
|
+
return {
|
|
4609
|
+
ok: false,
|
|
4610
|
+
error: "No datasources found in this workspace. Connect one in the Databrain UI first (technical setup)."
|
|
4611
|
+
};
|
|
4612
|
+
}
|
|
4613
|
+
return {
|
|
4614
|
+
ok: false,
|
|
4615
|
+
error: `Could not auto-resolve a datasource. Options: ${named.map((d) => `"${d.name}"`).join(", ")}. Either pass \`embedId\` (to inherit from the embed's existing widgets), or pass \`integrationId\` + \`integrationName\` explicitly.`
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
async function resolveDatasourceFromEmbed(services, guestToken, embedId, clientId) {
|
|
4619
|
+
const widgets = await services.widgetsApi.listWidgets(embedId, clientId);
|
|
4620
|
+
const first = widgets.find((w) => typeof w.metricId === "string" && w.metricId.length > 0);
|
|
4621
|
+
if (!first || !first.metricId) return null;
|
|
4622
|
+
const response = await services.widgetsApi.fetchEmbeddedMetric(guestToken, first.metricId);
|
|
4623
|
+
const metric = response.externalMetric;
|
|
4624
|
+
if (!metric || typeof metric !== "object") return null;
|
|
4625
|
+
const rec = metric;
|
|
4626
|
+
const integrationId = typeof rec.companyIntegrationId === "string" ? rec.companyIntegrationId : null;
|
|
4627
|
+
const integrationName = typeof rec.integrationName === "string" ? rec.integrationName : null;
|
|
4628
|
+
if (!integrationId || !integrationName) return null;
|
|
4629
|
+
return { integrationId, integrationName };
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4015
4632
|
// src/tools/registry.ts
|
|
4016
4633
|
function registerAllTools(server, client) {
|
|
4017
4634
|
registerListDatasources(server, client);
|
|
@@ -4035,6 +4652,11 @@ function registerAllTools(server, client) {
|
|
|
4035
4652
|
registerAskQuestion(server, client);
|
|
4036
4653
|
registerGetSemanticLayer(server, client);
|
|
4037
4654
|
registerUpdateSemanticLayer(server, client);
|
|
4655
|
+
registerListWidgets(server, client);
|
|
4656
|
+
registerAddWidget(server, client);
|
|
4657
|
+
registerUpdateWidget(server, client);
|
|
4658
|
+
registerRemoveWidget(server, client);
|
|
4659
|
+
registerGenerateWidget(server, client);
|
|
4038
4660
|
}
|
|
4039
4661
|
|
|
4040
4662
|
// src/resources/content/getting-started.ts
|
|
@@ -7035,6 +7657,79 @@ Suggest a sample question based on the tables you described and run it with \`as
|
|
|
7035
7657
|
- For very large schemas (50+ tables), start with the most important tables and expand as needed
|
|
7036
7658
|
`;
|
|
7037
7659
|
|
|
7660
|
+
// src/prompts/customize-customer-dashboard.ts
|
|
7661
|
+
var customizeCustomerDashboardPrompt = `You are guiding widget CRUD on a specific customer's dashboard embed. This is the "tweak per customer" flow: add, modify, or remove individual widgets (metrics) on a customer's embedded dashboard without touching the template or other customers.
|
|
7662
|
+
|
|
7663
|
+
**CRITICAL: This is an interactive, step-by-step guide. STOP after each step, show the user what you found, and wait for their input. Never batch multiple steps. Never assume the customer/embed.**
|
|
7664
|
+
|
|
7665
|
+
**Key concepts (read before starting):**
|
|
7666
|
+
- **Embed** = a customer's instantiation of a dashboard. Identified by \`embedId\`. Each embed is scoped to one \`clientId\` (the customer/tenant).
|
|
7667
|
+
- **Widget** = a single visualization on a dashboard. Stored as \`externalMetrics\` internally. Has two ids: \`metricId\` (user-facing slug, shown in lists) and \`externalMetricId\` (internal uuid, used for updates/deletes).
|
|
7668
|
+
- **Per-client scope**: \`remove_widget\` is a per-client soft delete \u2014 it hides the widget for one client only. The widget stays on the template dashboard and on other customers' embeds.
|
|
7669
|
+
- **Guest tokens auto-minted**: \`add_widget\`, \`update_widget\`, \`remove_widget\`, and \`generate_widget\` all mint a guest token per call. You do not need to call \`generate_guest_token\` manually for widget operations.
|
|
7670
|
+
|
|
7671
|
+
**Step 1 \u2014 Identify the target embed**
|
|
7672
|
+
STOP and ASK the user:
|
|
7673
|
+
- "Which customer/client are we customizing for?" (the \`clientId\`)
|
|
7674
|
+
- "Which embed/dashboard?" (the \`embedId\`)
|
|
7675
|
+
|
|
7676
|
+
If the user only knows the customer name, call \`list_data_apps\` then \`list_embeds\` (filtered by \`dataAppName\`) and present the matching embeds. Do NOT proceed until you have both \`embedId\` and \`clientId\` confirmed.
|
|
7677
|
+
|
|
7678
|
+
**Step 2 \u2014 Survey the current widgets**
|
|
7679
|
+
Call \`list_widgets({ embedId, clientId })\`.
|
|
7680
|
+
STOP and present the list: \`{metricId, name, isPublished, createdByUser}\` per widget.
|
|
7681
|
+
ASK: "What would you like to change? (add / modify / remove)" \u2014 or more specifically, "Which widgets should I add, update, or remove?"
|
|
7682
|
+
|
|
7683
|
+
**Step 3a \u2014 ADD a widget**
|
|
7684
|
+
This is a business-user flow. NEVER ask the user for a datasource, \`companyIntegrationId\`, \`integrationId\`, or raw SQL unless they explicitly say they want to write SQL. Prefer the two paths below in order.
|
|
7685
|
+
|
|
7686
|
+
1. **Clone an existing chart** (default \u2014 try this first):
|
|
7687
|
+
- ASK: "Which existing chart should I copy from?" Call \`list_widgets\` on this embed, or on the template embed / another customer's embed, and present the widgets by name. Let the user pick by name.
|
|
7688
|
+
- Call \`add_widget({ embedId, clientId, templateMetricId, name? })\` using the picked widget's \`metricId\`. The tool inherits the query, chart config, and datasource automatically.
|
|
7689
|
+
|
|
7690
|
+
2. **Describe the chart in plain English** (AI generate \u2014 use if no suitable template exists):
|
|
7691
|
+
- ASK: "Describe the chart you want" (e.g., "calls per day over last 30 days").
|
|
7692
|
+
- Call \`generate_widget({ embedId, clientId, userInputs: { prompt: "..." } })\` \u2014 data app and datasource auto-resolve from the embed's existing widgets. Do NOT ask the user for a datasource or data app.
|
|
7693
|
+
- \`embedId\` is optional on \`generate_widget\` (the tool will fall back to the sole data app / sole datasource if absent), but pass it when you have one \u2014 resolution is more reliable.
|
|
7694
|
+
- Review the returned \`draft\` with the user. If they approve, call \`add_widget({ embedId, clientId, body: <draft + externalDashboardIds> })\` to persist.
|
|
7695
|
+
- If they want changes, re-run \`generate_widget\` with a refined prompt.
|
|
7696
|
+
- If \`generate_widget\` reports "Multiple data apps" or "Could not auto-resolve a datasource", either pass \`embedId\` or ask the user which data app/chart to mirror \u2014 still avoid technical datasource IDs.
|
|
7697
|
+
|
|
7698
|
+
**Advanced \u2014 write SQL directly** (technical users only; ONLY if the user explicitly asks to write SQL):
|
|
7699
|
+
- ASK for: \`name\`, \`query\` (SQL), \`chartOptions\`, \`companyIntegrationId\` (use \`list_datasources\`).
|
|
7700
|
+
- Call \`add_widget({ embedId, clientId, name, query, chartOptions, companyIntegrationId, ... })\`.
|
|
7701
|
+
|
|
7702
|
+
Show the user the new widget's \`externalMetricId\` and \`metricId\` after each add.
|
|
7703
|
+
|
|
7704
|
+
**Step 3b \u2014 UPDATE a widget**
|
|
7705
|
+
If the user wants to modify a widget, STOP and ASK:
|
|
7706
|
+
- Which widget? (look up its \`externalMetricId\` from Step 2 \u2014 remember, this is the uuid, NOT the \`metricId\` slug)
|
|
7707
|
+
- What to change? (name, query, chartOptions, isPublished, etc.)
|
|
7708
|
+
|
|
7709
|
+
Call \`update_widget({ externalMetricId, clientId, dataAppName, set: {<partial patch>} })\`.
|
|
7710
|
+
Show the user the confirmation and list the changed fields.
|
|
7711
|
+
|
|
7712
|
+
**Step 3c \u2014 REMOVE a widget**
|
|
7713
|
+
If the user wants to remove a widget, STOP and CONFIRM:
|
|
7714
|
+
- "This is a per-client soft delete. The widget will be hidden for \`<clientId>\` only \u2014 other customers and the template dashboard are unaffected. Proceed?"
|
|
7715
|
+
|
|
7716
|
+
Wait for explicit confirmation before calling \`remove_widget({ externalMetricId, clientId, dataAppName })\`.
|
|
7717
|
+
|
|
7718
|
+
**Step 4 \u2014 Verify**
|
|
7719
|
+
Call \`list_widgets({ embedId, clientId })\` again. Present the updated list and confirm the changes match what the user asked for.
|
|
7720
|
+
|
|
7721
|
+
**Step 5 \u2014 (Optional) regenerate guest token for live preview**
|
|
7722
|
+
If the user wants to see the changes in their running app:
|
|
7723
|
+
- Remind them: "Existing guest tokens carry the old embed config. Regenerate via \`generate_guest_token({ clientId, dataAppName })\` and reload the frontend to see the changes."
|
|
7724
|
+
|
|
7725
|
+
**Important rules:**
|
|
7726
|
+
- STOP after each step. Present results and wait for user input before proceeding.
|
|
7727
|
+
- Always pass \`externalMetricId\` (uuid) to \`update_widget\` / \`remove_widget\`, NOT the \`metricId\` slug. When in doubt, use \`list_widgets\` output \u2014 but note that \`list_widgets\` shows \`metricId\`, so you may need \`get_embed_details\` or context from \`add_widget\`'s response to find the uuid.
|
|
7728
|
+
- Never batch add + update + remove without confirming each one.
|
|
7729
|
+
- If the customer-facing effect is not immediate (e.g., guest token cached), say so explicitly in your handoff message.
|
|
7730
|
+
|
|
7731
|
+
After completion, suggest: \`brand-my-embed\` if they want to adjust theme, or \`manage-embeds\` if they want to tweak access settings / options.`;
|
|
7732
|
+
|
|
7038
7733
|
// src/prompts/registry.ts
|
|
7039
7734
|
var prompts = [
|
|
7040
7735
|
{
|
|
@@ -7061,6 +7756,11 @@ var prompts = [
|
|
|
7061
7756
|
name: "populate-semantic-layer",
|
|
7062
7757
|
description: "Auto-generate and push table/column descriptions to enable natural language querying on a datamart",
|
|
7063
7758
|
content: populateSemanticLayerPrompt
|
|
7759
|
+
},
|
|
7760
|
+
{
|
|
7761
|
+
name: "customize-customer-dashboard",
|
|
7762
|
+
description: "Add, modify, or remove widgets on a specific customer's dashboard embed. Per-client scope \u2014 changes only affect that customer.",
|
|
7763
|
+
content: customizeCustomerDashboardPrompt
|
|
7064
7764
|
}
|
|
7065
7765
|
];
|
|
7066
7766
|
function registerAllPrompts(server) {
|