@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 +23 -2
- package/dist/apps/server.js +3 -3
- package/dist/apps/stdio.js +4 -4
- package/dist/{chunk-4DYPD273.js → chunk-DYFJ5GKQ.js} +3 -3
- package/dist/{chunk-PRE4CGOV.js → chunk-ETXH5HTT.js} +56 -24
- package/dist/chunk-ETXH5HTT.js.map +1 -0
- package/dist/{chunk-5IQ7MVUH.js → chunk-JSYG7ULR.js} +653 -165
- package/dist/chunk-JSYG7ULR.js.map +1 -0
- package/dist/{chunk-V64ZQINV.js → chunk-VSSGMPSL.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/{main-Q6XT4O46.js → main-AZ26BAEM.js} +9 -3
- package/dist/{main-Q6XT4O46.js.map → main-AZ26BAEM.js.map} +1 -1
- package/package.json +2 -1
- package/dist/chunk-5IQ7MVUH.js.map +0 -1
- package/dist/chunk-PRE4CGOV.js.map +0 -1
- /package/dist/{chunk-4DYPD273.js.map → chunk-DYFJ5GKQ.js.map} +0 -0
- /package/dist/{chunk-V64ZQINV.js.map → chunk-VSSGMPSL.js.map} +0 -0
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
|
-
|
|
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> (
|
|
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>
|
package/dist/apps/server.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
createChartAppServer
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-DYFJ5GKQ.js";
|
|
5
|
+
import "../chunk-JSYG7ULR.js";
|
|
6
|
+
import "../chunk-ETXH5HTT.js";
|
|
7
7
|
export {
|
|
8
8
|
createChartAppServer
|
|
9
9
|
};
|
package/dist/apps/stdio.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
loadConfig
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-VSSGMPSL.js";
|
|
5
5
|
import {
|
|
6
6
|
createChartAppServer
|
|
7
|
-
} from "../chunk-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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) =>
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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-
|
|
1935
|
+
//# sourceMappingURL=chunk-ETXH5HTT.js.map
|