@databrainhq/mcp-server 0.2.8 → 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 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
  };
@@ -936,14 +946,316 @@ var asRecord2 = (v) => {
936
946
  if (!v || typeof v !== "object" || Array.isArray(v)) return null;
937
947
  return v;
938
948
  };
949
+ var isActiveDatasource = (ds) => Boolean(ds.isAuthenticated) && !ds.isDisabled;
939
950
  var WidgetService = class {
940
- constructor(widgetsApi, embedsApi, dataAppsApi, guestTokenService, discoveryService) {
951
+ constructor(widgetsApi, embedsApi, dataAppsApi, datasourcesApi, dashboardsApi, guestTokenService, discoveryService) {
941
952
  this.widgetsApi = widgetsApi;
942
953
  this.embedsApi = embedsApi;
943
954
  this.dataAppsApi = dataAppsApi;
955
+ this.datasourcesApi = datasourcesApi;
956
+ this.dashboardsApi = dashboardsApi;
944
957
  this.guestTokenService = guestTokenService;
945
958
  this.discoveryService = discoveryService;
946
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
+ }
1080
+ /**
1081
+ * Resolve the data app name from either a caller override, an embed's
1082
+ * metadata, or the sole data app in the workspace. Used by every widget
1083
+ * tool so business users never have to name the data app when it's
1084
+ * unambiguous.
1085
+ */
1086
+ async resolveDataAppName(provided, embedId) {
1087
+ if (provided) return { ok: true, value: provided };
1088
+ if (embedId) {
1089
+ const ctx = await this.resolveEmbedContext(embedId);
1090
+ if (ctx.dataAppName) return { ok: true, value: ctx.dataAppName };
1091
+ }
1092
+ const apps = await this.dataAppsApi.listDataApps();
1093
+ const normalized = apps.map((a) => this.discoveryService.normalizeDataApp(a)).filter((a) => a.name && a.name !== "Unnamed Data App");
1094
+ if (normalized.length === 1) return { ok: true, value: normalized[0].name };
1095
+ if (normalized.length === 0) {
1096
+ return {
1097
+ ok: false,
1098
+ error: "No data apps found in this workspace. Create one in the Databrain UI first."
1099
+ };
1100
+ }
1101
+ return {
1102
+ ok: false,
1103
+ error: `Multiple data apps exist (${normalized.map((a) => `"${a.name}"`).join(", ")}). Pass \`dataAppName\` explicitly, or pass \`embedId\` to auto-resolve.`
1104
+ };
1105
+ }
1106
+ /**
1107
+ * Resolve a datasource for a widget. Priority:
1108
+ * 1. Explicit integrationId + integrationName (fast path).
1109
+ * 2. One of integrationId/integrationName supplied — look up the other.
1110
+ * 3. Probe existing widgets on the embed for inherited datasource metadata.
1111
+ * 4. Fall back to the sole active datasource in the workspace.
1112
+ * On failure, returns a business-user-friendly error listing the available
1113
+ * datasource names.
1114
+ */
1115
+ async resolveDatasource(guestToken, params) {
1116
+ if (params.integrationId && params.integrationName) {
1117
+ return {
1118
+ ok: true,
1119
+ value: {
1120
+ integrationId: params.integrationId,
1121
+ integrationName: params.integrationName
1122
+ }
1123
+ };
1124
+ }
1125
+ if (params.integrationId || params.integrationName) {
1126
+ return this.completeFromDatasources({
1127
+ integrationId: params.integrationId,
1128
+ integrationName: params.integrationName
1129
+ });
1130
+ }
1131
+ let embedProbe = null;
1132
+ if (params.embedId) {
1133
+ embedProbe = await this.probeDatasourceFromEmbed(
1134
+ guestToken,
1135
+ params.embedId,
1136
+ params.clientId
1137
+ );
1138
+ if (embedProbe.kind === "ok") {
1139
+ return { ok: true, value: embedProbe.value };
1140
+ }
1141
+ }
1142
+ const datasources = await this.datasourcesApi.listDatasources();
1143
+ const named = datasources.filter(isActiveDatasource).filter(
1144
+ (ds) => typeof ds.name === "string" && ds.name.trim().length > 0
1145
+ );
1146
+ if (named.length === 1) {
1147
+ const ds = named[0];
1148
+ const name = ds.name;
1149
+ const id = ds._id || ds.id || await this.datasourcesApi.resolveIdByName(name) || "";
1150
+ if (id && name) {
1151
+ return { ok: true, value: { integrationId: id, integrationName: name } };
1152
+ }
1153
+ }
1154
+ if (named.length === 0) {
1155
+ return {
1156
+ ok: false,
1157
+ error: "No datasources found in this workspace. Connect one in the Databrain UI first (technical setup)."
1158
+ };
1159
+ }
1160
+ const nameList = named.map((d) => `"${d.name}"`).join(", ");
1161
+ let embedReason = "";
1162
+ if (params.embedId && embedProbe) {
1163
+ if (embedProbe.kind === "no_widgets") {
1164
+ embedReason = ` This dashboard has no widgets for client "${params.clientId}" yet, so there's no existing widget to inherit the datasource from.`;
1165
+ } else if (embedProbe.kind === "no_metric_fields") {
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.`;
1167
+ }
1168
+ }
1169
+ return {
1170
+ ok: false,
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.`
1175
+ };
1176
+ }
1177
+ async completeFromDatasources(provided) {
1178
+ const allDatasources = await this.datasourcesApi.listDatasources();
1179
+ const datasources = allDatasources.filter(isActiveDatasource);
1180
+ const matchPredicate = (ds) => {
1181
+ const id2 = ds._id || ds.id || "";
1182
+ const name2 = typeof ds.name === "string" ? ds.name : "";
1183
+ if (provided.integrationId && id2 === provided.integrationId) return true;
1184
+ if (provided.integrationName && name2.toLowerCase() === provided.integrationName.toLowerCase()) {
1185
+ return true;
1186
+ }
1187
+ return false;
1188
+ };
1189
+ const target = datasources.find(matchPredicate);
1190
+ if (!target) {
1191
+ const available = datasources.filter((ds) => typeof ds.name === "string" && ds.name.trim().length > 0).map((ds) => `"${ds.name}"`).join(", ");
1192
+ const inactiveMatch = allDatasources.find(
1193
+ (ds) => !isActiveDatasource(ds) && matchPredicate(ds)
1194
+ );
1195
+ if (inactiveMatch) {
1196
+ const reason = inactiveMatch.isDisabled ? "disabled" : "not authenticated";
1197
+ return {
1198
+ ok: false,
1199
+ error: `Datasource "${inactiveMatch.name ?? provided.integrationName ?? provided.integrationId}" is ${reason}. Fix the connection in the Databrain UI, or pick another datasource. Active options: ${available || "(none)"}.`
1200
+ };
1201
+ }
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.`;
1203
+ return { ok: false, error: hint };
1204
+ }
1205
+ const name = typeof target.name === "string" ? target.name : "";
1206
+ const id = target._id || target.id || (name ? await this.datasourcesApi.resolveIdByName(name) : null) || "";
1207
+ if (!id || !name) {
1208
+ return {
1209
+ ok: false,
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.`
1211
+ };
1212
+ }
1213
+ return { ok: true, value: { integrationId: id, integrationName: name } };
1214
+ }
1215
+ async probeDatasourceFromEmbed(guestToken, embedId, clientId) {
1216
+ const widgets = await this.widgetsApi.listWidgets(embedId, clientId);
1217
+ const candidates = widgets.filter(
1218
+ (w) => typeof w.metricId === "string" && w.metricId.length > 0
1219
+ );
1220
+ if (candidates.length === 0) return { kind: "no_widgets" };
1221
+ const MAX_PROBES = 5;
1222
+ const toProbe = candidates.slice(0, MAX_PROBES);
1223
+ let fetchErrors = 0;
1224
+ let datasourcesCache = null;
1225
+ for (const widget of toProbe) {
1226
+ let response;
1227
+ try {
1228
+ response = await this.widgetsApi.fetchEmbeddedMetric(
1229
+ guestToken,
1230
+ widget.metricId
1231
+ );
1232
+ } catch {
1233
+ fetchErrors += 1;
1234
+ continue;
1235
+ }
1236
+ const metric = response?.externalMetric;
1237
+ if (!metric || typeof metric !== "object") continue;
1238
+ const rec = metric;
1239
+ const integrationId = typeof rec.companyIntegrationId === "string" && rec.companyIntegrationId ? rec.companyIntegrationId : null;
1240
+ let integrationName = typeof rec.integrationName === "string" && rec.integrationName ? rec.integrationName : null;
1241
+ if (integrationId && !integrationName) {
1242
+ if (!datasourcesCache) {
1243
+ datasourcesCache = await this.datasourcesApi.listDatasources();
1244
+ }
1245
+ const match = datasourcesCache.find((ds) => {
1246
+ const id = ds._id || ds.id || "";
1247
+ return id === integrationId;
1248
+ });
1249
+ if (match && typeof match.name === "string" && match.name) {
1250
+ integrationName = match.name;
1251
+ }
1252
+ }
1253
+ if (integrationId && integrationName) {
1254
+ return { kind: "ok", value: { integrationId, integrationName } };
1255
+ }
1256
+ }
1257
+ return { kind: "no_metric_fields", widgetsTried: toProbe.length, fetchErrors };
1258
+ }
947
1259
  async resolveEmbedContext(embedId) {
948
1260
  const response = await this.embedsApi.getByEmbedId(embedId);
949
1261
  const records = Array.isArray(response) ? response : response ? [response] : [];
@@ -1079,6 +1391,8 @@ function createServiceBundle(client) {
1079
1391
  widgetsApi,
1080
1392
  embedsApi,
1081
1393
  dataAppsApi,
1394
+ datasourcesApi,
1395
+ dashboardsApi,
1082
1396
  guestTokenService,
1083
1397
  discoveryService
1084
1398
  )
@@ -1479,9 +1793,7 @@ function registerListDatasources(server, client) {
1479
1793
  return toolSuccess(JSON.stringify({
1480
1794
  count: active.length,
1481
1795
  datasources: active.map((ds) => ({
1482
- id: ds._id || ds.id || null,
1483
- name: ds.name,
1484
- type: ds.dbName || ds.datasourceType || "unknown"
1796
+ name: ds.name
1485
1797
  })),
1486
1798
  inactive: inactive.length > 0 ? inactive.map((ds) => ({ name: ds.name })) : void 0
1487
1799
  }));
@@ -4217,29 +4529,41 @@ function registerListWidgets(server, client) {
4217
4529
  const services = createServiceBundle(client);
4218
4530
  server.tool(
4219
4531
  "list_widgets",
4220
- "List widgets (metrics) on a customer dashboard embed. Returns each widget's metricId, name, published state, and creator identifier.",
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.",
4221
4533
  {
4222
- embedId: z21.string().describe("Embed ID (customer dashboard instantiation)"),
4223
- clientId: z21.string().describe("Client/tenant identifier for this embed")
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.")
4224
4538
  },
4225
4539
  READ_ONLY,
4226
4540
  async (params) => {
4227
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;
4228
4550
  const widgets = await services.widgetsApi.listWidgets(
4229
- params.embedId,
4551
+ embedId,
4230
4552
  params.clientId
4231
4553
  );
4232
4554
  if (!Array.isArray(widgets) || widgets.length === 0) {
4233
4555
  return toolSuccess(
4234
- `No widgets found on embed "${params.embedId}" for client "${params.clientId}".`
4556
+ `No widgets found on dashboard "${embed.value.resolvedFrom?.dashboardName || embedId}" for client "${params.clientId}".`
4235
4557
  );
4236
4558
  }
4237
4559
  return toolSuccess(
4238
4560
  JSON.stringify({
4239
4561
  count: widgets.length,
4240
- embedId: params.embedId,
4562
+ dashboardName: embed.value.resolvedFrom?.dashboardName || null,
4563
+ embedId,
4241
4564
  clientId: params.clientId,
4242
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,
4243
4567
  metricId: w.metricId || null,
4244
4568
  name: w.name || null,
4245
4569
  isPublished: w.isPublished ?? null,
@@ -4261,41 +4585,50 @@ function registerAddWidget(server, client) {
4261
4585
  const services = createServiceBundle(client);
4262
4586
  server.tool(
4263
4587
  "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.",
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.",
4265
4589
  {
4266
- embedId: z22.string().describe("Embed ID the widget will be added to"),
4267
4590
  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."),
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."),
4269
4595
  templateMetricId: z22.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
4270
4596
  name: z22.string().optional().describe("Widget name (overrides template)"),
4271
4597
  metricId: z22.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
4272
4598
  query: z22.string().optional().describe("SQL query body (overrides template)"),
4273
4599
  chartOptions: z22.record(z22.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
4274
4600
  description: z22.string().optional(),
4275
- companyIntegrationId: z22.string().optional().describe("Datasource ID. Required when not cloning a template."),
4276
- integrationName: z22.string().optional(),
4601
+ companyIntegrationId: z22.string().optional().describe("Datasource UUID override. Auto-resolved from `embedId` or sole active datasource; leave blank unless the user explicitly picked one by ID."),
4602
+ integrationName: z22.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.'),
4277
4603
  isPublished: z22.boolean().optional().describe("Publish immediately (default true)"),
4278
4604
  body: z22.record(z22.unknown()).optional().describe("Raw createMetric body passthrough. If provided, used as-is (after defaults). Overrides all other params.")
4279
4605
  },
4280
4606
  WRITE_CREATE,
4281
4607
  async (params) => {
4282
4608
  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
- }
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);
4290
4618
  if (!ctx.externalDashboardId) {
4291
4619
  return toolError(
4292
- `Embed "${params.embedId}" has no externalDashboardId \u2014 cannot attach a widget. Verify via \`get_embed_details\`.`
4620
+ `Dashboard "${params.dashboardName || embedId}" has no externalDashboardId \u2014 cannot attach a widget. Verify via \`get_embed_details\`.`
4293
4621
  );
4294
4622
  }
4623
+ const dataAppName = await services.widgetService.resolveDataAppName(
4624
+ params.dataAppName,
4625
+ embedId
4626
+ );
4627
+ if (!dataAppName.ok) return toolError(dataAppName.error);
4295
4628
  const guestToken = await services.widgetService.mintGuestToken(
4296
4629
  params.clientId,
4297
- dataAppName,
4298
- [params.embedId]
4630
+ dataAppName.value,
4631
+ [embedId]
4299
4632
  );
4300
4633
  let createBody;
4301
4634
  if (params.body) {
@@ -4326,19 +4659,21 @@ function registerAddWidget(server, client) {
4326
4659
  } else {
4327
4660
  if (!params.name) return toolError("`name` is required when not cloning a template.");
4328
4661
  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
- }
4662
+ const datasource = await services.widgetService.resolveDatasource(guestToken, {
4663
+ embedId,
4664
+ clientId: params.clientId,
4665
+ integrationId: params.companyIntegrationId,
4666
+ integrationName: params.integrationName
4667
+ });
4668
+ if (!datasource.ok) return toolError(datasource.error);
4334
4669
  createBody = {
4335
4670
  name: params.name,
4336
4671
  metricId: params.metricId || `widget-${Date.now()}`,
4337
4672
  query: params.query,
4338
4673
  chartOptions: params.chartOptions || {},
4339
4674
  description: params.description || "",
4340
- companyIntegrationId: params.companyIntegrationId,
4341
- integrationName: params.integrationName,
4675
+ companyIntegrationId: datasource.value.integrationId,
4676
+ integrationName: datasource.value.integrationName,
4342
4677
  isPublished: params.isPublished ?? true,
4343
4678
  isCreatedByClient: false,
4344
4679
  externalDashboardIds: [{ externalDashboardId: ctx.externalDashboardId }]
@@ -4362,7 +4697,7 @@ ${JSON.stringify(createBody, null, 2).slice(0, 3e3)}`
4362
4697
  - externalMetricId: \`${newId}\`
4363
4698
  - metricId: \`${newMetricId}\`
4364
4699
  - name: \`${createBody.name}\`
4365
- - embedId: \`${params.embedId}\`
4700
+ - dashboard: \`${embed.value.resolvedFrom?.dashboardName || embedId}\`
4366
4701
  - clientId: \`${params.clientId}\`
4367
4702
 
4368
4703
  Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust.`
@@ -4381,12 +4716,14 @@ function registerUpdateWidget(server, client) {
4381
4716
  const services = createServiceBundle(client);
4382
4717
  server.tool(
4383
4718
  "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.",
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.",
4385
4720
  {
4386
4721
  externalMetricId: z23.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
4387
4722
  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."),
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."),
4390
4727
  set: z23.record(z23.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
4391
4728
  },
4392
4729
  WRITE_IDEMPOTENT,
@@ -4395,10 +4732,22 @@ function registerUpdateWidget(server, client) {
4395
4732
  if (!params.set || Object.keys(params.set).length === 0) {
4396
4733
  return toolError("`set` must contain at least one field to update.");
4397
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;
4742
+ const dataAppName = await services.widgetService.resolveDataAppName(
4743
+ params.dataAppName,
4744
+ embedId || void 0
4745
+ );
4746
+ if (!dataAppName.ok) return toolError(dataAppName.error);
4398
4747
  const guestToken = await services.widgetService.mintGuestToken(
4399
4748
  params.clientId,
4400
- params.dataAppName,
4401
- params.embedId ? [params.embedId] : void 0
4749
+ dataAppName.value,
4750
+ embedId ? [embedId] : void 0
4402
4751
  );
4403
4752
  const result = await services.widgetsApi.updateMetric(guestToken, {
4404
4753
  externalMetricId: params.externalMetricId,
@@ -4435,20 +4784,34 @@ function registerRemoveWidget(server, client) {
4435
4784
  const services = createServiceBundle(client);
4436
4785
  server.tool(
4437
4786
  "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.",
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.",
4439
4788
  {
4440
4789
  externalMetricId: z24.string().describe("Internal uuid of the widget (externalMetrics.id)"),
4441
4790
  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.")
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.")
4444
4795
  },
4445
4796
  DESTRUCTIVE,
4446
4797
  async (params) => {
4447
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;
4806
+ const dataAppName = await services.widgetService.resolveDataAppName(
4807
+ params.dataAppName,
4808
+ embedId || void 0
4809
+ );
4810
+ if (!dataAppName.ok) return toolError(dataAppName.error);
4448
4811
  const guestToken = await services.widgetService.mintGuestToken(
4449
4812
  params.clientId,
4450
- params.dataAppName,
4451
- params.embedId ? [params.embedId] : void 0
4813
+ dataAppName.value,
4814
+ embedId ? [embedId] : void 0
4452
4815
  );
4453
4816
  const result = await services.widgetsApi.markArchived(guestToken, {
4454
4817
  id: params.externalMetricId,
@@ -4481,31 +4844,40 @@ function registerGenerateWidget(server, client) {
4481
4844
  const services = createServiceBundle(client);
4482
4845
  server.tool(
4483
4846
  "generate_widget",
4484
- "AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft to review and pass to `add_widget`. Data app and datasource auto-resolve from `embedId` (preferred) or sole workspace defaults. On resolve failure, ask the user for the datasource by name and pass `integrationName`.",
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.",
4485
4848
  {
4486
4849
  clientId: z25.string().describe("Client/tenant identifier"),
4487
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.'),
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("Technical override \u2014 the datasource UUID. Most flows should leave this blank and rely on auto-resolution from `embedId`. If you must override, prefer `integrationName` (business-user friendly)."),
4491
- integrationName: z25.string().optional().describe('Datasource display name (e.g. "Snowflake", "demo datasource"). Use this when the user picks a datasource by name \u2014 the id is looked up automatically. Leave blank if `embedId` is provided (auto-resolved).')
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.")
4492
4857
  },
4493
4858
  READ_ONLY,
4494
4859
  async (params) => {
4495
4860
  try {
4496
- const dataAppName = await resolveDataAppName(
4497
- services,
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;
4869
+ const dataAppName = await services.widgetService.resolveDataAppName(
4498
4870
  params.dataAppName,
4499
- params.embedId
4871
+ embedId
4500
4872
  );
4501
4873
  if (!dataAppName.ok) return toolError(dataAppName.error);
4502
4874
  const guestToken = await services.widgetService.mintGuestToken(
4503
4875
  params.clientId,
4504
4876
  dataAppName.value,
4505
- params.embedId ? [params.embedId] : void 0
4877
+ [embedId]
4506
4878
  );
4507
- const datasource = await resolveDatasource(services, guestToken, {
4508
- embedId: params.embedId,
4879
+ const datasource = await services.widgetService.resolveDatasource(guestToken, {
4880
+ embedId,
4509
4881
  clientId: params.clientId,
4510
4882
  integrationId: params.integrationId,
4511
4883
  integrationName: params.integrationName
@@ -4545,177 +4917,6 @@ function registerGenerateWidget(server, client) {
4545
4917
  }
4546
4918
  );
4547
4919
  }
4548
- function isActiveDatasource(ds) {
4549
- return Boolean(ds.isAuthenticated) && !ds.isDisabled;
4550
- }
4551
- async function resolveDataAppName(services, provided, embedId) {
4552
- if (provided) return { ok: true, value: provided };
4553
- if (embedId) {
4554
- const ctx = await services.widgetService.resolveEmbedContext(embedId);
4555
- if (ctx.dataAppName) return { ok: true, value: ctx.dataAppName };
4556
- }
4557
- const apps = await services.dataAppsApi.listDataApps();
4558
- const normalized = apps.map((a) => services.discoveryService.normalizeDataApp(a)).filter((a) => a.name && a.name !== "Unnamed Data App");
4559
- if (normalized.length === 1) return { ok: true, value: normalized[0].name };
4560
- if (normalized.length === 0) {
4561
- return {
4562
- ok: false,
4563
- error: "No data apps found in this workspace. Create one in the Databrain UI first."
4564
- };
4565
- }
4566
- return {
4567
- ok: false,
4568
- error: `Multiple data apps exist (${normalized.map((a) => `"${a.name}"`).join(", ")}). Pass \`dataAppName\` explicitly, or pass \`embedId\` to auto-resolve.`
4569
- };
4570
- }
4571
- async function resolveDatasource(services, guestToken, params) {
4572
- if (params.integrationId && params.integrationName) {
4573
- return {
4574
- ok: true,
4575
- value: {
4576
- integrationId: params.integrationId,
4577
- integrationName: params.integrationName
4578
- }
4579
- };
4580
- }
4581
- if (params.integrationId || params.integrationName) {
4582
- return completeFromDatasources(services, {
4583
- integrationId: params.integrationId,
4584
- integrationName: params.integrationName
4585
- });
4586
- }
4587
- let embedProbe = null;
4588
- if (params.embedId) {
4589
- embedProbe = await probeDatasourceFromEmbed(
4590
- services,
4591
- guestToken,
4592
- params.embedId,
4593
- params.clientId
4594
- );
4595
- if (embedProbe.kind === "ok") {
4596
- return {
4597
- ok: true,
4598
- value: {
4599
- integrationId: params.integrationId || embedProbe.value.integrationId,
4600
- integrationName: params.integrationName || embedProbe.value.integrationName
4601
- }
4602
- };
4603
- }
4604
- }
4605
- const datasources = await services.datasourcesApi.listDatasources();
4606
- const named = datasources.filter(isActiveDatasource).filter(
4607
- (ds) => typeof ds.name === "string" && ds.name.trim().length > 0
4608
- );
4609
- if (named.length === 1) {
4610
- const ds = named[0];
4611
- const id = ds._id || ds.id || "";
4612
- const name = ds.name;
4613
- if (id && name) {
4614
- return { ok: true, value: { integrationId: id, integrationName: name } };
4615
- }
4616
- }
4617
- if (named.length === 0) {
4618
- return {
4619
- ok: false,
4620
- error: "No datasources found in this workspace. Connect one in the Databrain UI first (technical setup)."
4621
- };
4622
- }
4623
- const nameList = named.map((d) => `"${d.name}"`).join(", ");
4624
- let embedReason = "";
4625
- if (params.embedId && embedProbe) {
4626
- if (embedProbe.kind === "no_widgets") {
4627
- embedReason = ` Embed "${params.embedId}" has no widgets for clientId "${params.clientId}" to inherit from, so the datasource can't be auto-detected.`;
4628
- } else if (embedProbe.kind === "no_metric_fields") {
4629
- embedReason = ` Checked ${embedProbe.widgetsTried} widget(s) on embed "${params.embedId}"` + (embedProbe.fetchErrors > 0 ? ` (${embedProbe.fetchErrors} could not be fetched)` : "") + `, but none had datasource metadata to inherit from.`;
4630
- }
4631
- }
4632
- return {
4633
- ok: false,
4634
- error: `Could not auto-resolve a datasource.${embedReason} Ask the user which datasource this widget should query \u2014 available options: ${nameList}. Then retry with \`integrationName: "<chosen name>"\` (no other fields needed). Technical callers can pass \`integrationId\` instead \u2014 see \`list_datasources\` for UUIDs.`
4635
- };
4636
- }
4637
- async function completeFromDatasources(services, provided) {
4638
- const allDatasources = await services.datasourcesApi.listDatasources();
4639
- const datasources = allDatasources.filter(isActiveDatasource);
4640
- const matchPredicate = (ds) => {
4641
- const id2 = ds._id || ds.id || "";
4642
- const name2 = typeof ds.name === "string" ? ds.name : "";
4643
- if (provided.integrationId && id2 === provided.integrationId) return true;
4644
- if (provided.integrationName && name2.toLowerCase() === provided.integrationName.toLowerCase()) {
4645
- return true;
4646
- }
4647
- return false;
4648
- };
4649
- const target = datasources.find(matchPredicate);
4650
- if (!target) {
4651
- const available = datasources.filter((ds) => typeof ds.name === "string" && ds.name.trim().length > 0).map((ds) => `"${ds.name}"`).join(", ");
4652
- const inactiveMatch = allDatasources.find(
4653
- (ds) => !isActiveDatasource(ds) && matchPredicate(ds)
4654
- );
4655
- if (inactiveMatch) {
4656
- const reason = inactiveMatch.isDisabled ? "disabled" : "not authenticated";
4657
- return {
4658
- ok: false,
4659
- error: `Datasource "${inactiveMatch.name ?? provided.integrationName ?? provided.integrationId}" is ${reason}. Fix the connection in the Databrain UI, or pick another datasource. Active options: ${available || "(none)"}.`
4660
- };
4661
- }
4662
- 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.`;
4663
- return { ok: false, error: hint };
4664
- }
4665
- const id = target._id || target.id || "";
4666
- const name = typeof target.name === "string" ? target.name : "";
4667
- if (!id || !name) {
4668
- return {
4669
- ok: false,
4670
- error: `Datasource match is missing id or name (id: "${id}", name: "${name}").`
4671
- };
4672
- }
4673
- return { ok: true, value: { integrationId: id, integrationName: name } };
4674
- }
4675
- async function probeDatasourceFromEmbed(services, guestToken, embedId, clientId) {
4676
- const widgets = await services.widgetsApi.listWidgets(embedId, clientId);
4677
- const candidates = widgets.filter(
4678
- (w) => typeof w.metricId === "string" && w.metricId.length > 0
4679
- );
4680
- if (candidates.length === 0) return { kind: "no_widgets" };
4681
- const MAX_PROBES = 5;
4682
- const toProbe = candidates.slice(0, MAX_PROBES);
4683
- let fetchErrors = 0;
4684
- let datasourcesCache = null;
4685
- for (const widget of toProbe) {
4686
- let response;
4687
- try {
4688
- response = await services.widgetsApi.fetchEmbeddedMetric(
4689
- guestToken,
4690
- widget.metricId
4691
- );
4692
- } catch {
4693
- fetchErrors += 1;
4694
- continue;
4695
- }
4696
- const metric = response?.externalMetric;
4697
- if (!metric || typeof metric !== "object") continue;
4698
- const rec = metric;
4699
- const integrationId = typeof rec.companyIntegrationId === "string" && rec.companyIntegrationId ? rec.companyIntegrationId : null;
4700
- let integrationName = typeof rec.integrationName === "string" && rec.integrationName ? rec.integrationName : null;
4701
- if (integrationId && !integrationName) {
4702
- if (!datasourcesCache) {
4703
- datasourcesCache = await services.datasourcesApi.listDatasources();
4704
- }
4705
- const match = datasourcesCache.find((ds) => {
4706
- const id = ds._id || ds.id || "";
4707
- return id === integrationId;
4708
- });
4709
- if (match && typeof match.name === "string" && match.name) {
4710
- integrationName = match.name;
4711
- }
4712
- }
4713
- if (integrationId && integrationName) {
4714
- return { kind: "ok", value: { integrationId, integrationName } };
4715
- }
4716
- }
4717
- return { kind: "no_metric_fields", widgetsTried: toProbe.length, fetchErrors };
4718
- }
4719
4920
 
4720
4921
  // src/tools/registry.ts
4721
4922
  function registerAllTools(server, client) {
@@ -7746,75 +7947,85 @@ Suggest a sample question based on the tables you described and run it with \`as
7746
7947
  `;
7747
7948
 
7748
7949
  // src/prompts/customize-customer-dashboard.ts
7749
- 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.
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.
7750
7951
 
7751
- **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.**
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.**
7953
+
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.
7752
7955
 
7753
7956
  **Key concepts (read before starting):**
7754
- - **Embed** = a customer's instantiation of a dashboard. Identified by \`embedId\`. Each embed is scoped to one \`clientId\` (the customer/tenant).
7755
- - **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).
7756
- - **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.
7757
- - **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.
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.
7758
7970
 
7759
- **Step 1 \u2014 Identify the target embed**
7971
+ **Step 1 \u2014 Identify the customer and dashboard**
7760
7972
  STOP and ASK the user:
7761
- - "Which customer/client are we customizing for?" (the \`clientId\`)
7762
- - "Which embed/dashboard?" (the \`embedId\`)
7973
+ - "Which customer/client are we customizing for?" (capture as \`clientId\`)
7974
+ - "Which dashboard?" (capture as \`dashboardName\`)
7763
7975
 
7764
- 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.
7976
+ Only ask about workspace if the tool reports the dashboard name is ambiguous across workspaces.
7765
7977
 
7766
7978
  **Step 2 \u2014 Survey the current widgets**
7767
- Call \`list_widgets({ embedId, clientId })\`.
7768
- STOP and present the list: \`{metricId, name, isPublished, createdByUser}\` per widget.
7769
- ASK: "What would you like to change? (add / modify / remove)" \u2014 or more specifically, "Which widgets should I add, update, or 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?"
7770
7982
 
7771
7983
  **Step 3a \u2014 ADD a widget**
7772
- 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.
7984
+ Business-user flow. NEVER ask for SQL, datasource, or chart config unless the user explicitly says they want to write raw SQL.
7773
7985
 
7774
7986
  1. **Clone an existing chart** (default \u2014 try this first):
7775
- - 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.
7776
- - Call \`add_widget({ embedId, clientId, templateMetricId, name? })\` using the picked widget's \`metricId\`. The tool inherits the query, chart config, and datasource automatically.
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.
7777
7989
 
7778
7990
  2. **Describe the chart in plain English** (AI generate \u2014 use if no suitable template exists):
7779
7991
  - ASK: "Describe the chart you want" (e.g., "calls per day over last 30 days").
7780
- - 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.
7781
- - \`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.
7782
- - 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.
7783
7994
  - If they want changes, re-run \`generate_widget\` with a refined prompt.
7784
- - If \`generate_widget\` reports "Multiple data apps", pass \`embedId\` or \`dataAppName\`. If it reports "Could not auto-resolve a datasource", ASK the user which datasource to use by NAME (the error lists available options) and retry with \`integrationName: "<name>"\`. Never ask the user for \`integrationId\` or any UUID \u2014 \`integrationName\` alone is enough.
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.
7785
7996
 
7786
7997
  **Advanced \u2014 write SQL directly** (technical users only; ONLY if the user explicitly asks to write SQL):
7787
- - ASK for: \`name\`, \`query\` (SQL), \`chartOptions\`, \`companyIntegrationId\` (use \`list_datasources\`).
7788
- - Call \`add_widget({ embedId, clientId, name, query, chartOptions, companyIntegrationId, ... })\`.
7998
+ - ASK for: \`name\`, \`query\` (SQL), \`chartOptions\`.
7999
+ - Call \`add_widget({ clientId, dashboardName, name, query, chartOptions })\` \u2014 datasource still auto-resolves from the dashboard.
7789
8000
 
7790
8001
  Show the user the new widget's \`externalMetricId\` and \`metricId\` after each add.
7791
8002
 
7792
8003
  **Step 3b \u2014 UPDATE a widget**
7793
8004
  If the user wants to modify a widget, STOP and ASK:
7794
- - Which widget? (look up its \`externalMetricId\` from Step 2 \u2014 remember, this is the uuid, NOT the \`metricId\` slug)
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.)
7795
8006
  - What to change? (name, query, chartOptions, isPublished, etc.)
7796
8007
 
7797
- Call \`update_widget({ externalMetricId, clientId, dataAppName, set: {<partial patch>} })\`.
8008
+ Call \`update_widget({ externalMetricId, clientId, dashboardName, workspaceName?, set: {<partial patch>} })\`.
7798
8009
  Show the user the confirmation and list the changed fields.
7799
8010
 
7800
8011
  **Step 3c \u2014 REMOVE a widget**
7801
8012
  If the user wants to remove a widget, STOP and CONFIRM:
7802
- - "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?"
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?"
7803
8014
 
7804
- Wait for explicit confirmation before calling \`remove_widget({ externalMetricId, clientId, dataAppName })\`.
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? })\`.
7805
8017
 
7806
8018
  **Step 4 \u2014 Verify**
7807
- Call \`list_widgets({ embedId, clientId })\` again. Present the updated list and confirm the changes match what the user asked for.
8019
+ Call \`list_widgets({ clientId, dashboardName, workspaceName? })\` again. Present the updated list and confirm the changes match what the user asked for.
7808
8020
 
7809
8021
  **Step 5 \u2014 (Optional) regenerate guest token for live preview**
7810
- If the user wants to see the changes in their running app:
7811
- - 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.
7812
8023
 
7813
8024
  **Important rules:**
7814
8025
  - STOP after each step. Present results and wait for user input before proceeding.
7815
- - 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.
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\`.
7816
8028
  - Never batch add + update + remove without confirming each one.
7817
- - If the customer-facing effect is not immediate (e.g., guest token cached), say so explicitly in your handoff message.
7818
8029
 
7819
8030
  After completion, suggest: \`brand-my-embed\` if they want to adjust theme, or \`manage-embeds\` if they want to tweak access settings / options.`;
7820
8031