@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 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 (query, metadata) the caller can review and pass to `add_widget`. Mints a guest token internally.",
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 scopes the guest token to this embed.")
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
- params.dataAppName,
4504
+ dataAppName.value,
4499
4505
  params.embedId ? [params.embedId] : void 0
4500
4506
  );
4501
- const result = await services.widgetsApi.generateMetric(guestToken, {
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
- If the user wants to add a widget, STOP and ASK which sourcing mode:
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
- 2. **Fresh spec** (SQL + chart config):
7591
- - ASK for: \`name\`, \`query\` (SQL), \`chartOptions\` (chart type + config), \`companyIntegrationId\` (datasource ID \u2014 use \`list_datasources\` if needed).
7592
- - Call \`add_widget({ embedId, clientId, name, query, chartOptions, companyIntegrationId, ... })\`.
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
- 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: "..." } })\`.
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