@databrainhq/mcp-server 0.3.0-rc.5 → 0.3.0-rc.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/README.md CHANGED
@@ -319,7 +319,7 @@ Prompts are guided workflows your AI assistant can follow. Ask for them by name
319
319
 
320
320
  ## Tools Reference
321
321
 
322
- 44 tools organized by category (plus 6 chart App tools on the opt-in `databrain-mcp-chart-apps` entry). Your AI assistant picks the right tools automatically. You do not need to call them directly.
322
+ 50 tools organized by category (plus 6 chart App tools on the opt-in `databrain-mcp-chart-apps` entry). Your AI assistant picks the right tools automatically. You do not need to call them directly.
323
323
 
324
324
  > **Note:** Datasource and workspace creation happen outside MCP. Datamarts can be planned with `create_datamart` in `dry_run` mode and created only with `confirm: "APPLY_TO_PRODUCTION"`. The semantic layer is managed via MCP tools (`get_semantic_layer`, `update_semantic_layer`, `start_semantic_layer_generation`, `get_semantic_layer_generation_status`).
325
325
 
@@ -372,7 +372,7 @@ Prompts are guided workflows your AI assistant can follow. Ask for them by name
372
372
  </details>
373
373
 
374
374
  <details>
375
- <summary><b>Query & Semantic Layer</b> (5) — Ask questions and manage data documentation</summary>
375
+ <summary><b>Query & Semantic Layer</b> (11): Ask questions and manage data documentation</summary>
376
376
 
377
377
  | Tool | Description |
378
378
  |------|-------------|
@@ -381,8 +381,29 @@ Prompts are guided workflows your AI assistant can follow. Ask for them by name
381
381
  | `update_semantic_layer` | Improve semantic layer quality (add descriptions, synonyms, example questions) |
382
382
  | `start_semantic_layer_generation` | Start semantic-layer generation for a datamart |
383
383
  | `get_semantic_layer_generation_status` | Check semantic-layer generation progress |
384
+ | `search_semantic_model` | Find relevant v2 entities and their related entities |
385
+ | `get_semantic_entity` | Read one v2 entity by kind and name with the current content hash |
386
+ | `propose_semantic_change` | Validate candidate YAML or a patch and return its plan without writing |
387
+ | `apply_semantic_change` | Preview by default, or apply live with a reviewed hash and production confirmation |
388
+ | `eval_semantic_model` | Run saved queries and optional questions and return pass rate and verdicts |
389
+ | `semantic_readiness` | Read the server readiness number, verdict and open checks |
384
390
 
385
391
  Semantic-layer generation uses the datamart ID returned by `list_datamarts` and calls the service-token API. Pass `datamartId` directly when the ID is already known.
392
+
393
+ For a v2 model, the six model tools use `datamartId` and the configured service credential. They never request a token from the user or mint a guest token. Hosted eval also requires `mcp:query`; hosted apply requires `mcp:datamarts:write` even for its dry run.
394
+
395
+ 1. Use `search_semantic_model` with `question` and optional `k` (default 10, maximum 100). If the server has no retrieval route, the tool pulls the model and ranks name, alias, synonyms and description, then includes adjacent entities. Hidden tables and columns are excluded from local search.
396
+ 2. Use `get_semantic_entity` with `kind` and an exact `name`. Column names use `table.column`. Kinds are `table`, `column`, `measure`, `metric`, `join`, `entity`, `glossary`, `saved_query`, `instruction` and `access_rule`.
397
+ 3. Use `propose_semantic_change` with exactly one of `source` (complete YAML), `yaml_patch`, or `change`. It returns the candidate source, validation errors, the server plan and `expected_content_hash`. Invalid source returns errors and no plan.
398
+ 4. Use `eval_semantic_model` with the proposed `source` and optional `questions`. Omitting source evaluates the stored model. The server returns pass rate and individual verdicts. Use `semantic_readiness` to read the server score and open checks.
399
+ 5. Review the plan. Call `apply_semantic_change` with the exact returned `source` and `expected_content_hash`. Its `dry_run` defaults to true. A live apply requires `dry_run: false` and user supplied `confirm: "APPLY_TO_PRODUCTION"`. The assistant must never supply confirmation on its own. Explicit `expected_content_hash: null` selects the first apply only when no layer is stored. Apply changes the live model for all consumers.
400
+
401
+ A YAML patch merges mappings and named lists by name, including tables and columns. Other lists replace the original list. An empty list clears a list. Null removes a field. For example, `yaml_patch: "tables:\n - name: orders\n description: Customer purchases\n"` changes that description and preserves other tables and fields. Patch output normalizes YAML formatting and does not preserve comments. Review the returned source.
402
+
403
+ A structured change uses `{ "operation": "upsert", "kind": "column", "table": "orders", "name": "amount", "value": { "description": "Order total" } }`. Upsert merges fields. Use `operation: "remove"` without `value` to remove one named entity. The backend validates all resulting references.
404
+
405
+ HTTP 409 conflicts and 422 validation or projection refusals return tool errors with the backend reason and details. A conflict requires a fresh proposal and review. Plan, eval and readiness need the corresponding backend routes; these tools return backend errors if those routes are absent.
406
+
386
407
  </details>
