@databrainhq/mcp-server 0.2.6 → 0.2.8
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 +110 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1479,7 +1479,7 @@ function registerListDatasources(server, client) {
|
|
|
1479
1479
|
return toolSuccess(JSON.stringify({
|
|
1480
1480
|
count: active.length,
|
|
1481
1481
|
datasources: active.map((ds) => ({
|
|
1482
|
-
id: ds.id,
|
|
1482
|
+
id: ds._id || ds.id || null,
|
|
1483
1483
|
name: ds.name,
|
|
1484
1484
|
type: ds.dbName || ds.datasourceType || "unknown"
|
|
1485
1485
|
})),
|
|
@@ -4481,14 +4481,14 @@ function registerGenerateWidget(server, client) {
|
|
|
4481
4481
|
const services = createServiceBundle(client);
|
|
4482
4482
|
server.tool(
|
|
4483
4483
|
"generate_widget",
|
|
4484
|
-
"AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft
|
|
4484
|
+
"AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft to review and pass to `add_widget`. Data app and datasource auto-resolve from `embedId` (preferred) or sole workspace defaults. On resolve failure, ask the user for the datasource by name and pass `integrationName`.",
|
|
4485
4485
|
{
|
|
4486
4486
|
clientId: z25.string().describe("Client/tenant identifier"),
|
|
4487
4487
|
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
4488
|
embedId: z25.string().optional().describe("Optional. When provided, data app and datasource are inherited from the embed's existing widgets \u2014 the preferred path for CS/CX flows."),
|
|
4489
4489
|
dataAppName: z25.string().optional().describe("Optional override. Auto-resolved from embedId, or from the sole data app when there is only one."),
|
|
4490
|
-
integrationId: z25.string().optional().describe("
|
|
4491
|
-
integrationName: z25.string().optional().describe("
|
|
4490
|
+
integrationId: z25.string().optional().describe("Technical override \u2014 the datasource UUID. Most flows should leave this blank and rely on auto-resolution from `embedId`. If you must override, prefer `integrationName` (business-user friendly)."),
|
|
4491
|
+
integrationName: z25.string().optional().describe('Datasource display name (e.g. "Snowflake", "demo datasource"). Use this when the user picks a datasource by name \u2014 the id is looked up automatically. Leave blank if `embedId` is provided (auto-resolved).')
|
|
4492
4492
|
},
|
|
4493
4493
|
READ_ONLY,
|
|
4494
4494
|
async (params) => {
|
|
@@ -4545,6 +4545,9 @@ function registerGenerateWidget(server, client) {
|
|
|
4545
4545
|
}
|
|
4546
4546
|
);
|
|
4547
4547
|
}
|
|
4548
|
+
function isActiveDatasource(ds) {
|
|
4549
|
+
return Boolean(ds.isAuthenticated) && !ds.isDisabled;
|
|
4550
|
+
}
|
|
4548
4551
|
async function resolveDataAppName(services, provided, embedId) {
|
|
4549
4552
|
if (provided) return { ok: true, value: provided };
|
|
4550
4553
|
if (embedId) {
|
|
@@ -4575,25 +4578,32 @@ async function resolveDatasource(services, guestToken, params) {
|
|
|
4575
4578
|
}
|
|
4576
4579
|
};
|
|
4577
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;
|
|
4578
4588
|
if (params.embedId) {
|
|
4579
|
-
|
|
4589
|
+
embedProbe = await probeDatasourceFromEmbed(
|
|
4580
4590
|
services,
|
|
4581
4591
|
guestToken,
|
|
4582
4592
|
params.embedId,
|
|
4583
4593
|
params.clientId
|
|
4584
4594
|
);
|
|
4585
|
-
if (
|
|
4595
|
+
if (embedProbe.kind === "ok") {
|
|
4586
4596
|
return {
|
|
4587
4597
|
ok: true,
|
|
4588
4598
|
value: {
|
|
4589
|
-
integrationId: params.integrationId ||
|
|
4590
|
-
integrationName: params.integrationName ||
|
|
4599
|
+
integrationId: params.integrationId || embedProbe.value.integrationId,
|
|
4600
|
+
integrationName: params.integrationName || embedProbe.value.integrationName
|
|
4591
4601
|
}
|
|
4592
4602
|
};
|
|
4593
4603
|
}
|
|
4594
4604
|
}
|
|
4595
4605
|
const datasources = await services.datasourcesApi.listDatasources();
|
|
4596
|
-
const named = datasources.filter(
|
|
4606
|
+
const named = datasources.filter(isActiveDatasource).filter(
|
|
4597
4607
|
(ds) => typeof ds.name === "string" && ds.name.trim().length > 0
|
|
4598
4608
|
);
|
|
4599
4609
|
if (named.length === 1) {
|
|
@@ -4610,23 +4620,101 @@ async function resolveDatasource(services, guestToken, params) {
|
|
|
4610
4620
|
error: "No datasources found in this workspace. Connect one in the Databrain UI first (technical setup)."
|
|
4611
4621
|
};
|
|
4612
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
|
+
}
|
|
4613
4632
|
return {
|
|
4614
4633
|
ok: false,
|
|
4615
|
-
error: `Could not auto-resolve a datasource
|
|
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;
|
|
4616
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 } };
|
|
4617
4674
|
}
|
|
4618
|
-
async function
|
|
4675
|
+
async function probeDatasourceFromEmbed(services, guestToken, embedId, clientId) {
|
|
4619
4676
|
const widgets = await services.widgetsApi.listWidgets(embedId, clientId);
|
|
4620
|
-
const
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
const
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
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 };
|
|
4630
4718
|
}
|
|
4631
4719
|
|
|
4632
4720
|
// src/tools/registry.ts
|
|
@@ -7693,7 +7781,7 @@ This is a business-user flow. NEVER ask the user for a datasource, \`companyInte
|
|
|
7693
7781
|
- \`embedId\` is optional on \`generate_widget\` (the tool will fall back to the sole data app / sole datasource if absent), but pass it when you have one \u2014 resolution is more reliable.
|
|
7694
7782
|
- Review the returned \`draft\` with the user. If they approve, call \`add_widget({ embedId, clientId, body: <draft + externalDashboardIds> })\` to persist.
|
|
7695
7783
|
- If they want changes, re-run \`generate_widget\` with a refined prompt.
|
|
7696
|
-
- If \`generate_widget\` reports "Multiple data apps" or "Could not auto-resolve a datasource",
|
|
7784
|
+
- If \`generate_widget\` reports "Multiple data apps", pass \`embedId\` or \`dataAppName\`. If it reports "Could not auto-resolve a datasource", ASK the user which datasource to use by NAME (the error lists available options) and retry with \`integrationName: "<name>"\`. Never ask the user for \`integrationId\` or any UUID \u2014 \`integrationName\` alone is enough.
|
|
7697
7785
|
|
|
7698
7786
|
**Advanced \u2014 write SQL directly** (technical users only; ONLY if the user explicitly asks to write SQL):
|
|
7699
7787
|
- ASK for: \`name\`, \`query\` (SQL), \`chartOptions\`, \`companyIntegrationId\` (use \`list_datasources\`).
|