@databrainhq/mcp-server 0.3.0-rc.1 → 0.3.0-rc.10

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.
Files changed (45) hide show
  1. package/README.md +152 -7
  2. package/dist/apps/server.d.ts +6 -6
  3. package/dist/apps/server.js +4 -2
  4. package/dist/apps/stdio.js +5 -3
  5. package/dist/apps/stdio.js.map +1 -1
  6. package/dist/chunk-5IGP6A6T.js +496 -0
  7. package/dist/chunk-5IGP6A6T.js.map +1 -0
  8. package/dist/{chunk-RF3HTNO7.js → chunk-CUPL7CAN.js} +2 -2
  9. package/dist/{chunk-7DS7OLWP.js → chunk-ERTAKB2H.js} +1877 -1182
  10. package/dist/chunk-ERTAKB2H.js.map +1 -0
  11. package/dist/chunk-N7X762VU.js +34 -0
  12. package/dist/chunk-N7X762VU.js.map +1 -0
  13. package/dist/chunk-OCBTNAAO.js +1946 -0
  14. package/dist/chunk-OCBTNAAO.js.map +1 -0
  15. package/dist/{chunk-JCXUZ3QZ.js → chunk-Z3ZHN4UW.js} +34 -6
  16. package/dist/chunk-Z3ZHN4UW.js.map +1 -0
  17. package/dist/cli.d.ts +15 -0
  18. package/dist/cli.js +495 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/custom-chart/runtime-shim.js +146 -0
  21. package/dist/custom-chart/template/AGENTS.md +374 -0
  22. package/dist/custom-chart/template/README.md +374 -0
  23. package/dist/custom-chart/template/chart.css +28 -0
  24. package/dist/custom-chart/template/chart.ts +192 -0
  25. package/dist/custom-chart/template/custom-chart.d.ts +78 -0
  26. package/dist/custom-chart/template/icon.svg +1 -0
  27. package/dist/custom-chart/template/manifest.json +23 -0
  28. package/dist/custom-chart/template/package.json +14 -0
  29. package/dist/custom-chart/template/sample-data.json +18 -0
  30. package/dist/custom-chart/template/tsconfig.json +12 -0
  31. package/dist/index.js +14 -8
  32. package/dist/index.js.map +1 -1
  33. package/dist/main-KZRHWE3K.js +857 -0
  34. package/dist/main-KZRHWE3K.js.map +1 -0
  35. package/dist/{test-only-EJ5O4WN5.js → test-only-K7QTSVO4.js} +7 -6
  36. package/dist/test-only-K7QTSVO4.js.map +1 -0
  37. package/package.json +9 -4
  38. package/dist/chunk-7DS7OLWP.js.map +0 -1
  39. package/dist/chunk-JCXUZ3QZ.js.map +0 -1
  40. package/dist/chunk-NDVW3ZSJ.js +0 -310
  41. package/dist/chunk-NDVW3ZSJ.js.map +0 -1
  42. package/dist/main-7QNRH6X4.js +0 -438
  43. package/dist/main-7QNRH6X4.js.map +0 -1
  44. package/dist/test-only-EJ5O4WN5.js.map +0 -1
  45. /package/dist/{chunk-RF3HTNO7.js.map → chunk-CUPL7CAN.js.map} +0 -0