387
408
 
388
409
  <details>
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createChartAppServer
4
- } from "../chunk-4DYPD273.js";
5
- import "../chunk-5IQ7MVUH.js";
6
- import "../chunk-PRE4CGOV.js";
4
+ } from "../chunk-DYFJ5GKQ.js";
5
+ import "../chunk-JSYG7ULR.js";
6
+ import "../chunk-ETXH5HTT.js";
7
7
  export {
8
8
  createChartAppServer
9
9
  };
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadConfig
4
- } from "../chunk-V64ZQINV.js";
4
+ } from "../chunk-VSSGMPSL.js";
5
5
  import {
6
6
  createChartAppServer
7
- } from "../chunk-4DYPD273.js";
8
- import "../chunk-5IQ7MVUH.js";
9
- import "../chunk-PRE4CGOV.js";
7
+ } from "../chunk-DYFJ5GKQ.js";
8
+ import "../chunk-JSYG7ULR.js";
9
+ import "../chunk-ETXH5HTT.js";
10
10
 
11
11
  // src/apps/stdio.ts
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -22,13 +22,13 @@ import {
22
22
  saveDraftAsDashboard,
23
23
  viewSchema,
24
24
  workspacesForDatamart
25
- } from "./chunk-5IQ7MVUH.js";
25
+ } from "./chunk-JSYG7ULR.js";
26
26
  import {
27
27
  DatabrainApiError,
28
28
  DatabrainClient,
29
29
  MCP_SERVER_VERSION,
30
30
  toolContext
31
- } from "./chunk-PRE4CGOV.js";
31
+ } from "./chunk-ETXH5HTT.js";
32
32
 
33
33
  // src/apps/server.ts
