@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 +467 -256
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -479,6 +479,14 @@ var DatasourcesApi = class {
|
|
|
479
479
|
cacheTtlMs: 5 * 60 * 1e3
|
|
480
480
|
});
|
|
481
481
|
}
|
|
482
|
+
async resolveIdByName(datasourceName) {
|
|
483
|
+
try {
|
|
484
|
+
const schema = await this.getDatasourceSchema(datasourceName);
|
|
485
|
+
return schema?.datasourceId || null;
|
|
486
|
+
} catch {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
482
490
|
};
|
|
483
491
|
|
|
484
492
|
// src/api/embeds-api.ts
|
|
@@ -691,6 +699,7 @@ var DiscoveryService = class {
|
|
|
691
699
|
return {
|
|
692
700
|
name: isNonEmptyString(dashboard.name) && dashboard.name || isNonEmptyString(dashboard.dashboardName) && dashboard.dashboardName || "Unnamed Dashboard",
|
|
693
701
|
dashboardId: isNonEmptyString(dashboard.dashboardId) && dashboard.dashboardId || isNonEmptyString(dashboard.externalDashboardId) && dashboard.externalDashboardId || isNonEmptyString(dashboard._id) && dashboard._id || isNonEmptyString(dashboard.id) && dashboard.id || isNonEmptyString(dashboard.uid) && dashboard.uid || null,
|
|
702
|
+
externalDashboardId: isNonEmptyString(dashboard.externalDashboardId) && dashboard.externalDashboardId || null,
|
|
694
703
|
workspaceName: isNonEmptyString(dashboard.workspaceName) && dashboard.workspaceName || isNonEmptyString(dashboard.workspace) && dashboard.workspace || isNonEmptyString(dashboard.workspaceId) && dashboard.workspaceId || null,
|
|
695
704
|
metricCount: typeof dashboard.metricCount === "number" ? dashboard.metricCount : null
|
|
696
705
|
};
|
|
@@ -703,6 +712,7 @@ var DiscoveryService = class {
|
|
|
703
712
|
embedId: isNonEmptyString(embed.embedId) ? embed.embedId : null,
|
|
704
713
|
internalId: isNonEmptyString(embed._id) && embed._id || isNonEmptyString(embed.id) && embed.id || null,
|
|
705
714
|
dashboardId: metadata && isNonEmptyString(metadata.dashboardId) && metadata.dashboardId || isNonEmptyString(embed.dashboardId) && embed.dashboardId || null,
|
|
715
|
+
externalDashboardId: isNonEmptyString(embed.externalDashboardId) && embed.externalDashboardId || externalDashboard && isNonEmptyString(externalDashboard.externalDashboardId) && externalDashboard.externalDashboardId || metadata && isNonEmptyString(metadata.externalDashboardId) && metadata.externalDashboardId || null,
|
|
706
716
|
dataAppName: metadata && isNonEmptyString(metadata.dataAppName) && metadata.dataAppName || null,
|
|
707
717
|
embedType: isNonEmptyString(embed.embedType) ? embed.embedType : null
|
|
708
718
|
};
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
4223
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
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
|
-
`
|
|
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
|
-
[
|
|
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
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
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:
|
|
4341
|
-
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
|
-
-
|
|
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
|
|
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
|
-
|
|
4389
|
-
|
|
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
|
-
|
|
4401
|
-
|
|
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
|
|
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
|
-
|
|
4443
|
-
|
|
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
|
-
|
|
4451
|
-
|
|
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
|
|
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
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
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
|
|
4497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4877
|
+
[embedId]
|
|
4506
4878
|
);
|
|
4507
|
-
const datasource = await resolveDatasource(
|
|
4508
|
-
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
|
|
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
|
|
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
|
-
- **
|
|
7755
|
-
- **
|
|
7756
|
-
- **
|
|
7757
|
-
- **
|
|
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
|
|
7971
|
+
**Step 1 \u2014 Identify the customer and dashboard**
|
|
7760
7972
|
STOP and ASK the user:
|
|
7761
|
-
- "Which customer/client are we customizing for?" (
|
|
7762
|
-
- "Which
|
|
7973
|
+
- "Which customer/client are we customizing for?" (capture as \`clientId\`)
|
|
7974
|
+
- "Which dashboard?" (capture as \`dashboardName\`)
|
|
7763
7975
|
|
|
7764
|
-
|
|
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({
|
|
7768
|
-
STOP and present the list: \`{metricId, name, isPublished, createdByUser}\` per widget.
|
|
7769
|
-
ASK: "What would you like to change? (add / modify / remove)
|
|
7979
|
+
Call \`list_widgets({ clientId, dashboardName, workspaceName? })\`.
|
|
7980
|
+
STOP and present the list: \`{externalMetricId, metricId, name, isPublished, createdByUser}\` per widget. Keep the \`externalMetricId\` values from this response \u2014 you'll need them verbatim for update/remove. Show the user the human-readable names; the uuids are yours to reference internally.
|
|
7981
|
+
ASK: "What would you like to change? (add / modify / remove) \u2014 and which widgets?"
|
|
7770
7982
|
|
|
7771
7983
|
**Step 3a \u2014 ADD a widget**
|
|
7772
|
-
|
|
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\`
|
|
7776
|
-
- Call \`add_widget({
|
|
7987
|
+
- ASK: "Which existing chart should I copy from?" Call \`list_widgets\` and present the widgets by name. Let the user pick by name.
|
|
7988
|
+
- Call \`add_widget({ clientId, dashboardName, workspaceName?, templateMetricId, name? })\` using the picked widget's \`metricId\`. Query, chart config, and datasource are inherited automatically.
|
|
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({
|
|
7781
|
-
-
|
|
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 "
|
|
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
|
|
7788
|
-
- Call \`add_widget({
|
|
7998
|
+
- ASK for: \`name\`, \`query\` (SQL), \`chartOptions\`.
|
|
7999
|
+
- Call \`add_widget({ clientId, dashboardName, name, query, chartOptions })\` \u2014 datasource still auto-resolves from the dashboard.
|
|
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
|
|
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,
|
|
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-
|
|
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
|
-
|
|
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({
|
|
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.
|
|
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
|
|