@@ -1,619 +1,68 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/utils/logger.ts
4
- var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
5
- var currentLevel = process.env.DATABRAIN_LOG_LEVEL || "warn";
6
- function log(level, message, data) {
7
- if (LOG_LEVELS[level] < LOG_LEVELS[currentLevel]) return;
8
- const line = `[databrain-mcp] [${level.toUpperCase()}] ${message}`;
9
- if (data !== void 0) {
10
- process.stderr.write(`${line} ${JSON.stringify(data)}
11
- `);
12
- } else {
13
- process.stderr.write(`${line}
14
- `);
15
- }
16
- }
17
- var logger = {
18
- debug: (msg, data) => log("debug", msg, data),
19
- info: (msg, data) => log("info", msg, data),
20
- warn: (msg, data) => log("warn", msg, data),
21
- error: (msg, data) => log("error", msg, data)
22
- };
23
-
24
- // package.json
25
- var package_default = {
26
- name: "@databrainhq/mcp-server",
27
- version: "0.3.0-rc.1",
28
- description: "MCP server for Databrain embedded analytics - connect databases, create datamarts, configure embeds, and generate frontend code",
29
- type: "module",
30
- bin: {
31
- "mcp-server": "dist/index.js",
32
- "databrain-mcp-server": "dist/index.js",
33
- "databrain-mcp-chart-apps": "dist/apps/stdio.js"
34
- },
35
- main: "dist/index.js",
36
- types: "dist/index.d.ts",
37
- files: [
38
- "dist",
39
- "skills",
40
- ".claude-plugin",
41
- ".mcp.json"
42
- ],
43
- scripts: {
44
- build: "tsup",
45
- dev: "tsx src/index.ts",
46
- test: "vitest run",
47
- "test:watch": "vitest",
48
- "test:live": "vitest run tests/integration/prompt-flows.test.ts",
49
- lint: "tsc --noEmit",
50
- prepublishOnly: "npm run build"
51
- },
52
- keywords: [
53
- "mcp",
54
- "databrain",
55
- "embedded-analytics",
56
- "ai",
57
- "model-context-protocol"
58
- ],
59
- license: "MIT",
60
- dependencies: {
61
- "@modelcontextprotocol/sdk": "^1.12.1",
62
- "@posthog/mcp": "^0.10.7",
63
- "posthog-node": "^5.21.2",
64
- zod: "^3.24.0"
65
- },
66
- devDependencies: {
67
- "@types/node": "^25.3.5",
68
- tsup: "^8.4.0",
69
- tsx: "^4.19.0",
70
- typescript: "^5.7.0",
71
- vitest: "^3.0.0"
72
- },
73
- publishConfig: {
74
- access: "public"
75
- },
76
- repository: {
77
- type: "git",
78
- url: "git+https://github.com/databrainhq/mcp-server.git"
79
- },
80
- engines: {
81
- node: ">=18.0.0"
82
- }
83
- };
84
-
85
- // src/version.ts
86
- var MCP_SERVER_VERSION = package_default.version;
87
-
88
- // src/utils/cache.ts
89
- var TtlCache = class {
90
- store = /* @__PURE__ */ new Map();
91
- get(key) {
92
- const entry = this.store.get(key);
93
- if (!entry) return void 0;
94
- if (Date.now() > entry.expiresAt) {
95
- this.store.delete(key);
96
- return void 0;
97
- }
98
- return entry.value;
99
- }
100
- set(key, value, ttlMs) {
101
- this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
102
- }
103
- invalidateByPrefix(prefix) {
104
- for (const key of this.store.keys()) {
105
- if (key.startsWith(prefix)) {
106
- this.store.delete(key);
107
- }
108
- }
109
- }
110
- clear() {
111
- this.store.clear();
112
- }
113
- };
114
-
115
- // src/utils/errors.ts
116
- var DatabrainApiError = class extends Error {
117
- constructor(status, code, message, details) {
118
- super(message);
119
- this.status = status;
120
- this.code = code;
121
- this.details = details;
122
- this.name = "DatabrainApiError";
123
- }
124
- toToolResponse() {
125
- const parts = [`Error: ${this.message}`];
126
- const detailDump = this.formatDetails();
127
- if (detailDump) parts.push(detailDump);
128
- const hint = this.hint();
129
- if (hint) parts.push(hint);
130
- return {
131
- content: [{ type: "text", text: parts.join("\n\n") }],
132
- isError: true
133
- };
134
- }
135
- /**
136
- * Surface API-returned details for 4xx errors so the LLM can recover or
137
- * report the real reason (validation messages, field names, etc.). Skips
138
- * fields already collapsed into `message`/`code`, and skips 5xx where the
139
- * payload usually isn't actionable.
140
- */
141
- formatDetails() {
142
- if (this.status < 400 || this.status >= 500) return "";
143
- if (!this.details || typeof this.details !== "object") return "";
144
- const skipKeys = /* @__PURE__ */ new Set(["message", "detail", "error", "code"]);
145
- const extra = {};
146
- for (const [k, v] of Object.entries(this.details)) {
147
- if (skipKeys.has(k)) continue;
148
- if (v === null || v === void 0) continue;
149
- extra[k] = v;
150
- }
151
- if (Object.keys(extra).length === 0) return "";
152
- try {
153
- return `API details: ${JSON.stringify(extra)}`;
154
- } catch {
155
- return "";
156
- }
157
- }
158
- hint() {
159
- if (this.status === 401 || this.status === 403) {
160
- if (this.code === "OAUTH_API_ERROR") {
161
- 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.";
162
- }
163
- if (this.code === "INVALID_SERVICE_TOKEN" || this.code === "MISSING_SERVICE_TOKEN" || this.message.toLowerCase().includes("service token")) {
164
- return "Hint: The service token (DATABRAIN_SERVICE_TOKEN) is invalid, expired, or missing. It must be set in the MCP server config (not at runtime). Check Settings > Service Tokens in the Databrain UI.";
165
- }
166
- return "Hint: The per-data-app API token is invalid or expired. Use `create_api_token` to generate a new one. Do not ask the user \u2014 manage tokens via MCP tools.";
167
- }
168
- if (this.status === 400) {
169
- return "Hint: The request was invalid. Check parameter values match expected types and formats. Use the corresponding list tool to verify IDs and names.";
170
- }
171
- if (this.status === 404) {
172
- return "Hint: The requested resource was not found. Verify the ID or name is correct using the corresponding list tool.";
173
- }
174
- if (this.status === 409) {
175
- return "Hint: A resource with this name already exists. Use a different name or list existing resources first.";
176
- }
177
- if (this.status === 422) {
178
- return "Hint: The request parameters are invalid. Check the required fields and their formats.";
179
- }
180
- if (this.status === 429) {
181
- return "Hint: API rate limit reached. Wait a moment and try again.";
182
- }
183
- if (this.status >= 500) {
184
- return "Hint: The Databrain API is experiencing issues. Try again in a few moments.";
185
- }
186
- return "";
187
- }
188
- };
189
- function toolError(message) {
190
- return {
191
- content: [{ type: "text", text: message }],
192
- isError: true
193
- };
194
- }
195
- function stringify(value) {
196
- if (typeof value === "string") return value;
197
- try {
198
- return JSON.stringify(value, null, 2);
199
- } catch {
200
- return String(value);
201
- }
202
- }
203
- function toolSuccess(text) {
204
- return {
205
- content: [{ type: "text", text }]
206
- };
207
- }
208
-
209
- // src/utils/tool-context.ts
210
- import { AsyncLocalStorage } from "async_hooks";
211
- var toolContext = new AsyncLocalStorage();
212
-
213
- // src/client/oauth-client-credentials.ts
214
- import { z } from "zod";
215
- var oauthHttpsUrl = z.string().refine((value) => {
216
- try {
217
- const url = new URL(value);
218
- return url.protocol === "https:" && !url.username && !url.password && !url.hash;
219
- } catch {
220
- return false;
221
- }
222
- }, "OAuth URLs must use HTTPS without userinfo or fragments");
223
- var oauthConfigSchema = z.object({
224
- issuer: oauthHttpsUrl.refine((value) => {
225
- try {
226
- return !new URL(value).search;
227
- } catch {
228
- return false;
229
- }
230
- }, "OAuth issuer must not contain a query"),
231
- clientId: z.string().min(1),
232
- clientSecret: z.string().min(1),
233
- resource: oauthHttpsUrl,
234
- scope: z.string().min(1).optional(),
235
- authMethod: z.enum(["client_secret_basic", "client_secret_post"]).default("client_secret_basic")
236
- });
237
- var metadataSchema = z.object({
238
- issuer: z.string(),
239
- token_endpoint: oauthHttpsUrl,
240
- token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
241
- grant_types_supported: z.array(z.string()).optional()
242
- });
243
- var tokenSchema = z.object({
244
- access_token: z.string().regex(/^[A-Za-z0-9\-._~+/]+=*$/),
245
- token_type: z.string().refine((value) => value.toLowerCase() === "bearer"),
246
- expires_in: z.number().finite().positive().optional()
247
- });
248
- var TOKEN_TIMEOUT_MS = 1e4;
249
- var FAILURE_COOLDOWN_MS = 3e4;
250
- var MAX_REFRESH_SKEW_MS = 6e4;
251
- function authError() {
252
- return new Error("OAuth token acquisition failed. Check the configured issuer, client credentials, resource and scopes.");
253
- }
254
- var OAuthClientCredentials = class {
255
- config;
256
- tokenEndpoint;
257
- cached;
258
- pending;
259
- retryAt = 0;
260
- constructor(config) {
261
- const parsed = oauthConfigSchema.safeParse(config);
262
- if (!parsed.success) throw authError();
263
- this.config = Object.freeze(parsed.data);
264
- }
265
- getStatus() {
266
- if (this.cached && Date.now() < this.cached.renewAt) return "ready";
267
- if (this.retryAt > 0) return "unavailable";
268
- return "configured";
269
- }
270
- async getToken() {
271
- if (this.cached && Date.now() < this.cached.renewAt) return this.cached.value;
272
- if (this.pending) return this.pending;
273
- if (Date.now() < this.retryAt) throw authError();
274
- this.cached = void 0;
275
- this.pending = this.acquire().catch(() => {
276
- this.retryAt = Date.now() + FAILURE_COOLDOWN_MS;
277
- throw authError();
278
- }).finally(() => {
279
- this.pending = void 0;
280
- });
281
- return this.pending;
282
- }
283
- async discoverTokenEndpoint(signal) {
284
- if (this.tokenEndpoint) return this.tokenEndpoint;
285
- const issuer = new URL(this.config.issuer);
286
- const path = issuer.pathname.replace(/\/$/, "");
287
- const candidates = [.../* @__PURE__ */ new Set([
288
- `${issuer.origin}/.well-known/oauth-authorization-server${path}`,
289
- `${issuer.origin}/.well-known/openid-configuration${path}`,
290
- `${issuer.origin}${path}/.well-known/openid-configuration`
291
- ])];
292
- for (const url of candidates) {
293
- const response = await fetch(url, {
294
- headers: { Accept: "application/json" },
295
- redirect: "error",
296
- signal
297
- });
298
- if (response.status === 404 || response.status === 405) continue;
299
- if (!response.ok) throw authError();
300
- const metadata = metadataSchema.parse(await response.json());
301
- if (metadata.issuer !== this.config.issuer) throw authError();
302
- const methods = metadata.token_endpoint_auth_methods_supported ?? ["client_secret_basic"];
303
- if (!methods.includes(this.config.authMethod)) throw authError();
304
- if (metadata.grant_types_supported && !metadata.grant_types_supported.includes("client_credentials")) {
305
- throw authError();
306
- }
307
- this.tokenEndpoint = metadata.token_endpoint;
308
- return this.tokenEndpoint;
309
- }
310
- throw authError();
311
- }
312
- async acquire() {
313
- const signal = AbortSignal.timeout(TOKEN_TIMEOUT_MS);
314
- const endpoint = await this.discoverTokenEndpoint(signal);
315
- const body = new URLSearchParams({
316
- grant_type: "client_credentials",
317
- resource: this.config.resource
318
- });
319
- if (this.config.scope) body.set("scope", this.config.scope);
320
- const headers = {
321
- Accept: "application/json",
322
- "Content-Type": "application/x-www-form-urlencoded"
323
- };
324
- if (this.config.authMethod === "client_secret_post") {
325
- body.set("client_id", this.config.clientId);
326
- body.set("client_secret", this.config.clientSecret);
327
- } else {
328
- const encode = (value) => new URLSearchParams({ value }).toString().slice(6);
329
- headers.Authorization = `Basic ${Buffer.from(`${encode(this.config.clientId)}:${encode(this.config.clientSecret)}`).toString("base64")}`;
330
- }
331
- const startedAt = Date.now();
332
- const response = await fetch(endpoint, {
333
- method: "POST",
334
- headers,
335
- body: body.toString(),
336
- redirect: "error",
337
- signal
338
- });
339
- if (!response.ok) throw authError();
340
- const token = tokenSchema.parse(await response.json());
341
- if (token.expires_in !== void 0) {
342
- const lifetimeMs = token.expires_in * 1e3;
343
- const expiresAt = startedAt + lifetimeMs;
344
- if (!Number.isFinite(expiresAt) || Date.now() >= expiresAt) throw authError();
345
- this.cached = {
346
- value: token.access_token,
347
- renewAt: expiresAt - Math.min(MAX_REFRESH_SKEW_MS, lifetimeMs * 0.1)
348
- };
349
- }
350
- this.retryAt = 0;
351
- return token.access_token;
352
- }
353
- };
354
-
355
- // src/client/databrain-client.ts
356
- var MCP_USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION}`;
357
- var MAX_RETRIES = 3;
358
- var REQUEST_TIMEOUT_MS = 3e4;
359
- var NON_TRANSIENT_PATTERNS = [
360
- "out of context",
361
- "Definition not found",
362
- "not a data question",
363
- "Query generation failed",
364
- "not found",
365
- "not authenticated",
366
- "Invalid Service Token",
367
- "service token"
368
- ];
369
- var DatabrainClient = class {
370
- constructor(config) {
371
- this.config = config;
372
- if (config.oauth) {
373
- if (!oauthHttpsUrl.safeParse(config.apiUrl).success) {
374
- throw new Error("OAuth requires an HTTPS Databrain API URL without userinfo or fragments.");
375
- }
376
- this.oauth = new OAuthClientCredentials(config.oauth);
377
- this.config = Object.freeze({ ...config });
378
- }
379
- }
380
- cache = new TtlCache();
381
- runtimeApiToken;
382
- oauth;
383
- oauthFallback = false;
384
- hasApiToken() {
385
- return !!(this.runtimeApiToken || this.config.apiToken);
386
- }
387
- setRuntimeApiToken(token) {
388
- this.runtimeApiToken = token;
389
- logger.info("Runtime API token set for this session");
390
- }
391
- getServiceToken() {
392
- return this.config.serviceToken;
393
- }
394
- getApiUrl() {
395
- return this.config.apiUrl;
396
- }
397
- getDemoDomain() {
398
- return this.config.demoDomain;
399
- }
400
- /**
401
- * Describes which credential is currently used as the bearer for outgoing
402
- * API calls. Used by `whoami` so the LLM can give the user a truthful answer.
403
- */
404
- getActiveAuth() {
405
- if (this.oauth) {
406
- if (this.oauthFallback) return { source: "service-token", oauthFallback: true };
407
- return { source: "oauth-client-credentials", status: this.oauth.getStatus() };
408
- }
409
- if (this.config.serviceToken) return { source: "service-token" };
410
- if (this.runtimeApiToken || this.config.apiToken) return { source: "api-token" };
411
- return { source: "none" };
412
- }
413
- async getToken(auth) {
414
- if (auth === "none") return void 0;
415
- if (auth === "service") {
416
- if (this.oauth) {
417
- try {
418
- const token = await this.oauth.getToken();
419
- this.oauthFallback = false;
420
- return token;
421
- } catch {
422
- if (!this.config.serviceToken) {
423
- throw new DatabrainApiError(
424
- 0,
425
- "OAUTH_TOKEN_ERROR",
426
- "OAuth token acquisition failed. Check the configured issuer, client credentials, resource and scopes."
427
- );
428
- }
429
- if (!this.oauthFallback) logger.warn("OAuth unavailable; using the configured static service token.");
430
- this.oauthFallback = true;
431
- }
432
- }
433
- return this.config.serviceToken;
434
- }
435
- return this.runtimeApiToken || this.config.apiToken;
436
- }
437
- async request(options) {
438
- return this.requestInternal(options);
439
- }
440
- async requestInternal(options) {
441
- const usesOAuth = !!this.oauth && options.auth === "service" && !options.token;
442
- const cacheAllowed = !usesOAuth;
443
- if (options.invalidateCache) {
444
- for (const prefix of options.invalidateCache) {
445
- this.cache.invalidateByPrefix(prefix);
446
- }
447
- }
448
- if (cacheAllowed && options.cacheKey && options.cacheTtlMs) {
449
- const cached = this.cache.get(options.cacheKey);
450
- if (cached !== void 0) {
451
- logger.debug(`Cache hit: ${options.cacheKey}`);
452
- return cached;
453
- }
454
- }
455
- let token = options.token ?? await this.getToken(options.auth);
456
- if (options.auth !== "none" && !options.token && !token) {
457
- if (options.auth === "service") {
458
- throw new DatabrainApiError(
459
- 401,
460
- "MISSING_SERVICE_TOKEN",
461
- "DATABRAIN_SERVICE_TOKEN is required for this operation (org-level token for discovery and data app management). It must be set in the MCP server configuration \u2014 do not ask the user for it at runtime."
462
- );
463
- }
464
- throw new DatabrainApiError(
465
- 401,
466
- "MISSING_API_TOKEN",
467
- "A per-data-app API token is required for this embedding operation but none is configured. Call `create_api_token` to generate one, or `set_api_token` if you already have the value. Do not ask the user for this token \u2014 create it via the MCP tools."
468
- );
469
- }
470
- let url = `${this.config.apiUrl}${options.path}`;
471
- if (options.query) {
472
- const params = new URLSearchParams(options.query);
473
- url += `?${params.toString()}`;
474
- }
475
- let lastError;
476
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
477
- try {
478
- if (usesOAuth && attempt > 1) token = await this.getToken(options.auth);
479
- const headers = {
480
- "Content-Type": "application/json",
481
- Accept: "application/json",
482
- "User-Agent": MCP_USER_AGENT,
483
- "X-Databrain-Client": MCP_USER_AGENT,
484
- ...options.headers
485
- };
486
- const ctx = toolContext.getStore();
487
- if (ctx) {
488
- headers["X-Databrain-MCP-Tool"] = ctx.tool;
489
- headers["X-Databrain-MCP-Tool-Category"] = ctx.category;
490
- headers["X-Databrain-MCP-Workflow-Intent"] = ctx.workflowIntent;
491
- headers["X-Databrain-MCP-Input-Mode"] = ctx.inputMode;
492
- }
493
- if (token) {
494
- if (options.auth === "none") {
495
- headers["x-plugin-token"] = token;
496
- headers["Origin"] = "https://mcp.usedatabrain.com";
497
- } else {
498
- headers["Authorization"] = `Bearer ${token}`;
499
- }
500
- }
501
- const response = await fetch(url, {
502
- method: options.method,
503
- headers,
504
- body: options.body ? JSON.stringify(options.body) : void 0,
505
- signal: AbortSignal.timeout(options.timeoutMs || REQUEST_TIMEOUT_MS),
506
- ...usesOAuth ? { redirect: "error" } : {}
507
- });
508
- if (!response.ok) {
509
- if (usesOAuth) {
510
- throw new DatabrainApiError(
511
- response.status,
512
- "OAUTH_API_ERROR",
513
- `Databrain API request failed with status ${response.status} using configured service credentials.`
514
- );
515
- }
516
- const errorBody = await response.text();
517
- logger.warn(`[API Error] ${options.method} ${options.path} status=${response.status} body=${errorBody.slice(0, 500)}`);
518
- let parsed = {};
519
- try {
520
- parsed = JSON.parse(errorBody);
521
- } catch {
522
- }
523
- const errorMsg = parsed.message || parsed.detail || (typeof parsed.error === "string" ? parsed.error : JSON.stringify(parsed.error)) || `API request failed with status ${response.status}`;
524
- const error = new DatabrainApiError(
525
- response.status,
526
- parsed.code || `HTTP_${response.status}`,
527
- errorMsg,
528
- parsed
529
- );
530
- const isTransient = response.status === 429 || response.status >= 500 && !NON_TRANSIENT_PATTERNS.some((p) => errorMsg.includes(p));
531
- if (isTransient && attempt < MAX_RETRIES) {
532
- const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 8e3);
533
- logger.warn(`Retrying in ${delay}ms (attempt ${attempt}/${MAX_RETRIES})`, {
534
- status: response.status,
535
- path: options.path
536
- });
537
- await new Promise((r) => setTimeout(r, delay));
538
- lastError = error;
539
- continue;
540
- }
541
- throw error;
542
- }
543
- const text = await response.text();
544
- let data = text ? JSON.parse(text) : {};
545
- if (!options.skipDataUnwrap && data && typeof data === "object" && !Array.isArray(data) && "data" in data) {
546
- const inner = data.data;
547
- if (inner !== null && inner !== void 0) {
548
- data = inner;
549
- }
550
- }
551
- if (cacheAllowed && options.cacheKey && options.cacheTtlMs) {
552
- this.cache.set(options.cacheKey, data, options.cacheTtlMs);
553
- }
554
- return data;
555
- } catch (err) {
556
- if (err instanceof DatabrainApiError) throw err;
557
- if (attempt < MAX_RETRIES && err?.name === "TimeoutError" && !options.noRetryOnTimeout) {
558
- const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 8e3);
559
- logger.warn(`Request timed out, retrying in ${delay}ms`, { path: options.path });
560
- await new Promise((r) => setTimeout(r, delay));
561
- lastError = err;
562
- continue;
563
- }
564
- throw new DatabrainApiError(
565
- 0,
566
- "NETWORK_ERROR",
567
- usesOAuth ? "Failed to connect to Databrain API using configured service credentials." : `Failed to connect to Databrain API: ${err.message}`
568
- );
569
- }
570
- }
571
- throw lastError || new DatabrainApiError(0, "UNKNOWN", "Request failed after retries");
572
- }
573
- };
2
+ import {
3
+ CustomChartsApi,
4
+ DatabrainApiError,
5
+ apiProblems,
6
+ buildProject,
7
+ bundleSizeBytes,
8
+ contentHash,
9
+ createCustomChartsApi,
10
+ logger,
11
+ optionSchema,
12
+ planUpload,
13
+ readDist,
14
+ scaffoldProject,
15
+ slotSchema,
16
+ stringify,
17
+ toolContext,
18
+ toolError,
19
+ toolSuccess,
20
+ validateBundle
21
+ } from "./chunk-OCBTNAAO.js";
574
22
 
575
23
  // src/apps/contract.ts
576
- import { z as z2 } from "zod";
24
+ import { z } from "zod";
577
25
  var CHART_URI = "ui://databrain/chart-tile";
578
26
  var KPI_URI = "ui://databrain/kpi-tile";
579
27
  var APP_MIME = "text/html;profile=mcp-app";
28
+ var RESOURCE_URI_META_KEY = "ui/resourceUri";
580
29
  var PAGE_SIZE = 50;
581
- var MAX_RESULT_CHARS = 125e3;
582
- var id = z2.string().trim().min(1).max(256);
583
- var cell = z2.union([z2.string().max(2e3), z2.number().finite(), z2.boolean(), z2.null()]);
584
- var rowsSchema = z2.array(z2.record(cell)).max(PAGE_SIZE);
585
- var referenceSchema = z2.object({ embedId: id, clientId: id, widgetId: id }).strict();
586
- var point = z2.object({
30
+ var MAX_RESULT_CHARS = 124e3;
31
+ var id = z.string().trim().min(1).max(256);
32
+ var cell = z.union([z.string().max(2e3), z.number().finite(), z.boolean(), z.null()]);
33
+ var rowsSchema = z.array(z.record(cell)).max(PAGE_SIZE);
34
+ var referenceSchema = z.object({ embedId: id, clientId: id, widgetId: id }).strict();
35
+ var point = z.object({
587
36
  key: id,
588
- label: z2.string().max(2e3),
37
+ label: z.string().max(2e3),
589
38
  value: cell,
590
- x: z2.number().min(0),
591
- y: z2.number().min(0),
592
- width: z2.number().positive(),
593
- height: z2.number().positive(),
39
+ x: z.number().min(0),
40
+ y: z.number().min(0),
41
+ width: z.number().positive(),
42
+ height: z.number().positive(),
594
43
  drillId: id.optional()
595
44
  }).strict();
596
- var dateRangeSchema = z2.object({
597
- from: z2.string().date(),
598
- to: z2.string().date()
45
+ var dateRangeSchema = z.object({
46
+ from: z.string().date(),
47
+ to: z.string().date()
599
48
  }).strict().refine((value) => value.from <= value.to, "Date range must be ordered");
600
- var viewSchema = z2.object({
601
- cursor: z2.string().min(1).max(2048).optional(),
49
+ var viewSchema = z.object({
50
+ cursor: z.string().min(1).max(2048).optional(),
602
51
  dateRange: dateRangeSchema.optional(),
603
52
  drillId: id.optional()
604
53
  }).strict();
605
- var pageSchema = z2.object({
606
- kind: z2.enum(["chart", "kpi"]),
607
- title: z2.string().max(500),
608
- chartSpec: z2.object({ type: z2.string().max(80) }).strict(),
54
+ var pageSchema = z.object({
55
+ kind: z.enum(["chart", "kpi"]),
56
+ title: z.string().max(500),
57
+ chartSpec: z.object({ type: z.string().max(80) }).strict(),
609
58
  rows: rowsSchema,
610
- columns: z2.array(z2.string().max(128)).max(20),
611
- svg: z2.string().max(1e5).optional(),
612
- width: z2.number().int().min(240).max(1200),
613
- height: z2.number().int().min(120).max(800),
614
- points: z2.array(point).max(PAGE_SIZE),
59
+ columns: z.array(z.string().max(128)).max(20),
60
+ svg: z.string().max(1e5).optional(),
61
+ width: z.number().int().min(240).max(1200),
62
+ height: z.number().int().min(120).max(800),
63
+ points: z.array(point).max(PAGE_SIZE),
615
64
  value: cell.optional(),
616
- nextCursor: z2.string().min(1).max(2048).optional()
65
+ nextCursor: z.string().min(1).max(2048).optional()
617
66
  }).strict().superRefine((page, ctx) => {
618
67
  if (page.kind === "chart" && !page.svg) ctx.addIssue({ code: "custom", message: "SVG required" });
619
68
  if (page.kind === "kpi" && (page.svg || page.points.length || page.value === void 0)) {
@@ -639,6 +88,9 @@ function firstRows(value) {
639
88
  }
640
89
 
641
90
  // src/apps/feature.ts
91
+ function appMeta(visibility) {
92
+ return { ui: { resourceUri: CHART_URI, visibility }, [RESOURCE_URI_META_KEY]: CHART_URI };
93
+ }
642
94
  var enabled = /* @__PURE__ */ new WeakSet();
643
95
  function enableChartApps(server) {
644
96
  enabled.add(server);
@@ -647,79 +99,122 @@ function hasChartApps(server) {
647
99
  return enabled.has(server);
648
100
  }
649
101
  function tagChartTool(server, tool) {
650
- if (hasChartApps(server)) tool.update({ _meta: { ui: { resourceUri: CHART_URI, visibility: ["model", "app"] } } });
102
+ if (hasChartApps(server)) tool.update({ _meta: appMeta(["model", "app"]) });
651
103
  }
652
104
  function chartResult(summary, payload) {
653
- return bounded({ content: [{ type: "text", text: summary }], structuredContent: payload });
105
+ return bounded({
106
+ content: [{ type: "text", text: summary }],
107
+ structuredContent: payload,
108
+ _meta: appMeta(["model", "app"])
109
+ });
654
110
  }
655
111
 
656
112
  // src/tools/tool-metadata.ts
657
113
  var DEFAULT_TOOL_META = {
658
114
  category: "discovery",
659
- workflowIntent: "other"
115
+ workflowIntent: "other",
116
+ // Fail closed: an unregistered/unclassified tool is treated as a commit
117
+ // until someone deliberately marks it read or view.
118
+ effect: "commit"
660
119
  };
661
120
  var TOOL_METADATA = {
662
121
  // Identity / discovery
663
- whoami: { category: "discovery", workflowIntent: "discovery" },
122
+ whoami: { category: "discovery", workflowIntent: "discovery", effect: "read" },
664
123
  // Infrastructure / data setup
665
- list_datasources: { category: "infrastructure", workflowIntent: "setup_data" },
666
- discover_datasource_schema: { category: "infrastructure", workflowIntent: "setup_data" },
667
- list_datamarts: { category: "infrastructure", workflowIntent: "setup_data" },
668
- list_workspaces: { category: "infrastructure", workflowIntent: "setup_data" },
669
- sync_datasource: { category: "infrastructure", workflowIntent: "setup_data" },
670
- create_datamart: { category: "infrastructure", workflowIntent: "setup_data" },
124
+ list_datasources: { category: "infrastructure", workflowIntent: "setup_data", effect: "read" },
125
+ discover_datasource_schema: { category: "infrastructure", workflowIntent: "setup_data", effect: "read" },
126
+ list_datamarts: { category: "infrastructure", workflowIntent: "setup_data", effect: "read" },
127
+ list_workspaces: { category: "infrastructure", workflowIntent: "setup_data", effect: "read" },
128
+ // Triggers a backend sync job that refreshes stored schema metadata — a real mutation, not a read.
129
+ sync_datasource: { category: "infrastructure", workflowIntent: "setup_data", effect: "commit" },
130
+ // dry_run is the default, but mode:"apply" performs a real POST that creates the datamart.
131
+ create_datamart: { category: "infrastructure", workflowIntent: "setup_data", effect: "commit" },
132
+ // Hosted only (an "All workspaces" sign-in); stdio does not register it.
133
+ create_workspace: { category: "infrastructure", workflowIntent: "setup_data", effect: "commit" },
671
134
  // Embedding — data apps & tokens
672
- list_data_apps: { category: "embedding", workflowIntent: "embed_dashboard" },
673
- list_tenants: { category: "embedding", workflowIntent: "embed_dashboard" },
674
- create_data_app: { category: "embedding", workflowIntent: "embed_dashboard" },
675
- list_api_tokens: { category: "embedding", workflowIntent: "embed_dashboard" },
676
- create_api_token: { category: "embedding", workflowIntent: "embed_dashboard" },
677
- rotate_api_token: { category: "embedding", workflowIntent: "embed_dashboard" },
135
+ list_data_apps: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
136
+ list_tenants: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
137
+ create_data_app: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
138
+ list_api_tokens: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
139
+ create_api_token: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
140
+ rotate_api_token: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
678
141
  // Embedding — embed management
679
- create_embed: { category: "embedding", workflowIntent: "embed_dashboard" },
680
- list_embeds: { category: "embedding", workflowIntent: "embed_dashboard" },
681
- get_embed_details: { category: "embedding", workflowIntent: "embed_dashboard" },
682
- update_embed: { category: "embedding", workflowIntent: "embed_dashboard" },
683
- customize_embed_theme: { category: "embedding", workflowIntent: "embed_dashboard" },
684
- get_demo_link: { category: "embedding", workflowIntent: "embed_dashboard" },
142
+ create_embed: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
143
+ list_embeds: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
144
+ get_embed_details: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
145
+ update_embed: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
146
+ // Persists theme/access config on the embed record — a real write, not a display-only tweak.
147
+ customize_embed_theme: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
148
+ // Mints and returns a live guest token embedded in the demo URL.
149
+ get_demo_link: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
685
150
  // Embedding — frontend
686
- generate_guest_token: { category: "embedding", workflowIntent: "embed_dashboard" },
687
- generate_embed_code: { category: "embedding", workflowIntent: "embed_dashboard" },
151
+ generate_guest_token: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
152
+ generate_embed_code: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
688
153
  // Embedding — customer dashboards & widgets
689
- list_dashboards: { category: "embedding", workflowIntent: "embedded_widget" },
690
- create_dashboard: { category: "embedding", workflowIntent: "author_dashboard" },
691
- get_dashboard: { category: "embedding", workflowIntent: "author_dashboard" },
692
- generate_dashboard_tiles: { category: "embedding", workflowIntent: "author_dashboard" },
693
- list_widgets: { category: "embedding", workflowIntent: "embedded_widget" },
694
- add_widget: { category: "embedding", workflowIntent: "embedded_widget" },
695
- update_widget: { category: "embedding", workflowIntent: "embedded_widget" },
696
- remove_widget: { category: "embedding", workflowIntent: "embedded_widget" },
697
- generate_widget: { category: "embedding", workflowIntent: "embedded_widget" },
698
- validate_metric_spec: { category: "embedding", workflowIntent: "sql_to_embedded_metric" },
154
+ list_dashboards: { category: "embedding", workflowIntent: "embedded_widget", effect: "read" },
155
+ create_dashboard: { category: "embedding", workflowIntent: "author_dashboard", effect: "commit" },
156
+ get_dashboard: { category: "embedding", workflowIntent: "author_dashboard", effect: "read" },
157
+ // Persists nothing the user sees, but the backend mints (and then deletes) an internal guest token to render it.
158
+ dashboard_snapshot: { category: "internal_analytics", workflowIntent: "query_data", effect: "commit" },
159
+ // Does not persist a dashboard, but mints an internal guest token to render tile previews.
160
+ generate_dashboard_tiles: { category: "embedding", workflowIntent: "author_dashboard", effect: "commit" },
161
+ list_widgets: { category: "embedding", workflowIntent: "embedded_widget", effect: "read" },
162
+ add_widget: { category: "embedding", workflowIntent: "embedded_widget", effect: "commit" },
163
+ update_widget: { category: "embedding", workflowIntent: "embedded_widget", effect: "commit" },
164
+ remove_widget: { category: "embedding", workflowIntent: "embedded_widget", effect: "commit" },
165
+ // Returns an unsaved draft, but mints an internal write-capable guest token to generate it.
166
+ generate_widget: { category: "embedding", workflowIntent: "embedded_widget", effect: "commit" },
167
+ // Dry-run validation only, but mints an internal write-capable guest token to check conflicts.
168
+ validate_metric_spec: { category: "embedding", workflowIntent: "sql_to_embedded_metric", effect: "commit" },
699
169
  create_or_update_metric_from_query: {
700
170
  category: "embedding",
701
- workflowIntent: "sql_to_embedded_metric"
171
+ workflowIntent: "sql_to_embedded_metric",
172
+ effect: "commit"
702
173
  },
703
174
  // Internal analytics
704
175
  create_workspace_metric: {
705
176
  category: "internal_analytics",
706
- workflowIntent: "sql_to_workspace_metric"
177
+ workflowIntent: "sql_to_workspace_metric",
178
+ effect: "commit"
707
179
  },
708
- ask_question: { category: "internal_analytics", workflowIntent: "query_data" },
180
+ // Runs a query and returns rows; no dashboard/widget/row is persisted.
181
+ ask_question: { category: "internal_analytics", workflowIntent: "query_data", effect: "read" },
182
+ preview_chart: { category: "internal_analytics", workflowIntent: "query_data", effect: "view" },
183
+ save_chart_as_dashboard: { category: "internal_analytics", workflowIntent: "author_dashboard", effect: "commit" },
184
+ // Read-only to the user, but each interaction mints an embed-scoped guest credential.
185
+ render_chart: { category: "embedding", workflowIntent: "chart_app", effect: "commit" },
186
+ refresh_widget: { category: "embedding", workflowIntent: "chart_app", effect: "commit" },
187
+ drill_down: { category: "embedding", workflowIntent: "chart_app", effect: "commit" },
188
+ change_date_range: { category: "embedding", workflowIntent: "chart_app", effect: "commit" },
709
189
  // Semantic layer
710
- get_semantic_layer: { category: "semantic_layer", workflowIntent: "query_data" },
711
- update_semantic_layer: { category: "semantic_layer", workflowIntent: "query_data" },
712
- start_semantic_layer_generation: { category: "semantic_layer", workflowIntent: "query_data" },
190
+ search_semantic_model: { category: "semantic_layer", workflowIntent: "query_data", effect: "read" },
191
+ get_semantic_entity: { category: "semantic_layer", workflowIntent: "query_data", effect: "read" },
192
+ propose_semantic_change: { category: "semantic_layer", workflowIntent: "model_data", effect: "read" },
193
+ apply_semantic_change: { category: "semantic_layer", workflowIntent: "model_data", effect: "commit" },
194
+ eval_semantic_model: { category: "semantic_layer", workflowIntent: "query_data", effect: "read" },
195
+ get_semantic_layer: { category: "semantic_layer", workflowIntent: "query_data", effect: "read" },
196
+ update_semantic_layer: { category: "semantic_layer", workflowIntent: "query_data", effect: "commit" },
197
+ start_semantic_layer_generation: { category: "semantic_layer", workflowIntent: "query_data", effect: "commit" },
713
198
  get_semantic_layer_generation_status: {
714
199
  category: "semantic_layer",
715
- workflowIntent: "query_data"
200
+ workflowIntent: "query_data",
201
+ effect: "read"
716
202
  },
717
203
  // Reports
718
- list_scheduled_reports: { category: "reports", workflowIntent: "reports" }
204
+ list_scheduled_reports: { category: "reports", workflowIntent: "reports", effect: "read" },
205
+ // Custom chart types (DAT-925)
206
+ // Returns template files; nothing is written or persisted.
207
+ custom_chart_scaffold: { category: "custom_charts", workflowIntent: "custom_chart", effect: "read" },
208
+ // Local rules plus POST /validate, which never persists.
209
+ custom_chart_validate: { category: "custom_charts", workflowIntent: "custom_chart", effect: "read" },
210
+ // dry_run is the default, but mode:"apply" creates a chart or a version (and may publish).
211
+ custom_chart_upload: { category: "custom_charts", workflowIntent: "custom_chart", effect: "commit" },
212
+ list_custom_charts: { category: "custom_charts", workflowIntent: "custom_chart", effect: "read" },
213
+ publish_custom_chart: { category: "custom_charts", workflowIntent: "custom_chart", effect: "commit" }
719
214
  };
720
215
 
721
216
  // src/tools/data-app/list-data-apps.ts
722
- import { z as z3 } from "zod";
217
+ import { z as z2 } from "zod";
723
218
 
724
219
  // src/api/dashboards-api.ts
725
220
  var DashboardsApi = class {
@@ -1011,18 +506,18 @@ var QueryApi = class {
1011
506
  throw err;
1012
507
  }
1013
508
  }
1014
- async startSemanticLayerGeneration(datamartId) {
509
+ async startSemanticLayerGeneration(datamartId2) {
1015
510
  return this.client.request({
1016
511
  method: "POST",
1017
- path: `/schema/datamart/${encodeURIComponent(datamartId)}/semantic-layer/auto-generate`,
512
+ path: `/api/v2/data-app/datamarts/${encodeURIComponent(datamartId2)}/semantic-layer/generate`,
1018
513
  auth: "service",
1019
514
  invalidateCache: ["semantic_layer_"]
1020
515
  });
1021
516
  }
1022
- async getSemanticLayerGenerationStatus(datamartId) {
517
+ async getSemanticLayerGenerationStatus(datamartId2) {
1023
518
  return this.client.request({
1024
519
  method: "GET",
1025
- path: `/schema/datamart/${encodeURIComponent(datamartId)}/semantic-layer/generation-status`,
520
+ path: `/api/v2/data-app/datamarts/${encodeURIComponent(datamartId2)}/semantic-layer/generation-status`,
1026
521
  auth: "service"
1027
522
  });
1028
523
  }
@@ -1045,9 +540,9 @@ function normalizeRecords(value) {
1045
540
  record.items,
1046
541
  record.data
1047
542
  ];
1048
- for (const candidate of candidates) {
1049
- if (Array.isArray(candidate)) {
1050
- return candidate.filter((item) => asRecord(item) !== null);
543
+ for (const candidate2 of candidates) {
544
+ if (Array.isArray(candidate2)) {
545
+ return candidate2.filter((item) => asRecord(item) !== null);
1051
546
  }
1052
547
  }
1053
548
  return [];
@@ -1076,6 +571,62 @@ var ScheduledReportsApi = class {
1076
571
  }
1077
572
  };
1078
573
 
574
+ // src/api/semantic-model-api.ts
575
+ var SemanticModelApi = class {
576
+ constructor(client) {
577
+ this.client = client;
578
+ }
579
+ path(id2, verb) {
580
+ return `/api/v2/semantic/${encodeURIComponent(id2)}/${verb}`;
581
+ }
582
+ pull(id2) {
583
+ return this.client.request({ method: "GET", path: this.path(id2, "pull"), auth: "service" });
584
+ }
585
+ retrieve(id2, q, k) {
586
+ return this.client.request({
587
+ method: "GET",
588
+ path: this.path(id2, "retrieve"),
589
+ auth: "service",
590
+ query: { q, k: String(k) }
591
+ });
592
+ }
593
+ validate(id2, source2) {
594
+ return this.client.request({
595
+ method: "POST",
596
+ path: this.path(id2, "validate"),
597
+ auth: "service",
598
+ body: { source: source2 }
599
+ });
600
+ }
601
+ plan(id2, source2, expected_content_hash) {
602
+ return this.client.request({
603
+ method: "POST",
604
+ path: this.path(id2, "plan"),
605
+ auth: "service",
606
+ body: { source: source2, expected_content_hash }
607
+ });
608
+ }
609
+ apply(id2, source2, expected_content_hash) {
610
+ return this.client.request({
611
+ method: "POST",
612
+ path: this.path(id2, "apply"),
613
+ auth: "service",
614
+ body: { source: source2, expected_content_hash },
615
+ noRetryOnTimeout: true
616
+ });
617
+ }
618
+ evaluate(id2, body) {
619
+ return this.client.request({
620
+ method: "POST",
621
+ path: this.path(id2, "eval"),
622
+ auth: "service",
623
+ body,
624
+ timeoutMs: 3e5,
625
+ noRetryOnTimeout: true
626
+ });
627
+ }
628
+ };
629
+
1079
630
  // src/api/tenants-api.ts
1080
631
  var TenantsApi = class {
1081
632
  constructor(client) {
@@ -1258,8 +809,8 @@ function extractAccessSettings(embed) {
1258
809
  asRecord2(metadataConfig?.accessSettings),
1259
810
  asRecord2(options?.accessSettings)
1260
811
  ].filter((c) => !!c);
1261
- for (const candidate of candidates) {
1262
- if (Object.keys(candidate).length > 0) return candidate;
812
+ for (const candidate2 of candidates) {
813
+ if (Object.keys(candidate2).length > 0) return candidate2;
1263
814
  }
1264
815
  return null;
1265
816
  }
@@ -1672,6 +1223,54 @@ var WidgetService = class {
1672
1223
  }
1673
1224
  };
1674
1225
  }
1226
+ /**
1227
+ * Read-only sibling of `ensureTenantDashboardScope`.
1228
+ *
1229
+ * The tenant gate above exists to stop a WRITE landing on the data-app
1230
+ * dashboard when the caller meant a tenant's copy, so it insists the
1231
+ * tenant-scoped embed lookup returns the target. A data-app embed has no
1232
+ * tenant row, `listEmbeds(dataAppName, clientId)` answers with an empty list,
1233
+ * and the render path was denied before it ever reached the backend. That is
1234
+ * the wrong trade for a read: the chart route re-derives everything from the
1235
+ * guest token it is handed (embed inside the token's data app, `allowedEmbeds`,
1236
+ * the tenancy subset for the client, and per-metric client visibility), so the
1237
+ * only thing this check can usefully add is refusing another tenant's embed.
1238
+ */
1239
+ async ensureReadableDashboardScope(params) {
1240
+ const clientId = params.clientId.trim();
1241
+ if (!clientId) {
1242
+ return {
1243
+ ok: false,
1244
+ error: "`clientId` cannot be empty. Pick a tenant/client first."
1245
+ };
1246
+ }
1247
+ let ctx;
1248
+ try {
1249
+ ctx = await this.resolveEmbedContext(params.embedId);
1250
+ } catch (err) {
1251
+ return { ok: false, error: err.message };
1252
+ }
1253
+ if (ctx.embedClientId && ctx.embedClientId !== clientId) {
1254
+ return {
1255
+ ok: false,
1256
+ error: `Embed "${params.embedId}" belongs to another client. Pass the \`clientId\` the embed was created for.`
1257
+ };
1258
+ }
1259
+ if (!ctx.embedClientId && ctx.externalDashboardCreatedBy && ctx.externalDashboardCreatedBy !== clientId) {
1260
+ return {
1261
+ ok: false,
1262
+ error: `Dashboard behind embed "${params.embedId}" belongs to another client. Pass the \`clientId\` the dashboard was created for.`
1263
+ };
1264
+ }
1265
+ return {
1266
+ ok: true,
1267
+ value: {
1268
+ embedId: params.embedId,
1269
+ clientId,
1270
+ dataAppName: ctx.dataAppName || params.dataAppName || null
1271
+ }
1272
+ };
1273
+ }
1675
1274
  /**
1676
1275
  * Resolve a datasource for a widget. Priority:
1677
1276
  * 1. Explicit integrationId + integrationName (fast path).
@@ -1857,6 +1456,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
1857
1456
  dataAppName,
1858
1457
  externalDashboardId,
1859
1458
  dashboardId,
1459
+ embedClientId: isNonEmptyString3(exact.clientId) && exact.clientId || (metadata && isNonEmptyString3(metadata.clientId) ? metadata.clientId : null),
1860
1460
  externalDashboardCreatedBy: (externalDashboard && isNonEmptyString3(externalDashboard.createdBy) ? externalDashboard.createdBy : null) || (isNonEmptyString3(exact.createdBy) ? exact.createdBy : null) || (metadata && isNonEmptyString3(metadata.createdBy) ? metadata.createdBy : null) || (isNonEmptyString3(exact.clientId) ? exact.clientId : null)
1861
1461
  };
1862
1462
  }
@@ -1942,6 +1542,8 @@ function createServiceBundle(client) {
1942
1542
  const guestTokenApi = new GuestTokenApi(client);
1943
1543
  const queryApi = new QueryApi(client);
1944
1544
  const scheduledReportsApi = new ScheduledReportsApi(client);
1545
+ const semanticModelApi = new SemanticModelApi(client);
1546
+ const customChartsApi = new CustomChartsApi(client);
1945
1547
  const tenantsApi = new TenantsApi(client);
1946
1548
  const widgetsApi = new WidgetsApi(client);
1947
1549
  const workspacesApi = new WorkspacesApi(client);
@@ -1956,6 +1558,8 @@ function createServiceBundle(client) {
1956
1558
  workspacesApi,
1957
1559
  queryApi,
1958
1560
  scheduledReportsApi,
1561
+ semanticModelApi,
1562
+ customChartsApi,
1959
1563
  tenantsApi,
1960
1564
  widgetsApi,
1961
1565
  embedService: new EmbedService(embedsApi, datamartsApi),
@@ -2006,7 +1610,7 @@ function registerListDataApps(server, client) {
2006
1610
  "list_data_apps",
2007
1611
  "List data apps in your organization. Start here for embed setup.",
2008
1612
  {
2009
- workspaceName: z3.string().optional().describe("Filter by workspace name")
1613
+ workspaceName: z2.string().optional().describe("Filter by workspace name")
2010
1614
  },
2011
1615
  READ_ONLY,
2012
1616
  async (params) => {
@@ -2035,7 +1639,7 @@ function registerListDataApps(server, client) {
2035
1639
  }
2036
1640
 
2037
1641
  // src/tools/data-app/list-tenants.ts
2038
- import { z as z4 } from "zod";
1642
+ import { z as z3 } from "zod";
2039
1643
  var isNonEmptyString4 = (value) => typeof value === "string" && value.trim().length > 0;
2040
1644
  function workspaceIdFrom(workspace) {
2041
1645
  return isNonEmptyString4(workspace.id) && workspace.id || isNonEmptyString4(workspace._id) && workspace._id || isNonEmptyString4(workspace.workspaceId) && workspace.workspaceId || null;
@@ -2073,12 +1677,12 @@ function registerListTenants(server, client) {
2073
1677
  "list_tenants",
2074
1678
  "List available tenants/clients from the workspace tenancy database. Use this to pick a valid clientId before creating guest tokens or editing customer dashboards.",
2075
1679
  {
2076
- workspaceName: z4.string().optional().describe("Workspace name. Preferred for non-technical callers; the tool resolves it to a workspaceId."),
2077
- workspaceId: z4.string().optional().describe("Technical workspace UUID. Provide this instead of workspaceName if already known."),
2078
- searchValue: z4.string().optional().describe("Optional case-insensitive search string for tenant/client labels."),
2079
- limit: z4.number().int().positive().max(500).optional().describe("Maximum number of tenants to return. Backend may apply connector-specific limits."),
2080
- datasourceId: z4.string().optional().describe("Optional datasource UUID override when the workspace has multiple datasource contexts."),
2081
- datamartId: z4.string().optional().describe("Optional datamart UUID override when tenant lookup should use a specific datamart.")
1680
+ workspaceName: z3.string().optional().describe("Workspace name. Preferred for non-technical callers; the tool resolves it to a workspaceId."),
1681
+ workspaceId: z3.string().optional().describe("Technical workspace UUID. Provide this instead of workspaceName if already known."),
1682
+ searchValue: z3.string().optional().describe("Optional case-insensitive search string for tenant/client labels."),
1683
+ limit: z3.number().int().positive().max(500).optional().describe("Maximum number of tenants to return. Backend may apply connector-specific limits."),
1684
+ datasourceId: z3.string().optional().describe("Optional datasource UUID override when the workspace has multiple datasource contexts."),
1685
+ datamartId: z3.string().optional().describe("Optional datamart UUID override when tenant lookup should use a specific datamart.")
2082
1686
  },
2083
1687
  READ_ONLY,
2084
1688
  async (params) => {
@@ -2172,15 +1776,15 @@ function registerListTenants(server, client) {
2172
1776
  }
2173
1777
 
2174
1778
  // src/tools/data-app/create-data-app.ts
2175
- import { z as z5 } from "zod";
1779
+ import { z as z4 } from "zod";
2176
1780
  function registerCreateDataApp(server, client) {
2177
1781
  server.tool(
2178
1782
  "create_data_app",
2179
1783
  "Create a new data app.",
2180
1784
  {
2181
- name: z5.string().describe("Data app name"),
2182
- workspaceName: z5.string().optional().describe("Optional workspace name if required by backend"),
2183
- body: z5.record(z5.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/")
1785
+ name: z4.string().describe("Data app name"),
1786
+ workspaceName: z4.string().optional().describe("Optional workspace name if required by backend"),
1787
+ body: z4.record(z4.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/")
2184
1788
  },
2185
1789
  WRITE_CREATE,
2186
1790
  async (params) => {
@@ -2214,7 +1818,7 @@ function registerCreateDataApp(server, client) {
2214
1818
  }
2215
1819
 
2216
1820
  // src/tools/data-app/list-api-tokens.ts
2217
- import { z as z6 } from "zod";
1821
+ import { z as z5 } from "zod";
2218
1822
 
2219
1823
  // src/utils/resolve-resource.ts
2220
1824
  function resolveResource(options) {
@@ -2283,7 +1887,7 @@ function registerListApiTokens(server, client) {
2283
1887
  "list_api_tokens",
2284
1888
  "List API tokens for a data app. Omit dataAppName to auto-resolve.",
2285
1889
  {
2286
- dataAppName: z6.string().optional().describe("Data app name. Omit to list available data apps.")
1890
+ dataAppName: z5.string().optional().describe("Data app name. Omit to list available data apps.")
2287
1891
  },
2288
1892
  READ_ONLY,
2289
1893
  async (params) => {
@@ -2328,16 +1932,16 @@ function registerListApiTokens(server, client) {
2328
1932
  }
2329
1933
 
2330
1934
  // src/tools/data-app/create-api-token.ts
2331
- import { z as z7 } from "zod";
1935
+ import { z as z6 } from "zod";
2332
1936
  function registerCreateApiToken(server, client) {
2333
1937
  const services = createServiceBundle(client);
2334
1938
  server.tool(
2335
1939
  "create_api_token",
2336
1940
  "Create an API token for a data app and activate it for this session. Omit dataAppName to auto-resolve.",
2337
1941
  {
2338
- dataAppName: z7.string().optional().describe("Data app name. Omit to list available data apps."),
2339
- name: z7.string().optional().describe("Optional token name"),
2340
- body: z7.record(z7.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/api-tokens")
1942
+ dataAppName: z6.string().optional().describe("Data app name. Omit to list available data apps."),
1943
+ name: z6.string().optional().describe("Optional token name"),
1944
+ body: z6.record(z6.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/api-tokens")
2341
1945
  },
2342
1946
  WRITE_CREATE,
2343
1947
  async (params) => {
@@ -2373,8 +1977,9 @@ function registerCreateApiToken(server, client) {
2373
1977
  );
2374
1978
  }
2375
1979
  client.setRuntimeApiToken(key);
1980
+ const used = client.hasApiToken() ? "API token created and set for this MCP session." : "API token created. This session keeps acting as the signed-in user; the token is for your backend.";
2376
1981
  return toolSuccess(
2377
- `API token created and set for this MCP session.
1982
+ `${used}
2378
1983
 
2379
1984
  **Token:** \`${key}\`
2380
1985
 
@@ -2389,16 +1994,16 @@ Save this in backend secrets immediately. This value may not be returned again.`
2389
1994
  }
2390
1995
 
2391
1996
  // src/tools/data-app/rotate-api-token.ts
2392
- import { z as z8 } from "zod";
1997
+ import { z as z7 } from "zod";
2393
1998
  function registerRotateApiToken(server, client) {
2394
1999
  const services = createServiceBundle(client);
2395
2000
  server.tool(
2396
2001
  "rotate_api_token",
2397
2002
  "Rotate (invalidate and replace) an API token. Previous token becomes stale.",
2398
2003
  {
2399
- dataAppName: z8.string().optional().describe("Data app name. Omit to list available data apps."),
2400
- tokenId: z8.string().optional().describe("Optional token ID to rotate"),
2401
- body: z8.record(z8.unknown()).optional().describe("Raw API body passthrough for rotate endpoint")
2004
+ dataAppName: z7.string().optional().describe("Data app name. Omit to list available data apps."),
2005
+ tokenId: z7.string().optional().describe("Optional token ID to rotate"),
2006
+ body: z7.record(z7.unknown()).optional().describe("Raw API body passthrough for rotate endpoint")
2402
2007
  },
2403
2008
  DESTRUCTIVE,
2404
2009
  async (params) => {
@@ -2517,14 +2122,14 @@ function registerListDatasources(server, client) {
2517
2122
  }
2518
2123
 
2519
2124
  // src/tools/infrastructure/discover-datasource-schema.ts
2520
- import { z as z9 } from "zod";
2125
+ import { z as z8 } from "zod";
2521
2126
  function registerDiscoverDatasourceSchema(server, client) {
2522
2127
  const services = createServiceBundle(client);
2523
2128
  server.tool(
2524
2129
  "discover_datasource_schema",
2525
2130
  "Inspect tables and columns in a datasource. Omit datasourceName to list available datasources.",
2526
2131
  {
2527
- datasourceName: z9.string().optional().describe(
2132
+ datasourceName: z8.string().optional().describe(
2528
2133
  "Name of the datasource to inspect. Omit to list available datasources."
2529
2134
  )
2530
2135
  },
@@ -2607,13 +2212,13 @@ Fix the connection or add a new database in the **Databrain UI**.`
2607
2212
  }
2608
2213
 
2609
2214
  // src/tools/infrastructure/list-datamarts.ts
2610
- import { z as z10 } from "zod";
2215
+ import { z as z9 } from "zod";
2611
2216
  function registerListDatamarts(server, client) {
2612
2217
  server.tool(
2613
2218
  "list_datamarts",
2614
2219
  "List datamarts with table counts and semantic layer scores.",
2615
2220
  {
2616
- dataAppName: z10.string().optional().describe("Filter by data app name")
2221
+ dataAppName: z9.string().optional().describe("Filter by data app name")
2617
2222
  },
2618
2223
  READ_ONLY,
2619
2224
  async (params) => {
@@ -2702,7 +2307,7 @@ function registerListWorkspaces(server, client) {
2702
2307
  }
2703
2308
 
2704
2309
  // src/tools/infrastructure/create-datamart.ts
2705
- import { z as z11 } from "zod";
2310
+ import { z as z10 } from "zod";
2706
2311
 
2707
2312
  // src/tools/widgets/production-guards.ts
2708
2313
  var APPLY_TO_PRODUCTION_CONFIRMATION = "APPLY_TO_PRODUCTION";
@@ -2720,29 +2325,29 @@ function genericProductionSafetyNote() {
2720
2325
  }
2721
2326
 
2722
2327
  // src/tools/infrastructure/create-datamart.ts
2723
- var columnSchema = z11.object({
2724
- name: z11.string(),
2725
- alias: z11.string().optional(),
2726
- label: z11.string().optional(),
2727
- isHide: z11.boolean().optional(),
2728
- isCustomColumn: z11.boolean().optional(),
2729
- sql: z11.string().optional()
2328
+ var columnSchema = z10.object({
2329
+ name: z10.string(),
2330
+ alias: z10.string().optional(),
2331
+ label: z10.string().optional(),
2332
+ isHide: z10.boolean().optional(),
2333
+ isCustomColumn: z10.boolean().optional(),
2334
+ sql: z10.string().optional()
2730
2335
  });
2731
- var tableSchema = z11.object({
2732
- name: z11.string(),
2733
- schemaName: z11.string().optional(),
2734
- clientColumn: z11.string().optional(),
2735
- label: z11.string().optional(),
2736
- columns: z11.array(columnSchema)
2336
+ var tableSchema = z10.object({
2337
+ name: z10.string(),
2338
+ schemaName: z10.string().optional(),
2339
+ clientColumn: z10.string().optional(),
2340
+ label: z10.string().optional(),
2341
+ columns: z10.array(columnSchema)
2737
2342
  });
2738
- var relationshipSchema = z11.object({
2739
- parentTableName: z11.string(),
2740
- parentColumnName: z11.string(),
2741
- childTableName: z11.string(),
2742
- childColumnName: z11.string(),
2743
- cardinality: z11.enum(["OneToOne", "OneToMany", "ManyToOne", "ManyToMany"]),
2744
- join: z11.enum(["INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN"]),
2745
- relationshipName: z11.string()
2343
+ var relationshipSchema = z10.object({
2344
+ parentTableName: z10.string(),
2345
+ parentColumnName: z10.string(),
2346
+ childTableName: z10.string(),
2347
+ childColumnName: z10.string(),
2348
+ cardinality: z10.enum(["OneToOne", "OneToMany", "ManyToOne", "ManyToMany"]),
2349
+ join: z10.enum(["INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN"]),
2350
+ relationshipName: z10.string()
2746
2351
  });
2747
2352
  function datamartSuccessMessage(name, result) {
2748
2353
  return `Datamart "${name}" created successfully (ID: ${result.id || result._id || "UNKNOWN"}).
@@ -2763,21 +2368,21 @@ function registerCreateDatamart(server, client) {
2763
2368
  "create_datamart",
2764
2369
  "Create a production datamart from selected tables and relationships. Defaults to dry-run; apply requires explicit confirmation.",
2765
2370
  {
2766
- mode: z11.enum(["dry_run", "apply"]).default("dry_run").describe("dry_run returns the plan only; apply creates the datamart"),
2767
- confirm: z11.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
2768
- name: z11.string().describe("Unique datamart name"),
2769
- datasourceName: z11.string().optional().describe("Name of the datasource to use. Omit to list available datasources."),
2770
- tableList: z11.array(tableSchema).optional().describe("Array of tables with their columns"),
2771
- relationships: z11.array(relationshipSchema).optional().describe("Join definitions between tables"),
2772
- tenancySettings: z11.object({
2773
- tenancyLevel: z11.enum(["TABLE", "DATABASE"]).optional(),
2774
- clientColumnType: z11.string().optional(),
2775
- schemaName: z11.string().optional(),
2776
- tableName: z11.string().optional(),
2777
- tableClientNameColumn: z11.string().optional(),
2778
- tablePrimaryKeyColumn: z11.string().optional()
2371
+ mode: z10.enum(["dry_run", "apply"]).default("dry_run").describe("dry_run returns the plan only; apply creates the datamart"),
2372
+ confirm: z10.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
2373
+ name: z10.string().describe("Unique datamart name"),
2374
+ datasourceName: z10.string().optional().describe("Name of the datasource to use. Omit to list available datasources."),
2375
+ tableList: z10.array(tableSchema).optional().describe("Array of tables with their columns"),
2376
+ relationships: z10.array(relationshipSchema).optional().describe("Join definitions between tables"),
2377
+ tenancySettings: z10.object({
2378
+ tenancyLevel: z10.enum(["TABLE", "DATABASE"]).optional(),
2379
+ clientColumnType: z10.string().optional(),
2380
+ schemaName: z10.string().optional(),
2381
+ tableName: z10.string().optional(),
2382
+ tableClientNameColumn: z10.string().optional(),
2383
+ tablePrimaryKeyColumn: z10.string().optional()
2779
2384
  }).optional().describe("Multi-tenant configuration"),
2780
- body: z11.record(z11.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/datamarts")
2385
+ body: z10.record(z10.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/datamarts")
2781
2386
  },
2782
2387
  WRITE_CREATE,
2783
2388
  async (params) => {
@@ -2872,14 +2477,14 @@ function registerCreateDatamart(server, client) {
2872
2477
  }
2873
2478
 
2874
2479
  // src/tools/infrastructure/sync-datasource.ts
2875
- import { z as z12 } from "zod";
2480
+ import { z as z11 } from "zod";
2876
2481
  function registerSyncDatasource(server, client) {
2877
2482
  const datasourcesApi = new DatasourcesApi(client);
2878
2483
  server.tool(
2879
2484
  "sync_datasource",
2880
2485
  "Trigger a datasource schema sync to refresh table and column metadata. If datasourceName is omitted and there are multiple datasources, returns the list so the user can choose.",
2881
2486
  {
2882
- datasourceName: z12.string().optional().describe("Name of the datasource to sync. Omit to list available datasources.")
2487
+ datasourceName: z11.string().optional().describe("Name of the datasource to sync. Omit to list available datasources.")
2883
2488
  },
2884
2489
  WRITE_IDEMPOTENT,
2885
2490
  async (params) => {
@@ -2928,7 +2533,7 @@ The schema metadata will be refreshed. You can now use \`discover_datasource_sch
2928
2533
  }
2929
2534
 
2930
2535
  // src/tools/embed/create-embed.ts
2931
- import { z as z13 } from "zod";
2536
+ import { z as z12 } from "zod";
2932
2537
  var DEFAULT_ACCESS_SETTINGS2 = {
2933
2538
  isAllowMetricCreation: false,
2934
2539
  isAllowMetricDeletion: false,
@@ -2941,18 +2546,18 @@ var DEFAULT_ACCESS_SETTINGS2 = {
2941
2546
  isAllowCreateDashboardView: false,
2942
2547
  metricCreationMode: "DRAG_DROP"
2943
2548
  };
2944
- var accessSettingsSchema = z13.object({
2945
- datamartName: z13.string().describe("Name of the datamart (required)"),
2946
- isAllowMetricCreation: z13.boolean().optional().describe("Allow end-users to create metrics"),
2947
- metricCreationMode: z13.enum(["DRAG_DROP", "CHAT"]).optional().describe("Metric creation mode"),
2948
- isAllowMetricDeletion: z13.boolean().optional().describe("Allow metric deletion"),
2949
- isAllowMetricUpdate: z13.boolean().optional().describe("Allow metric editing"),
2950
- isAllowMetricLayoutChange: z13.boolean().optional().describe("Allow layout changes"),
2951
- isAllowManageMetrics: z13.boolean().optional().describe("Show manage metrics UI"),
2952
- isAllowUnderlyingData: z13.boolean().optional().describe("Allow viewing underlying data"),
2953
- isAllowAiPilot: z13.boolean().optional().describe("Enable AI pilot features"),
2954
- isAllowEmailReports: z13.boolean().optional().describe("Allow email report scheduling"),
2955
- isAllowCreateDashboardView: z13.boolean().optional().describe("Allow creating dashboard views")
2549
+ var accessSettingsSchema = z12.object({
2550
+ datamartName: z12.string().describe("Name of the datamart (required)"),
2551
+ isAllowMetricCreation: z12.boolean().optional().describe("Allow end-users to create metrics"),
2552
+ metricCreationMode: z12.enum(["DRAG_DROP", "CHAT"]).optional().describe("Metric creation mode"),
2553
+ isAllowMetricDeletion: z12.boolean().optional().describe("Allow metric deletion"),
2554
+ isAllowMetricUpdate: z12.boolean().optional().describe("Allow metric editing"),
2555
+ isAllowMetricLayoutChange: z12.boolean().optional().describe("Allow layout changes"),
2556
+ isAllowManageMetrics: z12.boolean().optional().describe("Show manage metrics UI"),
2557
+ isAllowUnderlyingData: z12.boolean().optional().describe("Allow viewing underlying data"),
2558
+ isAllowAiPilot: z12.boolean().optional().describe("Enable AI pilot features"),
2559
+ isAllowEmailReports: z12.boolean().optional().describe("Allow email report scheduling"),
2560
+ isAllowCreateDashboardView: z12.boolean().optional().describe("Allow creating dashboard views")
2956
2561
  }).describe("Embed access settings. Only datamartName is required \u2014 all flags default to false.");
2957
2562
  function registerCreateEmbed(server, client) {
2958
2563
  const services = createServiceBundle(client);
@@ -2960,23 +2565,23 @@ function registerCreateEmbed(server, client) {
2960
2565
  "create_embed",
2961
2566
  "Create an embed. Provide dashboardId, workspaceName, accessSettings.datamartName. Add clientId for multi-tenant per-client dashboards. All isAllow* flags default to false.",
2962
2567
  {
2963
- body: z13.record(z13.unknown()).optional().describe(
2568
+ body: z12.record(z12.unknown()).optional().describe(
2964
2569
  "Raw API body passthrough. If provided, all other fields are ignored."
2965
2570
  ),
2966
- dashboardId: z13.string().optional().describe("Dashboard identifier for embed creation"),
2967
- workspaceName: z13.string().optional().describe("Workspace name. Omit to auto-resolve if only one exists."),
2968
- embedType: z13.enum(["dashboard", "metric"]).optional().describe("Embed type"),
2969
- metricId: z13.string().optional().describe("Required for metric embed type"),
2970
- name: z13.string().optional().describe("Human-readable embed config name"),
2571
+ dashboardId: z12.string().optional().describe("Dashboard identifier for embed creation"),
2572
+ workspaceName: z12.string().optional().describe("Workspace name. Omit to auto-resolve if only one exists."),
2573
+ embedType: z12.enum(["dashboard", "metric"]).optional().describe("Embed type"),
2574
+ metricId: z12.string().optional().describe("Required for metric embed type"),
2575
+ name: z12.string().optional().describe("Human-readable embed config name"),
2971
2576
  accessSettings: accessSettingsSchema.optional(),
2972
2577
  // Multi-tenant (dashboard embed) params
2973
- clientId: z13.string().optional().describe(
2578
+ clientId: z12.string().optional().describe(
2974
2579
  "Client identifier for multi-tenant isolation. When provided, creates a dashboard embed (each client gets their own dashboard copy)."
2975
2580
  ),
2976
- templateDashboardId: z13.string().optional().describe("Template dashboard ID to clone metrics from"),
2977
- isImportMetrics: z13.boolean().optional().describe("Import metrics from template dashboard"),
2978
- metricImportMode: z13.enum(["COPY", "LINK"]).optional().describe("COPY duplicates metrics, LINK shares them"),
2979
- isRenameDashboard: z13.boolean().optional().describe("Rename the dashboard for this client")
2581
+ templateDashboardId: z12.string().optional().describe("Template dashboard ID to clone metrics from"),
2582
+ isImportMetrics: z12.boolean().optional().describe("Import metrics from template dashboard"),
2583
+ metricImportMode: z12.enum(["COPY", "LINK"]).optional().describe("COPY duplicates metrics, LINK shares them"),
2584
+ isRenameDashboard: z12.boolean().optional().describe("Rename the dashboard for this client")
2980
2585
  },
2981
2586
  WRITE_CREATE,
2982
2587
  async (params) => {
@@ -3078,14 +2683,14 @@ Next: run \`get_embed_details\` to verify the embed configuration.`
3078
2683
  }
3079
2684
 
3080
2685
  // src/tools/embed/list-embeds.ts
3081
- import { z as z14 } from "zod";
2686
+ import { z as z13 } from "zod";
3082
2687
  function registerListEmbeds(server, client) {
3083
2688
  const services = createServiceBundle(client);
3084
2689
  server.tool(
3085
2690
  "list_embeds",
3086
2691
  "List embed configurations. Requires an API token (use create_api_token first). Use `list_tenants` for authoritative client discovery from the tenancy database.",
3087
2692
  {
3088
- dataAppName: z14.string().optional().describe("Filter by data app name")
2693
+ dataAppName: z13.string().optional().describe("Filter by data app name")
3089
2694
  },
3090
2695
  READ_ONLY,
3091
2696
  async (params) => {
@@ -3136,14 +2741,14 @@ function registerListEmbeds(server, client) {
3136
2741
  }
3137
2742
 
3138
2743
  // src/tools/embed/get-embed-details.ts
3139
- import { z as z15 } from "zod";
2744
+ import { z as z14 } from "zod";
3140
2745
  function registerGetEmbedDetails(server, client) {
3141
2746
  const services = createServiceBundle(client);
3142
2747
  server.tool(
3143
2748
  "get_embed_details",
3144
2749
  "Get full embed configuration including access settings and theme.",
3145
2750
  {
3146
- embedId: z15.string().describe("ID of the embed")
2751
+ embedId: z14.string().describe("ID of the embed")
3147
2752
  },
3148
2753
  READ_ONLY,
3149
2754
  async (params) => {
@@ -3159,7 +2764,7 @@ function registerGetEmbedDetails(server, client) {
3159
2764
  }
3160
2765
 
3161
2766
  // src/tools/embed/update-embed.ts
3162
- import { z as z16 } from "zod";
2767
+ import { z as z15 } from "zod";
3163
2768
 
3164
2769
  // src/tools/embed/helpers.ts
3165
2770
  var GUEST_TOKEN_REMINDER = "\n\n\u26A0\uFE0F **Guest tokens must be regenerated** for these changes to take effect. Existing guest tokens still carry the old settings. Call `generate_guest_token` to create a new token with the updated configuration.";
@@ -3197,15 +2802,15 @@ function registerUpdateEmbed(server, client) {
3197
2802
  "update_embed",
3198
2803
  "Update embed access settings, permissions, theme, or name.",
3199
2804
  {
3200
- embedId: z16.string().describe("ID of the embed to update"),
3201
- body: z16.record(z16.unknown()).optional().describe(
2805
+ embedId: z15.string().describe("ID of the embed to update"),
2806
+ body: z15.record(z15.unknown()).optional().describe(
3202
2807
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, it is sent directly (passthrough mode)."
3203
2808
  ),
3204
- name: z16.string().optional().describe("New name for the embed"),
3205
- accessSettings: z16.record(z16.unknown()).optional().describe(
2809
+ name: z15.string().optional().describe("New name for the embed"),
2810
+ accessSettings: z15.record(z15.unknown()).optional().describe(
3206
2811
  "Partial update of access settings. See `get_embed_details` for current settings."
3207
2812
  ),
3208
- theme: z16.record(z16.unknown()).optional().describe(
2813
+ theme: z15.record(z15.unknown()).optional().describe(
3209
2814
  "Theme configuration object. See databrain://theme-reference for the full schema."
3210
2815
  )
3211
2816
  },
@@ -3251,7 +2856,7 @@ function registerUpdateEmbed(server, client) {
3251
2856
  }
3252
2857
 
3253
2858
  // src/tools/embed/customize-theme.ts
3254
- import { z as z17 } from "zod";
2859
+ import { z as z16 } from "zod";
3255
2860
  var THEME_PRESETS = {
3256
2861
  light: {
3257
2862
  general: {
@@ -3385,7 +2990,7 @@ var ACCESS_PRESETS = {
3385
2990
  metricCreationMode: "CHAT"
3386
2991
  }
3387
2992
  };
3388
- var fontFamilyEnum = z17.enum([
2993
+ var fontFamilyEnum = z16.enum([
3389
2994
  "Inter",
3390
2995
  "Roboto",
3391
2996
  "Open Sans",
@@ -3397,17 +3002,17 @@ var fontFamilyEnum = z17.enum([
3397
3002
  "Nunito",
3398
3003
  "system-ui"
3399
3004
  ]).describe("Font family");
3400
- var tooltipFontSchema = z17.object({
3401
- fontFamily: z17.string().optional(),
3402
- fontSize: z17.number().optional(),
3403
- fontWeight: z17.number().optional(),
3404
- fontColor: z17.string().optional()
3005
+ var tooltipFontSchema = z16.object({
3006
+ fontFamily: z16.string().optional(),
3007
+ fontSize: z16.number().optional(),
3008
+ fontWeight: z16.number().optional(),
3009
+ fontColor: z16.string().optional()
3405
3010
  }).optional();
3406
- var fontConfigInputSchema = z17.object({
3407
- fontSize: z17.number().optional(),
3408
- fontFamily: z17.string().optional(),
3409
- fontWeight: z17.number().optional(),
3410
- color: z17.string().optional()
3011
+ var fontConfigInputSchema = z16.object({
3012
+ fontSize: z16.number().optional(),
3013
+ fontFamily: z16.string().optional(),
3014
+ fontWeight: z16.number().optional(),
3015
+ color: z16.string().optional()
3411
3016
  }).optional();
3412
3017
  function mapToFrontendFontConfig(input) {
3413
3018
  if (!input) return void 0;
@@ -3418,23 +3023,23 @@ function mapToFrontendFontConfig(input) {
3418
3023
  ...input.color && { color: input.color }
3419
3024
  };
3420
3025
  }
3421
- var axisSchema = z17.object({
3422
- hideAxisLines: z17.boolean().optional().describe("Hide axis lines"),
3423
- hideSplitLines: z17.boolean().optional().describe("Hide grid/split lines"),
3424
- hideAxisLabels: z17.boolean().optional().describe("Hide axis labels"),
3425
- hideAxisTicks: z17.boolean().optional().describe("Hide axis tick marks"),
3426
- axisName: z17.string().optional().describe("Axis title text"),
3427
- axisNameOffset: z17.number().optional().describe("Axis title offset from axis"),
3428
- axisLabelMargin: z17.number().optional().describe("Margin between axis labels and chart area in pixels"),
3429
- fontSize: z17.number().optional().describe("Axis label font size"),
3430
- fontFamily: z17.string().optional().describe("Axis label font family"),
3431
- fontWeight: z17.number().optional().describe("Axis label font weight"),
3432
- color: z17.string().optional().describe("Axis label color (hex)"),
3433
- axisColor: z17.string().optional().describe("Axis line color (hex)"),
3434
- axisNameFontConfig: z17.object({
3435
- fontFamily: z17.string().optional(),
3436
- fontSize: z17.number().optional(),
3437
- fontWeight: z17.number().optional()
3026
+ var axisSchema = z16.object({
3027
+ hideAxisLines: z16.boolean().optional().describe("Hide axis lines"),
3028
+ hideSplitLines: z16.boolean().optional().describe("Hide grid/split lines"),
3029
+ hideAxisLabels: z16.boolean().optional().describe("Hide axis labels"),
3030
+ hideAxisTicks: z16.boolean().optional().describe("Hide axis tick marks"),
3031
+ axisName: z16.string().optional().describe("Axis title text"),
3032
+ axisNameOffset: z16.number().optional().describe("Axis title offset from axis"),
3033
+ axisLabelMargin: z16.number().optional().describe("Margin between axis labels and chart area in pixels"),
3034
+ fontSize: z16.number().optional().describe("Axis label font size"),
3035
+ fontFamily: z16.string().optional().describe("Axis label font family"),
3036
+ fontWeight: z16.number().optional().describe("Axis label font weight"),
3037
+ color: z16.string().optional().describe("Axis label color (hex)"),
3038
+ axisColor: z16.string().optional().describe("Axis line color (hex)"),
3039
+ axisNameFontConfig: z16.object({
3040
+ fontFamily: z16.string().optional(),
3041
+ fontSize: z16.number().optional(),
3042
+ fontWeight: z16.number().optional()
3438
3043
  }).optional().describe("Axis title font configuration")
3439
3044
  }).optional();
3440
3045
  function buildAdminTheme(p, presetBase) {
@@ -3590,94 +3195,94 @@ function registerCustomizeTheme(server, client) {
3590
3195
  "customize_embed_theme",
3591
3196
  `Theme an embed: presets (${Object.keys(THEME_PRESETS).join("/")}), colors, fonts, cards, chart appearance, responsive layout, or access presets (${Object.keys(ACCESS_PRESETS).join("/")}).`,
3592
3197
  {
3593
- embedId: z17.string().describe("ID of the embed to theme"),
3594
- body: z17.record(z17.unknown()).optional().describe(
3198
+ embedId: z16.string().describe("ID of the embed to theme"),
3199
+ body: z16.record(z16.unknown()).optional().describe(
3595
3200
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, sent directly and all convenience fields are ignored."
3596
3201
  ),
3597
3202
  // --- Presets ---
3598
- themePreset: z17.enum(["light", "dark", "corporate", "minimal"]).optional().describe("Apply a theme preset as the base. Granular params below override preset values."),
3599
- accessPreset: z17.enum(["view-only", "power-user", "export-only", "ai-enabled"]).optional().describe("Apply an access settings preset"),
3203
+ themePreset: z16.enum(["light", "dark", "corporate", "minimal"]).optional().describe("Apply a theme preset as the base. Granular params below override preset values."),
3204
+ accessPreset: z16.enum(["view-only", "power-user", "export-only", "ai-enabled"]).optional().describe("Apply an access settings preset"),
3600
3205
  // --- General ---
3601
3206
  fontFamily: fontFamilyEnum.optional().describe("Global font family"),
3602
- datePickerFormat: z17.string().optional().describe("Date picker display format"),
3207
+ datePickerFormat: z16.string().optional().describe("Date picker display format"),
3603
3208
  // --- Colors ---
3604
- primaryColor: z17.string().optional().describe("Primary accent color (hex)"),
3605
- backgroundColor: z17.string().optional().describe("Overall background color (hex)"),
3606
- textColor: z17.string().optional().describe("Default text color (hex)"),
3607
- ctaColor: z17.string().optional().describe("Call-to-action button color (hex)"),
3608
- ctaTextColor: z17.string().optional().describe("CTA button text color (hex)"),
3209
+ primaryColor: z16.string().optional().describe("Primary accent color (hex)"),
3210
+ backgroundColor: z16.string().optional().describe("Overall background color (hex)"),
3211
+ textColor: z16.string().optional().describe("Default text color (hex)"),
3212
+ ctaColor: z16.string().optional().describe("Call-to-action button color (hex)"),
3213
+ ctaTextColor: z16.string().optional().describe("CTA button text color (hex)"),
3609
3214
  // --- Dashboard ---
3610
- dashboardBackgroundColor: z17.string().optional().describe("Dashboard container background (hex)"),
3611
- selectBoxSize: z17.enum(["small", "medium", "large"]).optional().describe("Filter select box size"),
3612
- selectBoxVariant: z17.enum(["floating", "outlined", "standard"]).optional().describe("Filter select box style"),
3613
- selectBoxBorderRadius: z17.string().optional().describe('Select box border radius (e.g. "20px")'),
3614
- selectBoxTextColor: z17.string().optional().describe("Select box text color (hex)"),
3615
- metricCardColor: z17.string().optional().describe("Metric card background color (hex)"),
3215
+ dashboardBackgroundColor: z16.string().optional().describe("Dashboard container background (hex)"),
3216
+ selectBoxSize: z16.enum(["small", "medium", "large"]).optional().describe("Filter select box size"),
3217
+ selectBoxVariant: z16.enum(["floating", "outlined", "standard"]).optional().describe("Filter select box style"),
3218
+ selectBoxBorderRadius: z16.string().optional().describe('Select box border radius (e.g. "20px")'),
3219
+ selectBoxTextColor: z16.string().optional().describe("Select box text color (hex)"),
3220
+ metricCardColor: z16.string().optional().describe("Metric card background color (hex)"),
3616
3221
  // --- Card Title ---
3617
- cardTitleFontSize: z17.string().optional().describe('Card title font size (e.g. "16px")'),
3618
- cardTitleFontWeight: z17.string().optional().describe('Card title font weight (e.g. "600")'),
3619
- cardTitleColor: z17.string().optional().describe("Card title text color (hex)"),
3222
+ cardTitleFontSize: z16.string().optional().describe('Card title font size (e.g. "16px")'),
3223
+ cardTitleFontWeight: z16.string().optional().describe('Card title font weight (e.g. "600")'),
3224
+ cardTitleColor: z16.string().optional().describe("Card title text color (hex)"),
3620
3225
  // --- Card Description ---
3621
- cardDescriptionFontSize: z17.string().optional().describe("Card description font size"),
3622
- cardDescriptionFontWeight: z17.string().optional().describe("Card description font weight"),
3623
- cardDescriptionColor: z17.string().optional().describe("Card description text color (hex)"),
3226
+ cardDescriptionFontSize: z16.string().optional().describe("Card description font size"),
3227
+ cardDescriptionFontWeight: z16.string().optional().describe("Card description font weight"),
3228
+ cardDescriptionColor: z16.string().optional().describe("Card description text color (hex)"),
3624
3229
  // --- Card Customization ---
3625
- cardPadding: z17.string().optional().describe("Card internal padding"),
3626
- cardBorderRadius: z17.string().optional().describe("Card border radius"),
3627
- cardShadow: z17.string().optional().describe("Card box-shadow CSS value"),
3628
- disableShadowOnHover: z17.boolean().optional().describe("Disable shadow on card hover"),
3629
- disableCardStroke: z17.boolean().optional().describe("Remove card border stroke"),
3630
- cardStrokeColor: z17.string().optional().describe("Card border stroke color (hex)"),
3230
+ cardPadding: z16.string().optional().describe("Card internal padding"),
3231
+ cardBorderRadius: z16.string().optional().describe("Card border radius"),
3232
+ cardShadow: z16.string().optional().describe("Card box-shadow CSS value"),
3233
+ disableShadowOnHover: z16.boolean().optional().describe("Disable shadow on card hover"),
3234
+ disableCardStroke: z16.boolean().optional().describe("Remove card border stroke"),
3235
+ cardStrokeColor: z16.string().optional().describe("Card border stroke color (hex)"),
3631
3236
  // --- Chart Colors (admin theme level) ---
3632
- chartFontFamily: z17.string().optional().describe("Font family for chart text"),
3633
- chartFontColor: z17.string().optional().describe("Font color for chart text (hex)"),
3634
- chartColors: z17.array(z17.string()).optional().describe("Array of chart series colors (hex)"),
3635
- chartPaletteName: z17.string().optional().describe("Name for a custom palette"),
3636
- chartPaletteColors: z17.array(z17.string()).optional().describe("9 colors for a custom palette"),
3237
+ chartFontFamily: z16.string().optional().describe("Font family for chart text"),
3238
+ chartFontColor: z16.string().optional().describe("Font color for chart text (hex)"),
3239
+ chartColors: z16.array(z16.string()).optional().describe("Array of chart series colors (hex)"),
3240
+ chartPaletteName: z16.string().optional().describe("Name for a custom palette"),
3241
+ chartPaletteColors: z16.array(z16.string()).optional().describe("9 colors for a custom palette"),
3637
3242
  // --- Chart Tooltip (admin theme level) ---
3638
3243
  tooltipHeader: tooltipFontSchema.describe("Tooltip header font config"),
3639
3244
  tooltipLabel: tooltipFontSchema.describe("Tooltip label font config"),
3640
3245
  tooltipValue: tooltipFontSchema.describe("Tooltip value font config"),
3641
3246
  // --- Component Theme ---
3642
- buttonPrimaryColor: z17.string().optional().describe("Primary button background (hex)"),
3643
- buttonPrimaryTextColor: z17.string().optional().describe("Primary button text (hex)"),
3644
- buttonSecondaryColor: z17.string().optional().describe("Secondary button background (hex)"),
3645
- buttonSecondaryTextColor: z17.string().optional().describe("Secondary button text (hex)"),
3646
- checkboxCheckedColor: z17.string().optional().describe("Checked checkbox color (hex)"),
3647
- checkboxUncheckedColor: z17.string().optional().describe("Unchecked checkbox color (hex)"),
3648
- switchEnabledColor: z17.string().optional().describe("Enabled switch color (hex)"),
3649
- switchDisabledColor: z17.string().optional().describe("Disabled switch color (hex)"),
3650
- breadcrumbFontFamily: z17.string().optional().describe("Drill breadcrumb font family"),
3651
- breadcrumbFontColor: z17.string().optional().describe("Drill breadcrumb font color (hex)"),
3652
- breadcrumbActiveColor: z17.string().optional().describe("Active breadcrumb color (hex)"),
3653
- filterBadgeColor: z17.string().optional().describe("Multi-select filter badge color (hex)"),
3654
- filterBadgeTextColor: z17.string().optional().describe("Filter badge text color (hex)"),
3655
- datePickerColor: z17.string().optional().describe("Date picker accent color (hex)"),
3247
+ buttonPrimaryColor: z16.string().optional().describe("Primary button background (hex)"),
3248
+ buttonPrimaryTextColor: z16.string().optional().describe("Primary button text (hex)"),
3249
+ buttonSecondaryColor: z16.string().optional().describe("Secondary button background (hex)"),
3250
+ buttonSecondaryTextColor: z16.string().optional().describe("Secondary button text (hex)"),
3251
+ checkboxCheckedColor: z16.string().optional().describe("Checked checkbox color (hex)"),
3252
+ checkboxUncheckedColor: z16.string().optional().describe("Unchecked checkbox color (hex)"),
3253
+ switchEnabledColor: z16.string().optional().describe("Enabled switch color (hex)"),
3254
+ switchDisabledColor: z16.string().optional().describe("Disabled switch color (hex)"),
3255
+ breadcrumbFontFamily: z16.string().optional().describe("Drill breadcrumb font family"),
3256
+ breadcrumbFontColor: z16.string().optional().describe("Drill breadcrumb font color (hex)"),
3257
+ breadcrumbActiveColor: z16.string().optional().describe("Active breadcrumb color (hex)"),
3258
+ filterBadgeColor: z16.string().optional().describe("Multi-select filter badge color (hex)"),
3259
+ filterBadgeTextColor: z16.string().optional().describe("Filter badge text color (hex)"),
3260
+ datePickerColor: z16.string().optional().describe("Date picker accent color (hex)"),
3656
3261
  // --- Responsive Breakpoints ---
3657
- breakpointXl: z17.number().optional().describe("XL breakpoint width in px"),
3658
- breakpointLg: z17.number().optional().describe("LG breakpoint width in px"),
3659
- breakpointMd: z17.number().optional().describe("MD breakpoint width in px"),
3660
- breakpointSm: z17.number().optional().describe("SM breakpoint width in px"),
3661
- breakpointXs: z17.number().optional().describe("XS breakpoint width in px"),
3262
+ breakpointXl: z16.number().optional().describe("XL breakpoint width in px"),
3263
+ breakpointLg: z16.number().optional().describe("LG breakpoint width in px"),
3264
+ breakpointMd: z16.number().optional().describe("MD breakpoint width in px"),
3265
+ breakpointSm: z16.number().optional().describe("SM breakpoint width in px"),
3266
+ breakpointXs: z16.number().optional().describe("XS breakpoint width in px"),
3662
3267
  // --- Metric Layout Columns ---
3663
- layoutColsXl: z17.number().optional().describe("Grid columns at XL breakpoint"),
3664
- layoutColsLg: z17.number().optional().describe("Grid columns at LG breakpoint"),
3665
- layoutColsMd: z17.number().optional().describe("Grid columns at MD breakpoint"),
3666
- layoutColsSm: z17.number().optional().describe("Grid columns at SM breakpoint"),
3667
- layoutColsXs: z17.number().optional().describe("Grid columns at XS breakpoint"),
3668
- layoutColsXxs: z17.number().optional().describe("Grid columns at XXS breakpoint"),
3268
+ layoutColsXl: z16.number().optional().describe("Grid columns at XL breakpoint"),
3269
+ layoutColsLg: z16.number().optional().describe("Grid columns at LG breakpoint"),
3270
+ layoutColsMd: z16.number().optional().describe("Grid columns at MD breakpoint"),
3271
+ layoutColsSm: z16.number().optional().describe("Grid columns at SM breakpoint"),
3272
+ layoutColsXs: z16.number().optional().describe("Grid columns at XS breakpoint"),
3273
+ layoutColsXxs: z16.number().optional().describe("Grid columns at XXS breakpoint"),
3669
3274
  // --- Chart Appearance (detailed) ---
3670
3275
  tooltipHeaderStyle: fontConfigInputSchema.describe("Tooltip header: fontSize, fontFamily, fontWeight, color"),
3671
3276
  tooltipLabelStyle: fontConfigInputSchema.describe("Tooltip label: fontSize, fontFamily, fontWeight, color"),
3672
3277
  tooltipValueStyle: fontConfigInputSchema.describe("Tooltip value: fontSize, fontFamily, fontWeight, color"),
3673
- labelPosition: z17.enum(["hidden", "top", "left", "right", "bottom", "inside"]).optional().describe("Data label position on charts"),
3674
- radialLabelPosition: z17.enum(["outside", "inside"]).optional().describe("Label position for radial/pie charts"),
3675
- marginTop: z17.number().optional().describe("Chart top margin in pixels"),
3676
- marginLeft: z17.number().optional().describe("Chart left margin in pixels"),
3677
- marginRight: z17.number().optional().describe("Chart right margin in pixels"),
3678
- marginBottom: z17.number().optional().describe("Chart bottom margin in pixels"),
3679
- legendShow: z17.boolean().optional().describe("Show or hide chart legend"),
3680
- legendPosition: z17.enum([
3278
+ labelPosition: z16.enum(["hidden", "top", "left", "right", "bottom", "inside"]).optional().describe("Data label position on charts"),
3279
+ radialLabelPosition: z16.enum(["outside", "inside"]).optional().describe("Label position for radial/pie charts"),
3280
+ marginTop: z16.number().optional().describe("Chart top margin in pixels"),
3281
+ marginLeft: z16.number().optional().describe("Chart left margin in pixels"),
3282
+ marginRight: z16.number().optional().describe("Chart right margin in pixels"),
3283
+ marginBottom: z16.number().optional().describe("Chart bottom margin in pixels"),
3284
+ legendShow: z16.boolean().optional().describe("Show or hide chart legend"),
3285
+ legendPosition: z16.enum([
3681
3286
  "top-left",
3682
3287
  "top-center",
3683
3288
  "top-right",
@@ -3687,17 +3292,17 @@ function registerCustomizeTheme(server, client) {
3687
3292
  "bottom-center",
3688
3293
  "bottom-right"
3689
3294
  ]).optional().describe("Legend position"),
3690
- legendLayout: z17.enum(["horizontal", "vertical"]).optional().describe("Legend layout direction"),
3691
- legendShape: z17.enum(["circle", "rect", "roundRect", "triangle", "diamond", "arrow", "none"]).optional().describe("Legend icon shape"),
3692
- legendFontSize: z17.number().optional().describe("Legend font size"),
3693
- legendFontWeight: z17.number().optional().describe("Legend font weight"),
3694
- legendFontFamily: z17.string().optional().describe("Legend font family"),
3695
- legendColor: z17.string().optional().describe("Legend text color (hex)"),
3696
- legendTruncateLength: z17.number().optional().describe("Max characters before truncating legend"),
3697
- disableLegendScrolling: z17.boolean().optional().describe("Disable scrolling in legend"),
3698
- enableVariableLegendPosition: z17.boolean().optional().describe("Allow user to drag legend"),
3699
- legendTop: z17.number().optional().describe("Custom legend top offset"),
3700
- legendLeft: z17.number().optional().describe("Custom legend left offset"),
3295
+ legendLayout: z16.enum(["horizontal", "vertical"]).optional().describe("Legend layout direction"),
3296
+ legendShape: z16.enum(["circle", "rect", "roundRect", "triangle", "diamond", "arrow", "none"]).optional().describe("Legend icon shape"),
3297
+ legendFontSize: z16.number().optional().describe("Legend font size"),
3298
+ legendFontWeight: z16.number().optional().describe("Legend font weight"),
3299
+ legendFontFamily: z16.string().optional().describe("Legend font family"),
3300
+ legendColor: z16.string().optional().describe("Legend text color (hex)"),
3301
+ legendTruncateLength: z16.number().optional().describe("Max characters before truncating legend"),
3302
+ disableLegendScrolling: z16.boolean().optional().describe("Disable scrolling in legend"),
3303
+ enableVariableLegendPosition: z16.boolean().optional().describe("Allow user to drag legend"),
3304
+ legendTop: z16.number().optional().describe("Custom legend top offset"),
3305
+ legendLeft: z16.number().optional().describe("Custom legend left offset"),
3701
3306
  verticalAxis: axisSchema.describe("Vertical (Y) axis configuration"),
3702
3307
  horizontalAxis: axisSchema.describe("Horizontal (X) axis configuration")
3703
3308
  },
@@ -3772,7 +3377,7 @@ Use \`get_embed_details\` to view the full current theme configuration.` + GUEST
3772
3377
  }
3773
3378
 
3774
3379
  // src/tools/embed/get-demo-link.ts
3775
- import { z as z18 } from "zod";
3380
+ import { z as z17 } from "zod";
3776
3381
  var HOUR_MS = 60 * 60 * 1e3;
3777
3382
  var DEFAULT_EXPIRY_HOURS = 24;
3778
3383
  var MAX_EXPIRY_HOURS = 24 * 30;
@@ -3782,11 +3387,11 @@ function registerGetDemoLink(server, client) {
3782
3387
  "get_demo_link",
3783
3388
  `Get a shareable demo URL for a dashboard embed scoped to a specific clientId \u2014 same link the UI's "Share Demo Link" generates. Loads the dashboard as that end-customer sees it. Use to verify ops-built widgets without a test app.`,
3784
3389
  {
3785
- clientId: z18.string().describe("End-customer identifier to scope the preview to."),
3786
- dashboardName: z18.string().optional().describe("Dashboard name (non-technical). Preferred \u2014 pass with optional `workspaceName`."),
3787
- workspaceName: z18.string().optional().describe("Workspace name. Only needed if `dashboardName` is ambiguous."),
3788
- embedId: z18.string().optional().describe("Technical alternative to `dashboardName`."),
3789
- expiryHours: z18.number().min(1).max(MAX_EXPIRY_HOURS).optional().describe(`Token TTL in hours. Defaults to ${DEFAULT_EXPIRY_HOURS}. Max ${MAX_EXPIRY_HOURS} (30 days).`)
3390
+ clientId: z17.string().describe("End-customer identifier to scope the preview to."),
3391
+ dashboardName: z17.string().optional().describe("Dashboard name (non-technical). Preferred \u2014 pass with optional `workspaceName`."),
3392
+ workspaceName: z17.string().optional().describe("Workspace name. Only needed if `dashboardName` is ambiguous."),
3393
+ embedId: z17.string().optional().describe("Technical alternative to `dashboardName`."),
3394
+ expiryHours: z17.number().min(1).max(MAX_EXPIRY_HOURS).optional().describe(`Token TTL in hours. Defaults to ${DEFAULT_EXPIRY_HOURS}. Max ${MAX_EXPIRY_HOURS} (30 days).`)
3790
3395
  },
3791
3396
  WRITE_CREATE,
3792
3397
  async (params) => {
@@ -3842,70 +3447,70 @@ function registerGetDemoLink(server, client) {
3842
3447
  }
3843
3448
 
3844
3449
  // src/tools/frontend/generate-guest-token.ts
3845
- import { z as z19 } from "zod";
3450
+ import { z as z18 } from "zod";
3846
3451
  function registerGenerateGuestToken(server, client) {
3847
3452
  const services = createServiceBundle(client);
3848
3453
  server.tool(
3849
3454
  "generate_guest_token",
3850
3455
  "Generate a guest token for frontend embedding. Requires clientId. Omit dataAppName to auto-resolve.",
3851
3456
  {
3852
- clientId: z19.string().describe('Unique end-user identifier for multi-tenant filtering. Use "None" if no tenancy.'),
3853
- dataAppName: z19.string().optional().describe("Name of the data app. Omit to list available data apps."),
3854
- body: z19.record(z19.unknown()).optional().describe(
3457
+ clientId: z18.string().describe('Unique end-user identifier for multi-tenant filtering. Use "None" if no tenancy.'),
3458
+ dataAppName: z18.string().optional().describe("Name of the data app. Omit to list available data apps."),
3459
+ body: z18.record(z18.unknown()).optional().describe(
3855
3460
  "Raw API body for POST /api/v2/guest-token/create. If provided, sent directly and convenience fields are ignored."
3856
3461
  ),
3857
- params: z19.object({
3858
- allowedEmbeds: z19.array(z19.string()).optional().describe("Restrict token to specific embed IDs"),
3859
- rlsSettings: z19.array(z19.object({
3860
- metricId: z19.string().describe("Metric ID"),
3861
- values: z19.record(z19.unknown()).describe("RLS values by column name")
3462
+ params: z18.object({
3463
+ allowedEmbeds: z18.array(z18.string()).optional().describe("Restrict token to specific embed IDs"),
3464
+ rlsSettings: z18.array(z18.object({
3465
+ metricId: z18.string().describe("Metric ID"),
3466
+ values: z18.record(z18.unknown()).describe("RLS values by column name")
3862
3467
  })).optional().describe("Legacy alias used in docs/examples; forwarded as params.rlsSettings"),
3863
- appFilters: z19.array(z19.object({
3864
- metricId: z19.string().describe("Metric ID"),
3865
- values: z19.record(z19.unknown()).describe("Filter values by column name")
3468
+ appFilters: z18.array(z18.object({
3469
+ metricId: z18.string().describe("Metric ID"),
3470
+ values: z18.record(z18.unknown()).describe("Filter values by column name")
3866
3471
  })).optional().describe("Metric-level app filters"),
3867
- dashboardAppFilters: z19.array(z19.object({
3868
- dashboardId: z19.string().describe("Dashboard ID"),
3869
- values: z19.record(z19.unknown()).describe("Filter values by dashboard filter name"),
3870
- isShowOnUrl: z19.boolean().optional().describe("Whether filters are shown in URL params")
3472
+ dashboardAppFilters: z18.array(z18.object({
3473
+ dashboardId: z18.string().describe("Dashboard ID"),
3474
+ values: z18.record(z18.unknown()).describe("Filter values by dashboard filter name"),
3475
+ isShowOnUrl: z18.boolean().optional().describe("Whether filters are shown in URL params")
3871
3476
  })).optional().describe("Dashboard-level app filters"),
3872
- hideDashboardFilters: z19.array(z19.string()).optional().describe("Dashboard filter names to hide"),
3873
- userIdentifier: z19.string().optional().describe("End-user identifier for private metrics support"),
3874
- timezone: z19.string().optional().describe('IANA timezone, e.g. "America/New_York"')
3477
+ hideDashboardFilters: z18.array(z18.string()).optional().describe("Dashboard filter names to hide"),
3478
+ userIdentifier: z18.string().optional().describe("End-user identifier for private metrics support"),
3479
+ timezone: z18.string().optional().describe('IANA timezone, e.g. "America/New_York"')
3875
3480
  }).optional().describe("Advanced token params"),
3876
3481
  // Legacy convenience fields (auto-mapped into params for backward compatibility)
3877
- timezone: z19.string().optional().describe("Legacy: moved under params.timezone"),
3878
- allowedEmbeds: z19.array(z19.string()).optional().describe("Legacy: moved under params.allowedEmbeds"),
3879
- rlsSettings: z19.array(z19.object({
3880
- metricId: z19.string().describe("Metric ID"),
3881
- values: z19.record(z19.unknown()).describe("RLS values by column name")
3482
+ timezone: z18.string().optional().describe("Legacy: moved under params.timezone"),
3483
+ allowedEmbeds: z18.array(z18.string()).optional().describe("Legacy: moved under params.allowedEmbeds"),
3484
+ rlsSettings: z18.array(z18.object({
3485
+ metricId: z18.string().describe("Metric ID"),
3486
+ values: z18.record(z18.unknown()).describe("RLS values by column name")
3882
3487
  })).optional().describe("Legacy: moved under params.rlsSettings"),
3883
- appFilters: z19.array(z19.object({
3884
- metricId: z19.string().describe("Metric ID"),
3885
- values: z19.record(z19.unknown()).describe("Filter values by column name")
3488
+ appFilters: z18.array(z18.object({
3489
+ metricId: z18.string().describe("Metric ID"),
3490
+ values: z18.record(z18.unknown()).describe("Filter values by column name")
3886
3491
  })).optional().describe("Legacy: moved under params.appFilters"),
3887
- dashboardAppFilters: z19.array(z19.object({
3888
- dashboardId: z19.string().describe("Dashboard ID to apply filters to"),
3889
- values: z19.record(z19.unknown()).describe("Filter values for this dashboard"),
3890
- isShowOnUrl: z19.boolean().optional().describe("Whether filters are shown in URL params")
3492
+ dashboardAppFilters: z18.array(z18.object({
3493
+ dashboardId: z18.string().describe("Dashboard ID to apply filters to"),
3494
+ values: z18.record(z18.unknown()).describe("Filter values for this dashboard"),
3495
+ isShowOnUrl: z18.boolean().optional().describe("Whether filters are shown in URL params")
3891
3496
  })).optional().describe("Legacy: moved under params.dashboardAppFilters"),
3892
- permissions: z19.object({
3893
- isEnableArchiveMetrics: z19.boolean().optional(),
3894
- isEnableManageMetrics: z19.boolean().optional(),
3895
- isEnableCreateDashboardView: z19.boolean().optional(),
3896
- isEnableMetricUpdation: z19.boolean().optional(),
3897
- isEnableCustomizeLayout: z19.boolean().optional(),
3898
- isEnableUnderlyingData: z19.boolean().optional(),
3899
- isEnableDownloadMetrics: z19.boolean().optional(),
3900
- isShowSideBar: z19.boolean().optional(),
3901
- isShowDashboardName: z19.boolean().optional(),
3902
- isDisableMetricCreation: z19.boolean().optional()
3497
+ permissions: z18.object({
3498
+ isEnableArchiveMetrics: z18.boolean().optional(),
3499
+ isEnableManageMetrics: z18.boolean().optional(),
3500
+ isEnableCreateDashboardView: z18.boolean().optional(),
3501
+ isEnableMetricUpdation: z18.boolean().optional(),
3502
+ isEnableCustomizeLayout: z18.boolean().optional(),
3503
+ isEnableUnderlyingData: z18.boolean().optional(),
3504
+ isEnableDownloadMetrics: z18.boolean().optional(),
3505
+ isShowSideBar: z18.boolean().optional(),
3506
+ isShowDashboardName: z18.boolean().optional(),
3507
+ isDisableMetricCreation: z18.boolean().optional()
3903
3508
  }).optional().describe("Runtime permission overrides (merge with and override embed-level settings)"),
3904
- theme: z19.record(z19.unknown()).optional().describe("Runtime theme overrides (overrides embed-level theme)"),
3905
- hideDashboardFilters: z19.array(z19.string()).optional().describe("Legacy: moved under params.hideDashboardFilters"),
3906
- userIdentifier: z19.string().optional().describe("Legacy: moved under params.userIdentifier"),
3907
- expiryTime: z19.number().optional().describe("Token TTL in milliseconds"),
3908
- datasourceName: z19.string().optional().describe("Datasource name for multi-datasource routing")
3509
+ theme: z18.record(z18.unknown()).optional().describe("Runtime theme overrides (overrides embed-level theme)"),
3510
+ hideDashboardFilters: z18.array(z18.string()).optional().describe("Legacy: moved under params.hideDashboardFilters"),
3511
+ userIdentifier: z18.string().optional().describe("Legacy: moved under params.userIdentifier"),
3512
+ expiryTime: z18.number().optional().describe("Token TTL in milliseconds"),
3513
+ datasourceName: z18.string().optional().describe("Datasource name for multi-datasource routing")
3909
3514
  },
3910
3515
  WRITE_CREATE,
3911
3516
  async (params) => {
@@ -3958,7 +3563,7 @@ Next step: Use \`generate_embed_code\` to get copy-paste-ready frontend code.`
3958
3563
  }
3959
3564
 
3960
3565
  // src/tools/frontend/generate-embed-code.ts
3961
- import { z as z20 } from "zod";
3566
+ import { z as z19 } from "zod";
3962
3567
 
3963
3568
  // src/templates/react.ts
3964
3569
  function kebabCase(str) {
@@ -4954,22 +4559,22 @@ function registerGenerateEmbedCode(server, _client) {
4954
4559
  "generate_embed_code",
4955
4560
  "Generate framework-specific embed code (React, Next.js, Vue, Angular, Svelte, SolidJS, vanilla HTML).",
4956
4561
  {
4957
- embedId: z20.string().describe("The embed ID to generate code for"),
4958
- framework: z20.enum(FRAMEWORKS).describe("Frontend framework"),
4959
- embedType: z20.enum(["dashboard", "metric"]).optional().default("dashboard"),
4960
- options: z20.object({
4961
- enableDownloadCsv: z20.boolean().optional(),
4962
- enableEmailCsv: z20.boolean().optional(),
4963
- enableDownloadAllMetrics: z20.boolean().optional(),
4964
- enableDownloadAllPdf: z20.boolean().optional(),
4965
- disableFullscreen: z20.boolean().optional(),
4966
- isHideTablePreview: z20.boolean().optional(),
4967
- isHideChartSettings: z20.boolean().optional(),
4968
- isStickyDashboardFilters: z20.boolean().optional(),
4969
- enableMultiMetricFilters: z20.boolean().optional(),
4970
- themeName: z20.string().optional(),
4971
- customTheme: z20.record(z20.unknown()).optional(),
4972
- optionsIcon: z20.enum(["kebab-menu-vertical", "download"]).optional()
4562
+ embedId: z19.string().describe("The embed ID to generate code for"),
4563
+ framework: z19.enum(FRAMEWORKS).describe("Frontend framework"),
4564
+ embedType: z19.enum(["dashboard", "metric"]).optional().default("dashboard"),
4565
+ options: z19.object({
4566
+ enableDownloadCsv: z19.boolean().optional(),
4567
+ enableEmailCsv: z19.boolean().optional(),
4568
+ enableDownloadAllMetrics: z19.boolean().optional(),
4569
+ enableDownloadAllPdf: z19.boolean().optional(),
4570
+ disableFullscreen: z19.boolean().optional(),
4571
+ isHideTablePreview: z19.boolean().optional(),
4572
+ isHideChartSettings: z19.boolean().optional(),
4573
+ isStickyDashboardFilters: z19.boolean().optional(),
4574
+ enableMultiMetricFilters: z19.boolean().optional(),
4575
+ themeName: z19.string().optional(),
4576
+ customTheme: z19.record(z19.unknown()).optional(),
4577
+ optionsIcon: z19.enum(["kebab-menu-vertical", "download"]).optional()
4973
4578
  }).optional()
4974
4579
  },
4975
4580
  READ_ONLY,
@@ -5009,17 +4614,17 @@ ${generated.instructions}`;
5009
4614
  }
5010
4615
 
5011
4616
  // src/tools/analytics/list-dashboards.ts
5012
- import { z as z21 } from "zod";
4617
+ import { z as z20 } from "zod";
5013
4618
  function registerListDashboards(server, client) {
5014
4619
  const services = createServiceBundle(client);
5015
4620
  server.tool(
5016
4621
  "list_dashboards",
5017
4622
  "List dashboards globally or for a specific data app.",
5018
4623
  {
5019
- scope: z21.enum(["auto", "global", "data_app"]).optional().describe(
4624
+ scope: z20.enum(["auto", "global", "data_app"]).optional().describe(
5020
4625
  "Dashboard listing scope. auto: use data_app when dataAppName + API token are available, otherwise global."
5021
4626
  ),
5022
- dataAppName: z21.string().optional().describe("Data app name for data_app scope. Ignored for global scope.")
4627
+ dataAppName: z20.string().optional().describe("Data app name for data_app scope. Ignored for global scope.")
5023
4628
  },
5024
4629
  READ_ONLY,
5025
4630
  async (params) => {
@@ -5054,36 +4659,36 @@ function registerListDashboards(server, client) {
5054
4659
  }
5055
4660
 
5056
4661
  // src/tools/dashboard/schema.ts
5057
- import { z as z22 } from "zod";
5058
- var dashboardPositionSchema = z22.object({
5059
- x: z22.number().int().min(0).max(23),
5060
- y: z22.number().int().min(0),
5061
- w: z22.number().int().min(1).max(24),
5062
- h: z22.number().int().min(1).max(40)
4662
+ import { z as z21 } from "zod";
4663
+ var dashboardPositionSchema = z21.object({
4664
+ x: z21.number().int().min(0).max(23),
4665
+ y: z21.number().int().min(0),
4666
+ w: z21.number().int().min(1).max(24),
4667
+ h: z21.number().int().min(1).max(40)
5063
4668
  }).strict();
5064
- var dashboardChartSchema = z22.object({
5065
- type: z22.string(),
5066
- metricRef: z22.string().optional(),
5067
- query: z22.string().optional(),
5068
- companyIntegrationId: z22.string().optional(),
5069
- chartOptions: z22.record(z22.unknown()).optional()
4669
+ var dashboardChartSchema = z21.object({
4670
+ type: z21.string(),
4671
+ metricRef: z21.string().optional(),
4672
+ query: z21.string().optional(),
4673
+ companyIntegrationId: z21.string().optional(),
4674
+ chartOptions: z21.record(z21.unknown()).optional()
5070
4675
  }).passthrough();
5071
- var dashboardTileSchema = z22.object({
5072
- id: z22.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/),
5073
- kind: z22.literal("databrain-chart"),
4676
+ var dashboardTileSchema = z21.object({
4677
+ id: z21.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/),
4678
+ kind: z21.literal("databrain-chart"),
5074
4679
  position: dashboardPositionSchema,
5075
- title: z22.string().min(1).max(200),
4680
+ title: z21.string().min(1).max(200),
5076
4681
  chart: dashboardChartSchema.optional()
5077
4682
  }).strict();
5078
4683
  var dashboardDocumentShape = {
5079
- schemaVersion: z22.literal(1),
5080
- title: z22.string().min(1).max(200),
5081
- description: z22.string().max(2e3).optional(),
5082
- layout: z22.object({ columns: z22.union([z22.literal(12), z22.literal(24)]).default(24) }).strict().optional(),
5083
- tiles: z22.array(dashboardTileSchema).min(1).max(50)
4684
+ schemaVersion: z21.literal(1),
4685
+ title: z21.string().min(1).max(200),
4686
+ description: z21.string().max(2e3).optional(),
4687
+ layout: z21.object({ columns: z21.union([z21.literal(12), z21.literal(24)]).default(24) }).strict().optional(),
4688
+ tiles: z21.array(dashboardTileSchema).min(1).max(50)
5084
4689
  };
5085
- var workspaceNameSchema = z22.string().min(1).describe("Target workspace name");
5086
- var idempotencyKeySchema = z22.string().regex(
4690
+ var workspaceNameSchema = z21.string().min(1).describe("Target workspace name");
4691
+ var idempotencyKeySchema = z21.string().regex(
5087
4692
  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
5088
4693
  "idempotencyKey must be a UUID v4"
5089
4694
  ).describe("UUID v4 unique to this workspace dashboard create request");
@@ -5136,7 +4741,7 @@ ${stringify(validation)}`
5136
4741
  }
5137
4742
 
5138
4743
  // src/tools/dashboard/generate-dashboard-tiles.ts
5139
- import { z as z23 } from "zod";
4744
+ import { z as z22 } from "zod";
5140
4745
  function registerGenerateDashboardTiles(server, client) {
5141
4746
  const services = createServiceBundle(client);
5142
4747
  server.tool(
@@ -5144,15 +4749,15 @@ function registerGenerateDashboardTiles(server, client) {
5144
4749
  "Generate multiple workspace dashboard tile drafts without persisting them.",
5145
4750
  {
5146
4751
  workspaceName: workspaceNameSchema,
5147
- dataAppName: z23.string().optional().describe("Data app in the workspace; auto-resolved when unique"),
5148
- integrationId: z23.string().optional().describe("Datasource UUID override"),
5149
- integrationName: z23.string().optional().describe("Datasource name override"),
5150
- tiles: z23.array(
5151
- z23.object({
5152
- id: z23.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/),
5153
- title: z23.string().min(1).max(200),
4752
+ dataAppName: z22.string().optional().describe("Data app in the workspace; auto-resolved when unique"),
4753
+ integrationId: z22.string().optional().describe("Datasource UUID override"),
4754
+ integrationName: z22.string().optional().describe("Datasource name override"),
4755
+ tiles: z22.array(
4756
+ z22.object({
4757
+ id: z22.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/),
4758
+ title: z22.string().min(1).max(200),
5154
4759
  position: dashboardPositionSchema,
5155
- userInputs: z23.record(z23.unknown())
4760
+ userInputs: z22.record(z22.unknown())
5156
4761
  }).strict()
5157
4762
  ).min(1).max(50)
5158
4763
  },
@@ -5236,14 +4841,14 @@ function registerGenerateDashboardTiles(server, client) {
5236
4841
  }
5237
4842
 
5238
4843
  // src/tools/dashboard/get-dashboard.ts
5239
- import { z as z24 } from "zod";
4844
+ import { z as z23 } from "zod";
5240
4845
  function registerGetDashboard(server, client) {
5241
4846
  server.tool(
5242
4847
  "get_dashboard",
5243
4848
  "Retrieve a workspace dashboard projected back into the versioned tile-list contract.",
5244
4849
  {
5245
4850
  workspaceName: workspaceNameSchema,
5246
- dashboardId: z24.string().min(1).describe("Dashboard identifier returned by create_dashboard")
4851
+ dashboardId: z23.string().min(1).describe("Dashboard identifier returned by create_dashboard")
5247
4852
  },
5248
4853
  READ_ONLY,
5249
4854
  async (params) => {
@@ -5266,8 +4871,229 @@ function registerGetDashboard(server, client) {
5266
4871
  );
5267
4872
  }
5268
4873
 
5269
- // src/tools/query/ask-question.ts
4874
+ // src/tools/dashboard/dashboard-snapshot.ts
4875
+ import { z as z24 } from "zod";
4876
+ var SNAPSHOT_REQUEST_MS = 45e3;
4877
+ function registerDashboardSnapshot(server, client) {
4878
+ server.tool(
4879
+ "dashboard_snapshot",
4880
+ "Get a PNG picture of a dashboard, or of one chart on it (metricId), as the signed-in user sees it in Databrain. Light or dark, 480 to 1600 px wide. Needs a Databrain sign-in (OAuth), not a service token.",
4881
+ {
4882
+ workspaceName: workspaceNameSchema.optional(),
4883
+ dashboardId: z24.string().min(1).max(200).optional().describe("Dashboard id from list_dashboards. Needed unless metricId is given."),
4884
+ metricId: z24.string().min(1).max(200).optional().describe("One chart (metric) id: draw only that chart, from a dashboard it is on."),
4885
+ theme: z24.enum(["light", "dark"]).default("light").describe("Picture colours"),
4886
+ width: z24.number().int().min(480).max(1600).default(1200).describe("Picture width in pixels")
4887
+ },
4888
+ READ_ONLY,
4889
+ async (params) => {
4890
+ if (!params.dashboardId && !params.metricId) {
4891
+ return toolError("Name a dashboardId (from list_dashboards), or a metricId for one chart.");
4892
+ }
4893
+ try {
4894
+ const result = await client.request({
4895
+ method: "POST",
4896
+ path: "/api/v2/workspace/dashboard/snapshot",
4897
+ auth: "service",
4898
+ timeoutMs: SNAPSHOT_REQUEST_MS,
4899
+ body: {
4900
+ ...params.workspaceName ? { workspaceName: params.workspaceName } : {},
4901
+ ...params.dashboardId ? { dashboardId: params.dashboardId } : {},
4902
+ ...params.metricId ? { metricId: params.metricId } : {},
4903
+ theme: params.theme,
4904
+ width: params.width
4905
+ }
4906
+ });
4907
+ if (result?.mimeType !== "image/png" || typeof result.image !== "string" || !result.image) {
4908
+ return toolError("The snapshot answer had no picture. Try again.");
4909
+ }
4910
+ const what = result.metric?.name ? `Chart "${result.metric.name}" on dashboard "${result.dashboard?.name ?? ""}"` : `Dashboard "${result.dashboard?.name ?? ""}"`;
4911
+ const text2 = [
4912
+ `${what} in workspace "${result.workspace?.name ?? ""}", ${result.theme ?? params.theme}, ${result.width}x${result.height} px.`,
4913
+ result.summary ?? ""
4914
+ ].filter(Boolean).join("\n");
4915
+ return {
4916
+ content: [
4917
+ { type: "text", text: text2 },
4918
+ { type: "image", data: result.image, mimeType: "image/png" }
4919
+ ]
4920
+ };
4921
+ } catch (err) {
4922
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
4923
+ return toolError(`Unexpected error: ${err.message}`);
4924
+ }
4925
+ }
4926
+ );
4927
+ }
4928
+
4929
+ // src/apps/preview.ts
4930
+ import { createHash } from "crypto";
5270
4931
  import { z as z25 } from "zod";
4932
+ var draftSchema = z25.object({
4933
+ question: z25.string().min(1).max(2e3),
4934
+ sql: z25.string().min(1).max(2e4),
4935
+ datamartName: z25.string().min(1).max(256),
4936
+ dimensions: z25.array(z25.string().max(128)).max(20).default([]),
4937
+ measures: z25.array(z25.string().max(128)).max(20).default([]),
4938
+ chartType: z25.string().min(1).max(80),
4939
+ title: z25.string().min(1).max(200),
4940
+ sessionId: z25.string().max(256).optional(),
4941
+ /** Workspaces built on this datamart. One means Save needs no choice. */
4942
+ workspaces: z25.array(z25.string().max(256)).max(50).default([])
4943
+ }).strict();
4944
+ var PREVIEW_PATH = "/api/v2/data-app/chart-preview";
4945
+ var METRIC_PATH = "/api/mcp/metrics/create";
4946
+ var DASHBOARD_PATH = "/api/v2/workspace/dashboard";
4947
+ var PREVIEW_TYPES = ["bar", "row", "line", "area", "pie", "doughnut", "table"];
4948
+ var RENDER_TYPES = [...PREVIEW_TYPES, "single value"];
4949
+ async function previewPage(client, draft, rows, chartType) {
4950
+ const page = await client.request({
4951
+ method: "POST",
4952
+ path: PREVIEW_PATH,
4953
+ auth: "service",
4954
+ skipDataUnwrap: true,
4955
+ body: {
4956
+ rows: firstRows(rows),
4957
+ chartType,
4958
+ title: draft.title,
4959
+ dimensions: draft.dimensions,
4960
+ measures: draft.measures,
4961
+ width: 600,
4962
+ height: 320
4963
+ }
4964
+ });
4965
+ return pageSchema.parse(page);
4966
+ }
4967
+ function tablePage(draft, rows) {
4968
+ const page = firstRows(rows);
4969
+ return {
4970
+ kind: "table",
4971
+ title: draft.title,
4972
+ chartSpec: { type: "table" },
4973
+ rows: page,
4974
+ columns: Object.keys(page[0] ?? {})
4975
+ };
4976
+ }
4977
+ async function draftPage(client, draft, rows, chartType) {
4978
+ const data = rowsSchema.parse(firstRows(rows));
4979
+ const carry = { draft: { ...draft, chartType }, previewTypes: PREVIEW_TYPES };
4980
+ if (chartType === "table" || !data.length) return { ...tablePage(draft, data), ...carry };
4981
+ try {
4982
+ return { ...await previewPage(client, draft, data, chartType), ...carry };
4983
+ } catch (error) {
4984
+ const previewError = error instanceof DatabrainApiError ? error.code : "PREVIEW_UNAVAILABLE";
4985
+ return { ...tablePage(draft, data), ...carry, draft: { ...draft, chartType: "table" }, previewError };
4986
+ }
4987
+ }
4988
+ var sameName = (a, b) => typeof a === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
4989
+ async function workspacesForDatamart(client, datamartName, mode = "hint") {
4990
+ const cached = (key) => mode === "hint" ? { cacheKey: key, cacheTtlMs: 5 * 60 * 1e3 } : {};
4991
+ try {
4992
+ const [workspaces, datamarts] = await Promise.all([
4993
+ client.request({ method: "GET", path: "/api/v2/workspace", auth: "service", ...cached("list_workspaces") }),
4994
+ client.request({ method: "GET", path: "/api/v2/data-app/datamarts", auth: "service", ...cached("list_datamarts") })
4995
+ ]);
4996
+ const datasource = (Array.isArray(datamarts) ? datamarts : []).find((d) => sameName(d?.name ?? d?.datamartName, datamartName))?.companyIntegration?.id;
4997
+ const all = (Array.isArray(workspaces) ? workspaces : []).filter((w) => typeof w?.name === "string");
4998
+ const onDatamart = all.filter((w) => sameName(w.datamart?.name ?? w.datamartName, datamartName));
4999
+ const onDatasource = datasource ? all.filter((w) => !w.datamart && w.companyIntegration?.id === datasource) : [];
5000
+ return [...new Set([...onDatamart, ...onDatasource].map((w) => w.name))];
5001
+ } catch {
5002
+ return [];
5003
+ }
5004
+ }
5005
+ function saveKey(draft, input) {
5006
+ const h = createHash("sha256").update(JSON.stringify({
5007
+ sql: draft.sql,
5008
+ datamartName: draft.datamartName,
5009
+ workspaceName: input.workspaceName,
5010
+ dashboardTitle: input.dashboardTitle,
5011
+ chartType: input.chartType,
5012
+ // One attempt, many retries: the tile mints an id when the form opens and re-sends it on
5013
+ // retry, so a retry replays and a fresh Save is a fresh key. Without one, content is the key.
5014
+ attemptId: input.attemptId ?? null
5015
+ })).digest();
5016
+ h[6] = h[6] & 15 | 64;
5017
+ h[8] = h[8] & 63 | 128;
5018
+ const x = h.subarray(0, 16).toString("hex");
5019
+ return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20, 32)}`;
5020
+ }
5021
+ async function saveDraftAsDashboard(client, draft, input) {
5022
+ const metric = await client.request({
5023
+ method: "POST",
5024
+ path: METRIC_PATH,
5025
+ auth: "service",
5026
+ body: {
5027
+ workspaceName: input.workspaceName,
5028
+ name: input.dashboardTitle,
5029
+ query: draft.sql,
5030
+ description: draft.question,
5031
+ isPublished: false,
5032
+ validateOnly: false
5033
+ }
5034
+ });
5035
+ const metricId = metric?.metricId || metric?.id;
5036
+ if (!metricId) throw new DatabrainApiError(502, "METRIC_ID_MISSING", "The metric was created but returned no id.");
5037
+ const slug = input.dashboardTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63) || "chart";
5038
+ const body = {
5039
+ schemaVersion: 1,
5040
+ title: input.dashboardTitle,
5041
+ description: draft.question,
5042
+ layout: { columns: 24 },
5043
+ tiles: [{
5044
+ id: slug,
5045
+ kind: "databrain-chart",
5046
+ title: input.dashboardTitle,
5047
+ position: { x: 0, y: 0, w: 12, h: 8 },
5048
+ chart: { type: input.chartType, metricRef: metricId }
5049
+ }]
5050
+ };
5051
+ const dashboard = await client.request({
5052
+ method: "POST",
5053
+ path: DASHBOARD_PATH,
5054
+ auth: "service",
5055
+ query: { workspaceName: input.workspaceName },
5056
+ headers: { "Idempotency-Key": saveKey(draft, input) },
5057
+ body,
5058
+ invalidateCache: ["list_dashboards"],
5059
+ // A metadata insert: 15s keeps the attempt and its retries inside the host's 60s window.
5060
+ timeoutMs: 15e3
5061
+ });
5062
+ if (!dashboard?.dashboardId) {
5063
+ throw new DatabrainApiError(502, "DASHBOARD_ID_MISSING", `Metric ${metricId} was saved but the dashboard returned no id.`);
5064
+ }
5065
+ return { dashboardId: dashboard.dashboardId, metricId, workspaceName: input.workspaceName, title: input.dashboardTitle };
5066
+ }
5067
+
5068
+ // src/tools/query/ask-question.ts
5069
+ import { z as z26 } from "zod";
5070
+
5071
+ // src/tools/query/answer-format.ts
5072
+ function formatConfidence(value) {
5073
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
5074
+ const percent = value > 1 ? value : value * 100;
5075
+ return `${Math.round(Math.min(Math.max(percent, 0), 100))}%`;
5076
+ }
5077
+ function formatProse(value) {
5078
+ if (Array.isArray(value)) {
5079
+ const items = value.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => `- ${item.trim()}`);
5080
+ return items.length ? items.join("\n") : null;
5081
+ }
5082
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
5083
+ return null;
5084
+ }
5085
+
5086
+ // src/tools/query/ask-question.ts
5087
+ function normalizePreviewType(type) {
5088
+ const t = (type || "").toLowerCase().trim();
5089
+ if (RENDER_TYPES.includes(t)) return t;
5090
+ if (t === "single value" || t === "kpi" || t === "number") return "single value";
5091
+ if (t === "time series" || t === "stepped") return "line";
5092
+ if (t === "stack" || t === "column") return "bar";
5093
+ if (t === "horizontal stack" || t === "horizontal bar") return "row";
5094
+ if (t === "stacked area") return "area";
5095
+ return "table";
5096
+ }
5271
5097
  var MAX_DISPLAY_ROWS = 50;
5272
5098
  function formatDataTable(data, columns) {
5273
5099
  if (!Array.isArray(data) || data.length === 0) return "_No data returned._";
@@ -5297,16 +5123,16 @@ function registerAskQuestion(server, client) {
5297
5123
  "ask_question",
5298
5124
  "Ask a natural language question \u2014 converts to SQL, returns data + chart suggestion. Requires datamartName with a populated semantic layer.",
5299
5125
  {
5300
- question: z25.string().describe(
5126
+ question: z26.string().describe(
5301
5127
  'The natural language question to ask (e.g. "What was total revenue last month?")'
5302
5128
  ),
5303
- datamartName: z25.string().optional().describe(
5129
+ datamartName: z26.string().optional().describe(
5304
5130
  "Name of the datamart to query. Use `list_datamarts` to find available names. REQUIRED unless exactly one datamart exists (auto-resolved). Do NOT pass a workspace name here."
5305
5131
  ),
5306
- sessionId: z25.string().optional().describe(
5132
+ sessionId: z26.string().optional().describe(
5307
5133
  "Session ID from a previous ask_question response, for follow-up questions."
5308
5134
  ),
5309
- timezone: z25.string().optional().describe('IANA timezone (e.g. "America/New_York"). Defaults to UTC.')
5135
+ timezone: z26.string().optional().describe('IANA timezone (e.g. "America/New_York"). Defaults to UTC.')
5310
5136
  },
5311
5137
  WRITE_CREATE,
5312
5138
  async (params) => {
@@ -5411,15 +5237,26 @@ You can verify with \`get_semantic_layer\` using datamartName "${datamartName}".
5411
5237
  });
5412
5238
  const data = Array.isArray(result.data) ? result.data : [];
5413
5239
  if (hasChartApps(server)) {
5240
+ const chartType = normalizePreviewType(result.suggestedChartType);
5241
+ const draft = {
5242
+ question: params.question,
5243
+ sql: result.query || "",
5244
+ datamartName,
5245
+ dimensions: result.columns?.dimensions || [],
5246
+ measures: result.columns?.measures || [],
5247
+ chartType,
5248
+ title: params.question.slice(0, 200),
5249
+ sessionId: result.sessionId || void 0,
5250
+ workspaces: await workspacesForDatamart(client, datamartName)
5251
+ };
5414
5252
  const rows = firstRows(data);
5415
- return chartResult(`Query Results: ${data.length} rows returned; showing the first ${rows.length}.`, {
5416
- kind: "table",
5417
- title: "Query results",
5418
- chartSpec: { type: result.suggestedChartType || "table" },
5419
- rows,
5420
- columns: Object.keys(rows[0] ?? {}),
5421
- description: "Showing the first page. Interactive chart preview is not available for this query yet."
5422
- });
5253
+ const page = draft.sql ? await draftPage(client, draft, rows, chartType) : { ...tablePage(draft, rows), previewTypes: [] };
5254
+ const shown = rows.length === data.length ? `${data.length} rows` : `first ${rows.length} of ${data.length} rows`;
5255
+ try {
5256
+ return chartResult(`${page.kind === "table" ? "Table" : "Chart"} ready (${page.chartSpec && page.chartSpec.type}): ${shown}.` + (result.summary ? ` ${result.summary}` : ""), { ...page, rowsTotal: data.length });
5257
+ } catch (error) {
5258
+ if (!(error instanceof Error) || error.message !== "Chart page exceeds payload budget") throw error;
5259
+ }
5423
5260
  }
5424
5261
  const table = formatDataTable(data, result.columns);
5425
5262
  const parts = [];
@@ -5431,23 +5268,24 @@ You can verify with \`get_semantic_layer\` using datamartName "${datamartName}".
5431
5268
  parts.push(
5432
5269
  `**Suggested chart type:** ${result.suggestedChartType}`
5433
5270
  );
5434
- if (result.confidence !== null) {
5435
- parts.push(
5436
- `**Confidence:** ${Math.round(result.confidence * 100)}%`
5437
- );
5271
+ const confidence = formatConfidence(result.confidence);
5272
+ if (confidence) {
5273
+ parts.push(`**Confidence:** ${confidence}`);
5438
5274
  }
5439
5275
  parts.push(
5440
5276
  `**Rows returned:** ${data.length} | **Time:** ${result.timeTaken}ms`
5441
5277
  );
5442
- if (result.reasoning) {
5278
+ const reasoning = formatProse(result.reasoning);
5279
+ if (reasoning) {
5443
5280
  parts.push(`
