@databrainhq/mcp-server 0.2.5 → 0.2.7
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 +210 -20
- 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,26 +4481,39 @@ 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
|
-
dataAppName: z25.string().describe("Data app name (required to mint guest token)"),
|
|
4488
|
-
integrationId: z25.string().describe("Datasource ID to generate the widget against. Use `list_datasources` to find."),
|
|
4489
|
-
integrationName: z25.string().describe("Datasource name (matches integrationId)"),
|
|
4490
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.'),
|
|
4491
|
-
embedId: z25.string().optional().describe("Optional \u2014
|
|
4488
|
+
embedId: z25.string().optional().describe("Optional. When provided, data app and datasource are inherited from the embed's existing widgets \u2014 the preferred path for CS/CX flows."),
|
|
4489
|
+
dataAppName: z25.string().optional().describe("Optional override. Auto-resolved from embedId, or from the sole data app when there is only one."),
|
|
4490
|
+
integrationId: z25.string().optional().describe("Technical override \u2014 the datasource UUID. Most flows should leave this blank and rely on auto-resolution from `embedId`. If you must override, prefer `integrationName` (business-user friendly)."),
|
|
4491
|
+
integrationName: z25.string().optional().describe('Datasource display name (e.g. "Snowflake", "demo datasource"). Use this when the user picks a datasource by name \u2014 the id is looked up automatically. Leave blank if `embedId` is provided (auto-resolved).')
|
|
4492
4492
|
},
|
|
4493
4493
|
READ_ONLY,
|
|
4494
4494
|
async (params) => {
|
|
4495
4495
|
try {
|
|
4496
|
+
const dataAppName = await resolveDataAppName(
|
|
4497
|
+
services,
|
|
4498
|
+
params.dataAppName,
|
|
4499
|
+
params.embedId
|
|
4500
|
+
);
|
|
4501
|
+
if (!dataAppName.ok) return toolError(dataAppName.error);
|
|
4496
4502
|
const guestToken = await services.widgetService.mintGuestToken(
|
|
4497
4503
|
params.clientId,
|
|
4498
|
-
|
|
4504
|
+
dataAppName.value,
|
|
4499
4505
|
params.embedId ? [params.embedId] : void 0
|
|
4500
4506
|
);
|
|
4501
|
-
const
|
|
4507
|
+
const datasource = await resolveDatasource(services, guestToken, {
|
|
4508
|
+
embedId: params.embedId,
|
|
4509
|
+
clientId: params.clientId,
|
|
4502
4510
|
integrationId: params.integrationId,
|
|
4503
|
-
integrationName: params.integrationName
|
|
4511
|
+
integrationName: params.integrationName
|
|
4512
|
+
});
|
|
4513
|
+
if (!datasource.ok) return toolError(datasource.error);
|
|
4514
|
+
const result = await services.widgetsApi.generateMetric(guestToken, {
|
|
4515
|
+
integrationId: datasource.value.integrationId,
|
|
4516
|
+
integrationName: datasource.value.integrationName,
|
|
4504
4517
|
userInputs: params.userInputs
|
|
4505
4518
|
});
|
|
4506
4519
|
const resultRec = result;
|
|
@@ -4512,6 +4525,10 @@ function registerGenerateWidget(server, client) {
|
|
|
4512
4525
|
return toolSuccess(
|
|
4513
4526
|
JSON.stringify({
|
|
4514
4527
|
message: "Draft generated. Review the `query` and `result` fields below, then pass to `add_widget` (using raw `body` passthrough) to persist.",
|
|
4528
|
+
resolved: {
|
|
4529
|
+
dataAppName: dataAppName.value,
|
|
4530
|
+
integrationName: datasource.value.integrationName
|
|
4531
|
+
},
|
|
4515
4532
|
draft: {
|
|
4516
4533
|
query: resultRec.query ?? null,
|
|
4517
4534
|
result: resultRec.result ?? null,
|
|
@@ -4528,6 +4545,177 @@ function registerGenerateWidget(server, client) {
|
|
|
4528
4545
|
}
|
|
4529
4546
|
);
|
|
4530
4547
|
}
|
|
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
|
+
}
|
|
4531
4719
|
|
|
4532
4720
|
// src/tools/registry.ts
|
|
4533
4721
|
function registerAllTools(server, client) {
|
|
@@ -7581,21 +7769,23 @@ STOP and present the list: \`{metricId, name, isPublished, createdByUser}\` per
|
|
|
7581
7769
|
ASK: "What would you like to change? (add / modify / remove)" \u2014 or more specifically, "Which widgets should I add, update, or remove?"
|
|
7582
7770
|
|
|
7583
7771
|
**Step 3a \u2014 ADD a widget**
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
1. **Clone from template** (cheapest, most common):
|
|
7587
|
-
- ASK for the source \`templateMetricId\` \u2014 or call \`list_widgets\` on the template embed / another customer's embed to help pick.
|
|
7588
|
-
- Call \`add_widget({ embedId, clientId, templateMetricId, name? })\`. The tool inherits query, chartOptions, and datasource from the template.
|
|
7772
|
+
This is a business-user flow. NEVER ask the user for a datasource, \`companyIntegrationId\`, \`integrationId\`, or raw SQL unless they explicitly say they want to write SQL. Prefer the two paths below in order.
|
|
7589
7773
|
|
|
7590
|
-
|
|
7591
|
-
- ASK
|
|
7592
|
-
- Call \`add_widget({ embedId, clientId,
|
|
7774
|
+
1. **Clone an existing chart** (default \u2014 try this first):
|
|
7775
|
+
- ASK: "Which existing chart should I copy from?" Call \`list_widgets\` on this embed, or on the template embed / another customer's embed, and present the widgets by name. Let the user pick by name.
|
|
7776
|
+
- Call \`add_widget({ embedId, clientId, templateMetricId, name? })\` using the picked widget's \`metricId\`. The tool inherits the query, chart config, and datasource automatically.
|
|
7593
7777
|
|
|
7594
|
-
|
|
7595
|
-
- ASK
|
|
7596
|
-
- Call \`generate_widget({
|
|
7778
|
+
2. **Describe the chart in plain English** (AI generate \u2014 use if no suitable template exists):
|
|
7779
|
+
- ASK: "Describe the chart you want" (e.g., "calls per day over last 30 days").
|
|
7780
|
+
- Call \`generate_widget({ embedId, clientId, userInputs: { prompt: "..." } })\` \u2014 data app and datasource auto-resolve from the embed's existing widgets. Do NOT ask the user for a datasource or data app.
|
|
7781
|
+
- \`embedId\` is optional on \`generate_widget\` (the tool will fall back to the sole data app / sole datasource if absent), but pass it when you have one \u2014 resolution is more reliable.
|
|
7597
7782
|
- Review the returned \`draft\` with the user. If they approve, call \`add_widget({ embedId, clientId, body: <draft + externalDashboardIds> })\` to persist.
|
|
7598
7783
|
- If they want changes, re-run \`generate_widget\` with a refined prompt.
|
|
7784
|
+
- If \`generate_widget\` reports "Multiple data apps", pass \`embedId\` or \`dataAppName\`. If it reports "Could not auto-resolve a datasource", ASK the user which datasource to use by NAME (the error lists available options) and retry with \`integrationName: "<name>"\`. Never ask the user for \`integrationId\` or any UUID \u2014 \`integrationName\` alone is enough.
|
|
7785
|
+
|
|
7786
|
+
**Advanced \u2014 write SQL directly** (technical users only; ONLY if the user explicitly asks to write SQL):
|
|
7787
|
+
- ASK for: \`name\`, \`query\` (SQL), \`chartOptions\`, \`companyIntegrationId\` (use \`list_datasources\`).
|
|
7788
|
+
- Call \`add_widget({ embedId, clientId, name, query, chartOptions, companyIntegrationId, ... })\`.
|
|
7599
7789
|
|
|
7600
7790
|
Show the user the new widget's \`externalMetricId\` and \`metricId\` after each add.
|
|
7601
7791
|
|