@databrainhq/mcp-server 0.2.7 → 0.2.9
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 +216 -198
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -936,14 +936,192 @@ var asRecord2 = (v) => {
|
|
|
936
936
|
if (!v || typeof v !== "object" || Array.isArray(v)) return null;
|
|
937
937
|
return v;
|
|
938
938
|
};
|
|
939
|
+
var isActiveDatasource = (ds) => Boolean(ds.isAuthenticated) && !ds.isDisabled;
|
|
939
940
|
var WidgetService = class {
|
|
940
|
-
constructor(widgetsApi, embedsApi, dataAppsApi, guestTokenService, discoveryService) {
|
|
941
|
+
constructor(widgetsApi, embedsApi, dataAppsApi, datasourcesApi, guestTokenService, discoveryService) {
|
|
941
942
|
this.widgetsApi = widgetsApi;
|
|
942
943
|
this.embedsApi = embedsApi;
|
|
943
944
|
this.dataAppsApi = dataAppsApi;
|
|
945
|
+
this.datasourcesApi = datasourcesApi;
|
|
944
946
|
this.guestTokenService = guestTokenService;
|
|
945
947
|
this.discoveryService = discoveryService;
|
|
946
948
|
}
|
|
949
|
+
/**
|
|
950
|
+
* Resolve the data app name from either a caller override, an embed's
|
|
951
|
+
* metadata, or the sole data app in the workspace. Used by every widget
|
|
952
|
+
* tool so business users never have to name the data app when it's
|
|
953
|
+
* unambiguous.
|
|
954
|
+
*/
|
|
955
|
+
async resolveDataAppName(provided, embedId) {
|
|
956
|
+
if (provided) return { ok: true, value: provided };
|
|
957
|
+
if (embedId) {
|
|
958
|
+
const ctx = await this.resolveEmbedContext(embedId);
|
|
959
|
+
if (ctx.dataAppName) return { ok: true, value: ctx.dataAppName };
|
|
960
|
+
}
|
|
961
|
+
const apps = await this.dataAppsApi.listDataApps();
|
|
962
|
+
const normalized = apps.map((a) => this.discoveryService.normalizeDataApp(a)).filter((a) => a.name && a.name !== "Unnamed Data App");
|
|
963
|
+
if (normalized.length === 1) return { ok: true, value: normalized[0].name };
|
|
964
|
+
if (normalized.length === 0) {
|
|
965
|
+
return {
|
|
966
|
+
ok: false,
|
|
967
|
+
error: "No data apps found in this workspace. Create one in the Databrain UI first."
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
return {
|
|
971
|
+
ok: false,
|
|
972
|
+
error: `Multiple data apps exist (${normalized.map((a) => `"${a.name}"`).join(", ")}). Pass \`dataAppName\` explicitly, or pass \`embedId\` to auto-resolve.`
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Resolve a datasource for a widget. Priority:
|
|
977
|
+
* 1. Explicit integrationId + integrationName (fast path).
|
|
978
|
+
* 2. One of integrationId/integrationName supplied — look up the other.
|
|
979
|
+
* 3. Probe existing widgets on the embed for inherited datasource metadata.
|
|
980
|
+
* 4. Fall back to the sole active datasource in the workspace.
|
|
981
|
+
* On failure, returns a business-user-friendly error listing the available
|
|
982
|
+
* datasource names.
|
|
983
|
+
*/
|
|
984
|
+
async resolveDatasource(guestToken, params) {
|
|
985
|
+
if (params.integrationId && params.integrationName) {
|
|
986
|
+
return {
|
|
987
|
+
ok: true,
|
|
988
|
+
value: {
|
|
989
|
+
integrationId: params.integrationId,
|
|
990
|
+
integrationName: params.integrationName
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
if (params.integrationId || params.integrationName) {
|
|
995
|
+
return this.completeFromDatasources({
|
|
996
|
+
integrationId: params.integrationId,
|
|
997
|
+
integrationName: params.integrationName
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
let embedProbe = null;
|
|
1001
|
+
if (params.embedId) {
|
|
1002
|
+
embedProbe = await this.probeDatasourceFromEmbed(
|
|
1003
|
+
guestToken,
|
|
1004
|
+
params.embedId,
|
|
1005
|
+
params.clientId
|
|
1006
|
+
);
|
|
1007
|
+
if (embedProbe.kind === "ok") {
|
|
1008
|
+
return { ok: true, value: embedProbe.value };
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const datasources = await this.datasourcesApi.listDatasources();
|
|
1012
|
+
const named = datasources.filter(isActiveDatasource).filter(
|
|
1013
|
+
(ds) => typeof ds.name === "string" && ds.name.trim().length > 0
|
|
1014
|
+
);
|
|
1015
|
+
if (named.length === 1) {
|
|
1016
|
+
const ds = named[0];
|
|
1017
|
+
const id = ds._id || ds.id || "";
|
|
1018
|
+
const name = ds.name;
|
|
1019
|
+
if (id && name) {
|
|
1020
|
+
return { ok: true, value: { integrationId: id, integrationName: name } };
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (named.length === 0) {
|
|
1024
|
+
return {
|
|
1025
|
+
ok: false,
|
|
1026
|
+
error: "No datasources found in this workspace. Connect one in the Databrain UI first (technical setup)."
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
const nameList = named.map((d) => `"${d.name}"`).join(", ");
|
|
1030
|
+
let embedReason = "";
|
|
1031
|
+
if (params.embedId && embedProbe) {
|
|
1032
|
+
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.`;
|
|
1034
|
+
} 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.`;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
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.`
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
async completeFromDatasources(provided) {
|
|
1044
|
+
const allDatasources = await this.datasourcesApi.listDatasources();
|
|
1045
|
+
const datasources = allDatasources.filter(isActiveDatasource);
|
|
1046
|
+
const matchPredicate = (ds) => {
|
|
1047
|
+
const id2 = ds._id || ds.id || "";
|
|
1048
|
+
const name2 = typeof ds.name === "string" ? ds.name : "";
|
|
1049
|
+
if (provided.integrationId && id2 === provided.integrationId) return true;
|
|
1050
|
+
if (provided.integrationName && name2.toLowerCase() === provided.integrationName.toLowerCase()) {
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
return false;
|
|
1054
|
+
};
|
|
1055
|
+
const target = datasources.find(matchPredicate);
|
|
1056
|
+
if (!target) {
|
|
1057
|
+
const available = datasources.filter((ds) => typeof ds.name === "string" && ds.name.trim().length > 0).map((ds) => `"${ds.name}"`).join(", ");
|
|
1058
|
+
const inactiveMatch = allDatasources.find(
|
|
1059
|
+
(ds) => !isActiveDatasource(ds) && matchPredicate(ds)
|
|
1060
|
+
);
|
|
1061
|
+
if (inactiveMatch) {
|
|
1062
|
+
const reason = inactiveMatch.isDisabled ? "disabled" : "not authenticated";
|
|
1063
|
+
return {
|
|
1064
|
+
ok: false,
|
|
1065
|
+
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)"}.`
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
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
|
+
return { ok: false, error: hint };
|
|
1070
|
+
}
|
|
1071
|
+
const id = target._id || target.id || "";
|
|
1072
|
+
const name = typeof target.name === "string" ? target.name : "";
|
|
1073
|
+
if (!id || !name) {
|
|
1074
|
+
return {
|
|
1075
|
+
ok: false,
|
|
1076
|
+
error: `Datasource match is missing id or name (id: "${id}", name: "${name}").`
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
return { ok: true, value: { integrationId: id, integrationName: name } };
|
|
1080
|
+
}
|
|
1081
|
+
async probeDatasourceFromEmbed(guestToken, embedId, clientId) {
|
|
1082
|
+
const widgets = await this.widgetsApi.listWidgets(embedId, clientId);
|
|
1083
|
+
const candidates = widgets.filter(
|
|
1084
|
+
(w) => typeof w.metricId === "string" && w.metricId.length > 0
|
|
1085
|
+
);
|
|
1086
|
+
if (candidates.length === 0) return { kind: "no_widgets" };
|
|
1087
|
+
const MAX_PROBES = 5;
|
|
1088
|
+
const toProbe = candidates.slice(0, MAX_PROBES);
|
|
1089
|
+
let fetchErrors = 0;
|
|
1090
|
+
let datasourcesCache = null;
|
|
1091
|
+
for (const widget of toProbe) {
|
|
1092
|
+
let response;
|
|
1093
|
+
try {
|
|
1094
|
+
response = await this.widgetsApi.fetchEmbeddedMetric(
|
|
1095
|
+
guestToken,
|
|
1096
|
+
widget.metricId
|
|
1097
|
+
);
|
|
1098
|
+
} catch {
|
|
1099
|
+
fetchErrors += 1;
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
const metric = response?.externalMetric;
|
|
1103
|
+
if (!metric || typeof metric !== "object") continue;
|
|
1104
|
+
const rec = metric;
|
|
1105
|
+
const integrationId = typeof rec.companyIntegrationId === "string" && rec.companyIntegrationId ? rec.companyIntegrationId : null;
|
|
1106
|
+
let integrationName = typeof rec.integrationName === "string" && rec.integrationName ? rec.integrationName : null;
|
|
1107
|
+
if (integrationId && !integrationName) {
|
|
1108
|
+
if (!datasourcesCache) {
|
|
1109
|
+
datasourcesCache = await this.datasourcesApi.listDatasources();
|
|
1110
|
+
}
|
|
1111
|
+
const match = datasourcesCache.find((ds) => {
|
|
1112
|
+
const id = ds._id || ds.id || "";
|
|
1113
|
+
return id === integrationId;
|
|
1114
|
+
});
|
|
1115
|
+
if (match && typeof match.name === "string" && match.name) {
|
|
1116
|
+
integrationName = match.name;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (integrationId && integrationName) {
|
|
1120
|
+
return { kind: "ok", value: { integrationId, integrationName } };
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return { kind: "no_metric_fields", widgetsTried: toProbe.length, fetchErrors };
|
|
1124
|
+
}
|
|
947
1125
|
async resolveEmbedContext(embedId) {
|
|
948
1126
|
const response = await this.embedsApi.getByEmbedId(embedId);
|
|
949
1127
|
const records = Array.isArray(response) ? response : response ? [response] : [];
|
|
@@ -1079,6 +1257,7 @@ function createServiceBundle(client) {
|
|
|
1079
1257
|
widgetsApi,
|
|
1080
1258
|
embedsApi,
|
|
1081
1259
|
dataAppsApi,
|
|
1260
|
+
datasourcesApi,
|
|
1082
1261
|
guestTokenService,
|
|
1083
1262
|
discoveryService
|
|
1084
1263
|
)
|
|
@@ -4261,7 +4440,7 @@ function registerAddWidget(server, client) {
|
|
|
4261
4440
|
const services = createServiceBundle(client);
|
|
4262
4441
|
server.tool(
|
|
4263
4442
|
"add_widget",
|
|
4264
|
-
"Add a widget (metric) to a customer dashboard embed. Either clone
|
|
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.",
|
|
4265
4444
|
{
|
|
4266
4445
|
embedId: z22.string().describe("Embed ID the widget will be added to"),
|
|
4267
4446
|
clientId: z22.string().describe("Client/tenant identifier (widget is created in their scope)"),
|
|
@@ -4272,8 +4451,8 @@ function registerAddWidget(server, client) {
|
|
|
4272
4451
|
query: z22.string().optional().describe("SQL query body (overrides template)"),
|
|
4273
4452
|
chartOptions: z22.record(z22.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
|
|
4274
4453
|
description: z22.string().optional(),
|
|
4275
|
-
companyIntegrationId: z22.string().optional().describe("Datasource
|
|
4276
|
-
integrationName: z22.string().optional(),
|
|
4454
|
+
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."),
|
|
4455
|
+
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
4456
|
isPublished: z22.boolean().optional().describe("Publish immediately (default true)"),
|
|
4278
4457
|
body: z22.record(z22.unknown()).optional().describe("Raw createMetric body passthrough. If provided, used as-is (after defaults). Overrides all other params.")
|
|
4279
4458
|
},
|
|
@@ -4281,20 +4460,19 @@ function registerAddWidget(server, client) {
|
|
|
4281
4460
|
async (params) => {
|
|
4282
4461
|
try {
|
|
4283
4462
|
const ctx = await services.widgetService.resolveEmbedContext(params.embedId);
|
|
4284
|
-
const dataAppName = params.dataAppName || ctx.dataAppName;
|
|
4285
|
-
if (!dataAppName) {
|
|
4286
|
-
return toolError(
|
|
4287
|
-
`Could not resolve dataAppName for embed "${params.embedId}". Pass \`dataAppName\` explicitly or verify the embed exists via \`list_embeds\`.`
|
|
4288
|
-
);
|
|
4289
|
-
}
|
|
4290
4463
|
if (!ctx.externalDashboardId) {
|
|
4291
4464
|
return toolError(
|
|
4292
4465
|
`Embed "${params.embedId}" has no externalDashboardId \u2014 cannot attach a widget. Verify via \`get_embed_details\`.`
|
|
4293
4466
|
);
|
|
4294
4467
|
}
|
|
4468
|
+
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
4469
|
+
params.dataAppName,
|
|
4470
|
+
params.embedId
|
|
4471
|
+
);
|
|
4472
|
+
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
4295
4473
|
const guestToken = await services.widgetService.mintGuestToken(
|
|
4296
4474
|
params.clientId,
|
|
4297
|
-
dataAppName,
|
|
4475
|
+
dataAppName.value,
|
|
4298
4476
|
[params.embedId]
|
|
4299
4477
|
);
|
|
4300
4478
|
let createBody;
|
|
@@ -4326,19 +4504,21 @@ function registerAddWidget(server, client) {
|
|
|
4326
4504
|
} else {
|
|
4327
4505
|
if (!params.name) return toolError("`name` is required when not cloning a template.");
|
|
4328
4506
|
if (!params.query) return toolError("`query` is required when not cloning a template.");
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4507
|
+
const datasource = await services.widgetService.resolveDatasource(guestToken, {
|
|
4508
|
+
embedId: params.embedId,
|
|
4509
|
+
clientId: params.clientId,
|
|
4510
|
+
integrationId: params.companyIntegrationId,
|
|
4511
|
+
integrationName: params.integrationName
|
|
4512
|
+
});
|
|
4513
|
+
if (!datasource.ok) return toolError(datasource.error);
|
|
4334
4514
|
createBody = {
|
|
4335
4515
|
name: params.name,
|
|
4336
4516
|
metricId: params.metricId || `widget-${Date.now()}`,
|
|
4337
4517
|
query: params.query,
|
|
4338
4518
|
chartOptions: params.chartOptions || {},
|
|
4339
4519
|
description: params.description || "",
|
|
4340
|
-
companyIntegrationId:
|
|
4341
|
-
integrationName:
|
|
4520
|
+
companyIntegrationId: datasource.value.integrationId,
|
|
4521
|
+
integrationName: datasource.value.integrationName,
|
|
4342
4522
|
isPublished: params.isPublished ?? true,
|
|
4343
4523
|
isCreatedByClient: false,
|
|
4344
4524
|
externalDashboardIds: [{ externalDashboardId: ctx.externalDashboardId }]
|
|
@@ -4385,8 +4565,8 @@ function registerUpdateWidget(server, client) {
|
|
|
4385
4565
|
{
|
|
4386
4566
|
externalMetricId: z23.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
|
|
4387
4567
|
clientId: z23.string().describe("Client/tenant identifier"),
|
|
4388
|
-
dataAppName: z23.string().describe("
|
|
4389
|
-
embedId: z23.string().optional().describe("Optional \u2014 if provided, the guest token is scoped to just this embed."),
|
|
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."),
|
|
4390
4570
|
set: z23.record(z23.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
|
|
4391
4571
|
},
|
|
4392
4572
|
WRITE_IDEMPOTENT,
|
|
@@ -4395,9 +4575,14 @@ function registerUpdateWidget(server, client) {
|
|
|
4395
4575
|
if (!params.set || Object.keys(params.set).length === 0) {
|
|
4396
4576
|
return toolError("`set` must contain at least one field to update.");
|
|
4397
4577
|
}
|
|
4578
|
+
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
4579
|
+
params.dataAppName,
|
|
4580
|
+
params.embedId
|
|
4581
|
+
);
|
|
4582
|
+
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
4398
4583
|
const guestToken = await services.widgetService.mintGuestToken(
|
|
4399
4584
|
params.clientId,
|
|
4400
|
-
|
|
4585
|
+
dataAppName.value,
|
|
4401
4586
|
params.embedId ? [params.embedId] : void 0
|
|
4402
4587
|
);
|
|
4403
4588
|
const result = await services.widgetsApi.updateMetric(guestToken, {
|
|
@@ -4439,15 +4624,20 @@ function registerRemoveWidget(server, client) {
|
|
|
4439
4624
|
{
|
|
4440
4625
|
externalMetricId: z24.string().describe("Internal uuid of the widget (externalMetrics.id)"),
|
|
4441
4626
|
clientId: z24.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
|
|
4442
|
-
dataAppName: z24.string().describe("
|
|
4443
|
-
embedId: z24.string().optional().describe("Optional \u2014 scopes the guest token to this embed.")
|
|
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.")
|
|
4444
4629
|
},
|
|
4445
4630
|
DESTRUCTIVE,
|
|
4446
4631
|
async (params) => {
|
|
4447
4632
|
try {
|
|
4633
|
+
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
4634
|
+
params.dataAppName,
|
|
4635
|
+
params.embedId
|
|
4636
|
+
);
|
|
4637
|
+
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
4448
4638
|
const guestToken = await services.widgetService.mintGuestToken(
|
|
4449
4639
|
params.clientId,
|
|
4450
|
-
|
|
4640
|
+
dataAppName.value,
|
|
4451
4641
|
params.embedId ? [params.embedId] : void 0
|
|
4452
4642
|
);
|
|
4453
4643
|
const result = await services.widgetsApi.markArchived(guestToken, {
|
|
@@ -4493,8 +4683,7 @@ function registerGenerateWidget(server, client) {
|
|
|
4493
4683
|
READ_ONLY,
|
|
4494
4684
|
async (params) => {
|
|
4495
4685
|
try {
|
|
4496
|
-
const dataAppName = await resolveDataAppName(
|
|
4497
|
-
services,
|
|
4686
|
+
const dataAppName = await services.widgetService.resolveDataAppName(
|
|
4498
4687
|
params.dataAppName,
|
|
4499
4688
|
params.embedId
|
|
4500
4689
|
);
|
|
@@ -4504,7 +4693,7 @@ function registerGenerateWidget(server, client) {
|
|
|
4504
4693
|
dataAppName.value,
|
|
4505
4694
|
params.embedId ? [params.embedId] : void 0
|
|
4506
4695
|
);
|
|
4507
|
-
const datasource = await resolveDatasource(
|
|
4696
|
+
const datasource = await services.widgetService.resolveDatasource(guestToken, {
|
|
4508
4697
|
embedId: params.embedId,
|
|
4509
4698
|
clientId: params.clientId,
|
|
4510
4699
|
integrationId: params.integrationId,
|
|
@@ -4545,177 +4734,6 @@ function registerGenerateWidget(server, client) {
|
|
|
4545
4734
|
}
|
|
4546
4735
|
);
|
|
4547
4736
|
}
|
|
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
4737
|
|
|
4720
4738
|
// src/tools/registry.ts
|
|
4721
4739
|
function registerAllTools(server, client) {
|