34
34
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -513,4 +513,4 @@ function createChartAppServer(config, backend) {
513
513
  export {
514
514
  createChartAppServer
515
515
  };
516
- //# sourceMappingURL=chunk-4DYPD273.js.map
516
+ //# sourceMappingURL=chunk-DYFJ5GKQ.js.map
@@ -2,18 +2,20 @@
2
2
 
3
3
  // src/utils/errors.ts
4
4
  var DatabrainApiError = class extends Error {
5
- constructor(status, code, message, details) {
5
+ constructor(status, code, message, details, serverHint, retryable) {
6
6
  super(message);
7
7
  this.status = status;
8
8
  this.code = code;
9
9
  this.details = details;
10
+ this.serverHint = serverHint;
11
+ this.retryable = retryable;
10
12
  this.name = "DatabrainApiError";
11
13
  }
12
14
  toToolResponse() {
13
15
  const parts = [`Error: ${this.message}`];
14
16
  const detailDump = this.formatDetails();
15
17
  if (detailDump) parts.push(detailDump);
16
- const hint = this.hint();
18
+ const hint = this.serverHint ? `Hint: ${this.serverHint}` : this.hint();
17
19
  if (hint) parts.push(hint);
18
20
  return {
19
21
  content: [{ type: "text", text: parts.join("\n\n") }],
@@ -29,7 +31,7 @@ var DatabrainApiError = class extends Error {
29
31
  formatDetails() {
30
32
  if (this.status < 400 || this.status >= 500) return "";
31
33
  if (!this.details || typeof this.details !== "object") return "";
32
- const skipKeys = /* @__PURE__ */ new Set(["message", "detail", "error", "code"]);
34
+ const skipKeys = /* @__PURE__ */ new Set(["message", "detail", "error", "code", "data", "hint", "retryable"]);
33
35
  const extra = {};
34
36
  for (const [k, v] of Object.entries(this.details)) {
35
37
  if (skipKeys.has(k)) continue;
@@ -44,6 +46,9 @@ var DatabrainApiError = class extends Error {
44
46
  }
45
47
  }
46
48
  hint() {
49
+ if (this.code === "UNAUTHORIZED_ORIGIN") {
50
+ return "Hint: The embed origin is not on the allowed domains list. Add the domain to the allowed domains in the Databrain settings; the token itself is fine.";
51
+ }
47
52
  if (this.status === 401 || this.status === 403) {
48
53
  if (this.code === "OAUTH_API_ERROR") {
49
54
  return "Hint: Check that the backend accepts the configured OAuth issuer and resource, and that the service principal has permission for this operation. Static fallback applies only to token acquisition failures.";
@@ -123,7 +128,7 @@ var logger = {
123
128
  // package.json
124
129
  var package_default = {
125
130
  name: "@databrainhq/mcp-server",
126
- version: "0.3.0-rc.5",
131
+ version: "0.3.0-rc.7",
127
132
  description: "MCP server for Databrain embedded analytics - connect databases, create datamarts, configure embeds, and generate frontend code",
128
133
  type: "module",
129
134
  bin: {
@@ -163,6 +168,7 @@ var package_default = {
163
168
  esbuild: "0.27.3",
164
169
  fflate: "0.8.3",
165
170
  "posthog-node": "^5.21.2",
171
+ yaml: "^2.9.1",
166
172
  zod: "^3.24.0"
167
173
  },
168
174
  devDependencies: {
@@ -214,6 +220,29 @@ var TtlCache = class {
214
220
  }
215
221
  };
216
222
 
223
+ // src/client/error-body.ts
224
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
225
+ var str = (v) => typeof v === "string" && v.trim() ? v : void 0;
226
+ var fromEnvelope = (v) => {
227
+ if (!isRecord(v)) return void 0;
228
+ const out = {
229
+ code: str(v.code),
230
+ message: str(v.message) ?? str(v.detail),
231
+ hint: str(v.hint),
232
+ retryable: typeof v.retryable === "boolean" ? v.retryable : void 0,
233
+ details: v.details
234
+ };
235
+ return out.code || out.message || out.hint ? out : void 0;
236
+ };
237
+ function parseErrorBody(body) {
238
+ if (!isRecord(body)) return {};
239
+ const nested = fromEnvelope(isRecord(body.data) ? body.data.error : void 0) ?? fromEnvelope(body.error) ?? fromEnvelope(body);
240
+ if (nested) {
241
+ return { ...nested, message: nested.message ?? str(body.error) };
242
+ }
243
+ return { message: str(body.error) };
244
+ }
245
+
217
246
  // src/utils/tool-context.ts
218
247
  import { AsyncLocalStorage } from "async_hooks";
219
248
  var toolContext = new AsyncLocalStorage();
@@ -531,14 +560,17 @@ var DatabrainClient = class {
531
560
  parsed = JSON.parse(errorBody);
532
561
  } catch {
533
562
  }
534
- const errorMsg = parsed.message || parsed.detail || (typeof parsed.error === "string" ? parsed.error : JSON.stringify(parsed.error)) || `API request failed with status ${response.status}`;
563
+ const envelope = parseErrorBody(parsed);
564
+ const errorMsg = envelope.message || `API request failed with status ${response.status}`;
535
565
  const error = new DatabrainApiError(
536
566
  response.status,
537
- parsed.code || `HTTP_${response.status}`,
567
+ envelope.code || `HTTP_${response.status}`,
538
568
  errorMsg,
539
- parsed
569
+ parsed,
570
+ envelope.hint,
571
+ envelope.retryable
540
572
  );
541
- const isTransient = response.status === 429 || response.status >= 500 && !NON_TRANSIENT_PATTERNS.some((p) => errorMsg.includes(p));
573
+ const isTransient = envelope.retryable !== void 0 ? envelope.retryable && (response.status === 429 || response.status >= 500) : response.status === 429 || response.status >= 500 && !NON_TRANSIENT_PATTERNS.some((p) => errorMsg.includes(p));
542
574
  if (isTransient && attempt < MAX_RETRIES) {
543
575
  const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 8e3);
544
576
  logger.warn(`Retrying in ${delay}ms (attempt ${attempt}/${MAX_RETRIES})`, {
@@ -669,7 +701,7 @@ var V2_OPTION_EXTRA_KEYS = {
669
701
  var TRANSLATION_KEYS = ["name", "description", "slots", "options"];
670
702
  var SLOT_TRANSLATION_KEYS = ["label", "description"];
671
703
  var OPTION_TRANSLATION_KEYS = ["label", "group", "choices"];
672
- var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
704
+ var isRecord2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
673
705
  var isId = (value) => typeof value === "string" && ID_PATTERN.test(value) && !FORBIDDEN_IDS.includes(value);
674
706
  var isText = (value, max = MAX_TEXT) => typeof value === "string" && value.length > 0 && value.length <= max;
675
707
  var isTranslationText = (value) => typeof value === "string" && value.length <= MAX_TEXT;
@@ -704,7 +736,7 @@ var Collector = class {
704
736
  }
705
737
  };
706
738
  function validateSlot(c, value, path, v2) {
707
- if (!isRecord(value)) {
739
+ if (!isRecord2(value)) {
708
740
  c.add("manifest.slot.invalid", path, "A slot must be an object.");
709
741
  return;
710
742
  }
@@ -734,7 +766,7 @@ function validateSlot(c, value, path, v2) {
734
766
  }
735
767
  }
736
768
  function validateOption(c, value, path, v2) {
737
- if (!isRecord(value)) {
769
+ if (!isRecord2(value)) {
738
770
  c.add("manifest.option.invalid", path, "An option must be an object.");
739
771
  return;
740
772
  }
@@ -799,7 +831,7 @@ function validateOption(c, value, path, v2) {
799
831
  case "select": {
800
832
  const choices = value.choices;
801
833
  const choicesOk = Array.isArray(choices) && choices.length > 0 && choices.length <= MAX_CHOICES && choices.every(
802
- (choice) => isRecord(choice) && Object.keys(choice).length === 2 && isId(choice.value) && isText(choice.label)
834
+ (choice) => isRecord2(choice) && Object.keys(choice).length === 2 && isId(choice.value) && isText(choice.label)
803
835
  ) && new Set(choices.map((choice) => choice.value)).size === choices.length;
804
836
  if (!choicesOk) {
805
837
  c.add("manifest.option.choices", `${path}/choices`, `choices is a list of 1 to ${MAX_CHOICES} distinct { value, label } pairs; a value follows the id rule.`);
@@ -821,7 +853,7 @@ function validateOption(c, value, path, v2) {
821
853
  }
822
854
  function validateTranslations(c, value, slots, options) {
823
855
  const path = "/translations";
824
- if (!isRecord(value)) {
856
+ if (!isRecord2(value)) {
825
857
  c.add("manifest.translations.invalid", path, "translations is an object keyed by language code.");
826
858
  return;
827
859
  }
@@ -831,7 +863,7 @@ function validateTranslations(c, value, slots, options) {
831
863
  c.add("manifest.translations.language", langPath, `"${language}" is not a language code like "fr" or "pt-BR".`);
832
864
  continue;
833
865
  }
834
- if (!isRecord(entry)) {
866
+ if (!isRecord2(entry)) {
835
867
  c.add("manifest.translations.invalid", langPath, "A translation is an object with optional name, description, slots and options.");
836
868
  continue;
837
869
  }
@@ -843,7 +875,7 @@ function validateTranslations(c, value, slots, options) {
843
875
  }
844
876
  if (Object.hasOwn(entry, "slots")) {
845
877
  const entrySlots = entry.slots;
846
- if (!isRecord(entrySlots)) {
878
+ if (!isRecord2(entrySlots)) {
847
879
  c.add("manifest.translations.invalid", `${langPath}/slots`, "slots is an object keyed by slot id.");
848
880
  } else {
849
881
  for (const [slotId, slotEntry] of Object.entries(entrySlots)) {
@@ -852,7 +884,7 @@ function validateTranslations(c, value, slots, options) {
852
884
  c.add("manifest.translations.slot", slotPath, `"${slotId}" is not a slot id in this manifest.`);
853
885
  continue;
854
886
  }
855
- if (!isRecord(slotEntry)) {
887
+ if (!isRecord2(slotEntry)) {
856
888
  c.add("manifest.translations.invalid", slotPath, "A slot translation is an object with optional label and description.");
857
889
  continue;
858
890
  }
@@ -867,7 +899,7 @@ function validateTranslations(c, value, slots, options) {
867
899
  }
868
900
  if (Object.hasOwn(entry, "options")) {
869
901
  const entryOptions = entry.options;
870
- if (!isRecord(entryOptions)) {
902
+ if (!isRecord2(entryOptions)) {
871
903
  c.add("manifest.translations.invalid", `${langPath}/options`, "options is an object keyed by option id.");
872
904
  } else {
873
905
  for (const [optionId, optionEntry] of Object.entries(entryOptions)) {
@@ -877,7 +909,7 @@ function validateTranslations(c, value, slots, options) {
877
909
  c.add("manifest.translations.option", optionPath, `"${optionId}" is not an option id in this manifest.`);
878
910
  continue;
879
911
  }
880
- if (!isRecord(optionEntry)) {
912
+ if (!isRecord2(optionEntry)) {
881
913
  c.add("manifest.translations.invalid", optionPath, "An option translation is an object with optional label, group and choices.");
882
914
  continue;
883
915
  }
@@ -889,7 +921,7 @@ function validateTranslations(c, value, slots, options) {
889
921
  }
890
922
  if (!Object.hasOwn(optionEntry, "choices")) continue;
891
923
  const choices = optionEntry.choices;
892
- if (!isRecord(choices)) {
924
+ if (!isRecord2(choices)) {
893
925
  c.add("manifest.translations.invalid", `${optionPath}/choices`, "choices is an object keyed by choice value.");
894
926
  continue;
895
927
  }
@@ -908,7 +940,7 @@ function validateTranslations(c, value, slots, options) {
908
940
  }
909
941
  function validateManifest(value) {
910
942
  const c = new Collector();
911
- if (!isRecord(value)) {
943
+ if (!isRecord2(value)) {
912
944
  c.add("manifest.invalid", "", "manifest.json must be a JSON object.");
913
945
  return c.problems;
914
946
  }
@@ -944,7 +976,7 @@ function validateManifest(value) {
944
976
  slots.forEach((slot, index) => validateSlot(c, slot, `/slots/${index}`, v2));
945
977
  const seen = /* @__PURE__ */ new Set();
946
978
  slots.forEach((slot, index) => {
947
- if (!isRecord(slot) || typeof slot.id !== "string") return;
979
+ if (!isRecord2(slot) || typeof slot.id !== "string") return;
948
980
  if (seen.has(slot.id)) {
949
981
  c.add("manifest.slot.duplicate", `/slots/${index}/id`, `Slot id "${slot.id}" is used more than once.`);
950
982
  }
@@ -960,7 +992,7 @@ function validateManifest(value) {
960
992
  options.forEach((option, index) => validateOption(c, option, `/options/${index}`, v2));
961
993
  const seen = /* @__PURE__ */ new Set();
962
994
  options.forEach((option, index) => {
963
- if (!isRecord(option) || typeof option.id !== "string") return;
995
+ if (!isRecord2(option) || typeof option.id !== "string") return;
964
996
  if (seen.has(option.id)) {
965
997
  c.add("manifest.option.duplicate", `/options/${index}/id`, `Option id "${option.id}" is used more than once.`);
966
998
  }
@@ -1012,7 +1044,7 @@ var manifestSchema = z2.object({
1012
1044
  });
1013
1045
  function sortKeys(value) {
1014
1046
  if (Array.isArray(value)) return value.map((item) => sortKeys(item));
1015
- if (isRecord(value)) {
1047
+ if (isRecord2(value)) {
1016
1048
  const out = {};
1017
1049
  for (const key of Object.keys(value).sort()) out[key] = sortKeys(value[key]);
1018
1050
  return out;
@@ -1900,4 +1932,4 @@ export {
1900
1932
  apiProblems,
1901
1933
  planUpload
1902
1934
  };
1903
- //# sourceMappingURL=chunk-PRE4CGOV.js.map
1935
+ //# sourceMappingURL=chunk-ETXH5HTT.js.map