@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 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 = ` Embed "${params.embedId}" has no widgets for clientId "${params.clientId}" to inherit from, so the datasource can't be auto-detected.`;
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 embed "${params.embedId}"` + (embedProbe.fetchErrors > 0 ? ` (${embedProbe.fetchErrors} could not be fetched)` : "") + `, but none had datasource metadata to inherit from.`;
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-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.`
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: `Datasource match is missing id or name (id: "${id}", name: "${name}").`
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
- id: ds._id || ds.id || null,
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 (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.",
4400
4533
  {
4401
- embedId: z21.string().describe("Embed ID (customer dashboard instantiation)"),
4402
- 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.")
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
- params.embedId,
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 embed "${params.embedId}" for client "${params.clientId}".`
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
- embedId: params.embedId,
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 (metric) to a customer dashboard embed. Either clone via `templateMetricId` (recommended \u2014 inherits query, chartOptions, datasource), or supply a full spec (name + query + chartOptions). Datasource auto-resolves from the embed or sole active datasource; pass `integrationName` to override. Mints a guest token 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.",
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
- 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."),
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 ctx = await services.widgetService.resolveEmbedContext(params.embedId);
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
- `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\`.`
4466
4621
  );
4467
4622
  }
4468
4623
  const dataAppName = await services.widgetService.resolveDataAppName(
4469
4624
  params.dataAppName,
4470
- params.embedId
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
- [params.embedId]
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: params.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
- - embedId: \`${params.embedId}\`
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 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.",
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
- dataAppName: z23.string().optional().describe("Optional override. Auto-resolved from `embedId`, or from the sole data app when there is only one."),
4569
- embedId: z23.string().optional().describe("Optional \u2014 if provided, the guest token is scoped to just this embed and dataAppName is auto-resolved."),
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
- params.embedId
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
- params.embedId ? [params.embedId] : void 0
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 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.",
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
- dataAppName: z24.string().optional().describe("Optional override. Auto-resolved from `embedId`, or from the sole data app when there is only one."),
4628
- embedId: z24.string().optional().describe("Optional \u2014 scopes the guest token to this embed and auto-resolves dataAppName.")
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
- params.embedId
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
- params.embedId ? [params.embedId] : void 0
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 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.",
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
- 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."),
4679
- dataAppName: z25.string().optional().describe("Optional override. Auto-resolved from embedId, or from the sole data app when there is only one."),
4680
- 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)."),
4681
- 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.")
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
- params.embedId
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
- params.embedId ? [params.embedId] : void 0
4877
+ [embedId]
4695
4878
  );
4696
4879
  const datasource = await services.widgetService.resolveDatasource(guestToken, {
4697
- embedId: params.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 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.
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
- **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.**
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
- - **Embed** = a customer's instantiation of a dashboard. Identified by \`embedId\`. Each embed is scoped to one \`clientId\` (the customer/tenant).
7773
- - **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).
7774
- - **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.
7775
- - **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.
7776
7970
 
7777
- **Step 1 \u2014 Identify the target embed**
7971
+ **Step 1 \u2014 Identify the customer and dashboard**
7778
7972
  STOP and ASK the user:
7779
- - "Which customer/client are we customizing for?" (the \`clientId\`)
7780
- - "Which embed/dashboard?" (the \`embedId\`)
7973
+ - "Which customer/client are we customizing for?" (capture as \`clientId\`)
7974
+ - "Which dashboard?" (capture as \`dashboardName\`)
7781
7975
 
7782
- 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.
7783
7977
 
7784
7978
  **Step 2 \u2014 Survey the current widgets**
7785
- Call \`list_widgets({ embedId, clientId })\`.
7786
- STOP and present the list: \`{metricId, name, isPublished, createdByUser}\` per widget.
7787
- 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?"
7788
7982
 
7789
7983
  **Step 3a \u2014 ADD a widget**
7790
- 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.
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\` on this embed, or on the template embed / another customer's embed, and present the widgets by name. Let the user pick by name.
7794
- - 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.
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({ 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.
7799
- - \`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.
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 "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.
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\`, \`companyIntegrationId\` (use \`list_datasources\`).
7806
- - 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.
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 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.)
7813
8006
  - What to change? (name, query, chartOptions, isPublished, etc.)
7814
8007
 
7815
- Call \`update_widget({ externalMetricId, clientId, dataAppName, set: {<partial patch>} })\`.
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-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?"
7821
8014
 
7822
- 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? })\`.
7823
8017
 
7824
8018
  **Step 4 \u2014 Verify**
7825
- 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.
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. 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\`.
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