@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 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 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.",
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("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.")
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
- const fromEmbed = await resolveDatasourceFromEmbed(
4589
+ embedProbe = await probeDatasourceFromEmbed(
4580
4590
  services,
4581
4591
  guestToken,
4582
4592
  params.embedId,
4583
4593
  params.clientId
4584
4594
  );
4585
- if (fromEmbed) {
4595
+ if (embedProbe.kind === "ok") {
4586
4596
  return {
4587
4597
  ok: true,
4588
4598
  value: {
4589
- integrationId: params.integrationId || fromEmbed.integrationId,
4590
- integrationName: params.integrationName || fromEmbed.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. Options: ${named.map((d) => `"${d.name}"`).join(", ")}. Either pass \`embedId\` (to inherit from the embed's existing widgets), or pass \`integrationId\` + \`integrationName\` explicitly.`
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 resolveDatasourceFromEmbed(services, guestToken, embedId, clientId) {
4675
+ async function probeDatasourceFromEmbed(services, guestToken, embedId, clientId) {
4619
4676
  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 };
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", either pass \`embedId\` or ask the user which data app/chart to mirror \u2014 still avoid technical datasource IDs.
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\`).