5444
5281
  ### Reasoning
5445
- ${result.reasoning}`);
5282
+ ${reasoning}`);
5446
5283
  }
5447
- if (result.summary) {
5284
+ const summary = formatProse(result.summary);
5285
+ if (summary) {
5448
5286
  parts.push(`
5449
5287
  ### Summary
5450
- ${result.summary}`);
5288
+ ${summary}`);
5451
5289
  }
5452
5290
  parts.push(`
5453
5291
  ### Data
@@ -5494,14 +5332,14 @@ To fix this:
5494
5332
  }
5495
5333
 
5496
5334
  // src/tools/query/get-semantic-layer.ts
5497
- import { z as z26 } from "zod";
5335
+ import { z as z27 } from "zod";
5498
5336
  function registerGetSemanticLayer(server, client) {
5499
5337
  const services = createServiceBundle(client);
5500
5338
  server.tool(
5501
5339
  "get_semantic_layer",
5502
5340
  "Get semantic layer metadata for a datamart: table/column descriptions, synonyms, completion score. Omit datamartName to auto-resolve.",
5503
5341
  {
5504
- datamartName: z26.string().optional().describe(
5342
+ datamartName: z27.string().optional().describe(
5505
5343
  "Name of the datamart to inspect. Omit to list available datamarts."
5506
5344
  )
5507
5345
  },
@@ -5610,16 +5448,396 @@ ${result.feedback}`);
5610
5448
  );
5611
5449
  }
5612
5450
 
5451
+ // src/tools/query/semantic-model.ts
5452
+ import { z as z28 } from "zod";
5453
+
5454
+ // src/services/semantic-source.ts
5455
+ import { parseDocument, stringify as stringify2 } from "yaml";
5456
+ var ENTITY_COLLECTIONS = {
5457
+ table: ["tables"],
5458
+ column: ["columns"],
5459
+ measure: ["measures"],
5460
+ metric: ["metrics"],
5461
+ join: ["joins"],
5462
+ entity: ["entities"],
5463
+ glossary: ["glossary"],
5464
+ saved_query: ["saved_queries", "verified_queries"],
5465
+ instruction: ["instructions", "agent_instructions"],
5466
+ access_rule: ["access", "policies"]
5467
+ };
5468
+ function isObject(value) {
5469
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5470
+ }
5471
+ function assertSafe(value, seen = /* @__PURE__ */ new Set(), depth = 0) {
5472
+ if (depth > 100) throw new Error("Semantic source nesting exceeds 100 levels.");
5473
+ if (!value || typeof value !== "object") return;
5474
+ if (seen.has(value)) throw new Error("Recursive YAML aliases are not supported.");
5475
+ seen.add(value);
5476
+ for (const [key, child] of Object.entries(value)) {
5477
+ if (["__proto__", "constructor", "prototype"].includes(key)) throw new Error(`Unsupported source key: ${key}.`);
5478
+ assertSafe(child, seen, depth + 1);
5479
+ }
5480
+ seen.delete(value);
5481
+ }
5482
+ function parseSemanticSource(source2) {
5483
+ const document = parseDocument(source2, { prettyErrors: false, uniqueKeys: true });
5484
+ if (document.errors.length || document.warnings.length) {
5485
+ throw new Error((document.errors[0] ?? document.warnings[0]).message);
5486
+ }
5487
+ const value = document.toJS({ maxAliasCount: 100 });
5488
+ if (!isObject(value)) throw new Error("Semantic source must be a YAML mapping.");
5489
+ assertSafe(value);
5490
+ return value;
5491
+ }
5492
+ function semanticEntities(source2, visibleOnly = false) {
5493
+ const out = [];
5494
+ for (const [kind, keys] of Object.entries(ENTITY_COLLECTIONS)) {
5495
+ if (kind === "column") continue;
5496
+ const key = keys.find((key2) => Array.isArray(source2[key2]));
5497
+ const entries = key ? source2[key] : [];
5498
+ for (const entry of entries) {
5499
+ if (!isObject(entry) || typeof entry.name !== "string" || visibleOnly && entry.hidden === true) continue;
5500
+ const columns = Array.isArray(entry.columns) ? entry.columns.filter(isObject) : [];
5501
+ const entity = visibleOnly && kind === "table" ? { ...entry, columns: columns.filter((column) => column.hidden !== true) } : entry;
5502
+ out.push({ id: `${kind}:${entry.name}`, kind, name: entry.name, entity });
5503
+ if (kind === "table") for (const column of columns) {
5504
+ if (typeof column.name !== "string" || visibleOnly && column.hidden === true) continue;
5505
+ const name = `${entry.name}.${column.name}`;
5506
+ out.push({ id: `column:${name}`, kind: "column", name, entity: column });
5507
+ }
5508
+ }
5509
+ }
5510
+ return out;
5511
+ }
5512
+ function mergePatch(base, patch) {
5513
+ if (isObject(patch)) {
5514
+ const next = isObject(base) ? { ...base } : {};
5515
+ for (const [key, value] of Object.entries(patch)) {
5516
+ if (value === null) delete next[key];
5517
+ else next[key] = mergePatch(next[key], value);
5518
+ }
5519
+ return next;
5520
+ }
5521
+ if (Array.isArray(patch) && patch.length && patch.every((item) => isObject(item) && typeof item.name === "string")) {
5522
+ const next = Array.isArray(base) ? [...base] : [];
5523
+ const names = /* @__PURE__ */ new Set();
5524
+ for (const item of patch) {
5525
+ const name = item.name;
5526
+ if (names.has(name)) throw new Error(`Duplicate patch entity: ${name}.`);
5527
+ names.add(name);
5528
+ const index = next.findIndex((entry) => isObject(entry) && entry.name === name);
5529
+ if (index < 0) next.push(mergePatch({}, item));
5530
+ else next[index] = mergePatch(next[index], item);
5531
+ }
5532
+ return next;
5533
+ }
5534
+ return patch;
5535
+ }
5536
+ function changeSemanticSource(source2, input) {
5537
+ const model = parseSemanticSource(source2);
5538
+ if (input.yaml_patch !== void 0) return stringify2(mergePatch(model, parseSemanticSource(input.yaml_patch)));
5539
+ const change = input.change;
5540
+ assertSafe(change);
5541
+ let parent = model;
5542
+ if (change.kind === "column") {
5543
+ if (!change.table) throw new Error("A column change requires table and an unqualified column name.");
5544
+ const table = (Array.isArray(model.tables) ? model.tables : []).find(
5545
+ (item) => isObject(item) && item.name === change.table
5546
+ );
5547
+ if (!isObject(table)) throw new Error(`Table not found: ${change.table}.`);
5548
+ parent = table;
5549
+ } else if (change.table) throw new Error("table is only valid for a column change.");
5550
+ const keys = ENTITY_COLLECTIONS[change.kind];
5551
+ const key = keys.find((key2) => Array.isArray(parent[key2])) ?? keys[0];
5552
+ const entries = Array.isArray(parent[key]) ? [...parent[key]] : [];
5553
+ const index = entries.findIndex((entry) => isObject(entry) && entry.name === change.name);
5554
+ if (change.operation === "remove") {
5555
+ if (change.value !== void 0) throw new Error("A remove change must not include value.");
5556
+ if (index < 0) throw new Error(`Entity not found: ${change.kind} ${change.name}.`);
5557
+ entries.splice(index, 1);
5558
+ } else {
5559
+ if (!change.value) throw new Error("An upsert change requires value.");
5560
+ if (change.value.name !== void 0 && change.value.name !== change.name) throw new Error("value.name must match name.");
5561
+ const value = mergePatch(index < 0 ? {} : entries[index], { ...change.value, name: change.name });
5562
+ if (index < 0) entries.push(value);
5563
+ else entries[index] = value;
5564
+ }
5565
+ parent[key] = entries;
5566
+ return stringify2(model);
5567
+ }
5568
+
5569
+ // src/services/semantic-retrieval.ts
5570
+ function tokens(text2) {
5571
+ return text2.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
5572
+ }
5573
+ function text(value) {
5574
+ return typeof value === "string" ? value : Array.isArray(value) ? value.map(text).join(" ") : "";
5575
+ }
5576
+ function retrieveSemanticSource(source2, question, k) {
5577
+ const nodes = semanticEntities(parseSemanticSource(source2), true);
5578
+ const query = new Set(tokens(question));
5579
+ const score = (node) => {
5580
+ const fields = [
5581
+ [node.name, 8],
5582
+ [node.entity.alias ?? node.entity.term, 6],
5583
+ [node.entity.synonyms, 4],
5584
+ [node.entity.description ?? node.entity.definition ?? node.entity.question, 2]
5585
+ ];
5586
+ return fields.reduce((sum, [value, weight]) => {
5587
+ const words = new Set(tokens(text(value)));
5588
+ return sum + [...query].filter((word) => words.has(word)).length * weight;
5589
+ }, 0);
5590
+ };
5591
+ const ranked = nodes.map((node) => ({ ...node, score: score(node), match: "lexical" })).filter((node) => node.score > 0).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id)).slice(0, k);
5592
+ const edges = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
5593
+ const byName = /* @__PURE__ */ new Map();
5594
+ for (const node of nodes) byName.set(node.name, [...byName.get(node.name) ?? [], node.id]);
5595
+ const link = (a, b) => {
5596
+ if (a === b || !edges.has(b)) return;
5597
+ edges.get(a).add(b);
5598
+ edges.get(b).add(a);
5599
+ };
5600
+ const refKeys = /* @__PURE__ */ new Set([
5601
+ "table",
5602
+ "column",
5603
+ "columns",
5604
+ "references",
5605
+ "measure",
5606
+ "numerator",
5607
+ "denominator",
5608
+ "metrics",
5609
+ "measures",
5610
+ "glossary_refs",
5611
+ "left",
5612
+ "right",
5613
+ "from",
5614
+ "to",
5615
+ "on",
5616
+ "bindings",
5617
+ "scope",
5618
+ "targets",
5619
+ "tables",
5620
+ "grain",
5621
+ "identifiers",
5622
+ "default_time_dimension",
5623
+ "expression",
5624
+ "sql",
5625
+ "on_sql",
5626
+ "predicate",
5627
+ "type_config"
5628
+ ]);
5629
+ const refs = (value) => {
5630
+ if (typeof value === "string") return value.match(/[\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)?/gu) ?? [];
5631
+ if (Array.isArray(value)) return value.flatMap(refs);
5632
+ if (isObject(value)) return Object.entries(value).flatMap(([key, child]) => refKeys.has(key) ? refs(child) : []);
5633
+ return [];
5634
+ };
5635
+ for (const node of nodes) {
5636
+ if (node.kind === "column") link(node.id, `table:${node.name.split(".")[0]}`);
5637
+ for (const ref of refs(node.entity)) for (const id2 of byName.get(ref) ?? []) link(node.id, id2);
5638
+ }
5639
+ const chosen = new Map(ranked.map((node) => [node.id, node]));
5640
+ for (const node of ranked) {
5641
+ for (const id2 of [...edges.get(node.id)].sort()) {
5642
+ if (!chosen.has(id2)) {
5643
+ const adjacent = nodes.find((node2) => node2.id === id2);
5644
+ chosen.set(id2, { ...adjacent, score: 0, match: "adjacency" });
5645
+ }
5646
+ }
5647
+ }
5648
+ return { results: [...chosen.values()], k, lexical_matches: ranked.length, retrieval: "local_lexical" };
5649
+ }
5650
+
5651
+ // src/tools/query/semantic-model.ts
5652
+ var datamartId = z28.string().trim().min(1).max(256).describe("Datamart ID from list_datamarts.");
5653
+ var source = z28.string().min(1).max(2e6).describe("Complete proposed semantic model as YAML.");
5654
+ var entityKind = z28.enum(["table", "column", "measure", "metric", "join", "entity", "glossary", "saved_query", "instruction", "access_rule"]);
5655
+ var changeInputs = {
5656
+ source: source.optional(),
5657
+ yaml_patch: z28.string().min(1).max(2e6).optional().describe("YAML mapping merged into the pulled model. Named lists merge by name. Other lists replace. Null removes a field. An empty list clears it."),
5658
+ change: z28.object({
5659
+ operation: z28.enum(["upsert", "remove"]),
5660
+ kind: entityKind,
5661
+ name: z28.string().trim().min(1),
5662
+ table: z28.string().trim().min(1).optional().describe("Required for columns. Use the table name and an unqualified column name."),
5663
+ value: z28.record(z28.unknown()).optional().describe("Fields to merge for upsert. Omit for remove.")
5664
+ }).strict().optional().describe("One named entity change. Supply exactly one of source, yaml_patch or change.")
5665
+ };
5666
+ var expectedHash = z28.string().trim().min(1).nullable().optional().describe("Stored content_hash from the reviewed proposal. Null explicitly means the first apply. Required when dry_run is false.");
5667
+ function errorCode(error) {
5668
+ const nested = isObject(error.details) && isObject(error.details.error) ? error.details.error : void 0;
5669
+ return typeof nested?.code === "string" ? nested.code : error.code;
5670
+ }
5671
+ function failure(error) {
5672
+ if (error instanceof DatabrainApiError) {
5673
+ if (error.status === 409 || error.status === 422) {
5674
+ return toolError(stringify({
5675
+ status: error.status,
5676
+ code: errorCode(error),
5677
+ reason: error.message,
5678
+ details: error.details,
5679
+ recovery: error.status === 409 ? "Pull the current model and review a new proposal before applying." : "Correct the validation errors and propose the change again."
5680
+ }));
5681
+ }
5682
+ return error.toToolResponse();
5683
+ }
5684
+ return toolError(`Semantic model error: ${error instanceof Error ? error.message : String(error)}`);
5685
+ }
5686
+ async function candidate(api, id2, input) {
5687
+ if ([input.source, input.yaml_patch, input.change].filter((value) => value !== void 0).length !== 1) {
5688
+ throw new Error("Supply exactly one of source, yaml_patch or change.");
5689
+ }
5690
+ let stored;
5691
+ try {
5692
+ stored = await api.pull(id2);
5693
+ } catch (error) {
5694
+ if (!(error instanceof DatabrainApiError && error.status === 404 && errorCode(error) === "layer_not_found" && input.source !== void 0)) throw error;
5695
+ }
5696
+ const hash = stored?.content_hash ?? null;
5697
+ if (input.expected_content_hash !== void 0 && input.expected_content_hash !== hash) {
5698
+ throw new DatabrainApiError(409, "content_hash_mismatch", "The model changed since the proposal was reviewed.", { current_hash: hash });
5699
+ }
5700
+ return {
5701
+ source: input.source ?? changeSemanticSource(stored.source, input),
5702
+ expected_content_hash: input.expected_content_hash === void 0 ? hash : input.expected_content_hash
5703
+ };
5704
+ }
5705
+ async function proposal(api, id2, input) {
5706
+ const next = await candidate(api, id2, input);
5707
+ const validation = await api.validate(id2, next.source);
5708
+ let plan = null;
5709
+ let plan_note;
5710
+ if (validation.ok) {
5711
+ try {
5712
+ plan = await api.plan(id2, next.source, next.expected_content_hash);
5713
+ } catch (error) {
5714
+ if (!(error instanceof DatabrainApiError && [404, 405, 501].includes(error.status))) throw error;
5715
+ plan_note = "This Databrain API has no plan step. The validation above is the server check; apply runs it again.";
5716
+ }
5717
+ }
5718
+ return { ...next, validation, errors: validation.errors, plan, ...plan_note ? { plan_note } : {}, dry_run: true };
5719
+ }
5720
+ function registerSearchSemanticModel(server, client) {
5721
+ const api = createServiceBundle(client).semanticModelApi;
5722
+ server.tool(
5723
+ "search_semantic_model",
5724
+ "Find semantic model entities relevant to a question. Ranks names, aliases, synonyms and descriptions, then includes related entities. Uses the stored model and never writes.",
5725
+ { datamartId, question: z28.string().trim().min(1).max(1e4), k: z28.number().int().min(1).max(100).default(10) },
5726
+ READ_ONLY,
5727
+ async ({ datamartId: datamartId2, question, k }) => {
5728
+ try {
5729
+ try {
5730
+ return toolSuccess(stringify(await api.retrieve(datamartId2, question, k)));
5731
+ } catch (error) {
5732
+ if (!(error instanceof DatabrainApiError && [404, 405, 501].includes(error.status))) throw error;
5733
+ if (["datamart_not_found", "layer_not_found", "forbidden", "unauthorized"].includes(errorCode(error))) throw error;
5734
+ }
5735
+ const stored = await api.pull(datamartId2);
5736
+ return toolSuccess(stringify({ ...retrieveSemanticSource(stored.source, question, k), content_hash: stored.content_hash }));
5737
+ } catch (error) {
5738
+ return failure(error);
5739
+ }
5740
+ }
5741
+ );
5742
+ }
5743
+ function registerGetSemanticEntity(server, client) {
5744
+ const api = createServiceBundle(client).semanticModelApi;
5745
+ server.tool(
5746
+ "get_semantic_entity",
5747
+ "Read one entity by kind and exact name from the stored semantic model. For a column, use table.column. Returns the stored content hash for change review.",
5748
+ { datamartId, kind: entityKind, name: z28.string().trim().min(1) },
5749
+ READ_ONLY,
5750
+ async ({ datamartId: datamartId2, kind, name }) => {
5751
+ try {
5752
+ const stored = await api.pull(datamartId2);
5753
+ const found = semanticEntities(parseSemanticSource(stored.source)).filter((entity) => entity.kind === kind && entity.name === name);
5754
+ if (found.length !== 1) return toolError(`Expected one ${kind} named ${name}; found ${found.length}. Use search_semantic_model to find its exact name.`);
5755
+ return toolSuccess(stringify({ ...found[0], content_hash: stored.content_hash }));
5756
+ } catch (error) {
5757
+ return failure(error);
5758
+ }
5759
+ }
5760
+ );
5761
+ }
5762
+ function registerProposeSemanticChange(server, client) {
5763
+ const api = createServiceBundle(client).semanticModelApi;
5764
+ server.tool(
5765
+ "propose_semantic_change",
5766
+ "Prepare a semantic model change from complete YAML, a YAML patch or one structured change. Returns validation errors, the server plan, candidate YAML and the stored hash. Never writes.",
5767
+ { datamartId, ...changeInputs },
5768
+ READ_ONLY,
5769
+ async (params) => {
5770
+ try {
5771
+ return toolSuccess(stringify(await proposal(api, params.datamartId, params)));
5772
+ } catch (error) {
5773
+ return failure(error);
5774
+ }
5775
+ }
5776
+ );
5777
+ }
5778
+ function registerApplySemanticChange(server, client) {
5779
+ const api = createServiceBundle(client).semanticModelApi;
5780
+ server.tool(
5781
+ "apply_semantic_change",
5782
+ "Preview a semantic change by default. Setting dry_run to false applies it live and can remove entities. Requires the reviewed content hash and confirm set to APPLY_TO_PRODUCTION supplied by the user.",
5783
+ {
5784
+ datamartId,
5785
+ ...changeInputs,
5786
+ dry_run: z28.boolean().default(true),
5787
+ expected_content_hash: expectedHash,
5788
+ confirm: z28.string().optional().describe("User supplied APPLY_TO_PRODUCTION after reviewing the plan. Never invent confirmation.")
5789
+ },
5790
+ DESTRUCTIVE,
5791
+ async (params) => {
5792
+ try {
5793
+ if (params.dry_run !== false) return toolSuccess(stringify(await proposal(api, params.datamartId, params)));
5794
+ const refusal = validateProductionApply(params);
5795
+ if (refusal) return toolError(refusal);
5796
+ if (params.expected_content_hash === void 0) return toolError("expected_content_hash is required. Use the stored hash from the reviewed proposal, or null for the first apply.");
5797
+ const next = await candidate(api, params.datamartId, params);
5798
+ const validation = await api.validate(params.datamartId, next.source);
5799
+ if (!validation.ok) return failure(new DatabrainApiError(422, "validation_failed", "Apply refused because the model does not validate.", { validation }));
5800
+ return toolSuccess(stringify(await api.apply(params.datamartId, next.source, next.expected_content_hash)));
5801
+ } catch (error) {
5802
+ return failure(error);
5803
+ }
5804
+ }
5805
+ );
5806
+ }
5807
+ function registerEvalSemanticModel(server, client) {
5808
+ const api = createServiceBundle(client).semanticModelApi;
5809
+ server.tool(
5810
+ "eval_semantic_model",
5811
+ "Evaluate saved queries and optional questions against a proposed or stored semantic model. Returns the pass rate and each verdict with its reason. Runs warehouse queries without changing the model.",
5812
+ { datamartId, source: source.optional(), questions: z28.array(z28.union([z28.string().trim().min(1), z28.object({
5813
+ name: z28.string().optional(),
5814
+ question: z28.string().min(1),
5815
+ sql: z28.string().optional(),
5816
+ expected_columns: z28.array(z28.string()).optional(),
5817
+ expected_empty: z28.boolean().optional()
5818
+ }).strict()])).max(100).optional() },
5819
+ READ_ONLY,
5820
+ async ({ datamartId: datamartId2, source: source2, questions }) => {
5821
+ const shaped = questions?.map((entry) => typeof entry === "string" ? { question: entry } : entry);
5822
+ try {
5823
+ return toolSuccess(stringify(await api.evaluate(datamartId2, { source: source2, questions: shaped })));
5824
+ } catch (error) {
5825
+ return failure(error);
5826
+ }
5827
+ }
5828
+ );
5829
+ }
5830
+
5613
5831
  // src/tools/query/update-semantic-layer.ts
5614
- import { z as z27 } from "zod";
5615
- var columnSchema2 = z27.object({
5616
- name: z27.string().describe("Column name (must exist in the datamart)"),
5617
- description: z27.string().optional().describe("Business-friendly description of what this column represents"),
5618
- synonyms: z27.array(z27.string()).optional().describe(
5832
+ import { z as z29 } from "zod";
5833
+ var columnSchema2 = z29.object({
5834
+ name: z29.string().describe("Column name (must exist in the datamart)"),
5835
+ description: z29.string().optional().describe("Business-friendly description of what this column represents"),
5836
+ synonyms: z29.array(z29.string()).optional().describe(
5619
5837
  'Alternative names users might use for this column (e.g. ["revenue", "sales amount"])'
5620
5838
  ),
5621
- miscellaneousInfo: z27.string().optional().describe("Additional context such as valid values, units, or caveats"),
5622
- columnType: z27.enum([
5839
+ miscellaneousInfo: z29.string().optional().describe("Additional context such as valid values, units, or caveats"),
5840
+ columnType: z29.enum([
5623
5841
  "String",
5624
5842
  "Long String",
5625
5843
  "String (Custom)",
@@ -5630,18 +5848,25 @@ var columnSchema2 = z27.object({
5630
5848
  "Identifier",
5631
5849
  "Number",
5632
5850
  "JSON"
5633
- ]).nullable().optional().describe('Semantic column type. Use "Identifier" for IDs/foreign keys, "Number" for numeric values, "String" for text. Omit for date and boolean columns.')
5851
+ ]).nullable().optional().describe('Semantic column type. Use "Identifier" for IDs/foreign keys, "Number" for numeric values, "String" for text. Omit for date and boolean columns.'),
5852
+ // The public PUT accepts these three as well (docs: Update Semantic Layer).
5853
+ // They were missing here, and zod strips unknown keys, so a caller who set
5854
+ // `isIdentifier: true` got a success message and a row with `false`.
5855
+ columnTypeConfig: z29.union([z29.record(z29.string(), z29.string()), z29.string(), z29.object({ lowerLimit: z29.number(), upperLimit: z29.number() }), z29.null()]).optional().describe("Shape depends on columnType: value-to-description object for String / String (Custom) / ENUM / Mapper, { lowerLimit, upperLimit } for Range, a string for Expression and JSON. Omit for Identifier, Number and Long String."),
5856
+ isIdentifier: z29.boolean().optional().describe("Mark the column as an identifier (primary or foreign key)."),
5857
+ isNotIndexed: z29.boolean().optional().describe("Exclude the column from AI indexing.")
5634
5858
  });
5635
- var tableSchema2 = z27.object({
5636
- name: z27.string().describe("Table name (must exist in the datamart)"),
5637
- description: z27.string().optional().describe(
5859
+ var tableSchema2 = z29.object({
5860
+ name: z29.string().describe("Table name (must exist in the datamart)"),
5861
+ schemaName: z29.string().optional().describe("Schema the table lives in, when the datamart has the same table name in more than one schema."),
5862
+ description: z29.string().optional().describe(
5638
5863
  "Business-friendly description of what this table represents"
5639
5864
  ),
5640
- synonyms: z27.array(z27.string()).optional().describe("Alternative names for this table"),
5641
- miscellaneousInfo: z27.string().optional().describe(
5865
+ synonyms: z29.array(z29.string()).optional().describe("Alternative names for this table"),
5866
+ miscellaneousInfo: z29.string().optional().describe(
5642
5867
  "Additional context such as how the table relates to other tables"
5643
5868
  ),
5644
- columns: z27.array(columnSchema2).optional().describe("Column-level semantic definitions to update")
5869
+ columns: z29.array(columnSchema2).optional().describe("Column-level semantic definitions to update")
5645
5870
  });
5646
5871
  function columnKey(tableName, columnName) {
5647
5872
  return `${tableName.toLowerCase()}\0${columnName.toLowerCase()}`;
@@ -5673,6 +5898,7 @@ function normalizeTablesForApi(tables, datatypesByColumn) {
5673
5898
  if (!tables) return void 0;
5674
5899
  return tables.map((table) => {
5675
5900
  const normalized = { name: table.name };
5901
+ if (table.schemaName !== void 0) normalized.schemaName = table.schemaName;
5676
5902
  if (table.description !== void 0) normalized.description = table.description;
5677
5903
  if (table.synonyms !== void 0) normalized.synonyms = table.synonyms;
5678
5904
  if (table.miscellaneousInfo !== void 0) {
@@ -5696,6 +5922,15 @@ function normalizeTablesForApi(tables, datatypesByColumn) {
5696
5922
  normalizedColumn.columnType = column.columnType;
5697
5923
  }
5698
5924
  }
5925
+ if (column.columnTypeConfig !== void 0) {
5926
+ normalizedColumn.columnTypeConfig = column.columnTypeConfig;
5927
+ }
5928
+ if (column.isIdentifier !== void 0) {
5929
+ normalizedColumn.isIdentifier = column.isIdentifier;
5930
+ }
5931
+ if (column.isNotIndexed !== void 0) {
5932
+ normalizedColumn.isNotIndexed = column.isNotIndexed;
5933
+ }
5699
5934
  return normalizedColumn;
5700
5935
  });
5701
5936
  }
@@ -5708,13 +5943,13 @@ function registerUpdateSemanticLayer(server, client) {
5708
5943
  "update_semantic_layer",
5709
5944
  "Populate or update the semantic layer for a datamart. Add table/column descriptions, synonyms, context. Required before ask_question works. Auto-generate descriptions from column names when empty.",
5710
5945
  {
5711
- datamartName: z27.string().optional().describe(
5946
+ datamartName: z29.string().optional().describe(
5712
5947
  "Name of the datamart to update. Omit to list available datamarts."
5713
5948
  ),
5714
- tables: z27.array(tableSchema2).optional().describe(
5949
+ tables: z29.array(tableSchema2).optional().describe(
5715
5950
  "Table and column definitions to update. Only include tables/columns you want to change."
5716
5951
  ),
5717
- feedback: z27.string().optional().describe(
5952
+ feedback: z29.string().optional().describe(
5718
5953
  'General feedback or context about the datamart (e.g. "This datamart contains e-commerce data for the US market").'
5719
5954
  )
5720
5955
  },
@@ -5776,7 +6011,7 @@ You can now:
5776
6011
  }
5777
6012
 
5778
6013
  // src/tools/query/semantic-generation.ts
5779
- import { z as z28 } from "zod";
6014
+ import { z as z30 } from "zod";
5780
6015
  function nonEmptyString(value) {
5781
6016
  return typeof value === "string" && value.trim().length > 0 ? value : null;
5782
6017
  }
@@ -5808,9 +6043,9 @@ async function resolveDatamartFromWorkspace(params) {
5808
6043
  });
5809
6044
  if (resolution.kind !== "resolved") return { ok: false, response: resolution.response };
5810
6045
  const selected = resolution.selected;
5811
- const datamartId = workspaceDatamartIdFrom(selected);
6046
+ const datamartId2 = workspaceDatamartIdFrom(selected);
5812
6047
  const workspaceDatamartName = workspaceDatamartNameFrom(selected);
5813
- if (!datamartId) {
6048
+ if (!datamartId2) {
5814
6049
  return {
5815
6050
  ok: false,
5816
6051
  response: toolError(
@@ -5828,40 +6063,11 @@ async function resolveDatamartFromWorkspace(params) {
5828
6063
  }
5829
6064
  return {
5830
6065
  ok: true,
5831
- datamartName: workspaceDatamartName || params.datamartName || datamartId,
5832
- datamartId,
6066
+ datamartName: workspaceDatamartName || params.datamartName || datamartId2,
6067
+ datamartId: datamartId2,
5833
6068
  workspaceName: resolution.name
5834
6069
  };
5835
6070
  }
5836
- async function findDatamartIdInLinkedWorkspaces(params) {
5837
- const services = createServiceBundle(params.client);
5838
- const workspaces = await services.workspacesApi.listWorkspaces();
5839
- const matches = workspaces.map((workspace) => {
5840
- const record = workspace;
5841
- return {
5842
- name: nonEmptyString(record.name),
5843
- datamartName: workspaceDatamartNameFrom(record),
5844
- datamartId: workspaceDatamartIdFrom(record)
5845
- };
5846
- }).filter(
5847
- (workspace) => workspace.datamartName?.toLowerCase() === params.datamartName.toLowerCase() && workspace.datamartId
5848
- );
5849
- const uniqueIds = [...new Set(matches.map((workspace) => workspace.datamartId))];
5850
- if (uniqueIds.length === 1 && uniqueIds[0]) {
5851
- return {
5852
- kind: "resolved",
5853
- datamartId: uniqueIds[0],
5854
- workspaceName: matches.find((workspace) => workspace.datamartId === uniqueIds[0])?.name || void 0
5855
- };
5856
- }
5857
- if (uniqueIds.length > 1) {
5858
- return {
5859
- kind: "ambiguous",
5860
- workspaceNames: matches.map((workspace) => workspace.name).filter((name) => !!name)
5861
- };
5862
- }
5863
- return { kind: "none" };
5864
- }
5865
6071
  async function resolveDatamart(params) {
5866
6072
  if (params.datamartId) {
5867
6073
  return {
@@ -5890,39 +6096,19 @@ async function resolveDatamart(params) {
5890
6096
  toolName: params.toolName
5891
6097
  });
5892
6098
  if (resolution.kind !== "resolved") return { ok: false, response: resolution.response };
5893
- const datamartId = datamartIdFrom(resolution.selected);
5894
- if (!datamartId) {
5895
- const workspaceMatch = await findDatamartIdInLinkedWorkspaces({
5896
- client: params.client,
5897
- datamartName: resolution.name
5898
- });
5899
- if (workspaceMatch.kind === "resolved") {
5900
- return {
5901
- ok: true,
5902
- datamartName: resolution.name,
5903
- datamartId: workspaceMatch.datamartId,
5904
- workspaceName: workspaceMatch.workspaceName
5905
- };
5906
- }
5907
- if (workspaceMatch.kind === "ambiguous") {
5908
- return {
5909
- ok: false,
5910
- response: toolError(
5911
- `Datamart "${resolution.name}" is linked from multiple workspaces with different IDs. Call ${params.toolName} again with workspaceName. Matching workspaces: ${workspaceMatch.workspaceNames.join(", ")}`
5912
- )
5913
- };
5914
- }
6099
+ const datamartId2 = datamartIdFrom(resolution.selected);
6100
+ if (!datamartId2) {
5915
6101
  return {
5916
6102
  ok: false,
5917
6103
  response: toolError(
5918
- `Datamart "${resolution.name}" resolved by name but no id/_id/datamartId/dataMartId was returned and no linked workspace exposed its ID. Pass workspaceName for a workspace linked to this datamart, pass datamartId directly, or update the datamart list API to include IDs.`
6104
+ `Datamart "${resolution.name}" did not include an ID in the datamart list. Refresh the list and try again, or pass datamartId directly.`
5919
6105
  )
5920
6106
  };
5921
6107
  }
5922
6108
  return {
5923
6109
  ok: true,
5924
6110
  datamartName: resolution.name,
5925
- datamartId
6111
+ datamartId: datamartId2
5926
6112
  };
5927
6113
  }
5928
6114
  function registerStartSemanticLayerGeneration(server, client) {
@@ -5931,9 +6117,9 @@ function registerStartSemanticLayerGeneration(server, client) {
5931
6117
  "start_semantic_layer_generation",
5932
6118
  "Start backend semantic-layer auto-generation for a datamart. Returns an async job id to poll.",
5933
6119
  {
5934
- datamartName: z28.string().optional().describe("Datamart name. Omit to list available datamarts"),
5935
- workspaceName: z28.string().optional().describe("Workspace linked to the datamart. Use when datamartName resolution needs a dataMartId bridge."),
5936
- datamartId: z28.string().optional().describe("Datamart UUID. Use only when already known.")
6120
+ datamartName: z30.string().optional().describe("Datamart name. Omit to list available datamarts"),
6121
+ workspaceName: z30.string().optional().describe("Workspace linked to the datamart. Use when datamartName resolution needs a dataMartId bridge."),
6122
+ datamartId: z30.string().optional().describe("Datamart UUID. Use only when already known.")
5937
6123
  },
5938
6124
  WRITE_CREATE,
5939
6125
  async (params) => {
@@ -5975,9 +6161,9 @@ function registerGetSemanticLayerGenerationStatus(server, client) {
5975
6161
  "get_semantic_layer_generation_status",
5976
6162
  "Get semantic-layer auto-generation job status for a datamart.",
5977
6163
  {
5978
- datamartName: z28.string().optional().describe("Datamart name. Omit to list available datamarts"),
5979
- workspaceName: z28.string().optional().describe("Workspace linked to the datamart. Use when datamartName resolution needs a dataMartId bridge."),
5980
- datamartId: z28.string().optional().describe("Datamart UUID. Use only when already known.")
6164
+ datamartName: z30.string().optional().describe("Datamart name. Omit to list available datamarts"),
6165
+ workspaceName: z30.string().optional().describe("Workspace linked to the datamart. Use when datamartName resolution needs a dataMartId bridge."),
6166
+ datamartId: z30.string().optional().describe("Datamart UUID. Use only when already known.")
5981
6167
  },
5982
6168
  READ_ONLY,
5983
6169
  async (params) => {
@@ -6014,17 +6200,23 @@ function registerGetSemanticLayerGenerationStatus(server, client) {
6014
6200
  }
6015
6201
 
6016
6202
  // src/tools/widgets/list-widgets.ts
6017
- import { z as z29 } from "zod";
6203
+ import { z as z31 } from "zod";
6204
+ function widgetReferenceId(widget) {
6205
+ for (const candidate2 of [widget.externalMetricId, widget.id, widget._id, widget.metricId]) {
6206
+ if (typeof candidate2 === "string" && candidate2.trim().length > 0) return candidate2.trim();
6207
+ }
6208
+ return void 0;
6209
+ }
6018
6210
  function registerListWidgets(server, client) {
6019
6211
  const services = createServiceBundle(client);
6020
6212
  const tool = server.tool(
6021
6213
  "list_widgets",
6022
6214
  "List widgets on a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous). Returns each widget's externalMetricId (uuid, used for update/remove), metricId (slug), name, published state, and creator identifier.",
6023
6215
  {
6024
- clientId: z29.string().describe("Client/tenant identifier for this dashboard"),
6025
- dashboardName: z29.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6026
- workspaceName: z29.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6027
- embedId: z29.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier.")
6216
+ clientId: z31.string().describe("Client/tenant identifier for this dashboard"),
6217
+ dashboardName: z31.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6218
+ workspaceName: z31.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6219
+ embedId: z31.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier.")
6028
6220
  },
6029
6221
  READ_ONLY,
6030
6222
  async (params) => {
@@ -6043,17 +6235,18 @@ function registerListWidgets(server, client) {
6043
6235
  );
6044
6236
  if (hasChartApps(server)) {
6045
6237
  const visible = (Array.isArray(widgets) ? widgets : []).slice(0, 50);
6046
- return chartResult(`${visible.length} widgets available in the first dashboard page.`, {
6238
+ const references = visible.flatMap((w) => {
6239
+ const widgetId = widgetReferenceId(w);
6240
+ return widgetId ? [{
6241
+ name: typeof w.name === "string" ? w.name.slice(0, 500) : "Untitled widget",
6242
+ reference: { widgetId, embedId, clientId: params.clientId }
6243
+ }] : [];
6244
+ });
6245
+ return chartResult(`${references.length} widgets available in the first dashboard page.`, {
6047
6246
  kind: "dashboard",
6048
6247
  title: "Dashboard",
6049
6248
  rows: [],
6050
- widgets: visible.flatMap((w) => {
6051
- const widgetId = w.externalMetricId || w.id || w._id;
6052
- return typeof widgetId === "string" ? [{
6053
- name: typeof w.name === "string" ? w.name.slice(0, 500) : "Untitled widget",
6054
- reference: { widgetId, embedId, clientId: params.clientId }
6055
- }] : [];
6056
- }),
6249
+ widgets: references,
6057
6250
  description: Array.isArray(widgets) && widgets.length > 50 ? "Showing the first 50 widgets." : ""
6058
6251
  });
6059
6252
  }
@@ -6087,29 +6280,29 @@ function registerListWidgets(server, client) {
6087
6280
  }
6088
6281
 
6089
6282
  // src/tools/widgets/add-widget.ts
6090
- import { z as z30 } from "zod";
6283
+ import { z as z32 } from "zod";
6091
6284
  function registerAddWidget(server, client) {
6092
6285
  const services = createServiceBundle(client);
6093
6286
  server.tool(
6094
6287
  "add_widget",
6095
6288
  "Add a widget to a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous). Cloning via `templateMetricId` is the recommended path \u2014 query, chart config, and datasource are all inherited. Data app and datasource auto-resolve; never ask the user for them.",
6096
6289
  {
6097
- clientId: z30.string().describe("Client/tenant identifier (widget is created in their scope)"),
6098
- dashboardName: z30.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6099
- workspaceName: z30.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6100
- embedId: z30.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6101
- dataAppName: z30.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
6102
- templateMetricId: z30.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
6103
- name: z30.string().optional().describe("Widget name (overrides template)"),
6104
- metricId: z30.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
6105
- query: z30.string().optional().describe("SQL query body (overrides template)"),
6106
- chartOptions: z30.record(z30.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
6107
- description: z30.string().optional(),
6108
- companyIntegrationId: z30.string().optional().describe("Datasource UUID override. Auto-resolved from `embedId` or sole active datasource; leave blank unless the user explicitly picked one by ID."),
6109
- integrationName: z30.string().optional().describe('Datasource display name (e.g. "Snowflake"). Use this when the user picks a datasource by name \u2014 the id is looked up automatically.'),
6110
- isPublished: z30.boolean().optional().describe("Publish immediately (default true)"),
6111
- body: z30.record(z30.unknown()).optional().describe("Raw createMetric body passthrough. If provided, used as-is (after defaults). Overrides all other params."),
6112
- widgets: z30.array(z30.record(z30.unknown())).min(1).max(50).optional().describe("Batch of raw createMetric bodies. Dashboard attachment is added to each body.")
6290
+ clientId: z32.string().describe("Client/tenant identifier (widget is created in their scope)"),
6291
+ dashboardName: z32.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6292
+ workspaceName: z32.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6293
+ embedId: z32.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6294
+ dataAppName: z32.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
6295
+ templateMetricId: z32.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
6296
+ name: z32.string().optional().describe("Widget name (overrides template)"),
6297
+ metricId: z32.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
6298
+ query: z32.string().optional().describe("SQL query body (overrides template)"),
6299
+ chartOptions: z32.record(z32.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
6300
+ description: z32.string().optional(),
6301
+ companyIntegrationId: z32.string().optional().describe("Datasource UUID override. Auto-resolved from `embedId` or sole active datasource; leave blank unless the user explicitly picked one by ID."),
6302
+ integrationName: z32.string().optional().describe('Datasource display name (e.g. "Snowflake"). Use this when the user picks a datasource by name \u2014 the id is looked up automatically.'),
6303
+ isPublished: z32.boolean().optional().describe("Publish immediately (default true)"),
6304
+ body: z32.record(z32.unknown()).optional().describe("Raw createMetric body passthrough. If provided, used as-is (after defaults). Overrides all other params."),
6305
+ widgets: z32.array(z32.record(z32.unknown())).min(1).max(50).optional().describe("Batch of raw createMetric bodies. Dashboard attachment is added to each body.")
6113
6306
  },
6114
6307
  WRITE_CREATE,
6115
6308
  async (params) => {
@@ -6257,20 +6450,20 @@ Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust
6257
6450
  }
6258
6451
 
6259
6452
  // src/tools/widgets/update-widget.ts
6260
- import { z as z31 } from "zod";
6453
+ import { z as z33 } from "zod";
6261
6454
  function registerUpdateWidget(server, client) {
6262
6455
  const services = createServiceBundle(client);
6263
6456
  server.tool(
6264
6457
  "update_widget",
6265
6458
  "Update a widget on a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous) + `externalMetricId` + `set`. Data app auto-resolves from the dashboard.",
6266
6459
  {
6267
- externalMetricId: z31.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
6268
- clientId: z31.string().describe("Client/tenant identifier"),
6269
- dashboardName: z31.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6270
- workspaceName: z31.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6271
- embedId: z31.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6272
- dataAppName: z31.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank."),
6273
- set: z31.record(z31.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
6460
+ externalMetricId: z33.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
6461
+ clientId: z33.string().describe("Client/tenant identifier"),
6462
+ dashboardName: z33.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6463
+ workspaceName: z33.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6464
+ embedId: z33.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6465
+ dataAppName: z33.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank."),
6466
+ set: z33.record(z33.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
6274
6467
  },
6275
6468
  WRITE_IDEMPOTENT,
6276
6469
  async (params) => {
@@ -6333,19 +6526,19 @@ Use \`list_widgets\` to verify.`
6333
6526
  }
6334
6527
 
6335
6528
  // src/tools/widgets/remove-widget.ts
6336
- import { z as z32 } from "zod";
6529
+ import { z as z34 } from "zod";
6337
6530
  function registerRemoveWidget(server, client) {
6338
6531
  const services = createServiceBundle(client);
6339
6532
  server.tool(
6340
6533
  "remove_widget",
6341
6534
  "Remove a widget from a customer dashboard. Non-technical flow: pass `clientId` + `dashboardName` (+ optional `workspaceName` if ambiguous) + `externalMetricId`. Per-client soft delete \u2014 the widget stays on the template dashboard and on other customers' dashboards.",
6342
6535
  {
6343
- externalMetricId: z32.string().describe("Internal uuid of the widget (externalMetrics.id)"),
6344
- clientId: z32.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
6345
- dashboardName: z32.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6346
- workspaceName: z32.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6347
- embedId: z32.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6348
- dataAppName: z32.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank.")
6536
+ externalMetricId: z34.string().describe("Internal uuid of the widget (externalMetrics.id)"),
6537
+ clientId: z34.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
6538
+ dashboardName: z34.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6539
+ workspaceName: z34.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6540
+ embedId: z34.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6541
+ dataAppName: z34.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank.")
6349
6542
  },
6350
6543
  DESTRUCTIVE,
6351
6544
  async (params) => {
@@ -6401,21 +6594,21 @@ Note: this is a per-client soft delete. The widget remains available for other c
6401
6594
  }
6402
6595
 
6403
6596
  // src/tools/widgets/generate-widget.ts
6404
- import { z as z33 } from "zod";
6597
+ import { z as z35 } from "zod";
6405
6598
  function registerGenerateWidget(server, client) {
6406
6599
  const services = createServiceBundle(client);
6407
6600
  const tool = server.tool(
6408
6601
  "generate_widget",
6409
6602
  "AI-generate a widget draft from a natural-language prompt. Does NOT persist \u2014 returns a draft to pass to `add_widget`. Non-technical flow: `clientId` + `dashboardName` (+ `workspaceName` if ambiguous); data app and datasource auto-resolve. Never ask the user for data app, datasource, or integration.",
6410
6603
  {
6411
- clientId: z33.string().describe("Client/tenant identifier"),
6412
- userInputs: z33.record(z33.unknown()).describe('Prompt + context object. Typical shape: { prompt: "calls per day over last 30 days", schema: {...}, examples: [...] }. Backend decides which keys it honors.'),
6413
- dashboardName: z33.string().optional().describe("Dashboard name (non-technical). Preferred input for CS/CX flows \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6414
- workspaceName: z33.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6415
- embedId: z33.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6416
- dataAppName: z33.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
6417
- integrationId: z33.string().optional().describe("Technical override \u2014 the datasource UUID. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets."),
6418
- integrationName: z33.string().optional().describe("Technical override \u2014 datasource display name. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets.")
6604
+ clientId: z35.string().describe("Client/tenant identifier"),
6605
+ userInputs: z35.record(z35.unknown()).describe('Prompt + context object. Typical shape: { prompt: "calls per day over last 30 days", schema: {...}, examples: [...] }. Backend decides which keys it honors.'),
6606
+ dashboardName: z35.string().optional().describe("Dashboard name (non-technical). Preferred input for CS/CX flows \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6607
+ workspaceName: z35.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6608
+ embedId: z35.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6609
+ dataAppName: z35.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
6610
+ integrationId: z35.string().optional().describe("Technical override \u2014 the datasource UUID. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets."),
6611
+ integrationName: z35.string().optional().describe("Technical override \u2014 datasource display name. Non-technical callers must leave this blank; the datasource is inherited from the dashboard's existing widgets.")
6419
6612
  },
6420
6613
  READ_ONLY,
6421
6614
  async (params) => {
@@ -6500,9 +6693,9 @@ function registerGenerateWidget(server, client) {
6500
6693
  }
6501
6694
 
6502
6695
  // src/tools/widgets/create-or-update-metric-from-query.ts
6503
- import { z as z34 } from "zod";
6504
- var modeSchema = z34.enum(["dry_run", "apply"]).default("dry_run");
6505
- var conflictStrategySchema = z34.enum(["fail", "update", "create_new"]).default("fail");
6696
+ import { z as z36 } from "zod";
6697
+ var modeSchema = z36.enum(["dry_run", "apply"]).default("dry_run");
6698
+ var conflictStrategySchema = z36.enum(["fail", "update", "create_new"]).default("fail");
6506
6699
  function makePlanText(plan) {
6507
6700
  return JSON.stringify(plan, null, 2);
6508
6701
  }
@@ -6519,22 +6712,22 @@ function registerCreateOrUpdateMetricFromQuery(server, client) {
6519
6712
  "[EMBEDDING] Create or update a production metric/widget on a customer dashboard from validated SQL. Requires clientId (multi-tenant embed). Defaults to dry-run; apply requires explicit confirmation.",
6520
6713
  {
6521
6714
  mode: modeSchema.describe("dry_run returns the plan only; apply executes after confirmation"),
6522
- confirm: z34.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
6715
+ confirm: z36.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
6523
6716
  conflictStrategy: conflictStrategySchema.describe("What to do if metricId already exists on the target dashboard"),
6524
- clientId: z34.string().describe("Client/tenant identifier for the dashboard widget scope"),
6525
- workspaceName: z34.string().describe("Target workspace name"),
6526
- dashboardName: z34.string().optional().describe("Dashboard name to attach to"),
6527
- embedId: z34.string().optional().describe("Technical embed id alternative to dashboardName"),
6528
- dataAppName: z34.string().optional().describe("Optional data app override"),
6529
- name: z34.string().describe("Metric/widget display name"),
6530
- metricId: z34.string().describe("Stable metric slug to create or update"),
6531
- query: z34.string().describe("SQL query already validated against the warehouse"),
6532
- description: z34.string().optional().describe("Metric description"),
6533
- chartOptions: z34.record(z34.unknown()).optional().describe("Chart configuration JSON"),
6534
- companyIntegrationId: z34.string().optional().describe("Datasource UUID override"),
6535
- integrationName: z34.string().optional().describe("Datasource display name override"),
6536
- isPublished: z34.boolean().optional().describe("Publish after apply. Defaults to false for production safety"),
6537
- requestedBy: z34.string().optional().describe("Human operator/request context to store on the metric when supported")
6717
+ clientId: z36.string().describe("Client/tenant identifier for the dashboard widget scope"),
6718
+ workspaceName: z36.string().describe("Target workspace name"),
6719
+ dashboardName: z36.string().optional().describe("Dashboard name to attach to"),
6720
+ embedId: z36.string().optional().describe("Technical embed id alternative to dashboardName"),
6721
+ dataAppName: z36.string().optional().describe("Optional data app override"),
6722
+ name: z36.string().describe("Metric/widget display name"),
6723
+ metricId: z36.string().describe("Stable metric slug to create or update"),
6724
+ query: z36.string().describe("SQL query already validated against the warehouse"),
6725
+ description: z36.string().optional().describe("Metric description"),
6726
+ chartOptions: z36.record(z36.unknown()).optional().describe("Chart configuration JSON"),
6727
+ companyIntegrationId: z36.string().optional().describe("Datasource UUID override"),
6728
+ integrationName: z36.string().optional().describe("Datasource display name override"),
6729
+ isPublished: z36.boolean().optional().describe("Publish after apply. Defaults to false for production safety"),
6730
+ requestedBy: z36.string().optional().describe("Human operator/request context to store on the metric when supported")
6538
6731
  },
6539
6732
  WRITE_IDEMPOTENT,
6540
6733
  async (params) => {
@@ -6694,8 +6887,8 @@ function registerCreateOrUpdateMetricFromQuery(server, client) {
6694
6887
  }
6695
6888
 
6696
6889
  // src/tools/widgets/create-workspace-metric.ts
6697
- import { z as z35 } from "zod";
6698
- var modeSchema2 = z35.enum(["dry_run", "apply"]).default("dry_run");
6890
+ import { z as z37 } from "zod";
6891
+ var modeSchema2 = z37.enum(["dry_run", "apply"]).default("dry_run");
6699
6892
  function registerCreateWorkspaceMetric(server, client) {
6700
6893
  server.tool(
6701
6894
  "create_workspace_metric",
@@ -6704,16 +6897,18 @@ function registerCreateWorkspaceMetric(server, client) {
6704
6897
  mode: modeSchema2.describe(
6705
6898
  "dry_run validates the SQL only (no write); apply creates or updates the metric after confirmation"
6706
6899
  ),
6707
- confirm: z35.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
6708
- workspaceName: z35.string().describe("Target workspace name"),
6709
- name: z35.string().describe("Metric display name"),
6710
- query: z35.string().describe("SQL query to validate and store"),
6711
- metricId: z35.string().optional().describe(
6900
+ confirm: z37.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
6901
+ workspaceName: z37.string().describe("Target workspace name"),
6902
+ name: z37.string().describe("Metric display name"),
6903
+ query: z37.string().describe(
6904
+ "SQL query to validate and store. In a TABLE-tenancy workspace never compare the client column to a fixed client value; use `client_id_variable` so each client sees only its own rows (the backend refuses it with TENANT_KEY_LITERAL)."
6905
+ ),
6906
+ metricId: z37.string().optional().describe(
6712
6907
  "Stable metric slug. Pass an existing slug to UPDATE that metric in place. Omit to create \u2014 the slug is derived deterministically from the name (no random/uniqueness suffix), so re-running with the same name upserts the same metric rather than creating a duplicate."
6713
6908
  ),
6714
- description: z35.string().optional().describe("Metric description"),
6715
- datasourceId: z35.string().optional().describe("Datasource UUID override; defaults to the workspace datasource"),
6716
- isPublished: z35.boolean().optional().describe(
6909
+ description: z37.string().optional().describe("Metric description"),
6910
+ datasourceId: z37.string().optional().describe("Datasource UUID override; defaults to the workspace datasource"),
6911
+ isPublished: z37.boolean().optional().describe(
6717
6912
  "Publish after apply. Defaults to false. Applies to creates; on updates the existing publish state is preserved."
6718
6913
  )
6719
6914
  },
@@ -6769,25 +6964,25 @@ function registerCreateWorkspaceMetric(server, client) {
6769
6964
  }
6770
6965
 
6771
6966
  // src/tools/widgets/validate-metric-spec.ts
6772
- import { z as z36 } from "zod";
6967
+ import { z as z38 } from "zod";
6773
6968
  function registerValidateMetricSpec(server, client) {
6774
6969
  const services = createServiceBundle(client);
6775
6970
  server.tool(
6776
6971
  "validate_metric_spec",
6777
6972
  "Validate a production metric spec from SQL without saving it. Resolves dashboard, data app, datasource, conflicts, and required fields.",
6778
6973
  {
6779
- clientId: z36.string().describe("Client/tenant identifier used to validate dashboard widget scope"),
6780
- workspaceName: z36.string().describe("Target production-backed workspace name"),
6781
- dashboardName: z36.string().optional().describe("Dashboard name to attach to"),
6782
- embedId: z36.string().optional().describe("Technical embed id alternative to dashboardName"),
6783
- dataAppName: z36.string().optional().describe("Optional data app override"),
6784
- name: z36.string().describe("Metric/widget display name"),
6785
- metricId: z36.string().describe("Stable metric slug to create or update"),
6786
- query: z36.string().describe("SQL query already validated against the warehouse"),
6787
- description: z36.string().optional().describe("Metric description"),
6788
- chartOptions: z36.record(z36.unknown()).optional().describe("Chart configuration JSON"),
6789
- companyIntegrationId: z36.string().optional().describe("Datasource UUID override"),
6790
- integrationName: z36.string().optional().describe("Datasource display name override")
6974
+ clientId: z38.string().describe("Client/tenant identifier used to validate dashboard widget scope"),
6975
+ workspaceName: z38.string().describe("Target production-backed workspace name"),
6976
+ dashboardName: z38.string().optional().describe("Dashboard name to attach to"),
6977
+ embedId: z38.string().optional().describe("Technical embed id alternative to dashboardName"),
6978
+ dataAppName: z38.string().optional().describe("Optional data app override"),
6979
+ name: z38.string().describe("Metric/widget display name"),
6980
+ metricId: z38.string().describe("Stable metric slug to create or update"),
6981
+ query: z38.string().describe("SQL query already validated against the warehouse"),
6982
+ description: z38.string().optional().describe("Metric description"),
6983
+ chartOptions: z38.record(z38.unknown()).optional().describe("Chart configuration JSON"),
6984
+ companyIntegrationId: z38.string().optional().describe("Datasource UUID override"),
6985
+ integrationName: z38.string().optional().describe("Datasource display name override")
6791
6986
  },
6792
6987
  READ_ONLY,
6793
6988
  async (params) => {
@@ -6878,20 +7073,20 @@ function registerValidateMetricSpec(server, client) {
6878
7073
  }
6879
7074
 
6880
7075
  // src/tools/reports/list-scheduled-reports.ts
6881
- import { z as z37 } from "zod";
7076
+ import { z as z39 } from "zod";
6882
7077
  function registerListScheduledReports(server, client) {
6883
7078
  const services = createServiceBundle(client);
6884
7079
  server.tool(
6885
7080
  "list_scheduled_reports",
6886
7081
  "List configured scheduled reports for a specific client/dashboard embed. Shows schedule metadata, not delivery success history.",
6887
7082
  {
6888
- clientId: z37.string().describe("Client/tenant identifier. Required by the current backend API for non-empty results"),
6889
- dashboardName: z37.string().optional().describe("Dashboard name to inspect"),
6890
- workspaceName: z37.string().optional().describe("Workspace name if dashboardName is ambiguous"),
6891
- embedId: z37.string().optional().describe("Technical embed id alternative to dashboardName"),
6892
- userIdentifier: z37.string().optional().describe("Optional creator/user filter"),
6893
- pageNumber: z37.number().int().positive().optional().describe("Page number when pagination is enabled"),
6894
- isPagination: z37.boolean().optional().describe("Enable backend pagination")
7083
+ clientId: z39.string().describe("Client/tenant identifier. Required by the current backend API for non-empty results"),
7084
+ dashboardName: z39.string().optional().describe("Dashboard name to inspect"),
7085
+ workspaceName: z39.string().optional().describe("Workspace name if dashboardName is ambiguous"),
7086
+ embedId: z39.string().optional().describe("Technical embed id alternative to dashboardName"),
7087
+ userIdentifier: z39.string().optional().describe("Optional creator/user filter"),
7088
+ pageNumber: z39.number().int().positive().optional().describe("Page number when pagination is enabled"),
7089
+ isPagination: z39.boolean().optional().describe("Enable backend pagination")
6895
7090
  },
6896
7091
  READ_ONLY,
6897
7092
  async (params) => {
@@ -6965,7 +7160,7 @@ function registerWhoami(server, client) {
6965
7160
  status: "authenticated",
6966
7161
  activeBearer: "service-token",
6967
7162
  ...auth.oauthFallback ? { oauthFallback: true } : {},
6968
- note: auth.oauthFallback ? "OAuth token acquisition failed; the configured static service token is in use. Mutations use the static org-level principal, with actor=system. OAuth will be retried after a short cooldown." : "Org-level service token (DATABRAIN_SERVICE_TOKEN). Per-user identity is NOT bound \u2014 mutations are audit-logged as actor=system. Per-user binding will be available once the OAuth login flow ships."
7163
+ note: auth.oauthFallback ? "OAuth token acquisition failed; the configured static service token is in use. Mutations use the static org-level principal, with actor=system. OAuth will be retried after a short cooldown." : "Org-level service token (DATABRAIN_SERVICE_TOKEN). Per-user identity is NOT bound \u2014 mutations are audit-logged as actor=system. For per-user access, connect an AI app to the hosted server (it signs in with your Databrain account)."
6969
7164
  },
6970
7165
  null,
6971
7166
  2
@@ -7000,6 +7195,460 @@ function registerWhoami(server, client) {
7000
7195
  );
7001
7196
  }
7002
7197
 
7198
+ // src/tools/custom-chart/scaffold.ts
7199
+ import { z as z41 } from "zod";
7200
+
7201
+ // src/tools/custom-chart/shared.ts
7202
+ import { z as z40 } from "zod";
7203
+ var slotInputSchema = slotSchema;
7204
+ var optionInputSchema = optionSchema;
7205
+ var manifestInputSchema = z40.record(z40.unknown()).describe("manifest.json as an object (schemaVersion 2). Validated with the same rules as the server.");
7206
+ var bundleInputShape = {
7207
+ manifest: manifestInputSchema.optional(),
7208
+ bundleJs: z40.string().optional().describe("dist/index.js from `databrain custom-chart build` (the shim plus the chart module)."),
7209
+ bundleCss: z40.string().optional().describe("The raw chart.css, kept for provenance."),
7210
+ iconSvg: z40.string().optional().describe("icon.svg, at most 16 KiB, no script, no foreignObject, no external href."),
7211
+ projectDir: z40.string().optional().describe("Local only: a chart project folder. The tool reads it instead of manifest/bundleJs/iconSvg.")
7212
+ };
7213
+ function formatProblems(problems) {
7214
+ if (problems.length === 0) return "No problems.";
7215
+ return problems.map((p) => `${p.path || "/"} ${p.code} ${p.message}`).join("\n");
7216
+ }
7217
+ function projectDirRefusal() {
7218
+ if (process.env.DATABRAIN_HOSTED_PUBLIC_URL) {
7219
+ return "projectDir is not available on the hosted server. Pass manifest, bundleJs, bundleCss and iconSvg instead.";
7220
+ }
7221
+ return null;
7222
+ }
7223
+ async function resolveBundleInput(params, options) {
7224
+ if (params.projectDir) {
7225
+ const refusal = projectDirRefusal();
7226
+ if (refusal) return { problems: [], error: refusal, source: "projectDir" };
7227
+ if (params.manifest || params.bundleJs || params.iconSvg) {
7228
+ return { problems: [], error: "Pass either projectDir or manifest/bundleJs/iconSvg, not both.", source: "projectDir" };
7229
+ }
7230
+ if (options.build) {
7231
+ const result = await buildProject(params.projectDir, { write: false });
7232
+ if (!result.ok || !result.outputs) return { problems: result.problems, source: "projectDir" };
7233
+ const { manifest, bundleJs, bundleCss, iconSvg } = result.outputs;
7234
+ return { bundle: { manifest, bundleJs, bundleCss, iconSvg }, problems: [], source: "projectDir" };
7235
+ }
7236
+ const dist = readDist(params.projectDir);
7237
+ if (!dist.bundle) return { problems: dist.problems, source: "projectDir" };
7238
+ return { bundle: dist.bundle, problems: [], source: "projectDir" };
7239
+ }
7240
+ if (!params.manifest) {
7241
+ return { problems: [], error: "manifest is required (or pass projectDir when running locally).", source: "arguments" };
7242
+ }
7243
+ return {
7244
+ bundle: {
7245
+ manifest: params.manifest,
7246
+ bundleJs: params.bundleJs,
7247
+ bundleCss: params.bundleCss,
7248
+ iconSvg: params.iconSvg
7249
+ },
7250
+ problems: [],
7251
+ source: "arguments"
7252
+ };
7253
+ }
7254
+ async function remoteValidate(client, bundle) {
7255
+ try {
7256
+ const result = await createCustomChartsApi(client).validate(bundle);
7257
+ return { status: "ok", result };
7258
+ } catch (err) {
7259
+ if (err instanceof DatabrainApiError) {
7260
+ if (err.code === "MISSING_SERVICE_TOKEN") {
7261
+ return { status: "skipped", reason: "No service token is configured, so only the local validator ran." };
7262
+ }
7263
+ return { status: "failed", reason: err.message, problems: apiProblems(err) };
7264
+ }
7265
+ return { status: "failed", reason: err.message, problems: [] };
7266
+ }
7267
+ }
7268
+ function isCompleteBundle(bundle) {
7269
+ return typeof bundle.bundleJs === "string" && typeof bundle.iconSvg === "string";
7270
+ }
7271
+
7272
+ // src/tools/custom-chart/scaffold.ts
7273
+ function registerCustomChartScaffold(server, _client) {
7274
+ server.tool(
7275
+ "custom_chart_scaffold",
7276
+ "Return the files of a new custom chart project (manifest.json, chart.ts, chart.css, icon.svg, sample data, guide). Nothing is written or persisted; write the files yourself.",
7277
+ {
7278
+ key: z41.string().describe("Chart key, becomes chart type custom:<key>. Lowercase letters, digits, _ and -, 64 max."),
7279
+ name: z41.string().describe("Display name shown in the chart picker, 1 to 256 characters."),
7280
+ description: z41.string().optional().describe("Optional description, up to 500 characters."),
7281
+ slots: z41.array(slotInputSchema).optional().describe("Data slots. Defaults to category (dimension), value (measure), target (optional measure)."),
7282
+ options: z41.array(optionInputSchema).optional().describe("Chart options. Defaults to the template options (labels, subtitle, radius, colour, sort).")
7283
+ },
7284
+ READ_ONLY,
7285
+ async (params) => {
7286
+ try {
7287
+ const result = scaffoldProject({
7288
+ key: params.key,
7289
+ name: params.name,
7290
+ description: params.description,
7291
+ slots: params.slots,
7292
+ options: params.options
7293
+ });
7294
+ if (!result.ok) {
7295
+ return toolError(`The requested manifest is not valid:
7296
+ ${formatProblems(result.problems)}`);
7297
+ }
7298
+ return toolSuccess(
7299
+ JSON.stringify(
7300
+ {
7301
+ status: "scaffold_only",
7302
+ key: params.key,
7303
+ chartType: `custom:${params.key}`,
7304
+ files: result.files,
7305
+ nextStep: result.nextStep
7306
+ },
7307
+ null,
7308
+ 2
7309
+ )
7310
+ );
7311
+ } catch (err) {
7312
+ return toolError(`Unexpected error: ${err.message}`);
7313
+ }
7314
+ }
7315
+ );
7316
+ }
7317
+
7318
+ // src/tools/custom-chart/validate.ts
7319
+ function registerCustomChartValidate(server, client) {
7320
+ server.tool(
7321
+ "custom_chart_validate",
7322
+ "Validate a custom chart manifest and bundle with the local rules and, when a service token is configured, the server rules. Never persists anything.",
7323
+ bundleInputShape,
7324
+ READ_ONLY,
7325
+ async (params) => {
7326
+ try {
7327
+ const resolved = await resolveBundleInput(params, { build: true });
7328
+ if (resolved.error) return toolError(resolved.error);
7329
+ if (!resolved.bundle) {
7330
+ return toolSuccess(JSON.stringify({ ok: false, problems: resolved.problems, source: resolved.source }, null, 2));
7331
+ }
7332
+ const { bundle } = resolved;
7333
+ const problems = validateBundle(bundle);
7334
+ const complete = isCompleteBundle(bundle);
7335
+ const result = {
7336
+ ok: problems.length === 0,
7337
+ problems,
7338
+ source: resolved.source,
7339
+ contentHash: complete && problems.length === 0 ? contentHash(bundle) : void 0,
7340
+ sizeBytes: complete && problems.length === 0 ? bundleSizeBytes(bundle) : void 0
7341
+ };
7342
+ if (!complete) {
7343
+ result.note = "Only the manifest was checked. Pass bundleJs and iconSvg (or projectDir) to check the whole package.";
7344
+ } else if (problems.length === 0) {
7345
+ const remote = await remoteValidate(client, bundle);
7346
+ if (remote.status === "ok") {
7347
+ result.remote = remote.result;
7348
+ if (!remote.result.ok) {
7349
+ result.ok = false;
7350
+ result.problems = remote.result.problems;
7351
+ }
7352
+ if (remote.result.contentHash) result.contentHash = remote.result.contentHash;
7353
+ if (typeof remote.result.sizeBytes === "number") result.sizeBytes = remote.result.sizeBytes;
7354
+ } else if (remote.status === "skipped") {
7355
+ result.remote = { status: "skipped", reason: remote.reason };
7356
+ } else {
7357
+ result.remote = { status: "failed", reason: remote.reason };
7358
+ if (remote.problems.length > 0) {
7359
+ result.ok = false;
7360
+ result.problems = remote.problems;
7361
+ }
7362
+ }
7363
+ }
7364
+ result.nextStep = result.ok ? "Upload with custom_chart_upload (mode dry_run first) or `databrain custom-chart upload`." : "Fix every problem (path, code, message), then validate again.";
7365
+ return toolSuccess(JSON.stringify(result, null, 2));
7366
+ } catch (err) {
7367
+ return toolError(`Unexpected error: ${err.message}`);
7368
+ }
7369
+ }
7370
+ );
7371
+ }
7372
+
7373
+ // src/tools/custom-chart/upload.ts
7374
+ import { z as z42 } from "zod";
7375
+ function registerCustomChartUpload(server, client) {
7376
+ const api = createCustomChartsApi(client);
7377
+ server.tool(
7378
+ "custom_chart_upload",
7379
+ "Upload a built custom chart bundle to the company. Creates the chart (private, version 1) or a new preview version. Defaults to dry_run; apply requires explicit confirmation and can publish in the same call.",
7380
+ {
7381
+ mode: z42.enum(["dry_run", "apply"]).default("dry_run").describe("dry_run validates and returns the plan; apply uploads."),
7382
+ confirm: z42.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
7383
+ publish: z42.boolean().default(false).describe("After a successful apply, publish the uploaded version so every builder can use it."),
7384
+ name: z42.string().optional().describe("Display name override. Defaults to manifest.name."),
7385
+ description: z42.string().optional().describe("Description override. Defaults to manifest.description."),
7386
+ ...bundleInputShape
7387
+ },
7388
+ WRITE_CREATE,
7389
+ async (params) => {
7390
+ try {
7391
+ const mode = params.mode || "dry_run";
7392
+ const resolved = await resolveBundleInput(params, { build: false });
7393
+ if (resolved.error) return toolError(resolved.error);
7394
+ if (!resolved.bundle) {
7395
+ return toolError(`The project could not be read:
7396
+ ${formatProblems(resolved.problems)}`);
7397
+ }
7398
+ const { bundle } = resolved;
7399
+ if (!isCompleteBundle(bundle)) {
7400
+ return toolError("manifest, bundleJs and iconSvg are all required to upload. Run `databrain custom-chart build` and pass dist/index.js, dist/manifest.json and dist/icon.svg (or projectDir when local).");
7401
+ }
7402
+ const problems = validateBundle(bundle);
7403
+ if (problems.length > 0) {
7404
+ return toolError(`The bundle is not valid. Fix these and try again:
7405
+ ${formatProblems(problems)}`);
7406
+ }
7407
+ const manifest = bundle.manifest;
7408
+ const key = manifest.id;
7409
+ const body = {
7410
+ manifest,
7411
+ bundleJs: bundle.bundleJs,
7412
+ bundleCss: bundle.bundleCss,
7413
+ iconSvg: bundle.iconSvg,
7414
+ ...params.name ? { name: params.name } : {},
7415
+ ...params.description ? { description: params.description } : {}
7416
+ };
7417
+ const localHash = contentHash(bundle);
7418
+ const localSize = bundleSizeBytes(bundle);
7419
+ if (mode === "dry_run") {
7420
+ const remote = await remoteValidate(client, bundle);
7421
+ let existing;
7422
+ let planNote;
7423
+ try {
7424
+ existing = (await api.list()).find((chart) => chart.key === key);
7425
+ } catch (err) {
7426
+ planNote = err instanceof DatabrainApiError && err.code === "MISSING_SERVICE_TOKEN" ? "No service token is configured, so the plan could not check whether the chart exists." : `Could not list custom charts: ${err.message}`;
7427
+ }
7428
+ const plan = planUpload(key, existing);
7429
+ const remoteOk = remote.status !== "ok" || remote.result.ok;
7430
+ return toolSuccess(
7431
+ JSON.stringify(
7432
+ {
7433
+ status: "dry_run_only",
7434
+ safety: genericProductionSafetyNote(),
7435
+ ok: remoteOk,
7436
+ key,
7437
+ chartType: `custom:${key}`,
7438
+ contentHash: remote.status === "ok" && remote.result.contentHash ? remote.result.contentHash : localHash,
7439
+ sizeBytes: remote.status === "ok" && typeof remote.result.sizeBytes === "number" ? remote.result.sizeBytes : localSize,
7440
+ problems: remote.status === "ok" ? remote.result.problems : remote.status === "failed" ? remote.problems : [],
7441
+ remote: remote.status === "ok" ? "validated by the server" : remote.reason,
7442
+ plan: plan.summary,
7443
+ ...planNote ? { planNote } : {},
7444
+ publishAfterUpload: params.publish === true,
7445
+ requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
7446
+ nextStep: remoteOk ? `Review this plan. To execute, call again with mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}"${params.publish ? " (publish=true will publish the new version right away)" : ""}.` : "Fix every problem, rebuild, then run dry_run again."
7447
+ },
7448
+ null,
7449
+ 2
7450
+ )
7451
+ );
7452
+ }
7453
+ const guardError = validateProductionApply({ confirm: params.confirm });
7454
+ if (guardError) return toolError(guardError);
7455
+ const uploaded = await api.upload(body);
7456
+ const versionNumber = uploaded.version?.versionNumber;
7457
+ const lines = [];
7458
+ if (uploaded.created === "none") {
7459
+ lines.push(`This exact bundle is already v${versionNumber ?? "?"} of custom:${key}. Nothing was created.`);
7460
+ } else if (uploaded.created === "chart") {
7461
+ lines.push(`Created custom chart custom:${key} (v${versionNumber ?? 1}, status private).`);
7462
+ } else {
7463
+ lines.push(`Uploaded v${versionNumber ?? "?"} of custom:${key}. It is in preview until you publish it.`);
7464
+ }
7465
+ lines.push(`contentHash: ${uploaded.version?.contentHash ?? localHash}`);
7466
+ if (params.publish) {
7467
+ const published = await api.publish(key, uploaded.version?.id);
7468
+ lines.push(`Published v${versionNumber ?? "?"} of custom:${key}. Status: ${published.chart?.status ?? "published"}. Every dashboard using custom:${key} now renders this version.`);
7469
+ } else if (uploaded.created !== "none" || uploaded.chart?.status !== "published") {
7470
+ lines.push(`To make it available to builders: publish_custom_chart with key "${key}" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}", or Settings > Developers > Custom charts.`);
7471
+ }
7472
+ return toolSuccess(lines.join("\n"));
7473
+ } catch (err) {
7474
+ if (err instanceof DatabrainApiError) {
7475
+ const problems = apiProblems(err);
7476
+ if (problems.length > 0) {
7477
+ return toolError(`The server refused the bundle: ${err.message}
7478
+ ${formatProblems(problems)}`);
7479
+ }
7480
+ return err.toToolResponse();
7481
+ }
7482
+ return toolError(`Unexpected error: ${err.message}`);
7483
+ }
7484
+ }
7485
+ );
7486
+ }
7487
+
7488
+ // src/tools/custom-chart/list.ts
7489
+ function registerListCustomCharts(server, client) {
7490
+ server.tool(
7491
+ "list_custom_charts",
7492
+ "List the custom chart types of the company (key, name, status, published and preview versions). Uses the service token.",
7493
+ {},
7494
+ READ_ONLY,
7495
+ async () => {
7496
+ try {
7497
+ const charts = await createCustomChartsApi(client).list();
7498
+ const rows = charts.map((chart) => ({
7499
+ key: chart.key,
7500
+ chartType: `custom:${chart.key}`,
7501
+ name: chart.name,
7502
+ status: chart.status,
7503
+ publishedVersionId: chart.publishedVersionId ?? null,
7504
+ publishedVersionNumber: typeof chart.publishedVersionNumber === "number" ? chart.publishedVersionNumber : void 0,
7505
+ previewVersionId: chart.previewVersionId ?? null,
7506
+ previewVersionNumber: typeof chart.previewVersionNumber === "number" ? chart.previewVersionNumber : void 0,
7507
+ versionsCount: typeof chart.versionsCount === "number" ? chart.versionsCount : void 0,
7508
+ updatedAt: chart.updatedAt,
7509
+ createdBy: chart.createdBy ?? null
7510
+ }));
7511
+ if (rows.length === 0) {
7512
+ return toolSuccess(
7513
+ "No custom charts yet. Scaffold one with custom_chart_scaffold, build it with `databrain custom-chart build`, then upload with custom_chart_upload."
7514
+ );
7515
+ }
7516
+ return toolSuccess(JSON.stringify({ count: rows.length, charts: rows }, null, 2));
7517
+ } catch (err) {
7518
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
7519
+ return toolError(`Unexpected error: ${err.message}`);
7520
+ }
7521
+ }
7522
+ );
7523
+ }
7524
+
7525
+ // src/tools/custom-chart/publish.ts
7526
+ import { z as z43 } from "zod";
7527
+ function registerPublishCustomChart(server, client) {
7528
+ const api = createCustomChartsApi(client);
7529
+ server.tool(
7530
+ "publish_custom_chart",
7531
+ "Publish a custom chart version so every builder in the company can use chart type custom:<key>. Defaults to the preview version. Defaults to dry_run; apply requires explicit confirmation.",
7532
+ {
7533
+ key: z43.string().describe("The chart key (manifest.id), for example revenue-bars."),
7534
+ versionId: z43.string().optional().describe("A specific version id to publish. Defaults to the preview version."),
7535
+ mode: z43.enum(["dry_run", "apply"]).default("dry_run").describe("dry_run shows what would be published; apply publishes."),
7536
+ confirm: z43.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`)
7537
+ },
7538
+ WRITE_IDEMPOTENT,
7539
+ async (params) => {
7540
+ try {
7541
+ const mode = params.mode || "dry_run";
7542
+ const detail = await api.get(params.key);
7543
+ const versions = Array.isArray(detail.versions) ? detail.versions : [];
7544
+ const targetId = params.versionId ?? detail.previewVersionId ?? null;
7545
+ const target = versions.find((version) => version.id === targetId);
7546
+ if (!targetId) {
7547
+ return toolError(
7548
+ `custom:${params.key} has no preview version to publish${detail.publishedVersionId ? ` (v published: ${detail.publishedVersionId})` : ""}. Upload a new version first, or pass versionId.`
7549
+ );
7550
+ }
7551
+ if (params.versionId && versions.length > 0 && !target) {
7552
+ return toolError(`custom:${params.key} has no version with id ${params.versionId}. Versions: ${versions.map((v) => `${v.id} (v${v.versionNumber})`).join(", ")}.`);
7553
+ }
7554
+ if (detail.publishedVersionId === targetId) {
7555
+ return toolSuccess(`custom:${params.key} already has ${target ? `v${target.versionNumber}` : targetId} published. Nothing to do.`);
7556
+ }
7557
+ if (mode === "dry_run") {
7558
+ return toolSuccess(
7559
+ JSON.stringify(
7560
+ {
7561
+ status: "dry_run_only",
7562
+ safety: genericProductionSafetyNote(),
7563
+ key: params.key,
7564
+ chartType: `custom:${params.key}`,
7565
+ name: detail.name,
7566
+ currentStatus: detail.status,
7567
+ currentPublishedVersionId: detail.publishedVersionId ?? null,
7568
+ willPublish: { versionId: targetId, versionNumber: target?.versionNumber, contentHash: target?.contentHash },
7569
+ impact: `Every dashboard using custom:${params.key} will render this version.`,
7570
+ requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
7571
+ nextStep: `Ask the user to confirm, then call again with mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}".`
7572
+ },
7573
+ null,
7574
+ 2
7575
+ )
7576
+ );
7577
+ }
7578
+ const guardError = validateProductionApply({ confirm: params.confirm });
7579
+ if (guardError) return toolError(guardError);
7580
+ const result = await api.publish(params.key, targetId);
7581
+ return toolSuccess(
7582
+ `Published ${target ? `v${target.versionNumber}` : targetId} of custom:${params.key} (${result.chart?.name ?? detail.name}). Status: ${result.chart?.status ?? "published"}. Every dashboard using custom:${params.key} now renders this version.`
7583
+ );
7584
+ } catch (err) {
7585
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
7586
+ return toolError(`Unexpected error: ${err.message}`);
7587
+ }
7588
+ }
7589
+ );
7590
+ }
7591
+
7592
+ // src/tools/tool-titles.ts
7593
+ var TOOL_TITLES = {
7594
+ whoami: "Show active credential",
7595
+ list_datasources: "List data sources",
7596
+ discover_datasource_schema: "Discover data source schema",
7597
+ list_datamarts: "List datamarts",
7598
+ list_workspaces: "List workspaces",
7599
+ sync_datasource: "Sync data source",
7600
+ create_datamart: "Create datamart",
7601
+ create_workspace: "Create workspace",
7602
+ list_data_apps: "List data apps",
7603
+ list_tenants: "List tenants",
7604
+ create_data_app: "Create data app",
7605
+ list_api_tokens: "List API tokens",
7606
+ create_api_token: "Create API token",
7607
+ rotate_api_token: "Rotate API token",
7608
+ create_embed: "Create embed",
7609
+ list_embeds: "List embeds",
7610
+ get_embed_details: "Get embed details",
7611
+ update_embed: "Update embed",
7612
+ customize_embed_theme: "Customize embed theme",
7613
+ get_demo_link: "Get demo link",
7614
+ generate_guest_token: "Generate guest token",
7615
+ generate_embed_code: "Generate embed code",
7616
+ list_dashboards: "List dashboards",
7617
+ create_dashboard: "Create dashboard",
7618
+ get_dashboard: "Get dashboard",
7619
+ dashboard_snapshot: "Dashboard snapshot (picture)",
7620
+ generate_dashboard_tiles: "Generate dashboard tiles",
7621
+ ask_question: "Ask a data question",
7622
+ get_semantic_layer: "Get semantic layer",
7623
+ update_semantic_layer: "Update semantic layer",
7624
+ start_semantic_layer_generation: "Start semantic layer generation",
7625
+ get_semantic_layer_generation_status: "Get semantic layer generation status",
7626
+ search_semantic_model: "Search semantic model",
7627
+ get_semantic_entity: "Get semantic entity",
7628
+ propose_semantic_change: "Propose semantic model change",
7629
+ apply_semantic_change: "Apply semantic model change",
7630
+ eval_semantic_model: "Evaluate semantic model",
7631
+ list_widgets: "List widgets",
7632
+ validate_metric_spec: "Validate metric spec",
7633
+ create_or_update_metric_from_query: "Create or update metric from SQL",
7634
+ create_workspace_metric: "Create workspace metric",
7635
+ add_widget: "Add widget",
7636
+ update_widget: "Update widget",
7637
+ remove_widget: "Remove widget",
7638
+ generate_widget: "Generate widget",
7639
+ list_scheduled_reports: "List scheduled reports",
7640
+ custom_chart_scaffold: "Scaffold custom chart",
7641
+ custom_chart_validate: "Validate custom chart",
7642
+ custom_chart_upload: "Upload custom chart",
7643
+ list_custom_charts: "List custom charts",
7644
+ publish_custom_chart: "Publish custom chart"
7645
+ };
7646
+ function toolTitle(name) {
7647
+ if (TOOL_TITLES[name]) return TOOL_TITLES[name];
7648
+ const words = name.split("_").filter(Boolean);
7649
+ return words.map((word, index) => index === 0 ? word[0].toUpperCase() + word.slice(1) : word).join(" ");
7650
+ }
7651
+
7003
7652
  // src/tools/registry.ts
7004
7653
  function instrumentToolRegistration(server) {
7005
7654
  const original = server.tool.bind(server);
@@ -7020,7 +7669,15 @@ function instrumentToolRegistration(server) {
7020
7669
  () => handler(...handlerArgs)
7021
7670
  );
7022
7671
  }
7023
- return original(...args);
7672
+ const title = toolTitle(name);
7673
+ const annotationIndex = cbIndex - 1;
7674
+ const annotations = args[annotationIndex];
7675
+ if (annotationIndex > 1 && annotations && typeof annotations === "object" && ("readOnlyHint" in annotations || "destructiveHint" in annotations) && !("title" in annotations)) {
7676
+ args[annotationIndex] = { title, ...annotations };
7677
+ }
7678
+ const registered = original(...args);
7679
+ if (registered && typeof registered === "object" && !registered.title) registered.title = title;
7680
+ return registered;
7024
7681
  };
7025
7682
  }
7026
7683
  function registerAllTools(server, client) {
@@ -7049,12 +7706,18 @@ function registerAllTools(server, client) {
7049
7706
  registerListDashboards(server, client);
7050
7707
  registerCreateDashboard(server, client);
7051
7708
  registerGetDashboard(server, client);
7709
+ registerDashboardSnapshot(server, client);
7052
7710
  registerGenerateDashboardTiles(server, client);
7053
7711
  registerAskQuestion(server, client);
7054
7712
  registerGetSemanticLayer(server, client);
7055
7713
  registerUpdateSemanticLayer(server, client);
7056
7714
  registerStartSemanticLayerGeneration(server, client);
7057
7715
  registerGetSemanticLayerGenerationStatus(server, client);
7716
+ registerSearchSemanticModel(server, client);
7717
+ registerGetSemanticEntity(server, client);
7718
+ registerProposeSemanticChange(server, client);
7719
+ registerApplySemanticChange(server, client);
7720
+ registerEvalSemanticModel(server, client);
7058
7721
  registerListWidgets(server, client);
7059
7722
  registerValidateMetricSpec(server, client);
7060
7723
  registerCreateOrUpdateMetricFromQuery(server, client);
@@ -7064,6 +7727,11 @@ function registerAllTools(server, client) {
7064
7727
  registerRemoveWidget(server, client);
7065
7728
  registerGenerateWidget(server, client);
7066
7729
  registerListScheduledReports(server, client);
7730
+ registerCustomChartScaffold(server, client);
7731
+ registerCustomChartValidate(server, client);
7732
+ registerCustomChartUpload(server, client);
7733
+ registerListCustomCharts(server, client);
7734
+ registerPublishCustomChart(server, client);
7067
7735
  }
7068
7736
 
7069
7737
  // src/resources/content/getting-started.ts
@@ -8944,6 +9612,20 @@ When the same flag is set at multiple levels, the highest precedence wins:
8944
9612
 
8945
9613
  // src/resources/content/semantic-layer-guide.ts
8946
9614
  var semanticLayerGuideContent = `
9615
+
9616
+ ## Semantic models v2
9617
+
9618
+ The v2 source is one YAML document. Use the configured service credential and a datamart ID from \`list_datamarts\`.
9619
+
9620
+ 1. \`search_semantic_model\` finds entities by question. \`get_semantic_entity\` reads one entity by kind and name. Column names use table.column.
9621
+ 2. \`propose_semantic_change\` takes complete source YAML, a YAML patch or one structured change. It returns candidate source, validation errors, a plan and the stored expected hash without writing.
9622
+ 3. \`eval_semantic_model\` evaluates the proposed source.
9623
+ 4. \`apply_semantic_change\` defaults to a dry run. Live apply requires the exact reviewed source, \`expected_content_hash\`, \`dry_run: false\` and user supplied \`confirm: "APPLY_TO_PRODUCTION"\`. Never supply confirmation yourself. Apply changes the live model for all consumers. On a conflict, prepare a fresh proposal and review it again.
9624
+
9625
+ YAML patches merge mappings and named lists by name. Other lists replace. Empty lists clear; null removes a field. Structured changes use operation upsert or remove, kind, name, and value for upsert. Column changes also require table. Patch output normalizes formatting and comments, so review the returned source before apply.
9626
+
9627
+ The legacy maintenance workflow below applies to datamarts that have not moved to v2. Do not call \`update_semantic_layer\` for v2.
9628
+
8947
9629
  # Databrain Semantic Layer Guide
8948
9630
 
8949
9631
  ## What Is the Semantic Layer?
@@ -9930,6 +10612,9 @@ If **no datamarts exist**, tell the user they need to create one in the **Databr
9930
10612
 
9931
10613
  If **no datasources exist either**, the user must first connect their database in the **Databrain UI**.
9932
10614
 
10615
+ **For a v2 semantic model**
10616
+ If the datamart uses v2, inspect the question's entities with \`search_semantic_model\` and \`get_semantic_entity\`. Then continue to Step 2. If metadata needs correction, use \`propose_semantic_change\`, evaluate its source with \`eval_semantic_model\`, and show the plan. Only apply the reviewed source and expected hash through \`apply_semantic_change\` after the user supplies \`APPLY_TO_PRODUCTION\`. Never use the legacy \`update_semantic_layer\` steps below for v2.
10617
+
9933
10618
  **Step 1b \u2014 Verify the Semantic Layer**
9934
10619
  After confirming a datamart exists, use \`get_semantic_layer\` to check its tables. The semantic layer is built on the **datamart** (not the raw datasource) \u2014 do NOT use \`discover_datasource_schema\` to populate it.
9935
10620
 
@@ -10011,6 +10696,15 @@ If the confidence is low or results seem wrong:
10011
10696
  // src/prompts/populate-semantic-layer.ts
10012
10697
  var populateSemanticLayerPrompt = `You are helping the user populate the semantic layer for a Databrain datamart. The semantic layer provides table/column descriptions, synonyms, and context that the AI needs to convert natural language questions into accurate SQL.
10013
10698
 
10699
+ **For a v2 semantic model**
10700
+ If \`list_datamarts\` reports \`semanticLayerVersion: "v2"\`, or the selected datamart is already known to use v2, use this workflow instead of the legacy steps below:
10701
+ 1. Inspect relevant entities with \`search_semantic_model\` and \`get_semantic_entity\`, using the datamart ID.
10702
+ 2. Call \`propose_semantic_change\` with a YAML patch or structured change. Show its errors and plan. Correct errors before continuing.
10703
+ 3. Call \`eval_semantic_model\` with the returned source. Show the verdicts and let the user review the candidate.
10704
+ 4. Stop for the user's explicit \`APPLY_TO_PRODUCTION\` confirmation. Never supply it yourself.
10705
+ 5. Call \`apply_semantic_change\` with the exact reviewed source, its \`expected_content_hash\`, \`dry_run: false\` and the user's confirmation. A conflict requires a new proposal and review.
10706
+ 6. Call \`get_semantic_entity\` and inspect the changed entity. Do not call \`update_semantic_layer\` for a v2 model.
10707
+
10014
10708
  **Why this matters:** Without a populated semantic layer, \`ask_question\` cannot generate SQL \u2014 every query will fail with "out of context." Populating the semantic layer is the critical step between having a datamart and being able to query it.
10015
10709
 
10016
10710
  **Step 1 \u2014 Identify the Datamart**
@@ -10372,27 +11066,28 @@ function registerAllPrompts(server) {
10372
11066
  }
10373
11067
 
10374
11068
  export {
10375
- oauthHttpsUrl,
10376
- oauthConfigSchema,
10377
- DatabrainApiError,
10378
- toolError,
10379
- toolSuccess,
10380
- logger,
10381
- MCP_SERVER_VERSION,
10382
- toolContext,
10383
- DatabrainClient,
10384
11069
  createServiceBundle,
10385
11070
  READ_ONLY,
11071
+ WRITE_CREATE,
11072
+ APPLY_TO_PRODUCTION_CONFIRMATION,
10386
11073
  CHART_URI,
10387
11074
  KPI_URI,
10388
11075
  APP_MIME,
11076
+ rowsSchema,
10389
11077
  referenceSchema,
10390
11078
  viewSchema,
10391
11079
  pageSchema,
11080
+ appMeta,
10392
11081
  enableChartApps,
10393
11082
  chartResult,
11083
+ draftSchema,
11084
+ PREVIEW_PATH,
11085
+ RENDER_TYPES,
11086
+ draftPage,
11087
+ workspacesForDatamart,
11088
+ saveDraftAsDashboard,
10394
11089
  registerAllTools,
10395
11090
  registerAllResources,
10396
11091
  registerAllPrompts
10397
11092
  };
10398
- //# sourceMappingURL=chunk-7DS7OLWP.js.map
11093
+ //# sourceMappingURL=chunk-ERTAKB2H.js.map