@databrainhq/mcp-server 0.2.5 → 0.2.6
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 +121 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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 the caller can review and pass to `add_widget`. Data app and datasource auto-resolve from `embedId` (preferred) or from the sole workspace defaults. `dataAppName`/`integrationId`/`integrationName` are technical overrides.",
|
|
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("Optional datasource override (technical users only). Auto-resolved from the embed's widgets, or from the sole datasource when there is only one."),
|
|
4491
|
+
integrationName: z25.string().optional().describe("Optional datasource name override. Auto-resolved alongside integrationId.")
|
|
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,89 @@ function registerGenerateWidget(server, client) {
|
|
|
4528
4545
|
}
|
|
4529
4546
|
);
|
|
4530
4547
|
}
|
|
4548
|
+
async function resolveDataAppName(services, provided, embedId) {
|
|
4549
|
+
if (provided) return { ok: true, value: provided };
|
|
4550
|
+
if (embedId) {
|
|
4551
|
+
const ctx = await services.widgetService.resolveEmbedContext(embedId);
|
|
4552
|
+
if (ctx.dataAppName) return { ok: true, value: ctx.dataAppName };
|
|
4553
|
+
}
|
|
4554
|
+
const apps = await services.dataAppsApi.listDataApps();
|
|
4555
|
+
const normalized = apps.map((a) => services.discoveryService.normalizeDataApp(a)).filter((a) => a.name && a.name !== "Unnamed Data App");
|
|
4556
|
+
if (normalized.length === 1) return { ok: true, value: normalized[0].name };
|
|
4557
|
+
if (normalized.length === 0) {
|
|
4558
|
+
return {
|
|
4559
|
+
ok: false,
|
|
4560
|
+
error: "No data apps found in this workspace. Create one in the Databrain UI first."
|
|
4561
|
+
};
|
|
4562
|
+
}
|
|
4563
|
+
return {
|
|
4564
|
+
ok: false,
|
|
4565
|
+
error: `Multiple data apps exist (${normalized.map((a) => `"${a.name}"`).join(", ")}). Pass \`dataAppName\` explicitly, or pass \`embedId\` to auto-resolve.`
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
async function resolveDatasource(services, guestToken, params) {
|
|
4569
|
+
if (params.integrationId && params.integrationName) {
|
|
4570
|
+
return {
|
|
4571
|
+
ok: true,
|
|
4572
|
+
value: {
|
|
4573
|
+
integrationId: params.integrationId,
|
|
4574
|
+
integrationName: params.integrationName
|
|
4575
|
+
}
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
if (params.embedId) {
|
|
4579
|
+
const fromEmbed = await resolveDatasourceFromEmbed(
|
|
4580
|
+
services,
|
|
4581
|
+
guestToken,
|
|
4582
|
+
params.embedId,
|
|
4583
|
+
params.clientId
|
|
4584
|
+
);
|
|
4585
|
+
if (fromEmbed) {
|
|
4586
|
+
return {
|
|
4587
|
+
ok: true,
|
|
4588
|
+
value: {
|
|
4589
|
+
integrationId: params.integrationId || fromEmbed.integrationId,
|
|
4590
|
+
integrationName: params.integrationName || fromEmbed.integrationName
|
|
4591
|
+
}
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
const datasources = await services.datasourcesApi.listDatasources();
|
|
4596
|
+
const named = datasources.filter(
|
|
4597
|
+
(ds) => typeof ds.name === "string" && ds.name.trim().length > 0
|
|
4598
|
+
);
|
|
4599
|
+
if (named.length === 1) {
|
|
4600
|
+
const ds = named[0];
|
|
4601
|
+
const id = ds._id || ds.id || "";
|
|
4602
|
+
const name = ds.name;
|
|
4603
|
+
if (id && name) {
|
|
4604
|
+
return { ok: true, value: { integrationId: id, integrationName: name } };
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
if (named.length === 0) {
|
|
4608
|
+
return {
|
|
4609
|
+
ok: false,
|
|
4610
|
+
error: "No datasources found in this workspace. Connect one in the Databrain UI first (technical setup)."
|
|
4611
|
+
};
|
|
4612
|
+
}
|
|
4613
|
+
return {
|
|
4614
|
+
ok: false,
|
|
4615
|
+
error: `Could not auto-resolve a datasource. Options: ${named.map((d) => `"${d.name}"`).join(", ")}. Either pass \`embedId\` (to inherit from the embed's existing widgets), or pass \`integrationId\` + \`integrationName\` explicitly.`
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
async function resolveDatasourceFromEmbed(services, guestToken, embedId, clientId) {
|
|
4619
|
+
const widgets = await services.widgetsApi.listWidgets(embedId, clientId);
|
|
4620
|
+
const first = widgets.find((w) => typeof w.metricId === "string" && w.metricId.length > 0);
|
|
4621
|
+
if (!first || !first.metricId) return null;
|
|
4622
|
+
const response = await services.widgetsApi.fetchEmbeddedMetric(guestToken, first.metricId);
|
|
4623
|
+
const metric = response.externalMetric;
|
|
4624
|
+
if (!metric || typeof metric !== "object") return null;
|
|
4625
|
+
const rec = metric;
|
|
4626
|
+
const integrationId = typeof rec.companyIntegrationId === "string" ? rec.companyIntegrationId : null;
|
|
4627
|
+
const integrationName = typeof rec.integrationName === "string" ? rec.integrationName : null;
|
|
4628
|
+
if (!integrationId || !integrationName) return null;
|
|
4629
|
+
return { integrationId, integrationName };
|
|
4630
|
+
}
|
|
4531
4631
|
|
|
4532
4632
|
// src/tools/registry.ts
|
|
4533
4633
|
function registerAllTools(server, client) {
|
|
@@ -7581,21 +7681,23 @@ STOP and present the list: \`{metricId, name, isPublished, createdByUser}\` per
|
|
|
7581
7681
|
ASK: "What would you like to change? (add / modify / remove)" \u2014 or more specifically, "Which widgets should I add, update, or remove?"
|
|
7582
7682
|
|
|
7583
7683
|
**Step 3a \u2014 ADD a widget**
|
|
7584
|
-
|
|
7684
|
+
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.
|
|
7585
7685
|
|
|
7586
|
-
1. **Clone
|
|
7587
|
-
- ASK
|
|
7588
|
-
- Call \`add_widget({ embedId, clientId, templateMetricId, name? })\`. The tool inherits query,
|
|
7686
|
+
1. **Clone an existing chart** (default \u2014 try this first):
|
|
7687
|
+
- 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.
|
|
7688
|
+
- Call \`add_widget({ embedId, clientId, templateMetricId, name? })\` using the picked widget's \`metricId\`. The tool inherits the query, chart config, and datasource automatically.
|
|
7589
7689
|
|
|
7590
|
-
2. **
|
|
7591
|
-
- ASK
|
|
7592
|
-
- Call \`
|
|
7593
|
-
|
|
7594
|
-
3. **AI generate** (NL prompt):
|
|
7595
|
-
- ASK for a prompt describing the widget (e.g., "calls per day over last 30 days") and the \`integrationId\` / \`integrationName\` to query against.
|
|
7596
|
-
- Call \`generate_widget({ clientId, dataAppName, integrationId, integrationName, userInputs: { prompt: "..." } })\`.
|
|
7690
|
+
2. **Describe the chart in plain English** (AI generate \u2014 use if no suitable template exists):
|
|
7691
|
+
- ASK: "Describe the chart you want" (e.g., "calls per day over last 30 days").
|
|
7692
|
+
- 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.
|
|
7693
|
+
- \`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
7694
|
- Review the returned \`draft\` with the user. If they approve, call \`add_widget({ embedId, clientId, body: <draft + externalDashboardIds> })\` to persist.
|
|
7598
7695
|
- 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", either pass \`embedId\` or ask the user which data app/chart to mirror \u2014 still avoid technical datasource IDs.
|
|
7697
|
+
|
|
7698
|
+
**Advanced \u2014 write SQL directly** (technical users only; ONLY if the user explicitly asks to write SQL):
|
|
7699
|
+
- ASK for: \`name\`, \`query\` (SQL), \`chartOptions\`, \`companyIntegrationId\` (use \`list_datasources\`).
|
|
7700
|
+
- Call \`add_widget({ embedId, clientId, name, query, chartOptions, companyIntegrationId, ... })\`.
|
|
7599
7701
|
|
|
7600
7702
|
Show the user the new widget's \`externalMetricId\` and \`metricId\` after each add.
|
|
7601
7703
|
|