@databrainhq/mcp-server 0.2.9 → 0.2.10
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/dist/index.js +267 -74
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -479,6 +479,14 @@ var DatasourcesApi = class {
|
|
|
479
479
|
cacheTtlMs: 5 * 60 * 1e3
|
|
480
480
|
});
|
|
481
481
|
}
|
|
482
|
+
async resolveIdByName(datasourceName) {
|
|
483
|
+
try {
|
|
484
|
+
const schema = await this.getDatasourceSchema(datasourceName);
|
|
485
|
+
return schema?.datasourceId || null;
|
|
486
|
+
} catch {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
482
490
|
};
|
|
483
491
|
|
|
484
492
|
// src/api/embeds-api.ts
|
|
@@ -691,6 +699,7 @@ var DiscoveryService = class {
|
|
|
691
699
|
return {
|
|
692
700
|
name: isNonEmptyString(dashboard.name) && dashboard.name || isNonEmptyString(dashboard.dashboardName) && dashboard.dashboardName || "Unnamed Dashboard",
|
|
693
701
|
dashboardId: isNonEmptyString(dashboard.dashboardId) && dashboard.dashboardId || isNonEmptyString(dashboard.externalDashboardId) && dashboard.externalDashboardId || isNonEmptyString(dashboard._id) && dashboard._id || isNonEmptyString(dashboard.id) && dashboard.id || isNonEmptyString(dashboard.uid) && dashboard.uid || null,
|
|
702
|
+
externalDashboardId: isNonEmptyString(dashboard.externalDashboardId) && dashboard.externalDashboardId || null,
|
|
694
703
|
workspaceName: isNonEmptyString(dashboard.workspaceName) && dashboard.workspaceName || isNonEmptyString(dashboard.workspace) && dashboard.workspace || isNonEmptyString(dashboard.workspaceId) && dashboard.workspaceId || null,
|
|
695
704
|
metricCount: typeof dashboard.metricCount === "number" ? dashboard.metricCount : null
|
|
696
705
|
};
|
|
@@ -703,6 +712,7 @@ var DiscoveryService = class {
|
|
|
703
712
|
embedId: isNonEmptyString(embed.embedId) ? embed.embedId : null,
|
|
704
713
|
internalId: isNonEmptyString(embed._id) && embed._id || isNonEmptyString(embed.id) && embed.id || null,
|
|
705
714
|
dashboardId: metadata && isNonEmptyString(metadata.dashboardId) && metadata.dashboardId || isNonEmptyString(embed.dashboardId) && embed.dashboardId || null,
|
|
715
|
+
externalDashboardId: isNonEmptyString(embed.externalDashboardId) && embed.externalDashboardId || externalDashboard && isNonEmptyString(externalDashboard.externalDashboardId) && externalDashboard.externalDashboardId || metadata && isNonEmptyString(metadata.externalDashboardId) && metadata.externalDashboardId || null,
|
|
706
716
|
dataAppName: metadata && isNonEmptyString(metadata.dataAppName) && metadata.dataAppName || null,
|
|
707
717
|
embedType: isNonEmptyString(embed.embedType) ? embed.embedType : null
|
|
708
718
|
};
|
|
@@ -938,14 +948,135 @@ var asRecord2 = (v) => {
|
|
|
938
948
|
};
|
|
939
949
|
var isActiveDatasource = (ds) => Boolean(ds.isAuthenticated) && !ds.isDisabled;
|
|
940
950
|
var WidgetService = class {
|
|
941
|
-
constructor(widgetsApi, embedsApi, dataAppsApi, datasourcesApi, guestTokenService, discoveryService) {
|
|
951
|
+
constructor(widgetsApi, embedsApi, dataAppsApi, datasourcesApi, dashboardsApi, guestTokenService, discoveryService) {
|
|
942
952
|
this.widgetsApi = widgetsApi;
|
|
943
953
|
this.embedsApi = embedsApi;
|
|
944
954
|
this.dataAppsApi = dataAppsApi;
|
|
945
955
|
this.datasourcesApi = datasourcesApi;
|
|
956
|
+
this.dashboardsApi = dashboardsApi;
|
|
946
957
|
this.guestTokenService = guestTokenService;
|
|
947
958
|
this.discoveryService = discoveryService;
|
|
948
959
|
}
|
|
960
|
+
/**
|
|
961
|
+
* Resolve the target embedId from a non-technical dashboard name (optionally
|
|
962
|
+
* scoped to a workspace). This is the preferred entry point for CS/CX flows
|
|
963
|
+
* where the user only knows the dashboard + workspace, not the embedId or
|
|
964
|
+
* data app name. Returns a business-user-friendly error on ambiguity.
|
|
965
|
+
*/
|
|
966
|
+
async resolveEmbedFromDashboard(dashboardName, workspaceName) {
|
|
967
|
+
const wsProvided = workspaceName && workspaceName.trim().length > 0 ? workspaceName.trim() : void 0;
|
|
968
|
+
let dashboards;
|
|
969
|
+
try {
|
|
970
|
+
dashboards = wsProvided ? await this.dashboardsApi.listDashboardsForWorkspace(wsProvided) : await this.dashboardsApi.listDashboardsAcrossWorkspaces();
|
|
971
|
+
} catch (err) {
|
|
972
|
+
return {
|
|
973
|
+
ok: false,
|
|
974
|
+
error: `Could not list dashboards: ${err.message}`
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
const normalized = dashboards.map(
|
|
978
|
+
(d) => this.discoveryService.normalizeDashboard(d)
|
|
979
|
+
);
|
|
980
|
+
const target = dashboardName.trim().toLowerCase();
|
|
981
|
+
const matches = normalized.filter(
|
|
982
|
+
(d) => d.name.trim().toLowerCase() === target
|
|
983
|
+
);
|
|
984
|
+
if (matches.length === 0) {
|
|
985
|
+
const available = normalized.filter((d) => d.name && d.name !== "Unnamed Dashboard").map(
|
|
986
|
+
(d) => d.workspaceName ? `"${d.name}" (workspace: ${d.workspaceName})` : `"${d.name}"`
|
|
987
|
+
).join(", ");
|
|
988
|
+
return {
|
|
989
|
+
ok: false,
|
|
990
|
+
error: `No dashboard named "${dashboardName}"${wsProvided ? ` in workspace "${wsProvided}"` : ""}. ` + (available ? `Available dashboards: ${available}. Ask the user to pick by dashboard name.` : "No dashboards exist in this organization.")
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
if (matches.length > 1) {
|
|
994
|
+
const workspaces = matches.map((d) => d.workspaceName || "(unknown workspace)").join(", ");
|
|
995
|
+
return {
|
|
996
|
+
ok: false,
|
|
997
|
+
error: `Dashboard name "${dashboardName}" is ambiguous \u2014 it exists in multiple workspaces (${workspaces}). Ask the user which workspace and retry with \`workspaceName\`.`
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
const dashboard = matches[0];
|
|
1001
|
+
if (!dashboard.dashboardId) {
|
|
1002
|
+
return {
|
|
1003
|
+
ok: false,
|
|
1004
|
+
error: `Dashboard "${dashboard.name}" has no ID. This is unexpected \u2014 ask the user to verify the dashboard in the Databrain UI.`
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
let embeds;
|
|
1008
|
+
try {
|
|
1009
|
+
embeds = await this.embedsApi.listEmbeds();
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
return {
|
|
1012
|
+
ok: false,
|
|
1013
|
+
error: `Could not list embeds: ${err.message}`
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
const dashboardIds = new Set(
|
|
1017
|
+
[dashboard.dashboardId, dashboard.externalDashboardId].filter(
|
|
1018
|
+
(v) => typeof v === "string" && v.length > 0
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
1021
|
+
const embedMatches = embeds.map((e) => ({ raw: e, n: this.discoveryService.normalizeEmbed(e) })).filter((x) => {
|
|
1022
|
+
if (!x.n.embedId) return false;
|
|
1023
|
+
const embedIds = [x.n.dashboardId, x.n.externalDashboardId].filter(
|
|
1024
|
+
(v) => typeof v === "string" && v.length > 0
|
|
1025
|
+
);
|
|
1026
|
+
return embedIds.some((id) => dashboardIds.has(id));
|
|
1027
|
+
});
|
|
1028
|
+
if (embedMatches.length === 0) {
|
|
1029
|
+
return {
|
|
1030
|
+
ok: false,
|
|
1031
|
+
error: `Dashboard "${dashboard.name}" has no embed configured. An embed must be created in the Databrain UI before widgets can be managed for customers.`
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
if (embedMatches.length > 1) {
|
|
1035
|
+
const names = embedMatches.map((x) => `"${x.n.name}"`).join(", ");
|
|
1036
|
+
return {
|
|
1037
|
+
ok: false,
|
|
1038
|
+
error: `Dashboard "${dashboard.name}" has multiple embeds (${names}). Ask the user which embed name they want to customize.`
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
ok: true,
|
|
1043
|
+
value: {
|
|
1044
|
+
embedId: embedMatches[0].n.embedId,
|
|
1045
|
+
dashboardName: dashboard.name,
|
|
1046
|
+
workspaceName: dashboard.workspaceName
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Normalize widget-tool input: callers may supply `embedId` directly (legacy
|
|
1052
|
+
* / technical callers) or `dashboardName` + optional `workspaceName`
|
|
1053
|
+
* (non-technical CS/CX flow). Returns a single embedId and the resolved
|
|
1054
|
+
* dashboard info if lookup happened. Pass `required: true` to enforce that
|
|
1055
|
+
* the user named a target dashboard (recommended for CS/CX flows).
|
|
1056
|
+
*/
|
|
1057
|
+
async resolveEmbedId(params) {
|
|
1058
|
+
if (params.embedId && params.embedId.trim().length > 0) {
|
|
1059
|
+
return { ok: true, value: { embedId: params.embedId.trim() } };
|
|
1060
|
+
}
|
|
1061
|
+
if (params.dashboardName && params.dashboardName.trim().length > 0) {
|
|
1062
|
+
const res = await this.resolveEmbedFromDashboard(
|
|
1063
|
+
params.dashboardName,
|
|
1064
|
+
params.workspaceName
|
|
1065
|
+
);
|
|
1066
|
+
if (!res.ok) return res;
|
|
1067
|
+
return {
|
|
1068
|
+
ok: true,
|
|
1069
|
+
value: { embedId: res.value.embedId, resolvedFrom: res.value }
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
if (params.required) {
|
|
1073
|
+
return {
|
|
1074
|
+
ok: false,
|
|
1075
|
+
error: "Missing dashboard. Ask the user which dashboard to customize (by name) and optionally which workspace, then pass `dashboardName` (and `workspaceName` if needed). Technical callers may pass `embedId` directly."
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
return { ok: true, value: { embedId: null } };
|
|
1079
|
+
}
|
|
949
1080
|
/**
|
|
950
1081
|
* Resolve the data app name from either a caller override, an embed's
|
|
951
1082
|
* metadata, or the sole data app in the workspace. Used by every widget
|
|
@@ -1014,8 +1145,8 @@ var WidgetService = class {
|
|
|
1014
1145
|
);
|
|
1015
1146
|
if (named.length === 1) {
|
|
1016
1147
|
const ds = named[0];
|
|
1017
|
-
const id = ds._id || ds.id || "";
|
|
1018
1148
|
const name = ds.name;
|
|
1149
|
+
const id = ds._id || ds.id || await this.datasourcesApi.resolveIdByName(name) || "";
|
|
1019
1150
|
if (id && name) {
|
|
1020
1151
|
return { ok: true, value: { integrationId: id, integrationName: name } };
|
|
1021
1152
|
}
|
|
@@ -1030,14 +1161,17 @@ var WidgetService = class {
|
|
|
1030
1161
|
let embedReason = "";
|
|
1031
1162
|
if (params.embedId && embedProbe) {
|
|
1032
1163
|
if (embedProbe.kind === "no_widgets") {
|
|
1033
|
-
embedReason = `
|
|
1164
|
+
embedReason = ` This dashboard has no widgets for client "${params.clientId}" yet, so there's no existing widget to inherit the datasource from.`;
|
|
1034
1165
|
} else if (embedProbe.kind === "no_metric_fields") {
|
|
1035
|
-
embedReason = ` Checked ${embedProbe.widgetsTried} widget(s) on
|
|
1166
|
+
embedReason = ` Checked ${embedProbe.widgetsTried} existing widget(s) on this dashboard` + (embedProbe.fetchErrors > 0 ? ` (${embedProbe.fetchErrors} could not be fetched)` : "") + `, but none had datasource metadata to inherit from.`;
|
|
1036
1167
|
}
|
|
1037
1168
|
}
|
|
1038
1169
|
return {
|
|
1039
1170
|
ok: false,
|
|
1040
|
-
error: `Could not auto-
|
|
1171
|
+
error: `Could not auto-detect the datasource for this dashboard.${embedReason} STOP and ASK the user to choose one of these two options:
|
|
1172
|
+
(1) Pick an existing chart to copy from \u2014 call \`list_widgets\` and present the widgets by name, then retry via \`add_widget\` with \`templateMetricId\` (datasource inherited automatically). This is the recommended path for non-technical users.
|
|
1173
|
+
(2) Pick a datasource by name from: ${nameList}. Retry with \`integrationName: "<chosen name>"\`. Only use this if the user already knows their datasource.
|
|
1174
|
+
Do not guess between the two \u2014 wait for the user to choose.`
|
|
1041
1175
|
};
|
|
1042
1176
|
}
|
|
1043
1177
|
async completeFromDatasources(provided) {
|
|
@@ -1068,12 +1202,12 @@ var WidgetService = class {
|
|
|
1068
1202
|
const hint = provided.integrationId ? `No active datasource matches integrationId "${provided.integrationId}". Available datasources: ${available || "(none)"}.` : `No active datasource matches integrationName "${provided.integrationName}". Available datasources: ${available || "(none)"}. Match is case-insensitive \u2014 double-check the name with the user.`;
|
|
1069
1203
|
return { ok: false, error: hint };
|
|
1070
1204
|
}
|
|
1071
|
-
const id = target._id || target.id || "";
|
|
1072
1205
|
const name = typeof target.name === "string" ? target.name : "";
|
|
1206
|
+
const id = target._id || target.id || (name ? await this.datasourcesApi.resolveIdByName(name) : null) || "";
|
|
1073
1207
|
if (!id || !name) {
|
|
1074
1208
|
return {
|
|
1075
1209
|
ok: false,
|
|
1076
|
-
error: `
|
|
1210
|
+
error: `Could not resolve datasource "${name || provided.integrationName || provided.integrationId}" to a UUID. The datasource list API does not return IDs; the schema lookup also failed \u2014 the datasource may need to be synced in the Databrain UI.`
|
|
1077
1211
|
};
|
|
1078
1212
|
}
|
|
1079
1213
|
return { ok: true, value: { integrationId: id, integrationName: name } };
|
|
@@ -1258,6 +1392,7 @@ function createServiceBundle(client) {
|
|
|
1258
1392
|
embedsApi,
|
|
1259
1393
|
dataAppsApi,
|
|
1260
1394
|
datasourcesApi,
|
|
1395
|
+
dashboardsApi,
|
|
1261
1396
|
guestTokenService,
|
|
1262
1397
|
discoveryService
|
|
1263
1398
|
)
|
|
@@ -1658,9 +1793,7 @@ function registerListDatasources(server, client) {
|
|
|
1658
1793
|
return toolSuccess(JSON.stringify({
|
|
1659
1794
|
count: active.length,
|
|
1660
1795
|
datasources: active.map((ds) => ({
|
|
1661
|
-
|
|
1662
|
-
name: ds.name,
|
|
1663
|
-
type: ds.dbName || ds.datasourceType || "unknown"
|
|
1796
|
+
name: ds.name
|
|
1664
1797
|
})),
|
|
1665
1798
|
inactive: inactive.length > 0 ? inactive.map((ds) => ({ name: ds.name })) : void 0
|
|
1666
1799
|
}));
|
|
@@ -4396,29 +4529,41 @@ function registerListWidgets(server, client) {
|
|
|
4396
4529
|
const services = createServiceBundle(client);
|
|
4397
4530
|
server.tool(
|
|
4398
4531
|
"list_widgets",
|
|
4399
|
-
"List widgets
|
|
4532
|
+
"List widgets on a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous). Returns each widget's externalMetricId (uuid, used for update/remove), metricId (slug), name, published state, and creator identifier.",
|
|
4400
4533
|
{
|
|
4401
|
-
|
|
4402
|
-
|
|
4534
|
+
clientId: z21.string().describe("Client/tenant identifier for this dashboard"),
|
|
4535
|
+
dashboardName: z21.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
4536
|
+
workspaceName: z21.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
4537
|
+
embedId: z21.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier.")
|
|
4403
4538
|
},
|
|
4404
4539
|
READ_ONLY,
|
|
4405
4540
|
async (params) => {
|
|
4406
4541
|
try {
|
|
4542
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
4543
|
+
embedId: params.embedId,
|
|
4544
|
+
dashboardName: params.dashboardName,
|
|
4545
|
+
workspaceName: params.workspaceName,
|
|
4546
|
+
required: true
|
|
4547
|
+
});
|
|
4548
|
+
if (!embed.ok) return toolError(embed.error);
|
|
4549
|
+
const embedId = embed.value.embedId;
|
|
4407
4550
|
const widgets = await services.widgetsApi.listWidgets(
|
|
4408
|
-
|
|
4551
|
+
embedId,
|
|
4409
4552
|
params.clientId
|
|
4410
4553
|
);
|
|
4411
4554
|
if (!Array.isArray(widgets) || widgets.length === 0) {
|
|
4412
4555
|
return toolSuccess(
|
|
4413
|
-
`No widgets found on
|
|
4556
|
+
`No widgets found on dashboard "${embed.value.resolvedFrom?.dashboardName || embedId}" for client "${params.clientId}".`
|
|
4414
4557
|
);
|
|
4415
4558
|
}
|
|
4416
4559
|
return toolSuccess(
|
|
4417
4560
|
JSON.stringify({
|
|
4418
4561
|
count: widgets.length,
|
|
4419
|
-
|
|
4562
|
+
dashboardName: embed.value.resolvedFrom?.dashboardName || null,
|
|
4563
|
+
embedId,
|
|
4420
4564
|
clientId: params.clientId,
|
|
4421
4565
|
widgets: widgets.map((w) => ({
|
|
4566
|
+
externalMetricId: typeof w.externalMetricId === "string" && w.externalMetricId || typeof w.id === "string" && w.id || typeof w._id === "string" && w._id || null,
|
|
4422
4567
|
metricId: w.metricId || null,
|
|
4423
4568
|
name: w.name || null,
|
|
4424
4569
|
isPublished: w.isPublished ?? null,
|
|
@@ -4440,11 +4585,13 @@ function registerAddWidget(server, client) {
|
|
|
4440
4585
|
const services = createServiceBundle(client);
|
|
4441
4586
|
server.tool(
|
|
4442
4587
|
"add_widget",
|
|
4443
|
-
"Add a widget
|
|
4588
|
+
"Add a widget to a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous). Cloning via `templateMetricId` is the recommended path \u2014 query, chart config, and datasource are all inherited. Data app and datasource auto-resolve; never ask the user for them.",
|
|
4444
4589
|
{
|
|
4445
|
-
embedId: z22.string().describe("Embed ID the widget will be added to"),
|
|
4446
4590
|
clientId: z22.string().describe("Client/tenant identifier (widget is created in their scope)"),
|
|
4447
|
-
|
|
4591
|
+
dashboardName: z22.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
4592
|
+
workspaceName: z22.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
4593
|
+
embedId: z22.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
4594
|
+
dataAppName: z22.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
|
|
4448
4595
|
templateMetricId: z22.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
|
|
4449
4596
|
name: z22.string().optional().describe("Widget name (overrides template)"),
|
|
4450
4597
|
metricId: z22.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
|
|
@@ -4459,21 +4606,29 @@ function registerAddWidget(server, client) {
|
|
|
4459
4606
|
WRITE_CREATE,
|
|
4460
4607
|
async (params) => {
|
|
4461
4608
|
try {
|
|
4462
|
-
const
|
|
4609
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
4610
|
+
embedId: params.embedId,
|
|
4611
|
+
dashboardName: params.dashboardName,
|
|
4612
|
+
workspaceName: params.workspaceName,
|
|
4613
|
+
required: true
|
|
4614
|
+
});
|
|
4615
|
+
if (!embed.ok) return toolError(embed.error);
|
|
4616
|
+
const embedId = embed.value.embedId;
|
|
4617
|
+
const ctx = await services.widgetService.resolveEmbedContext(embedId);
|
|
4463
4618
|
if (!ctx.externalDashboardId) {
|
|
4464
4619
|
return toolError(
|
|
4465
|
-
`
|
|
4620
|
+
`Dashboard "${params.dashboardName || embedId}" has no externalDashboardId \u2014 cannot attach a widget. Verify via \`get_embed_details\`.`
|
|
4466
4621
|
);
|
|
4467
4622
|
}
|
|
4468
4623
|
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
4469
4624
|
params.dataAppName,
|
|
4470
|
-
|
|
4625
|
+
embedId
|
|
4471
4626
|
);
|
|
4472
4627
|
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
4473
4628
|
const guestToken = await services.widgetService.mintGuestToken(
|
|
4474
4629
|
params.clientId,
|
|
4475
4630
|
dataAppName.value,
|
|
4476
|
-
[
|
|
4631
|
+
[embedId]
|
|
4477
4632
|
);
|
|
4478
4633
|
let createBody;
|
|
4479
4634
|
if (params.body) {
|
|
@@ -4505,7 +4660,7 @@ function registerAddWidget(server, client) {
|
|
|
4505
4660
|
if (!params.name) return toolError("`name` is required when not cloning a template.");
|
|
4506
4661
|
if (!params.query) return toolError("`query` is required when not cloning a template.");
|
|
4507
4662
|
const datasource = await services.widgetService.resolveDatasource(guestToken, {
|
|
4508
|
-
embedId
|
|
4663
|
+
embedId,
|
|
4509
4664
|
clientId: params.clientId,
|
|
4510
4665
|
integrationId: params.companyIntegrationId,
|
|
4511
4666
|
integrationName: params.integrationName
|
|
@@ -4542,7 +4697,7 @@ ${JSON.stringify(createBody, null, 2).slice(0, 3e3)}`
|
|
|
4542
4697
|
- externalMetricId: \`${newId}\`
|
|
4543
4698
|
- metricId: \`${newMetricId}\`
|
|
4544
4699
|
- name: \`${createBody.name}\`
|
|
4545
|
-
-
|
|
4700
|
+
- dashboard: \`${embed.value.resolvedFrom?.dashboardName || embedId}\`
|
|
4546
4701
|
- clientId: \`${params.clientId}\`
|
|
4547
4702
|
|
|
4548
4703
|
Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust.`
|
|
@@ -4561,12 +4716,14 @@ function registerUpdateWidget(server, client) {
|
|
|
4561
4716
|
const services = createServiceBundle(client);
|
|
4562
4717
|
server.tool(
|
|
4563
4718
|
"update_widget",
|
|
4564
|
-
"Update a widget on a customer dashboard
|
|
4719
|
+
"Update a widget on a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous) + `externalMetricId` + `set`. Data app auto-resolves from the dashboard.",
|
|
4565
4720
|
{
|
|
4566
4721
|
externalMetricId: z23.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
|
|
4567
4722
|
clientId: z23.string().describe("Client/tenant identifier"),
|
|
4568
|
-
|
|
4569
|
-
|
|
4723
|
+
dashboardName: z23.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
4724
|
+
workspaceName: z23.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
4725
|
+
embedId: z23.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
4726
|
+
dataAppName: z23.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank."),
|
|
4570
4727
|
set: z23.record(z23.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
|
|
4571
4728
|
},
|
|
4572
4729
|
WRITE_IDEMPOTENT,
|
|
@@ -4575,15 +4732,22 @@ function registerUpdateWidget(server, client) {
|
|
|
4575
4732
|
if (!params.set || Object.keys(params.set).length === 0) {
|
|
4576
4733
|
return toolError("`set` must contain at least one field to update.");
|
|
4577
4734
|
}
|
|
4735
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
4736
|
+
embedId: params.embedId,
|
|
4737
|
+
dashboardName: params.dashboardName,
|
|
4738
|
+
workspaceName: params.workspaceName
|
|
4739
|
+
});
|
|
4740
|
+
if (!embed.ok) return toolError(embed.error);
|
|
4741
|
+
const embedId = embed.value.embedId;
|
|
4578
4742
|
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
4579
4743
|
params.dataAppName,
|
|
4580
|
-
|
|
4744
|
+
embedId || void 0
|
|
4581
4745
|
);
|
|
4582
4746
|
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
4583
4747
|
const guestToken = await services.widgetService.mintGuestToken(
|
|
4584
4748
|
params.clientId,
|
|
4585
4749
|
dataAppName.value,
|
|
4586
|
-
|
|
4750
|
+
embedId ? [embedId] : void 0
|
|
4587
4751
|
);
|
|
4588
4752
|
const result = await services.widgetsApi.updateMetric(guestToken, {
|
|
4589
4753
|
externalMetricId: params.externalMetricId,
|
|
@@ -4620,25 +4784,34 @@ function registerRemoveWidget(server, client) {
|
|
|
4620
4784
|
const services = createServiceBundle(client);
|
|
4621
4785
|
server.tool(
|
|
4622
4786
|
"remove_widget",
|
|
4623
|
-
"Remove a widget from a customer dashboard
|
|
4787
|
+
"Remove a widget from a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous) + `externalMetricId`. Per-client soft delete \u2014 the widget stays on the template dashboard and on other customers' dashboards.",
|
|
4624
4788
|
{
|
|
4625
4789
|
externalMetricId: z24.string().describe("Internal uuid of the widget (externalMetrics.id)"),
|
|
4626
4790
|
clientId: z24.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
|
|
4627
|
-
|
|
4628
|
-
|
|
4791
|
+
dashboardName: z24.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
4792
|
+
workspaceName: z24.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
4793
|
+
embedId: z24.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
4794
|
+
dataAppName: z24.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank.")
|
|
4629
4795
|
},
|
|
4630
4796
|
DESTRUCTIVE,
|
|
4631
4797
|
async (params) => {
|
|
4632
4798
|
try {
|
|
4799
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
4800
|
+
embedId: params.embedId,
|
|
4801
|
+
dashboardName: params.dashboardName,
|
|
4802
|
+
workspaceName: params.workspaceName
|
|
4803
|
+
});
|
|
4804
|
+
if (!embed.ok) return toolError(embed.error);
|
|
4805
|
+
const embedId = embed.value.embedId;
|
|
4633
4806
|
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
4634
4807
|
params.dataAppName,
|
|
4635
|
-
|
|
4808
|
+
embedId || void 0
|
|
4636
4809
|
);
|
|
4637
4810
|
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
4638
4811
|
const guestToken = await services.widgetService.mintGuestToken(
|
|
4639
4812
|
params.clientId,
|
|
4640
4813
|
dataAppName.value,
|
|
4641
|
-
|
|
4814
|
+
embedId ? [embedId] : void 0
|
|
4642
4815
|
);
|
|
4643
4816
|
const result = await services.widgetsApi.markArchived(guestToken, {
|
|
4644
4817
|
id: params.externalMetricId,
|
|
@@ -4671,30 +4844,40 @@ function registerGenerateWidget(server, client) {
|
|
|
4671
4844
|
const services = createServiceBundle(client);
|
|
4672
4845
|
server.tool(
|
|
4673
4846
|
"generate_widget",
|
|
4674
|
-
"AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft to
|
|
4847
|
+
"AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft to pass to `add_widget`. Non-technical flow: `clientId` + `dashboardName` (+ `workspaceName` if ambiguous); data app and datasource auto-resolve. Never ask the user for data app, datasource, or integration.",
|
|
4675
4848
|
{
|
|
4676
4849
|
clientId: z25.string().describe("Client/tenant identifier"),
|
|
4677
4850
|
userInputs: z25.record(z25.unknown()).describe('Prompt + context object. Typical shape: { prompt: "calls per day over last 30 days", schema: {...}, examples: [...] }. Backend decides which keys it honors.'),
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4851
|
+
dashboardName: z25.string().optional().describe("Dashboard name (non-technical). Preferred input for CS/CX flows \u2014 pass this with optional `workspaceName` instead of `embedId`."),
|
|
4852
|
+
workspaceName: z25.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
|
|
4853
|
+
embedId: z25.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
|
|
4854
|
+
dataAppName: z25.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
|
|
4855
|
+
integrationId: z25.string().optional().describe("Technical override \u2014 the datasource UUID. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets."),
|
|
4856
|
+
integrationName: z25.string().optional().describe("Technical override \u2014 datasource display name. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets.")
|
|
4682
4857
|
},
|
|
4683
4858
|
READ_ONLY,
|
|
4684
4859
|
async (params) => {
|
|
4685
4860
|
try {
|
|
4861
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
4862
|
+
embedId: params.embedId,
|
|
4863
|
+
dashboardName: params.dashboardName,
|
|
4864
|
+
workspaceName: params.workspaceName,
|
|
4865
|
+
required: true
|
|
4866
|
+
});
|
|
4867
|
+
if (!embed.ok) return toolError(embed.error);
|
|
4868
|
+
const embedId = embed.value.embedId;
|
|
4686
4869
|
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
4687
4870
|
params.dataAppName,
|
|
4688
|
-
|
|
4871
|
+
embedId
|
|
4689
4872
|
);
|
|
4690
4873
|
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
4691
4874
|
const guestToken = await services.widgetService.mintGuestToken(
|
|
4692
4875
|
params.clientId,
|
|
4693
4876
|
dataAppName.value,
|
|
4694
|
-
|
|
4877
|
+
[embedId]
|
|
4695
4878
|
);
|
|
4696
4879
|
const datasource = await services.widgetService.resolveDatasource(guestToken, {
|
|
4697
|
-
embedId
|
|
4880
|
+
embedId,
|
|
4698
4881
|
clientId: params.clientId,
|
|
4699
4882
|
integrationId: params.integrationId,
|
|
4700
4883
|
integrationName: params.integrationName
|
|
@@ -7764,75 +7947,85 @@ Suggest a sample question based on the tables you described and run it with \`as
|
|
|
7764
7947
|
`;
|
|
7765
7948
|
|
|
7766
7949
|
// src/prompts/customize-customer-dashboard.ts
|
|
7767
|
-
var customizeCustomerDashboardPrompt = `You are guiding widget CRUD on a specific customer's dashboard
|
|
7950
|
+
var customizeCustomerDashboardPrompt = `You are guiding widget CRUD on a specific customer's dashboard. This is the "tweak per customer" flow for a non-technical CS/CX/support user: add, modify, or remove individual widgets for one customer without touching the template or other customers.
|
|
7951
|
+
|
|
7952
|
+
**CRITICAL: This is an interactive, step-by-step guide. STOP after each step, show the user what you found, and wait for their input. Never batch multiple steps. Never assume the customer or dashboard.**
|
|
7768
7953
|
|
|
7769
|
-
**
|
|
7954
|
+
**Minimum info the user needs to provide: \`clientId\` + \`dashboardName\`.** Everything else (data app, datasource, embed id, integration id) is auto-resolved. NEVER ask the user for any of those.
|
|
7770
7955
|
|
|
7771
7956
|
**Key concepts (read before starting):**
|
|
7772
|
-
- **
|
|
7773
|
-
- **
|
|
7774
|
-
- **
|
|
7775
|
-
- **
|
|
7957
|
+
- **Dashboard** = the named analytics view the customer sees (e.g. "Demo Dashboard", "Airline Marketing"). This is what the user will name.
|
|
7958
|
+
- **Workspace** = an optional container grouping dashboards. Only ask for this if the dashboard name is ambiguous.
|
|
7959
|
+
- **Client/customer** = the tenant whose copy of the dashboard we're editing. Identified by \`clientId\`.
|
|
7960
|
+
- **Widget** = a single visualization on a dashboard. Has two ids: \`metricId\` (slug, shown in lists) and \`externalMetricId\` (uuid, used for update/remove).
|
|
7961
|
+
- **Per-client scope**: \`remove_widget\` is a per-client soft delete \u2014 the widget stays on the template dashboard and on other customers' copies.
|
|
7962
|
+
|
|
7963
|
+
**Forbidden questions \u2014 DO NOT ASK the user for any of these:**
|
|
7964
|
+
- Data app name, \`dataAppName\`
|
|
7965
|
+
- Datasource name, \`integrationName\`, \`integrationId\`, \`companyIntegrationId\`
|
|
7966
|
+
- Embed id, \`embedId\`
|
|
7967
|
+
- External dashboard id, dashboard id, internal UUIDs
|
|
7968
|
+
|
|
7969
|
+
If the user offers any of these proactively, accept and use them, but never prompt for them.
|
|
7776
7970
|
|
|
7777
|
-
**Step 1 \u2014 Identify the
|
|
7971
|
+
**Step 1 \u2014 Identify the customer and dashboard**
|
|
7778
7972
|
STOP and ASK the user:
|
|
7779
|
-
- "Which customer/client are we customizing for?" (
|
|
7780
|
-
- "Which
|
|
7973
|
+
- "Which customer/client are we customizing for?" (capture as \`clientId\`)
|
|
7974
|
+
- "Which dashboard?" (capture as \`dashboardName\`)
|
|
7781
7975
|
|
|
7782
|
-
|
|
7976
|
+
Only ask about workspace if the tool reports the dashboard name is ambiguous across workspaces.
|
|
7783
7977
|
|
|
7784
7978
|
**Step 2 \u2014 Survey the current widgets**
|
|
7785
|
-
Call \`list_widgets({
|
|
7786
|
-
STOP and present the list: \`{metricId, name, isPublished, createdByUser}\` per widget.
|
|
7787
|
-
ASK: "What would you like to change? (add / modify / remove)
|
|
7979
|
+
Call \`list_widgets({ clientId, dashboardName, workspaceName? })\`.
|
|
7980
|
+
STOP and present the list: \`{externalMetricId, metricId, name, isPublished, createdByUser}\` per widget. Keep the \`externalMetricId\` values from this response \u2014 you'll need them verbatim for update/remove. Show the user the human-readable names; the uuids are yours to reference internally.
|
|
7981
|
+
ASK: "What would you like to change? (add / modify / remove) \u2014 and which widgets?"
|
|
7788
7982
|
|
|
7789
7983
|
**Step 3a \u2014 ADD a widget**
|
|
7790
|
-
|
|
7984
|
+
Business-user flow. NEVER ask for SQL, datasource, or chart config unless the user explicitly says they want to write raw SQL.
|
|
7791
7985
|
|
|
7792
7986
|
1. **Clone an existing chart** (default \u2014 try this first):
|
|
7793
|
-
- ASK: "Which existing chart should I copy from?" Call \`list_widgets\`
|
|
7794
|
-
- Call \`add_widget({
|
|
7987
|
+
- ASK: "Which existing chart should I copy from?" Call \`list_widgets\` and present the widgets by name. Let the user pick by name.
|
|
7988
|
+
- Call \`add_widget({ clientId, dashboardName, workspaceName?, templateMetricId, name? })\` using the picked widget's \`metricId\`. Query, chart config, and datasource are inherited automatically.
|
|
7795
7989
|
|
|
7796
7990
|
2. **Describe the chart in plain English** (AI generate \u2014 use if no suitable template exists):
|
|
7797
7991
|
- ASK: "Describe the chart you want" (e.g., "calls per day over last 30 days").
|
|
7798
|
-
- Call \`generate_widget({
|
|
7799
|
-
-
|
|
7800
|
-
- Review the returned \`draft\` with the user. If they approve, call \`add_widget({ embedId, clientId, body: <draft + externalDashboardIds> })\` to persist.
|
|
7992
|
+
- Call \`generate_widget({ clientId, dashboardName, workspaceName?, userInputs: { prompt: "..." } })\`.
|
|
7993
|
+
- Review the returned \`draft\` with the user. If they approve, call \`add_widget({ clientId, dashboardName, workspaceName?, body: <draft + externalDashboardIds> })\` to persist.
|
|
7801
7994
|
- If they want changes, re-run \`generate_widget\` with a refined prompt.
|
|
7802
|
-
- If \`generate_widget\` reports "
|
|
7995
|
+
- If \`generate_widget\` reports "Could not auto-detect the datasource", STOP and ASK the user to choose: (a) pick an existing chart to copy from (recommended \u2014 call \`list_widgets\`, present widgets by name, then use \`add_widget\` with \`templateMetricId\`); or (b) pick a datasource by name from the list in the error (only if the user already knows it). Wait for the user's choice \u2014 do not guess.
|
|
7803
7996
|
|
|
7804
7997
|
**Advanced \u2014 write SQL directly** (technical users only; ONLY if the user explicitly asks to write SQL):
|
|
7805
|
-
- ASK for: \`name\`, \`query\` (SQL), \`chartOptions
|
|
7806
|
-
- Call \`add_widget({
|
|
7998
|
+
- ASK for: \`name\`, \`query\` (SQL), \`chartOptions\`.
|
|
7999
|
+
- Call \`add_widget({ clientId, dashboardName, name, query, chartOptions })\` \u2014 datasource still auto-resolves from the dashboard.
|
|
7807
8000
|
|
|
7808
8001
|
Show the user the new widget's \`externalMetricId\` and \`metricId\` after each add.
|
|
7809
8002
|
|
|
7810
8003
|
**Step 3b \u2014 UPDATE a widget**
|
|
7811
8004
|
If the user wants to modify a widget, STOP and ASK:
|
|
7812
|
-
- Which widget? (look up
|
|
8005
|
+
- Which widget? (let the user pick by name, then look up the widget's \`externalMetricId\` from the Step 2 \`list_widgets\` response \u2014 this is the uuid, NOT the \`metricId\` slug. If you skipped Step 2, call \`list_widgets\` first to fetch it \u2014 never ask the user for a uuid.)
|
|
7813
8006
|
- What to change? (name, query, chartOptions, isPublished, etc.)
|
|
7814
8007
|
|
|
7815
|
-
Call \`update_widget({ externalMetricId, clientId,
|
|
8008
|
+
Call \`update_widget({ externalMetricId, clientId, dashboardName, workspaceName?, set: {<partial patch>} })\`.
|
|
7816
8009
|
Show the user the confirmation and list the changed fields.
|
|
7817
8010
|
|
|
7818
8011
|
**Step 3c \u2014 REMOVE a widget**
|
|
7819
8012
|
If the user wants to remove a widget, STOP and CONFIRM:
|
|
7820
|
-
- "This is a per-
|
|
8013
|
+
- "This is a per-customer soft delete. The widget will be hidden for \`<clientId>\` only \u2014 other customers and the template dashboard are unaffected. Proceed?"
|
|
7821
8014
|
|
|
7822
|
-
|
|
8015
|
+
Look up the widget's \`externalMetricId\` from the Step 2 \`list_widgets\` response (call it first if you skipped Step 2). Never ask the user for the uuid.
|
|
8016
|
+
Wait for explicit confirmation before calling \`remove_widget({ externalMetricId, clientId, dashboardName, workspaceName? })\`.
|
|
7823
8017
|
|
|
7824
8018
|
**Step 4 \u2014 Verify**
|
|
7825
|
-
Call \`list_widgets({
|
|
8019
|
+
Call \`list_widgets({ clientId, dashboardName, workspaceName? })\` again. Present the updated list and confirm the changes match what the user asked for.
|
|
7826
8020
|
|
|
7827
8021
|
**Step 5 \u2014 (Optional) regenerate guest token for live preview**
|
|
7828
|
-
If the user wants to see the changes in their running app
|
|
7829
|
-
- Remind them: "Existing guest tokens carry the old embed config. Regenerate via \`generate_guest_token({ clientId, dataAppName })\` and reload the frontend to see the changes."
|
|
8022
|
+
If the user wants to see the changes in their running app, regenerate their guest token and reload. The MCP tools mint tokens internally for widget operations; a cached frontend token will not show the changes until refreshed.
|
|
7830
8023
|
|
|
7831
8024
|
**Important rules:**
|
|
7832
8025
|
- STOP after each step. Present results and wait for user input before proceeding.
|
|
7833
|
-
- Always pass \`externalMetricId\` (uuid) to \`update_widget\` / \`remove_widget\`, NOT the \`metricId\` slug.
|
|
8026
|
+
- Always pass \`externalMetricId\` (uuid) to \`update_widget\` / \`remove_widget\`, NOT the \`metricId\` slug.
|
|
8027
|
+
- Never ask for data app, datasource, integration, or embed identifiers \u2014 they all auto-resolve from \`dashboardName\`.
|
|
7834
8028
|
- Never batch add + update + remove without confirming each one.
|
|
7835
|
-
- If the customer-facing effect is not immediate (e.g., guest token cached), say so explicitly in your handoff message.
|
|
7836
8029
|
|
|
7837
8030
|
After completion, suggest: \`brand-my-embed\` if they want to adjust theme, or \`manage-embeds\` if they want to tweak access settings / options.`;
|
|
7838
8031
|
|