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

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 (50) hide show
  1. package/README.md +158 -7
  2. package/dist/apps/server.d.ts +82 -44
  3. package/dist/apps/server.js +5 -2
  4. package/dist/apps/stdio.js +6 -3
  5. package/dist/apps/stdio.js.map +1 -1
  6. package/dist/bundle-TDQXWIPM.js +24 -0
  7. package/dist/bundle-TDQXWIPM.js.map +1 -0
  8. package/dist/{chunk-RF3HTNO7.js → chunk-DM7S4R57.js} +2 -2
  9. package/dist/{chunk-7DS7OLWP.js → chunk-ES2LUP4U.js} +2030 -1214
  10. package/dist/chunk-ES2LUP4U.js.map +1 -0
  11. package/dist/chunk-IBYBK5ZE.js +34 -0
  12. package/dist/chunk-IBYBK5ZE.js.map +1 -0
  13. package/dist/chunk-NBFUKVWP.js +603 -0
  14. package/dist/chunk-NBFUKVWP.js.map +1 -0
  15. package/dist/chunk-Q5EYDKSA.js +1224 -0
  16. package/dist/chunk-Q5EYDKSA.js.map +1 -0
  17. package/dist/chunk-RHK62ZJ6.js +884 -0
  18. package/dist/chunk-RHK62ZJ6.js.map +1 -0
  19. package/dist/{chunk-JCXUZ3QZ.js → chunk-Z3ZHN4UW.js} +34 -6
  20. package/dist/chunk-Z3ZHN4UW.js.map +1 -0
  21. package/dist/cli.d.ts +15 -0
  22. package/dist/cli.js +497 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/custom-chart/custom-runtime.js +63 -0
  25. package/dist/custom-chart/runtime-shim.js +146 -0
  26. package/dist/custom-chart/template/AGENTS.md +374 -0
  27. package/dist/custom-chart/template/README.md +374 -0
  28. package/dist/custom-chart/template/chart.css +28 -0
  29. package/dist/custom-chart/template/chart.ts +192 -0
  30. package/dist/custom-chart/template/custom-chart.d.ts +78 -0
  31. package/dist/custom-chart/template/icon.svg +1 -0
  32. package/dist/custom-chart/template/manifest.json +23 -0
  33. package/dist/custom-chart/template/package.json +14 -0
  34. package/dist/custom-chart/template/sample-data.json +18 -0
  35. package/dist/custom-chart/template/tsconfig.json +12 -0
  36. package/dist/index.js +15 -8
  37. package/dist/index.js.map +1 -1
  38. package/dist/main-JEYYFGJM.js +872 -0
  39. package/dist/main-JEYYFGJM.js.map +1 -0
  40. package/dist/{test-only-EJ5O4WN5.js → test-only-K7QTSVO4.js} +7 -6
  41. package/dist/test-only-K7QTSVO4.js.map +1 -0
  42. package/package.json +9 -4
  43. package/dist/chunk-7DS7OLWP.js.map +0 -1
  44. package/dist/chunk-JCXUZ3QZ.js.map +0 -1
  45. package/dist/chunk-NDVW3ZSJ.js +0 -310
  46. package/dist/chunk-NDVW3ZSJ.js.map +0 -1
  47. package/dist/main-7QNRH6X4.js +0 -438
  48. package/dist/main-7QNRH6X4.js.map +0 -1
  49. package/dist/test-only-EJ5O4WN5.js.map +0 -1
  50. /package/dist/{chunk-RF3HTNO7.js.map → chunk-DM7S4R57.js.map} +0 -0
@@ -1,621 +1,87 @@
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
+ createCustomChartsApi,
7
+ editWithBryn,
8
+ logger,
9
+ planUpload,
10
+ scaffoldProject,
11
+ stringify,
12
+ studioDraftSchema,
13
+ toolContext,
14
+ toolError,
15
+ toolSuccess
16
+ } from "./chunk-RHK62ZJ6.js";
17
+ import {
18
+ MAX_BUNDLE_CSS_BYTES,
19
+ MAX_BUNDLE_JS_BYTES,
20
+ MAX_ICON_BYTES,
21
+ MAX_MANIFEST_BYTES,
22
+ buildProject,
23
+ buildSource,
24
+ bundleSizeBytes,
25
+ contentHash,
26
+ optionSchema,
27
+ readDist,
28
+ slotSchema,
29
+ validateBundle
30
+ } from "./chunk-Q5EYDKSA.js";
574
31
 
575
32
  // src/apps/contract.ts
576
- import { z as z2 } from "zod";
33
+ import { z } from "zod";
577
34
  var CHART_URI = "ui://databrain/chart-tile";
578
35
  var KPI_URI = "ui://databrain/kpi-tile";
579
36
  var APP_MIME = "text/html;profile=mcp-app";
37
+ var RESOURCE_URI_META_KEY = "ui/resourceUri";
580
38
  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({
39
+ var MAX_RESULT_CHARS = 124e3;
40
+ var MAX_CUSTOM_BUNDLE_CHARS = 6 * (MAX_BUNDLE_JS_BYTES + MAX_BUNDLE_CSS_BYTES + MAX_ICON_BYTES) + MAX_MANIFEST_BYTES + 128;
41
+ var CUSTOM_BUNDLE_CHUNK_CHARS = 32e3;
42
+ var id = z.string().trim().min(1).max(256);
43
+ var cell = z.union([z.string().max(2e3), z.number().finite(), z.boolean(), z.null()]);
44
+ var rowsSchema = z.array(z.record(cell)).max(PAGE_SIZE);
45
+ var referenceSchema = z.object({ embedId: id, clientId: id, widgetId: id }).strict();
46
+ var point = z.object({
587
47
  key: id,
588
- label: z2.string().max(2e3),
48
+ label: z.string().max(2e3),
589
49
  value: cell,
590
- x: z2.number().min(0),
591
- y: z2.number().min(0),
592
- width: z2.number().positive(),
593
- height: z2.number().positive(),
50
+ x: z.number().min(0),
51
+ y: z.number().min(0),
52
+ width: z.number().positive(),
53
+ height: z.number().positive(),
594
54
  drillId: id.optional()
595
55
  }).strict();
596
- var dateRangeSchema = z2.object({
597
- from: z2.string().date(),
598
- to: z2.string().date()
56
+ var dateRangeSchema = z.object({
57
+ from: z.string().date(),
58
+ to: z.string().date()
599
59
  }).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(),
60
+ var viewSchema = z.object({
61
+ cursor: z.string().min(1).max(2048).optional(),
602
62
  dateRange: dateRangeSchema.optional(),
603
63
  drillId: id.optional()
604
64
  }).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(),
65
+ var pageSchema = z.object({
66
+ kind: z.enum(["chart", "kpi"]),
67
+ title: z.string().max(500),
68
+ chartSpec: z.object({ type: z.string().max(80) }).strict(),
609
69
  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),
70
+ columns: z.array(z.string().max(128)).max(20),
71
+ svg: z.string().max(1e5).optional(),
72
+ customChart: z.object({
73
+ key: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/),
74
+ bindings: z.record(z.array(z.string().max(128)).max(20)),
75
+ options: z.record(z.union([z.string().max(2e3), z.number().finite(), z.boolean()]))
76
+ }).strict().optional(),
77
+ fallback: z.string().max(1e3).optional(),
78
+ width: z.number().int().min(240).max(1200),
79
+ height: z.number().int().min(120).max(800),
80
+ points: z.array(point).max(PAGE_SIZE),
615
81
  value: cell.optional(),
616
- nextCursor: z2.string().min(1).max(2048).optional()
82
+ nextCursor: z.string().min(1).max(2048).optional()
617
83
  }).strict().superRefine((page, ctx) => {
618
- if (page.kind === "chart" && !page.svg) ctx.addIssue({ code: "custom", message: "SVG required" });
84
+ if (page.kind === "chart" && !page.svg && !page.customChart) ctx.addIssue({ code: "custom", message: "SVG required" });
619
85
  if (page.kind === "kpi" && (page.svg || page.points.length || page.value === void 0)) {
620
86
  ctx.addIssue({ code: "custom", message: "KPI requires a value and no SVG/points" });
621
87
  }
@@ -639,6 +105,9 @@ function firstRows(value) {
639
105
  }
640
106
 
641
107
  // src/apps/feature.ts
108
+ function appMeta(visibility) {
109
+ return { ui: { resourceUri: CHART_URI, visibility }, [RESOURCE_URI_META_KEY]: CHART_URI };
110
+ }
642
111
  var enabled = /* @__PURE__ */ new WeakSet();
643
112
  function enableChartApps(server) {
644
113
  enabled.add(server);
@@ -647,79 +116,308 @@ function hasChartApps(server) {
647
116
  return enabled.has(server);
648
117
  }
649
118
  function tagChartTool(server, tool) {
650
- if (hasChartApps(server)) tool.update({ _meta: { ui: { resourceUri: CHART_URI, visibility: ["model", "app"] } } });
119
+ if (hasChartApps(server)) tool.update({ _meta: appMeta(["model", "app"]) });
651
120
  }
652
121
  function chartResult(summary, payload) {
653
- return bounded({ content: [{ type: "text", text: summary }], structuredContent: payload });
122
+ return bounded({
123
+ content: [{ type: "text", text: summary }],
124
+ structuredContent: payload,
125
+ _meta: appMeta(["model", "app"])
126
+ });
127
+ }
128
+
129
+ // src/tools/custom-chart/author.ts
130
+ import { z as z3 } from "zod";
131
+
132
+ // src/tools/annotations.ts
133
+ var READ_ONLY = {
134
+ readOnlyHint: true,
135
+ destructiveHint: false,
136
+ idempotentHint: true,
137
+ openWorldHint: false
138
+ };
139
+ var WRITE_IDEMPOTENT = {
140
+ readOnlyHint: false,
141
+ destructiveHint: false,
142
+ idempotentHint: true,
143
+ openWorldHint: false
144
+ };
145
+ var WRITE_CREATE = {
146
+ readOnlyHint: false,
147
+ destructiveHint: false,
148
+ idempotentHint: false,
149
+ openWorldHint: false
150
+ };
151
+ var DESTRUCTIVE = {
152
+ readOnlyHint: false,
153
+ destructiveHint: true,
154
+ idempotentHint: false,
155
+ openWorldHint: false
156
+ };
157
+
158
+ // src/tools/custom-chart/shared.ts
159
+ import { z as z2 } from "zod";
160
+ var slotInputSchema = slotSchema;
161
+ var optionInputSchema = optionSchema;
162
+ var manifestInputSchema = z2.record(z2.unknown()).describe("manifest.json as an object (schemaVersion 2). Validated with the same rules as the server.");
163
+ var bundleInputShape = {
164
+ manifest: manifestInputSchema.optional(),
165
+ chartTs: z2.string().max(64e3).optional().describe("Hosted source: export render(ctx) in TypeScript or JavaScript. Compiled in memory without filesystem or imports. Pass instead of bundleJs/projectDir."),
166
+ bundleJs: z2.string().optional().describe("dist/index.js from `databrain custom-chart build` (the shim plus the chart module)."),
167
+ bundleCss: z2.string().optional().describe("The raw chart.css, kept for provenance."),
168
+ iconSvg: z2.string().optional().describe("icon.svg, at most 16 KiB, no script, no foreignObject, no external href."),
169
+ projectDir: z2.string().optional().describe("Local only: a chart project folder. The tool reads it instead of manifest/bundleJs/iconSvg.")
170
+ };
171
+ function formatProblems(problems) {
172
+ if (problems.length === 0) return "No problems.";
173
+ return problems.map((p) => `${p.path || "/"} ${p.code} ${p.message}`).join("\n");
174
+ }
175
+ function projectDirRefusal() {
176
+ if (process.env.DATABRAIN_HOSTED_PUBLIC_URL) {
177
+ return "projectDir is not available on the hosted server. Pass chartTs, manifest, bundleCss and iconSvg to compile in memory, or pass an already built bundleJs.";
178
+ }
179
+ return null;
180
+ }
181
+ async function resolveBundleInput(params, options) {
182
+ if (params.chartTs !== void 0) {
183
+ if (params.projectDir || params.bundleJs || !params.manifest || !params.iconSvg) {
184
+ return { problems: [], error: "chartTs needs manifest and iconSvg, and cannot be combined with bundleJs or projectDir.", source: "arguments" };
185
+ }
186
+ const result = await buildSource({ manifest: params.manifest, chartTs: params.chartTs, bundleCss: params.bundleCss, iconSvg: params.iconSvg });
187
+ return { bundle: result.outputs, problems: result.problems, source: "arguments" };
188
+ }
189
+ if (params.projectDir) {
190
+ const refusal = projectDirRefusal();
191
+ if (refusal) return { problems: [], error: refusal, source: "projectDir" };
192
+ if (params.manifest || params.bundleJs || params.iconSvg) {
193
+ return { problems: [], error: "Pass either projectDir or manifest/bundleJs/iconSvg, not both.", source: "projectDir" };
194
+ }
195
+ if (options.build) {
196
+ const result = await buildProject(params.projectDir, { write: false });
197
+ if (!result.ok || !result.outputs) return { problems: result.problems, source: "projectDir" };
198
+ const { manifest, bundleJs, bundleCss, iconSvg } = result.outputs;
199
+ return { bundle: { manifest, bundleJs, bundleCss, iconSvg }, problems: [], source: "projectDir" };
200
+ }
201
+ const dist = readDist(params.projectDir);
202
+ if (!dist.bundle) return { problems: dist.problems, source: "projectDir" };
203
+ return { bundle: dist.bundle, problems: [], source: "projectDir" };
204
+ }
205
+ if (!params.manifest) {
206
+ return { problems: [], error: "manifest is required (or pass projectDir when running locally).", source: "arguments" };
207
+ }
208
+ return {
209
+ bundle: {
210
+ manifest: params.manifest,
211
+ bundleJs: params.bundleJs,
212
+ bundleCss: params.bundleCss,
213
+ iconSvg: params.iconSvg
214
+ },
215
+ problems: [],
216
+ source: "arguments"
217
+ };
218
+ }
219
+ async function remoteValidate(client, bundle) {
220
+ try {
221
+ const result = await createCustomChartsApi(client).validate(bundle);
222
+ return { status: "ok", result };
223
+ } catch (err) {
224
+ if (err instanceof DatabrainApiError) {
225
+ if (err.code === "MISSING_SERVICE_TOKEN") {
226
+ return { status: "skipped", reason: "No service token is configured, so only the local validator ran." };
227
+ }
228
+ return { status: "failed", reason: err.message, problems: apiProblems(err) };
229
+ }
230
+ return { status: "failed", reason: err.message, problems: [] };
231
+ }
232
+ }
233
+ function isCompleteBundle(bundle) {
234
+ return typeof bundle.bundleJs === "string" && typeof bundle.iconSvg === "string";
235
+ }
236
+
237
+ // src/tools/custom-chart/author.ts
238
+ function registerCustomChartAuthor(server, client) {
239
+ server.tool(
240
+ "custom_chart_edit",
241
+ "Iterate a custom chart draft with Chart studio Bryn in a granted workspace. Never saves a version or publishes. Feature-detects the backend route. If absent, returns the draft for the calling agent to edit and validate with chartTs, without a local build.",
242
+ {
243
+ key: z3.string().regex(/^[a-z][a-z0-9_-]{0,63}$/),
244
+ workspaceId: z3.string().uuid(),
245
+ message: z3.string().trim().min(1).max(8e3),
246
+ draft: studioDraftSchema,
247
+ sessionId: z3.string().uuid().optional()
248
+ },
249
+ WRITE_CREATE,
250
+ async (input) => {
251
+ try {
252
+ if (input.draft.manifest.id !== input.key) return toolError("The draft manifest id must match key.");
253
+ const result = await editWithBryn(client, input);
254
+ return toolSuccess(JSON.stringify({
255
+ ...result,
256
+ nextStep: result.available ? "Check the returned draft. Validate and preview before saving. Publishing still requires the author to confirm." : "Studio Bryn is not installed. Edit draft.code as plain JavaScript defining render(ctx), then use custom_chart_validate / custom_chart_preview / custom_chart_upload with chartTs=draft.code, manifest, bundleCss=draft.css and iconSvg. Nothing was saved."
257
+ }));
258
+ } catch (error) {
259
+ return error instanceof DatabrainApiError ? error.toToolResponse() : toolError("Bryn could not edit the draft. No version was saved.");
260
+ }
261
+ }
262
+ );
263
+ const preview = server.tool(
264
+ "custom_chart_preview",
265
+ "Validate and preview a draft custom chart as an MCP App tile using bounded sample query rows. No version is saved. Pass chartTs to compile on the server without local files; bindings map manifest slot ids to row column names. Hosts without MCP Apps get the rows and validation result.",
266
+ {
267
+ ...bundleInputShape,
268
+ rows: rowsSchema,
269
+ bindings: z3.record(z3.array(z3.string().max(128)).max(20)),
270
+ options: z3.record(z3.union([z3.string().max(2e3), z3.number().finite(), z3.boolean()])).default({})
271
+ },
272
+ READ_ONLY,
273
+ async (input) => {
274
+ try {
275
+ const resolved = await resolveBundleInput(input, { build: true });
276
+ if (resolved.error) return toolError(resolved.error);
277
+ if (!resolved.bundle || !isCompleteBundle(resolved.bundle)) return toolError(formatProblems(resolved.problems));
278
+ const { validateBundle: validateBundle2 } = await import("./bundle-TDQXWIPM.js");
279
+ const problems = validateBundle2(resolved.bundle);
280
+ if (problems.length) return toolError(formatProblems(problems));
281
+ const manifest = resolved.bundle.manifest;
282
+ const columns = Object.keys(input.rows[0] ?? {});
283
+ const missing = manifest.slots.filter((slot) => slot.required && !(input.bindings[slot.id] ?? []).some((column) => columns.includes(column)));
284
+ if (input.rows.length && missing.length) return toolError(`Bind required slots ${missing.map((slot) => slot.id).join(", ")} to sample columns: ${columns.join(", ") || "(none)"}.`);
285
+ const customChart = { key: manifest.id, bindings: input.bindings, options: input.options };
286
+ const payload = {
287
+ kind: "chart",
288
+ title: manifest.name,
289
+ chartSpec: { type: `custom:${manifest.id}` },
290
+ rows: input.rows,
291
+ columns: Object.keys(input.rows[0] ?? {}),
292
+ width: 600,
293
+ height: 320,
294
+ points: [],
295
+ customChart,
296
+ fallback: "Draft validated. An MCP Apps host can preview it. These are sample rows, not a saved metric."
297
+ };
298
+ if (!hasChartApps(server)) return toolSuccess(JSON.stringify({ ...payload, ok: true }));
299
+ const result = chartResult("Custom chart draft validated. Preview uses sample rows. Nothing was saved or published.", payload);
300
+ result._meta = { ...result._meta, "databrain/custom-chart-bundle": resolved.bundle };
301
+ if (JSON.stringify(result).length > MAX_RESULT_CHARS) return toolError("Draft preview exceeds the MCP host result budget. Reduce the source, CSS or sample rows before previewing.");
302
+ return result;
303
+ } catch (error) {
304
+ return error instanceof DatabrainApiError ? error.toToolResponse() : toolError(error instanceof Error && error.message === "Chart page exceeds payload budget" ? "Draft preview exceeds the MCP host result budget. Reduce the source, CSS or sample rows before previewing." : "Custom chart preview could not be prepared. Check the manifest, source and bindings.");
305
+ }
306
+ }
307
+ );
308
+ tagChartTool(server, preview);
654
309
  }
655
310
 
656
311
  // src/tools/tool-metadata.ts
657
312
  var DEFAULT_TOOL_META = {
658
313
  category: "discovery",
659
- workflowIntent: "other"
314
+ workflowIntent: "other",
315
+ // Fail closed: an unregistered/unclassified tool is treated as a commit
316
+ // until someone deliberately marks it read or view.
317
+ effect: "commit"
660
318
  };
661
319
  var TOOL_METADATA = {
662
320
  // Identity / discovery
663
- whoami: { category: "discovery", workflowIntent: "discovery" },
321
+ whoami: { category: "discovery", workflowIntent: "discovery", effect: "read" },
664
322
  // 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" },
323
+ list_datasources: { category: "infrastructure", workflowIntent: "setup_data", effect: "read" },
324
+ discover_datasource_schema: { category: "infrastructure", workflowIntent: "setup_data", effect: "read" },
325
+ list_datamarts: { category: "infrastructure", workflowIntent: "setup_data", effect: "read" },
326
+ list_workspaces: { category: "infrastructure", workflowIntent: "setup_data", effect: "read" },
327
+ // Triggers a backend sync job that refreshes stored schema metadata — a real mutation, not a read.
328
+ sync_datasource: { category: "infrastructure", workflowIntent: "setup_data", effect: "commit" },
329
+ // dry_run is the default, but mode:"apply" performs a real POST that creates the datamart.
330
+ create_datamart: { category: "infrastructure", workflowIntent: "setup_data", effect: "commit" },
331
+ // Hosted only (an "All workspaces" sign-in); stdio does not register it.
332
+ create_workspace: { category: "infrastructure", workflowIntent: "setup_data", effect: "commit" },
671
333
  // 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" },
334
+ list_data_apps: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
335
+ list_tenants: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
336
+ create_data_app: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
337
+ list_api_tokens: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
338
+ create_api_token: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
339
+ rotate_api_token: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
678
340
  // 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" },
341
+ create_embed: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
342
+ list_embeds: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
343
+ get_embed_details: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
344
+ update_embed: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
345
+ // Persists theme/access config on the embed record — a real write, not a display-only tweak.
346
+ customize_embed_theme: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
347
+ // Mints and returns a live guest token embedded in the demo URL.
348
+ get_demo_link: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
685
349
  // Embedding — frontend
686
- generate_guest_token: { category: "embedding", workflowIntent: "embed_dashboard" },
687
- generate_embed_code: { category: "embedding", workflowIntent: "embed_dashboard" },
350
+ generate_guest_token: { category: "embedding", workflowIntent: "embed_dashboard", effect: "commit" },
351
+ generate_embed_code: { category: "embedding", workflowIntent: "embed_dashboard", effect: "read" },
688
352
  // 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" },
353
+ list_dashboards: { category: "embedding", workflowIntent: "embedded_widget", effect: "read" },
354
+ create_dashboard: { category: "embedding", workflowIntent: "author_dashboard", effect: "commit" },
355
+ get_dashboard: { category: "embedding", workflowIntent: "author_dashboard", effect: "read" },
356
+ // Persists nothing the user sees, but the backend mints (and then deletes) an internal guest token to render it.
357
+ dashboard_snapshot: { category: "internal_analytics", workflowIntent: "query_data", effect: "commit" },
358
+ // Does not persist a dashboard, but mints an internal guest token to render tile previews.
359
+ generate_dashboard_tiles: { category: "embedding", workflowIntent: "author_dashboard", effect: "commit" },
360
+ list_widgets: { category: "embedding", workflowIntent: "embedded_widget", effect: "read" },
361
+ add_widget: { category: "embedding", workflowIntent: "embedded_widget", effect: "commit" },
362
+ update_widget: { category: "embedding", workflowIntent: "embedded_widget", effect: "commit" },
363
+ remove_widget: { category: "embedding", workflowIntent: "embedded_widget", effect: "commit" },
364
+ // Returns an unsaved draft, but mints an internal write-capable guest token to generate it.
365
+ generate_widget: { category: "embedding", workflowIntent: "embedded_widget", effect: "commit" },
366
+ // Dry-run validation only, but mints an internal write-capable guest token to check conflicts.
367
+ validate_metric_spec: { category: "embedding", workflowIntent: "sql_to_embedded_metric", effect: "commit" },
699
368
  create_or_update_metric_from_query: {
700
369
  category: "embedding",
701
- workflowIntent: "sql_to_embedded_metric"
370
+ workflowIntent: "sql_to_embedded_metric",
371
+ effect: "commit"
702
372
  },
703
373
  // Internal analytics
704
374
  create_workspace_metric: {
705
375
  category: "internal_analytics",
706
- workflowIntent: "sql_to_workspace_metric"
376
+ workflowIntent: "sql_to_workspace_metric",
377
+ effect: "commit"
707
378
  },
708
- ask_question: { category: "internal_analytics", workflowIntent: "query_data" },
379
+ // Runs a query and returns rows; no dashboard/widget/row is persisted.
380
+ ask_question: { category: "internal_analytics", workflowIntent: "query_data", effect: "read" },
381
+ preview_chart: { category: "internal_analytics", workflowIntent: "query_data", effect: "view" },
382
+ save_chart_as_dashboard: { category: "internal_analytics", workflowIntent: "author_dashboard", effect: "commit" },
383
+ // Read-only to the user, but each interaction mints an embed-scoped guest credential.
384
+ render_chart: { category: "embedding", workflowIntent: "chart_app", effect: "commit" },
385
+ refresh_widget: { category: "embedding", workflowIntent: "chart_app", effect: "commit" },
386
+ drill_down: { category: "embedding", workflowIntent: "chart_app", effect: "commit" },
387
+ change_date_range: { category: "embedding", workflowIntent: "chart_app", effect: "commit" },
709
388
  // 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" },
389
+ search_semantic_model: { category: "semantic_layer", workflowIntent: "query_data", effect: "read" },
390
+ get_semantic_entity: { category: "semantic_layer", workflowIntent: "query_data", effect: "read" },
391
+ propose_semantic_change: { category: "semantic_layer", workflowIntent: "model_data", effect: "read" },
392
+ apply_semantic_change: { category: "semantic_layer", workflowIntent: "model_data", effect: "commit" },
393
+ eval_semantic_model: { category: "semantic_layer", workflowIntent: "query_data", effect: "read" },
394
+ get_semantic_layer: { category: "semantic_layer", workflowIntent: "query_data", effect: "read" },
395
+ update_semantic_layer: { category: "semantic_layer", workflowIntent: "query_data", effect: "commit" },
396
+ start_semantic_layer_generation: { category: "semantic_layer", workflowIntent: "query_data", effect: "commit" },
713
397
  get_semantic_layer_generation_status: {
714
398
  category: "semantic_layer",
715
- workflowIntent: "query_data"
399
+ workflowIntent: "query_data",
400
+ effect: "read"
716
401
  },
717
402
  // Reports
718
- list_scheduled_reports: { category: "reports", workflowIntent: "reports" }
403
+ list_scheduled_reports: { category: "reports", workflowIntent: "reports", effect: "read" },
404
+ // Custom chart types (DAT-925)
405
+ // Returns template files; nothing is written or persisted.
406
+ custom_chart_edit: { category: "custom_charts", workflowIntent: "custom_chart", effect: "commit" },
407
+ custom_chart_preview: { category: "custom_charts", workflowIntent: "custom_chart", effect: "read" },
408
+ load_custom_chart_bundle: { category: "custom_charts", workflowIntent: "custom_chart", effect: "read" },
409
+ load_custom_chart_runtime: { category: "custom_charts", workflowIntent: "custom_chart", effect: "read" },
410
+ custom_chart_scaffold: { category: "custom_charts", workflowIntent: "custom_chart", effect: "read" },
411
+ // Local rules plus POST /validate, which never persists.
412
+ custom_chart_validate: { category: "custom_charts", workflowIntent: "custom_chart", effect: "read" },
413
+ // dry_run is the default, but mode:"apply" creates a chart or a version (and may publish).
414
+ custom_chart_upload: { category: "custom_charts", workflowIntent: "custom_chart", effect: "commit" },
415
+ list_custom_charts: { category: "custom_charts", workflowIntent: "custom_chart", effect: "read" },
416
+ publish_custom_chart: { category: "custom_charts", workflowIntent: "custom_chart", effect: "commit" }
719
417
  };
720
418
 
721
419
  // src/tools/data-app/list-data-apps.ts
722
- import { z as z3 } from "zod";
420
+ import { z as z4 } from "zod";
723
421
 
724
422
  // src/api/dashboards-api.ts
725
423
  var DashboardsApi = class {
@@ -1011,18 +709,18 @@ var QueryApi = class {
1011
709
  throw err;
1012
710
  }
1013
711
  }
1014
- async startSemanticLayerGeneration(datamartId) {
712
+ async startSemanticLayerGeneration(datamartId2) {
1015
713
  return this.client.request({
1016
714
  method: "POST",
1017
- path: `/schema/datamart/${encodeURIComponent(datamartId)}/semantic-layer/auto-generate`,
715
+ path: `/api/v2/data-app/datamarts/${encodeURIComponent(datamartId2)}/semantic-layer/generate`,
1018
716
  auth: "service",
1019
717
  invalidateCache: ["semantic_layer_"]
1020
718
  });
1021
719
  }
1022
- async getSemanticLayerGenerationStatus(datamartId) {
720
+ async getSemanticLayerGenerationStatus(datamartId2) {
1023
721
  return this.client.request({
1024
722
  method: "GET",
1025
- path: `/schema/datamart/${encodeURIComponent(datamartId)}/semantic-layer/generation-status`,
723
+ path: `/api/v2/data-app/datamarts/${encodeURIComponent(datamartId2)}/semantic-layer/generation-status`,
1026
724
  auth: "service"
1027
725
  });
1028
726
  }
@@ -1045,9 +743,9 @@ function normalizeRecords(value) {
1045
743
  record.items,
1046
744
  record.data
1047
745
  ];
1048
- for (const candidate of candidates) {
1049
- if (Array.isArray(candidate)) {
1050
- return candidate.filter((item) => asRecord(item) !== null);
746
+ for (const candidate2 of candidates) {
747
+ if (Array.isArray(candidate2)) {
748
+ return candidate2.filter((item) => asRecord(item) !== null);
1051
749
  }
1052
750
  }
1053
751
  return [];
@@ -1076,6 +774,62 @@ var ScheduledReportsApi = class {
1076
774
  }
1077
775
  };
1078
776
 
777
+ // src/api/semantic-model-api.ts
778
+ var SemanticModelApi = class {
779
+ constructor(client) {
780
+ this.client = client;
781
+ }
782
+ path(id2, verb) {
783
+ return `/api/v2/semantic/${encodeURIComponent(id2)}/${verb}`;
784
+ }
785
+ pull(id2) {
786
+ return this.client.request({ method: "GET", path: this.path(id2, "pull"), auth: "service" });
787
+ }
788
+ retrieve(id2, q, k) {
789
+ return this.client.request({
790
+ method: "GET",
791
+ path: this.path(id2, "retrieve"),
792
+ auth: "service",
793
+ query: { q, k: String(k) }
794
+ });
795
+ }
796
+ validate(id2, source2) {
797
+ return this.client.request({
798
+ method: "POST",
799
+ path: this.path(id2, "validate"),
800
+ auth: "service",
801
+ body: { source: source2 }
802
+ });
803
+ }
804
+ plan(id2, source2, expected_content_hash) {
805
+ return this.client.request({
806
+ method: "POST",
807
+ path: this.path(id2, "plan"),
808
+ auth: "service",
809
+ body: { source: source2, expected_content_hash }
810
+ });
811
+ }
812
+ apply(id2, source2, expected_content_hash) {
813
+ return this.client.request({
814
+ method: "POST",
815
+ path: this.path(id2, "apply"),
816
+ auth: "service",
817
+ body: { source: source2, expected_content_hash },
818
+ noRetryOnTimeout: true
819
+ });
820
+ }
821
+ evaluate(id2, body) {
822
+ return this.client.request({
823
+ method: "POST",
824
+ path: this.path(id2, "eval"),
825
+ auth: "service",
826
+ body,
827
+ timeoutMs: 3e5,
828
+ noRetryOnTimeout: true
829
+ });
830
+ }
831
+ };
832
+
1079
833
  // src/api/tenants-api.ts
1080
834
  var TenantsApi = class {
1081
835
  constructor(client) {
@@ -1258,8 +1012,8 @@ function extractAccessSettings(embed) {
1258
1012
  asRecord2(metadataConfig?.accessSettings),
1259
1013
  asRecord2(options?.accessSettings)
1260
1014
  ].filter((c) => !!c);
1261
- for (const candidate of candidates) {
1262
- if (Object.keys(candidate).length > 0) return candidate;
1015
+ for (const candidate2 of candidates) {
1016
+ if (Object.keys(candidate2).length > 0) return candidate2;
1263
1017
  }
1264
1018
  return null;
1265
1019
  }
@@ -1672,6 +1426,54 @@ var WidgetService = class {
1672
1426
  }
1673
1427
  };
1674
1428
  }
1429
+ /**
1430
+ * Read-only sibling of `ensureTenantDashboardScope`.
1431
+ *
1432
+ * The tenant gate above exists to stop a WRITE landing on the data-app
1433
+ * dashboard when the caller meant a tenant's copy, so it insists the
1434
+ * tenant-scoped embed lookup returns the target. A data-app embed has no
1435
+ * tenant row, `listEmbeds(dataAppName, clientId)` answers with an empty list,
1436
+ * and the render path was denied before it ever reached the backend. That is
1437
+ * the wrong trade for a read: the chart route re-derives everything from the
1438
+ * guest token it is handed (embed inside the token's data app, `allowedEmbeds`,
1439
+ * the tenancy subset for the client, and per-metric client visibility), so the
1440
+ * only thing this check can usefully add is refusing another tenant's embed.
1441
+ */
1442
+ async ensureReadableDashboardScope(params) {
1443
+ const clientId = params.clientId.trim();
1444
+ if (!clientId) {
1445
+ return {
1446
+ ok: false,
1447
+ error: "`clientId` cannot be empty. Pick a tenant/client first."
1448
+ };
1449
+ }
1450
+ let ctx;
1451
+ try {
1452
+ ctx = await this.resolveEmbedContext(params.embedId);
1453
+ } catch (err) {
1454
+ return { ok: false, error: err.message };
1455
+ }
1456
+ if (ctx.embedClientId && ctx.embedClientId !== clientId) {
1457
+ return {
1458
+ ok: false,
1459
+ error: `Embed "${params.embedId}" belongs to another client. Pass the \`clientId\` the embed was created for.`
1460
+ };
1461
+ }
1462
+ if (!ctx.embedClientId && ctx.externalDashboardCreatedBy && ctx.externalDashboardCreatedBy !== clientId) {
1463
+ return {
1464
+ ok: false,
1465
+ error: `Dashboard behind embed "${params.embedId}" belongs to another client. Pass the \`clientId\` the dashboard was created for.`
1466
+ };
1467
+ }
1468
+ return {
1469
+ ok: true,
1470
+ value: {
1471
+ embedId: params.embedId,
1472
+ clientId,
1473
+ dataAppName: ctx.dataAppName || params.dataAppName || null
1474
+ }
1475
+ };
1476
+ }
1675
1477
  /**
1676
1478
  * Resolve a datasource for a widget. Priority:
1677
1479
  * 1. Explicit integrationId + integrationName (fast path).
@@ -1857,6 +1659,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
1857
1659
  dataAppName,
1858
1660
  externalDashboardId,
1859
1661
  dashboardId,
1662
+ embedClientId: isNonEmptyString3(exact.clientId) && exact.clientId || (metadata && isNonEmptyString3(metadata.clientId) ? metadata.clientId : null),
1860
1663
  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
1664
  };
1862
1665
  }
@@ -1942,6 +1745,8 @@ function createServiceBundle(client) {
1942
1745
  const guestTokenApi = new GuestTokenApi(client);
1943
1746
  const queryApi = new QueryApi(client);
1944
1747
  const scheduledReportsApi = new ScheduledReportsApi(client);
1748
+ const semanticModelApi = new SemanticModelApi(client);
1749
+ const customChartsApi = new CustomChartsApi(client);
1945
1750
  const tenantsApi = new TenantsApi(client);
1946
1751
  const widgetsApi = new WidgetsApi(client);
1947
1752
  const workspacesApi = new WorkspacesApi(client);
@@ -1956,6 +1761,8 @@ function createServiceBundle(client) {
1956
1761
  workspacesApi,
1957
1762
  queryApi,
1958
1763
  scheduledReportsApi,
1764
+ semanticModelApi,
1765
+ customChartsApi,
1959
1766
  tenantsApi,
1960
1767
  widgetsApi,
1961
1768
  embedService: new EmbedService(embedsApi, datamartsApi),
@@ -1973,32 +1780,6 @@ function createServiceBundle(client) {
1973
1780
  };
1974
1781
  }
1975
1782
 
1976
- // src/tools/annotations.ts
1977
- var READ_ONLY = {
1978
- readOnlyHint: true,
1979
- destructiveHint: false,
1980
- idempotentHint: true,
1981
- openWorldHint: false
1982
- };
1983
- var WRITE_IDEMPOTENT = {
1984
- readOnlyHint: false,
1985
- destructiveHint: false,
1986
- idempotentHint: true,
1987
- openWorldHint: false
1988
- };
1989
- var WRITE_CREATE = {
1990
- readOnlyHint: false,
1991
- destructiveHint: false,
1992
- idempotentHint: false,
1993
- openWorldHint: false
1994
- };
1995
- var DESTRUCTIVE = {
1996
- readOnlyHint: false,
1997
- destructiveHint: true,
1998
- idempotentHint: false,
1999
- openWorldHint: false
2000
- };
2001
-
2002
1783
  // src/tools/data-app/list-data-apps.ts
2003
1784
  function registerListDataApps(server, client) {
2004
1785
  const services = createServiceBundle(client);
@@ -2006,7 +1787,7 @@ function registerListDataApps(server, client) {
2006
1787
  "list_data_apps",
2007
1788
  "List data apps in your organization. Start here for embed setup.",
2008
1789
  {
2009
- workspaceName: z3.string().optional().describe("Filter by workspace name")
1790
+ workspaceName: z4.string().optional().describe("Filter by workspace name")
2010
1791
  },
2011
1792
  READ_ONLY,
2012
1793
  async (params) => {
@@ -2035,7 +1816,7 @@ function registerListDataApps(server, client) {
2035
1816
  }
2036
1817
 
2037
1818
  // src/tools/data-app/list-tenants.ts
2038
- import { z as z4 } from "zod";
1819
+ import { z as z5 } from "zod";
2039
1820
  var isNonEmptyString4 = (value) => typeof value === "string" && value.trim().length > 0;
2040
1821
  function workspaceIdFrom(workspace) {
2041
1822
  return isNonEmptyString4(workspace.id) && workspace.id || isNonEmptyString4(workspace._id) && workspace._id || isNonEmptyString4(workspace.workspaceId) && workspace.workspaceId || null;
@@ -2073,12 +1854,12 @@ function registerListTenants(server, client) {
2073
1854
  "list_tenants",
2074
1855
  "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
1856
  {
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.")
1857
+ workspaceName: z5.string().optional().describe("Workspace name. Preferred for non-technical callers; the tool resolves it to a workspaceId."),
1858
+ workspaceId: z5.string().optional().describe("Technical workspace UUID. Provide this instead of workspaceName if already known."),
1859
+ searchValue: z5.string().optional().describe("Optional case-insensitive search string for tenant/client labels."),
1860
+ limit: z5.number().int().positive().max(500).optional().describe("Maximum number of tenants to return. Backend may apply connector-specific limits."),
1861
+ datasourceId: z5.string().optional().describe("Optional datasource UUID override when the workspace has multiple datasource contexts."),
1862
+ datamartId: z5.string().optional().describe("Optional datamart UUID override when tenant lookup should use a specific datamart.")
2082
1863
  },
2083
1864
  READ_ONLY,
2084
1865
  async (params) => {
@@ -2172,15 +1953,15 @@ function registerListTenants(server, client) {
2172
1953
  }
2173
1954
 
2174
1955
  // src/tools/data-app/create-data-app.ts
2175
- import { z as z5 } from "zod";
1956
+ import { z as z6 } from "zod";
2176
1957
  function registerCreateDataApp(server, client) {
2177
1958
  server.tool(
2178
1959
  "create_data_app",
2179
1960
  "Create a new data app.",
2180
1961
  {
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/")
1962
+ name: z6.string().describe("Data app name"),
1963
+ workspaceName: z6.string().optional().describe("Optional workspace name if required by backend"),
1964
+ body: z6.record(z6.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/")
2184
1965
  },
2185
1966
  WRITE_CREATE,
2186
1967
  async (params) => {
@@ -2214,7 +1995,7 @@ function registerCreateDataApp(server, client) {
2214
1995
  }
2215
1996
 
2216
1997
  // src/tools/data-app/list-api-tokens.ts
2217
- import { z as z6 } from "zod";
1998
+ import { z as z7 } from "zod";
2218
1999
 
2219
2000
  // src/utils/resolve-resource.ts
2220
2001
  function resolveResource(options) {
@@ -2283,7 +2064,7 @@ function registerListApiTokens(server, client) {
2283
2064
  "list_api_tokens",
2284
2065
  "List API tokens for a data app. Omit dataAppName to auto-resolve.",
2285
2066
  {
2286
- dataAppName: z6.string().optional().describe("Data app name. Omit to list available data apps.")
2067
+ dataAppName: z7.string().optional().describe("Data app name. Omit to list available data apps.")
2287
2068
  },
2288
2069
  READ_ONLY,
2289
2070
  async (params) => {
@@ -2328,16 +2109,16 @@ function registerListApiTokens(server, client) {
2328
2109
  }
2329
2110
 
2330
2111
  // src/tools/data-app/create-api-token.ts
2331
- import { z as z7 } from "zod";
2112
+ import { z as z8 } from "zod";
2332
2113
  function registerCreateApiToken(server, client) {
2333
2114
  const services = createServiceBundle(client);
2334
2115
  server.tool(
2335
2116
  "create_api_token",
2336
2117
  "Create an API token for a data app and activate it for this session. Omit dataAppName to auto-resolve.",
2337
2118
  {
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")
2119
+ dataAppName: z8.string().optional().describe("Data app name. Omit to list available data apps."),
2120
+ name: z8.string().optional().describe("Optional token name"),
2121
+ body: z8.record(z8.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/api-tokens")
2341
2122
  },
2342
2123
  WRITE_CREATE,
2343
2124
  async (params) => {
@@ -2373,8 +2154,9 @@ function registerCreateApiToken(server, client) {
2373
2154
  );
2374
2155
  }
2375
2156
  client.setRuntimeApiToken(key);
2157
+ 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
2158
  return toolSuccess(
2377
- `API token created and set for this MCP session.
2159
+ `${used}
2378
2160
 
2379
2161
  **Token:** \`${key}\`
2380
2162
 
@@ -2389,16 +2171,16 @@ Save this in backend secrets immediately. This value may not be returned again.`
2389
2171
  }
2390
2172
 
2391
2173
  // src/tools/data-app/rotate-api-token.ts
2392
- import { z as z8 } from "zod";
2174
+ import { z as z9 } from "zod";
2393
2175
  function registerRotateApiToken(server, client) {
2394
2176
  const services = createServiceBundle(client);
2395
2177
  server.tool(
2396
2178
  "rotate_api_token",
2397
2179
  "Rotate (invalidate and replace) an API token. Previous token becomes stale.",
2398
2180
  {
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")
2181
+ dataAppName: z9.string().optional().describe("Data app name. Omit to list available data apps."),
2182
+ tokenId: z9.string().optional().describe("Optional token ID to rotate"),
2183
+ body: z9.record(z9.unknown()).optional().describe("Raw API body passthrough for rotate endpoint")
2402
2184
  },
2403
2185
  DESTRUCTIVE,
2404
2186
  async (params) => {
@@ -2517,14 +2299,14 @@ function registerListDatasources(server, client) {
2517
2299
  }
2518
2300
 
2519
2301
  // src/tools/infrastructure/discover-datasource-schema.ts
2520
- import { z as z9 } from "zod";
2302
+ import { z as z10 } from "zod";
2521
2303
  function registerDiscoverDatasourceSchema(server, client) {
2522
2304
  const services = createServiceBundle(client);
2523
2305
  server.tool(
2524
2306
  "discover_datasource_schema",
2525
2307
  "Inspect tables and columns in a datasource. Omit datasourceName to list available datasources.",
2526
2308
  {
2527
- datasourceName: z9.string().optional().describe(
2309
+ datasourceName: z10.string().optional().describe(
2528
2310
  "Name of the datasource to inspect. Omit to list available datasources."
2529
2311
  )
2530
2312
  },
@@ -2607,13 +2389,13 @@ Fix the connection or add a new database in the **Databrain UI**.`
2607
2389
  }
2608
2390
 
2609
2391
  // src/tools/infrastructure/list-datamarts.ts
2610
- import { z as z10 } from "zod";
2392
+ import { z as z11 } from "zod";
2611
2393
  function registerListDatamarts(server, client) {
2612
2394
  server.tool(
2613
2395
  "list_datamarts",
2614
2396
  "List datamarts with table counts and semantic layer scores.",
2615
2397
  {
2616
- dataAppName: z10.string().optional().describe("Filter by data app name")
2398
+ dataAppName: z11.string().optional().describe("Filter by data app name")
2617
2399
  },
2618
2400
  READ_ONLY,
2619
2401
  async (params) => {
@@ -2702,7 +2484,7 @@ function registerListWorkspaces(server, client) {
2702
2484
  }
2703
2485
 
2704
2486
  // src/tools/infrastructure/create-datamart.ts
2705
- import { z as z11 } from "zod";
2487
+ import { z as z12 } from "zod";
2706
2488
 
2707
2489
  // src/tools/widgets/production-guards.ts
2708
2490
  var APPLY_TO_PRODUCTION_CONFIRMATION = "APPLY_TO_PRODUCTION";
@@ -2720,29 +2502,29 @@ function genericProductionSafetyNote() {
2720
2502
  }
2721
2503
 
2722
2504
  // 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()
2505
+ var columnSchema = z12.object({
2506
+ name: z12.string(),
2507
+ alias: z12.string().optional(),
2508
+ label: z12.string().optional(),
2509
+ isHide: z12.boolean().optional(),
2510
+ isCustomColumn: z12.boolean().optional(),
2511
+ sql: z12.string().optional()
2730
2512
  });
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)
2513
+ var tableSchema = z12.object({
2514
+ name: z12.string(),
2515
+ schemaName: z12.string().optional(),
2516
+ clientColumn: z12.string().optional(),
2517
+ label: z12.string().optional(),
2518
+ columns: z12.array(columnSchema)
2737
2519
  });
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()
2520
+ var relationshipSchema = z12.object({
2521
+ parentTableName: z12.string(),
2522
+ parentColumnName: z12.string(),
2523
+ childTableName: z12.string(),
2524
+ childColumnName: z12.string(),
2525
+ cardinality: z12.enum(["OneToOne", "OneToMany", "ManyToOne", "ManyToMany"]),
2526
+ join: z12.enum(["INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN"]),
2527
+ relationshipName: z12.string()
2746
2528
  });
2747
2529
  function datamartSuccessMessage(name, result) {
2748
2530
  return `Datamart "${name}" created successfully (ID: ${result.id || result._id || "UNKNOWN"}).
@@ -2763,21 +2545,21 @@ function registerCreateDatamart(server, client) {
2763
2545
  "create_datamart",
2764
2546
  "Create a production datamart from selected tables and relationships. Defaults to dry-run; apply requires explicit confirmation.",
2765
2547
  {
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()
2548
+ mode: z12.enum(["dry_run", "apply"]).default("dry_run").describe("dry_run returns the plan only; apply creates the datamart"),
2549
+ confirm: z12.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
2550
+ name: z12.string().describe("Unique datamart name"),
2551
+ datasourceName: z12.string().optional().describe("Name of the datasource to use. Omit to list available datasources."),
2552
+ tableList: z12.array(tableSchema).optional().describe("Array of tables with their columns"),
2553
+ relationships: z12.array(relationshipSchema).optional().describe("Join definitions between tables"),
2554
+ tenancySettings: z12.object({
2555
+ tenancyLevel: z12.enum(["TABLE", "DATABASE"]).optional(),
2556
+ clientColumnType: z12.string().optional(),
2557
+ schemaName: z12.string().optional(),
2558
+ tableName: z12.string().optional(),
2559
+ tableClientNameColumn: z12.string().optional(),
2560
+ tablePrimaryKeyColumn: z12.string().optional()
2779
2561
  }).optional().describe("Multi-tenant configuration"),
2780
- body: z11.record(z11.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/datamarts")
2562
+ body: z12.record(z12.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/data-app/datamarts")
2781
2563
  },
2782
2564
  WRITE_CREATE,
2783
2565
  async (params) => {
@@ -2872,14 +2654,14 @@ function registerCreateDatamart(server, client) {
2872
2654
  }
2873
2655
 
2874
2656
  // src/tools/infrastructure/sync-datasource.ts
2875
- import { z as z12 } from "zod";
2657
+ import { z as z13 } from "zod";
2876
2658
  function registerSyncDatasource(server, client) {
2877
2659
  const datasourcesApi = new DatasourcesApi(client);
2878
2660
  server.tool(
2879
2661
  "sync_datasource",
2880
2662
  "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
2663
  {
2882
- datasourceName: z12.string().optional().describe("Name of the datasource to sync. Omit to list available datasources.")
2664
+ datasourceName: z13.string().optional().describe("Name of the datasource to sync. Omit to list available datasources.")
2883
2665
  },
2884
2666
  WRITE_IDEMPOTENT,
2885
2667
  async (params) => {
@@ -2928,7 +2710,7 @@ The schema metadata will be refreshed. You can now use \`discover_datasource_sch
2928
2710
  }
2929
2711
 
2930
2712
  // src/tools/embed/create-embed.ts
2931
- import { z as z13 } from "zod";
2713
+ import { z as z14 } from "zod";
2932
2714
  var DEFAULT_ACCESS_SETTINGS2 = {
2933
2715
  isAllowMetricCreation: false,
2934
2716
  isAllowMetricDeletion: false,
@@ -2941,18 +2723,18 @@ var DEFAULT_ACCESS_SETTINGS2 = {
2941
2723
  isAllowCreateDashboardView: false,
2942
2724
  metricCreationMode: "DRAG_DROP"
2943
2725
  };
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")
2726
+ var accessSettingsSchema = z14.object({
2727
+ datamartName: z14.string().describe("Name of the datamart (required)"),
2728
+ isAllowMetricCreation: z14.boolean().optional().describe("Allow end-users to create metrics"),
2729
+ metricCreationMode: z14.enum(["DRAG_DROP", "CHAT"]).optional().describe("Metric creation mode"),
2730
+ isAllowMetricDeletion: z14.boolean().optional().describe("Allow metric deletion"),
2731
+ isAllowMetricUpdate: z14.boolean().optional().describe("Allow metric editing"),
2732
+ isAllowMetricLayoutChange: z14.boolean().optional().describe("Allow layout changes"),
2733
+ isAllowManageMetrics: z14.boolean().optional().describe("Show manage metrics UI"),
2734
+ isAllowUnderlyingData: z14.boolean().optional().describe("Allow viewing underlying data"),
2735
+ isAllowAiPilot: z14.boolean().optional().describe("Enable AI pilot features"),
2736
+ isAllowEmailReports: z14.boolean().optional().describe("Allow email report scheduling"),
2737
+ isAllowCreateDashboardView: z14.boolean().optional().describe("Allow creating dashboard views")
2956
2738
  }).describe("Embed access settings. Only datamartName is required \u2014 all flags default to false.");
2957
2739
  function registerCreateEmbed(server, client) {
2958
2740
  const services = createServiceBundle(client);
@@ -2960,23 +2742,23 @@ function registerCreateEmbed(server, client) {
2960
2742
  "create_embed",
2961
2743
  "Create an embed. Provide dashboardId, workspaceName, accessSettings.datamartName. Add clientId for multi-tenant per-client dashboards. All isAllow* flags default to false.",
2962
2744
  {
2963
- body: z13.record(z13.unknown()).optional().describe(
2745
+ body: z14.record(z14.unknown()).optional().describe(
2964
2746
  "Raw API body passthrough. If provided, all other fields are ignored."
2965
2747
  ),
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"),
2748
+ dashboardId: z14.string().optional().describe("Dashboard identifier for embed creation"),
2749
+ workspaceName: z14.string().optional().describe("Workspace name. Omit to auto-resolve if only one exists."),
2750
+ embedType: z14.enum(["dashboard", "metric"]).optional().describe("Embed type"),
2751
+ metricId: z14.string().optional().describe("Required for metric embed type"),
2752
+ name: z14.string().optional().describe("Human-readable embed config name"),
2971
2753
  accessSettings: accessSettingsSchema.optional(),
2972
2754
  // Multi-tenant (dashboard embed) params
2973
- clientId: z13.string().optional().describe(
2755
+ clientId: z14.string().optional().describe(
2974
2756
  "Client identifier for multi-tenant isolation. When provided, creates a dashboard embed (each client gets their own dashboard copy)."
2975
2757
  ),
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")
2758
+ templateDashboardId: z14.string().optional().describe("Template dashboard ID to clone metrics from"),
2759
+ isImportMetrics: z14.boolean().optional().describe("Import metrics from template dashboard"),
2760
+ metricImportMode: z14.enum(["COPY", "LINK"]).optional().describe("COPY duplicates metrics, LINK shares them"),
2761
+ isRenameDashboard: z14.boolean().optional().describe("Rename the dashboard for this client")
2980
2762
  },
2981
2763
  WRITE_CREATE,
2982
2764
  async (params) => {
@@ -3078,14 +2860,14 @@ Next: run \`get_embed_details\` to verify the embed configuration.`
3078
2860
  }
3079
2861
 
3080
2862
  // src/tools/embed/list-embeds.ts
3081
- import { z as z14 } from "zod";
2863
+ import { z as z15 } from "zod";
3082
2864
  function registerListEmbeds(server, client) {
3083
2865
  const services = createServiceBundle(client);
3084
2866
  server.tool(
3085
2867
  "list_embeds",
3086
2868
  "List embed configurations. Requires an API token (use create_api_token first). Use `list_tenants` for authoritative client discovery from the tenancy database.",
3087
2869
  {
3088
- dataAppName: z14.string().optional().describe("Filter by data app name")
2870
+ dataAppName: z15.string().optional().describe("Filter by data app name")
3089
2871
  },
3090
2872
  READ_ONLY,
3091
2873
  async (params) => {
@@ -3136,14 +2918,14 @@ function registerListEmbeds(server, client) {
3136
2918
  }
3137
2919
 
3138
2920
  // src/tools/embed/get-embed-details.ts
3139
- import { z as z15 } from "zod";
2921
+ import { z as z16 } from "zod";
3140
2922
  function registerGetEmbedDetails(server, client) {
3141
2923
  const services = createServiceBundle(client);
3142
2924
  server.tool(
3143
2925
  "get_embed_details",
3144
2926
  "Get full embed configuration including access settings and theme.",
3145
2927
  {
3146
- embedId: z15.string().describe("ID of the embed")
2928
+ embedId: z16.string().describe("ID of the embed")
3147
2929
  },
3148
2930
  READ_ONLY,
3149
2931
  async (params) => {
@@ -3159,7 +2941,7 @@ function registerGetEmbedDetails(server, client) {
3159
2941
  }
3160
2942
 
3161
2943
  // src/tools/embed/update-embed.ts
3162
- import { z as z16 } from "zod";
2944
+ import { z as z17 } from "zod";
3163
2945
 
3164
2946
  // src/tools/embed/helpers.ts
3165
2947
  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 +2979,15 @@ function registerUpdateEmbed(server, client) {
3197
2979
  "update_embed",
3198
2980
  "Update embed access settings, permissions, theme, or name.",
3199
2981
  {
3200
- embedId: z16.string().describe("ID of the embed to update"),
3201
- body: z16.record(z16.unknown()).optional().describe(
2982
+ embedId: z17.string().describe("ID of the embed to update"),
2983
+ body: z17.record(z17.unknown()).optional().describe(
3202
2984
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, it is sent directly (passthrough mode)."
3203
2985
  ),
3204
- name: z16.string().optional().describe("New name for the embed"),
3205
- accessSettings: z16.record(z16.unknown()).optional().describe(
2986
+ name: z17.string().optional().describe("New name for the embed"),
2987
+ accessSettings: z17.record(z17.unknown()).optional().describe(
3206
2988
  "Partial update of access settings. See `get_embed_details` for current settings."
3207
2989
  ),
3208
- theme: z16.record(z16.unknown()).optional().describe(
2990
+ theme: z17.record(z17.unknown()).optional().describe(
3209
2991
  "Theme configuration object. See databrain://theme-reference for the full schema."
3210
2992
  )
3211
2993
  },
@@ -3251,7 +3033,7 @@ function registerUpdateEmbed(server, client) {
3251
3033
  }
3252
3034
 
3253
3035
  // src/tools/embed/customize-theme.ts
3254
- import { z as z17 } from "zod";
3036
+ import { z as z18 } from "zod";
3255
3037
  var THEME_PRESETS = {
3256
3038
  light: {
3257
3039
  general: {
@@ -3385,7 +3167,7 @@ var ACCESS_PRESETS = {
3385
3167
  metricCreationMode: "CHAT"
3386
3168
  }
3387
3169
  };
3388
- var fontFamilyEnum = z17.enum([
3170
+ var fontFamilyEnum = z18.enum([
3389
3171
  "Inter",
3390
3172
  "Roboto",
3391
3173
  "Open Sans",
@@ -3397,17 +3179,17 @@ var fontFamilyEnum = z17.enum([
3397
3179
  "Nunito",
3398
3180
  "system-ui"
3399
3181
  ]).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()
3182
+ var tooltipFontSchema = z18.object({
3183
+ fontFamily: z18.string().optional(),
3184
+ fontSize: z18.number().optional(),
3185
+ fontWeight: z18.number().optional(),
3186
+ fontColor: z18.string().optional()
3405
3187
  }).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()
3188
+ var fontConfigInputSchema = z18.object({
3189
+ fontSize: z18.number().optional(),
3190
+ fontFamily: z18.string().optional(),
3191
+ fontWeight: z18.number().optional(),
3192
+ color: z18.string().optional()
3411
3193
  }).optional();
3412
3194
  function mapToFrontendFontConfig(input) {
3413
3195
  if (!input) return void 0;
@@ -3418,23 +3200,23 @@ function mapToFrontendFontConfig(input) {
3418
3200
  ...input.color && { color: input.color }
3419
3201
  };
3420
3202
  }
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()
3203
+ var axisSchema = z18.object({
3204
+ hideAxisLines: z18.boolean().optional().describe("Hide axis lines"),
3205
+ hideSplitLines: z18.boolean().optional().describe("Hide grid/split lines"),
3206
+ hideAxisLabels: z18.boolean().optional().describe("Hide axis labels"),
3207
+ hideAxisTicks: z18.boolean().optional().describe("Hide axis tick marks"),
3208
+ axisName: z18.string().optional().describe("Axis title text"),
3209
+ axisNameOffset: z18.number().optional().describe("Axis title offset from axis"),
3210
+ axisLabelMargin: z18.number().optional().describe("Margin between axis labels and chart area in pixels"),
3211
+ fontSize: z18.number().optional().describe("Axis label font size"),
3212
+ fontFamily: z18.string().optional().describe("Axis label font family"),
3213
+ fontWeight: z18.number().optional().describe("Axis label font weight"),
3214
+ color: z18.string().optional().describe("Axis label color (hex)"),
3215
+ axisColor: z18.string().optional().describe("Axis line color (hex)"),
3216
+ axisNameFontConfig: z18.object({
3217
+ fontFamily: z18.string().optional(),
3218
+ fontSize: z18.number().optional(),
3219
+ fontWeight: z18.number().optional()
3438
3220
  }).optional().describe("Axis title font configuration")
3439
3221
  }).optional();
3440
3222
  function buildAdminTheme(p, presetBase) {
@@ -3590,94 +3372,94 @@ function registerCustomizeTheme(server, client) {
3590
3372
  "customize_embed_theme",
3591
3373
  `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
3374
  {
3593
- embedId: z17.string().describe("ID of the embed to theme"),
3594
- body: z17.record(z17.unknown()).optional().describe(
3375
+ embedId: z18.string().describe("ID of the embed to theme"),
3376
+ body: z18.record(z18.unknown()).optional().describe(
3595
3377
  "Raw API body for PUT /api/v2/data-app/embeds. If provided, sent directly and all convenience fields are ignored."
3596
3378
  ),
3597
3379
  // --- 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"),
3380
+ themePreset: z18.enum(["light", "dark", "corporate", "minimal"]).optional().describe("Apply a theme preset as the base. Granular params below override preset values."),
3381
+ accessPreset: z18.enum(["view-only", "power-user", "export-only", "ai-enabled"]).optional().describe("Apply an access settings preset"),
3600
3382
  // --- General ---
3601
3383
  fontFamily: fontFamilyEnum.optional().describe("Global font family"),
3602
- datePickerFormat: z17.string().optional().describe("Date picker display format"),
3384
+ datePickerFormat: z18.string().optional().describe("Date picker display format"),
3603
3385
  // --- 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)"),
3386
+ primaryColor: z18.string().optional().describe("Primary accent color (hex)"),
3387
+ backgroundColor: z18.string().optional().describe("Overall background color (hex)"),
3388
+ textColor: z18.string().optional().describe("Default text color (hex)"),
3389
+ ctaColor: z18.string().optional().describe("Call-to-action button color (hex)"),
3390
+ ctaTextColor: z18.string().optional().describe("CTA button text color (hex)"),
3609
3391
  // --- 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)"),
3392
+ dashboardBackgroundColor: z18.string().optional().describe("Dashboard container background (hex)"),
3393
+ selectBoxSize: z18.enum(["small", "medium", "large"]).optional().describe("Filter select box size"),
3394
+ selectBoxVariant: z18.enum(["floating", "outlined", "standard"]).optional().describe("Filter select box style"),
3395
+ selectBoxBorderRadius: z18.string().optional().describe('Select box border radius (e.g. "20px")'),
3396
+ selectBoxTextColor: z18.string().optional().describe("Select box text color (hex)"),
3397
+ metricCardColor: z18.string().optional().describe("Metric card background color (hex)"),
3616
3398
  // --- 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)"),
3399
+ cardTitleFontSize: z18.string().optional().describe('Card title font size (e.g. "16px")'),
3400
+ cardTitleFontWeight: z18.string().optional().describe('Card title font weight (e.g. "600")'),
3401
+ cardTitleColor: z18.string().optional().describe("Card title text color (hex)"),
3620
3402
  // --- 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)"),
3403
+ cardDescriptionFontSize: z18.string().optional().describe("Card description font size"),
3404
+ cardDescriptionFontWeight: z18.string().optional().describe("Card description font weight"),
3405
+ cardDescriptionColor: z18.string().optional().describe("Card description text color (hex)"),
3624
3406
  // --- 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)"),
3407
+ cardPadding: z18.string().optional().describe("Card internal padding"),
3408
+ cardBorderRadius: z18.string().optional().describe("Card border radius"),
3409
+ cardShadow: z18.string().optional().describe("Card box-shadow CSS value"),
3410
+ disableShadowOnHover: z18.boolean().optional().describe("Disable shadow on card hover"),
3411
+ disableCardStroke: z18.boolean().optional().describe("Remove card border stroke"),
3412
+ cardStrokeColor: z18.string().optional().describe("Card border stroke color (hex)"),
3631
3413
  // --- 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"),
3414
+ chartFontFamily: z18.string().optional().describe("Font family for chart text"),
3415
+ chartFontColor: z18.string().optional().describe("Font color for chart text (hex)"),
3416
+ chartColors: z18.array(z18.string()).optional().describe("Array of chart series colors (hex)"),
3417
+ chartPaletteName: z18.string().optional().describe("Name for a custom palette"),
3418
+ chartPaletteColors: z18.array(z18.string()).optional().describe("9 colors for a custom palette"),
3637
3419
  // --- Chart Tooltip (admin theme level) ---
3638
3420
  tooltipHeader: tooltipFontSchema.describe("Tooltip header font config"),
3639
3421
  tooltipLabel: tooltipFontSchema.describe("Tooltip label font config"),
3640
3422
  tooltipValue: tooltipFontSchema.describe("Tooltip value font config"),
3641
3423
  // --- 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)"),
3424
+ buttonPrimaryColor: z18.string().optional().describe("Primary button background (hex)"),
3425
+ buttonPrimaryTextColor: z18.string().optional().describe("Primary button text (hex)"),
3426
+ buttonSecondaryColor: z18.string().optional().describe("Secondary button background (hex)"),
3427
+ buttonSecondaryTextColor: z18.string().optional().describe("Secondary button text (hex)"),
3428
+ checkboxCheckedColor: z18.string().optional().describe("Checked checkbox color (hex)"),
3429
+ checkboxUncheckedColor: z18.string().optional().describe("Unchecked checkbox color (hex)"),
3430
+ switchEnabledColor: z18.string().optional().describe("Enabled switch color (hex)"),
3431
+ switchDisabledColor: z18.string().optional().describe("Disabled switch color (hex)"),
3432
+ breadcrumbFontFamily: z18.string().optional().describe("Drill breadcrumb font family"),
3433
+ breadcrumbFontColor: z18.string().optional().describe("Drill breadcrumb font color (hex)"),
3434
+ breadcrumbActiveColor: z18.string().optional().describe("Active breadcrumb color (hex)"),
3435
+ filterBadgeColor: z18.string().optional().describe("Multi-select filter badge color (hex)"),
3436
+ filterBadgeTextColor: z18.string().optional().describe("Filter badge text color (hex)"),
3437
+ datePickerColor: z18.string().optional().describe("Date picker accent color (hex)"),
3656
3438
  // --- 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"),
3439
+ breakpointXl: z18.number().optional().describe("XL breakpoint width in px"),
3440
+ breakpointLg: z18.number().optional().describe("LG breakpoint width in px"),
3441
+ breakpointMd: z18.number().optional().describe("MD breakpoint width in px"),
3442
+ breakpointSm: z18.number().optional().describe("SM breakpoint width in px"),
3443
+ breakpointXs: z18.number().optional().describe("XS breakpoint width in px"),
3662
3444
  // --- 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"),
3445
+ layoutColsXl: z18.number().optional().describe("Grid columns at XL breakpoint"),
3446
+ layoutColsLg: z18.number().optional().describe("Grid columns at LG breakpoint"),
3447
+ layoutColsMd: z18.number().optional().describe("Grid columns at MD breakpoint"),
3448
+ layoutColsSm: z18.number().optional().describe("Grid columns at SM breakpoint"),
3449
+ layoutColsXs: z18.number().optional().describe("Grid columns at XS breakpoint"),
3450
+ layoutColsXxs: z18.number().optional().describe("Grid columns at XXS breakpoint"),
3669
3451
  // --- Chart Appearance (detailed) ---
3670
3452
  tooltipHeaderStyle: fontConfigInputSchema.describe("Tooltip header: fontSize, fontFamily, fontWeight, color"),
3671
3453
  tooltipLabelStyle: fontConfigInputSchema.describe("Tooltip label: fontSize, fontFamily, fontWeight, color"),
3672
3454
  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([
3455
+ labelPosition: z18.enum(["hidden", "top", "left", "right", "bottom", "inside"]).optional().describe("Data label position on charts"),
3456
+ radialLabelPosition: z18.enum(["outside", "inside"]).optional().describe("Label position for radial/pie charts"),
3457
+ marginTop: z18.number().optional().describe("Chart top margin in pixels"),
3458
+ marginLeft: z18.number().optional().describe("Chart left margin in pixels"),
3459
+ marginRight: z18.number().optional().describe("Chart right margin in pixels"),
3460
+ marginBottom: z18.number().optional().describe("Chart bottom margin in pixels"),
3461
+ legendShow: z18.boolean().optional().describe("Show or hide chart legend"),
3462
+ legendPosition: z18.enum([
3681
3463
  "top-left",
3682
3464
  "top-center",
3683
3465
  "top-right",
@@ -3687,17 +3469,17 @@ function registerCustomizeTheme(server, client) {
3687
3469
  "bottom-center",
3688
3470
  "bottom-right"
3689
3471
  ]).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"),
3472
+ legendLayout: z18.enum(["horizontal", "vertical"]).optional().describe("Legend layout direction"),
3473
+ legendShape: z18.enum(["circle", "rect", "roundRect", "triangle", "diamond", "arrow", "none"]).optional().describe("Legend icon shape"),
3474
+ legendFontSize: z18.number().optional().describe("Legend font size"),
3475
+ legendFontWeight: z18.number().optional().describe("Legend font weight"),
3476
+ legendFontFamily: z18.string().optional().describe("Legend font family"),
3477
+ legendColor: z18.string().optional().describe("Legend text color (hex)"),
3478
+ legendTruncateLength: z18.number().optional().describe("Max characters before truncating legend"),
3479
+ disableLegendScrolling: z18.boolean().optional().describe("Disable scrolling in legend"),
3480
+ enableVariableLegendPosition: z18.boolean().optional().describe("Allow user to drag legend"),
3481
+ legendTop: z18.number().optional().describe("Custom legend top offset"),
3482
+ legendLeft: z18.number().optional().describe("Custom legend left offset"),
3701
3483
  verticalAxis: axisSchema.describe("Vertical (Y) axis configuration"),
3702
3484
  horizontalAxis: axisSchema.describe("Horizontal (X) axis configuration")
3703
3485
  },
@@ -3772,7 +3554,7 @@ Use \`get_embed_details\` to view the full current theme configuration.` + GUEST
3772
3554
  }
3773
3555
 
3774
3556
  // src/tools/embed/get-demo-link.ts
3775
- import { z as z18 } from "zod";
3557
+ import { z as z19 } from "zod";
3776
3558
  var HOUR_MS = 60 * 60 * 1e3;
3777
3559
  var DEFAULT_EXPIRY_HOURS = 24;
3778
3560
  var MAX_EXPIRY_HOURS = 24 * 30;
@@ -3782,11 +3564,11 @@ function registerGetDemoLink(server, client) {
3782
3564
  "get_demo_link",
3783
3565
  `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
3566
  {
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).`)
3567
+ clientId: z19.string().describe("End-customer identifier to scope the preview to."),
3568
+ dashboardName: z19.string().optional().describe("Dashboard name (non-technical). Preferred \u2014 pass with optional `workspaceName`."),
3569
+ workspaceName: z19.string().optional().describe("Workspace name. Only needed if `dashboardName` is ambiguous."),
3570
+ embedId: z19.string().optional().describe("Technical alternative to `dashboardName`."),
3571
+ expiryHours: z19.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
3572
  },
3791
3573
  WRITE_CREATE,
3792
3574
  async (params) => {
@@ -3842,70 +3624,70 @@ function registerGetDemoLink(server, client) {
3842
3624
  }
3843
3625
 
3844
3626
  // src/tools/frontend/generate-guest-token.ts
3845
- import { z as z19 } from "zod";
3627
+ import { z as z20 } from "zod";
3846
3628
  function registerGenerateGuestToken(server, client) {
3847
3629
  const services = createServiceBundle(client);
3848
3630
  server.tool(
3849
3631
  "generate_guest_token",
3850
3632
  "Generate a guest token for frontend embedding. Requires clientId. Omit dataAppName to auto-resolve.",
3851
3633
  {
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(
3634
+ clientId: z20.string().describe('Unique end-user identifier for multi-tenant filtering. Use "None" if no tenancy.'),
3635
+ dataAppName: z20.string().optional().describe("Name of the data app. Omit to list available data apps."),
3636
+ body: z20.record(z20.unknown()).optional().describe(
3855
3637
  "Raw API body for POST /api/v2/guest-token/create. If provided, sent directly and convenience fields are ignored."
3856
3638
  ),
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")
3639
+ params: z20.object({
3640
+ allowedEmbeds: z20.array(z20.string()).optional().describe("Restrict token to specific embed IDs"),
3641
+ rlsSettings: z20.array(z20.object({
3642
+ metricId: z20.string().describe("Metric ID"),
3643
+ values: z20.record(z20.unknown()).describe("RLS values by column name")
3862
3644
  })).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")
3645
+ appFilters: z20.array(z20.object({
3646
+ metricId: z20.string().describe("Metric ID"),
3647
+ values: z20.record(z20.unknown()).describe("Filter values by column name")
3866
3648
  })).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")
3649
+ dashboardAppFilters: z20.array(z20.object({
3650
+ dashboardId: z20.string().describe("Dashboard ID"),
3651
+ values: z20.record(z20.unknown()).describe("Filter values by dashboard filter name"),
3652
+ isShowOnUrl: z20.boolean().optional().describe("Whether filters are shown in URL params")
3871
3653
  })).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"')
3654
+ hideDashboardFilters: z20.array(z20.string()).optional().describe("Dashboard filter names to hide"),
3655
+ userIdentifier: z20.string().optional().describe("End-user identifier for private metrics support"),
3656
+ timezone: z20.string().optional().describe('IANA timezone, e.g. "America/New_York"')
3875
3657
  }).optional().describe("Advanced token params"),
3876
3658
  // 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")
3659
+ timezone: z20.string().optional().describe("Legacy: moved under params.timezone"),
3660
+ allowedEmbeds: z20.array(z20.string()).optional().describe("Legacy: moved under params.allowedEmbeds"),
3661
+ rlsSettings: z20.array(z20.object({
3662
+ metricId: z20.string().describe("Metric ID"),
3663
+ values: z20.record(z20.unknown()).describe("RLS values by column name")
3882
3664
  })).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")
3665
+ appFilters: z20.array(z20.object({
3666
+ metricId: z20.string().describe("Metric ID"),
3667
+ values: z20.record(z20.unknown()).describe("Filter values by column name")
3886
3668
  })).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")
3669
+ dashboardAppFilters: z20.array(z20.object({
3670
+ dashboardId: z20.string().describe("Dashboard ID to apply filters to"),
3671
+ values: z20.record(z20.unknown()).describe("Filter values for this dashboard"),
3672
+ isShowOnUrl: z20.boolean().optional().describe("Whether filters are shown in URL params")
3891
3673
  })).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()
3674
+ permissions: z20.object({
3675
+ isEnableArchiveMetrics: z20.boolean().optional(),
3676
+ isEnableManageMetrics: z20.boolean().optional(),
3677
+ isEnableCreateDashboardView: z20.boolean().optional(),
3678
+ isEnableMetricUpdation: z20.boolean().optional(),
3679
+ isEnableCustomizeLayout: z20.boolean().optional(),
3680
+ isEnableUnderlyingData: z20.boolean().optional(),
3681
+ isEnableDownloadMetrics: z20.boolean().optional(),
3682
+ isShowSideBar: z20.boolean().optional(),
3683
+ isShowDashboardName: z20.boolean().optional(),
3684
+ isDisableMetricCreation: z20.boolean().optional()
3903
3685
  }).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")
3686
+ theme: z20.record(z20.unknown()).optional().describe("Runtime theme overrides (overrides embed-level theme)"),
3687
+ hideDashboardFilters: z20.array(z20.string()).optional().describe("Legacy: moved under params.hideDashboardFilters"),
3688
+ userIdentifier: z20.string().optional().describe("Legacy: moved under params.userIdentifier"),
3689
+ expiryTime: z20.number().optional().describe("Token TTL in milliseconds"),
3690
+ datasourceName: z20.string().optional().describe("Datasource name for multi-datasource routing")
3909
3691
  },
3910
3692
  WRITE_CREATE,
3911
3693
  async (params) => {
@@ -3958,7 +3740,7 @@ Next step: Use \`generate_embed_code\` to get copy-paste-ready frontend code.`
3958
3740
  }
3959
3741
 
3960
3742
  // src/tools/frontend/generate-embed-code.ts
3961
- import { z as z20 } from "zod";
3743
+ import { z as z21 } from "zod";
3962
3744
 
3963
3745
  // src/templates/react.ts
3964
3746
  function kebabCase(str) {
@@ -4954,22 +4736,22 @@ function registerGenerateEmbedCode(server, _client) {
4954
4736
  "generate_embed_code",
4955
4737
  "Generate framework-specific embed code (React, Next.js, Vue, Angular, Svelte, SolidJS, vanilla HTML).",
4956
4738
  {
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()
4739
+ embedId: z21.string().describe("The embed ID to generate code for"),
4740
+ framework: z21.enum(FRAMEWORKS).describe("Frontend framework"),
4741
+ embedType: z21.enum(["dashboard", "metric"]).optional().default("dashboard"),
4742
+ options: z21.object({
4743
+ enableDownloadCsv: z21.boolean().optional(),
4744
+ enableEmailCsv: z21.boolean().optional(),
4745
+ enableDownloadAllMetrics: z21.boolean().optional(),
4746
+ enableDownloadAllPdf: z21.boolean().optional(),
4747
+ disableFullscreen: z21.boolean().optional(),
4748
+ isHideTablePreview: z21.boolean().optional(),
4749
+ isHideChartSettings: z21.boolean().optional(),
4750
+ isStickyDashboardFilters: z21.boolean().optional(),
4751
+ enableMultiMetricFilters: z21.boolean().optional(),
4752
+ themeName: z21.string().optional(),
4753
+ customTheme: z21.record(z21.unknown()).optional(),
4754
+ optionsIcon: z21.enum(["kebab-menu-vertical", "download"]).optional()
4973
4755
  }).optional()
4974
4756
  },
4975
4757
  READ_ONLY,
@@ -5009,17 +4791,17 @@ ${generated.instructions}`;
5009
4791
  }
5010
4792
 
5011
4793
  // src/tools/analytics/list-dashboards.ts
5012
- import { z as z21 } from "zod";
4794
+ import { z as z22 } from "zod";
5013
4795
  function registerListDashboards(server, client) {
5014
4796
  const services = createServiceBundle(client);
5015
4797
  server.tool(
5016
4798
  "list_dashboards",
5017
4799
  "List dashboards globally or for a specific data app.",
5018
4800
  {
5019
- scope: z21.enum(["auto", "global", "data_app"]).optional().describe(
4801
+ scope: z22.enum(["auto", "global", "data_app"]).optional().describe(
5020
4802
  "Dashboard listing scope. auto: use data_app when dataAppName + API token are available, otherwise global."
5021
4803
  ),
5022
- dataAppName: z21.string().optional().describe("Data app name for data_app scope. Ignored for global scope.")
4804
+ dataAppName: z22.string().optional().describe("Data app name for data_app scope. Ignored for global scope.")
5023
4805
  },
5024
4806
  READ_ONLY,
5025
4807
  async (params) => {
@@ -5054,36 +4836,36 @@ function registerListDashboards(server, client) {
5054
4836
  }
5055
4837
 
5056
4838
  // 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)
4839
+ import { z as z23 } from "zod";
4840
+ var dashboardPositionSchema = z23.object({
4841
+ x: z23.number().int().min(0).max(23),
4842
+ y: z23.number().int().min(0),
4843
+ w: z23.number().int().min(1).max(24),
4844
+ h: z23.number().int().min(1).max(40)
5063
4845
  }).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()
4846
+ var dashboardChartSchema = z23.object({
4847
+ type: z23.string(),
4848
+ metricRef: z23.string().optional(),
4849
+ query: z23.string().optional(),
4850
+ companyIntegrationId: z23.string().optional(),
4851
+ chartOptions: z23.record(z23.unknown()).optional()
5070
4852
  }).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"),
4853
+ var dashboardTileSchema = z23.object({
4854
+ id: z23.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/),
4855
+ kind: z23.literal("databrain-chart"),
5074
4856
  position: dashboardPositionSchema,
5075
- title: z22.string().min(1).max(200),
4857
+ title: z23.string().min(1).max(200),
5076
4858
  chart: dashboardChartSchema.optional()
5077
4859
  }).strict();
5078
4860
  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)
4861
+ schemaVersion: z23.literal(1),
4862
+ title: z23.string().min(1).max(200),
4863
+ description: z23.string().max(2e3).optional(),
4864
+ layout: z23.object({ columns: z23.union([z23.literal(12), z23.literal(24)]).default(24) }).strict().optional(),
4865
+ tiles: z23.array(dashboardTileSchema).min(1).max(50)
5084
4866
  };
5085
- var workspaceNameSchema = z22.string().min(1).describe("Target workspace name");
5086
- var idempotencyKeySchema = z22.string().regex(
4867
+ var workspaceNameSchema = z23.string().min(1).describe("Target workspace name");
4868
+ var idempotencyKeySchema = z23.string().regex(
5087
4869
  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
5088
4870
  "idempotencyKey must be a UUID v4"
5089
4871
  ).describe("UUID v4 unique to this workspace dashboard create request");
@@ -5136,7 +4918,7 @@ ${stringify(validation)}`
5136
4918
  }
5137
4919
 
5138
4920
  // src/tools/dashboard/generate-dashboard-tiles.ts
5139
- import { z as z23 } from "zod";
4921
+ import { z as z24 } from "zod";
5140
4922
  function registerGenerateDashboardTiles(server, client) {
5141
4923
  const services = createServiceBundle(client);
5142
4924
  server.tool(
@@ -5144,15 +4926,15 @@ function registerGenerateDashboardTiles(server, client) {
5144
4926
  "Generate multiple workspace dashboard tile drafts without persisting them.",
5145
4927
  {
5146
4928
  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),
4929
+ dataAppName: z24.string().optional().describe("Data app in the workspace; auto-resolved when unique"),
4930
+ integrationId: z24.string().optional().describe("Datasource UUID override"),
4931
+ integrationName: z24.string().optional().describe("Datasource name override"),
4932
+ tiles: z24.array(
4933
+ z24.object({
4934
+ id: z24.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/),
4935
+ title: z24.string().min(1).max(200),
5154
4936
  position: dashboardPositionSchema,
5155
- userInputs: z23.record(z23.unknown())
4937
+ userInputs: z24.record(z24.unknown())
5156
4938
  }).strict()
5157
4939
  ).min(1).max(50)
5158
4940
  },
@@ -5236,14 +5018,14 @@ function registerGenerateDashboardTiles(server, client) {
5236
5018
  }
5237
5019
 
5238
5020
  // src/tools/dashboard/get-dashboard.ts
5239
- import { z as z24 } from "zod";
5021
+ import { z as z25 } from "zod";
5240
5022
  function registerGetDashboard(server, client) {
5241
5023
  server.tool(
5242
5024
  "get_dashboard",
5243
5025
  "Retrieve a workspace dashboard projected back into the versioned tile-list contract.",
5244
5026
  {
5245
5027
  workspaceName: workspaceNameSchema,
5246
- dashboardId: z24.string().min(1).describe("Dashboard identifier returned by create_dashboard")
5028
+ dashboardId: z25.string().min(1).describe("Dashboard identifier returned by create_dashboard")
5247
5029
  },
5248
5030
  READ_ONLY,
5249
5031
  async (params) => {
@@ -5266,8 +5048,229 @@ function registerGetDashboard(server, client) {
5266
5048
  );
5267
5049
  }
5268
5050
 
5051
+ // src/tools/dashboard/dashboard-snapshot.ts
5052
+ import { z as z26 } from "zod";
5053
+ var SNAPSHOT_REQUEST_MS = 45e3;
5054
+ function registerDashboardSnapshot(server, client) {
5055
+ server.tool(
5056
+ "dashboard_snapshot",
5057
+ "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.",
5058
+ {
5059
+ workspaceName: workspaceNameSchema.optional(),
5060
+ dashboardId: z26.string().min(1).max(200).optional().describe("Dashboard id from list_dashboards. Needed unless metricId is given."),
5061
+ metricId: z26.string().min(1).max(200).optional().describe("One chart (metric) id: draw only that chart, from a dashboard it is on."),
5062
+ theme: z26.enum(["light", "dark"]).default("light").describe("Picture colours"),
5063
+ width: z26.number().int().min(480).max(1600).default(1200).describe("Picture width in pixels")
5064
+ },
5065
+ READ_ONLY,
5066
+ async (params) => {
5067
+ if (!params.dashboardId && !params.metricId) {
5068
+ return toolError("Name a dashboardId (from list_dashboards), or a metricId for one chart.");
5069
+ }
5070
+ try {
5071
+ const result = await client.request({
5072
+ method: "POST",
5073
+ path: "/api/v2/workspace/dashboard/snapshot",
5074
+ auth: "service",
5075
+ timeoutMs: SNAPSHOT_REQUEST_MS,
5076
+ body: {
5077
+ ...params.workspaceName ? { workspaceName: params.workspaceName } : {},
5078
+ ...params.dashboardId ? { dashboardId: params.dashboardId } : {},
5079
+ ...params.metricId ? { metricId: params.metricId } : {},
5080
+ theme: params.theme,
5081
+ width: params.width
5082
+ }
5083
+ });
5084
+ if (result?.mimeType !== "image/png" || typeof result.image !== "string" || !result.image) {
5085
+ return toolError("The snapshot answer had no picture. Try again.");
5086
+ }
5087
+ const what = result.metric?.name ? `Chart "${result.metric.name}" on dashboard "${result.dashboard?.name ?? ""}"` : `Dashboard "${result.dashboard?.name ?? ""}"`;
5088
+ const text2 = [
5089
+ `${what} in workspace "${result.workspace?.name ?? ""}", ${result.theme ?? params.theme}, ${result.width}x${result.height} px.`,
5090
+ result.summary ?? ""
5091
+ ].filter(Boolean).join("\n");
5092
+ return {
5093
+ content: [
5094
+ { type: "text", text: text2 },
5095
+ { type: "image", data: result.image, mimeType: "image/png" }
5096
+ ]
5097
+ };
5098
+ } catch (err) {
5099
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
5100
+ return toolError(`Unexpected error: ${err.message}`);
5101
+ }
5102
+ }
5103
+ );
5104
+ }
5105
+
5106
+ // src/apps/preview.ts
5107
+ import { createHash } from "crypto";
5108
+ import { z as z27 } from "zod";
5109
+ var draftSchema = z27.object({
5110
+ question: z27.string().min(1).max(2e3),
5111
+ sql: z27.string().min(1).max(2e4),
5112
+ datamartName: z27.string().min(1).max(256),
5113
+ dimensions: z27.array(z27.string().max(128)).max(20).default([]),
5114
+ measures: z27.array(z27.string().max(128)).max(20).default([]),
5115
+ chartType: z27.string().min(1).max(80),
5116
+ title: z27.string().min(1).max(200),
5117
+ sessionId: z27.string().max(256).optional(),
5118
+ /** Workspaces built on this datamart. One means Save needs no choice. */
5119
+ workspaces: z27.array(z27.string().max(256)).max(50).default([])
5120
+ }).strict();
5121
+ var PREVIEW_PATH = "/api/v2/data-app/chart-preview";
5122
+ var METRIC_PATH = "/api/mcp/metrics/create";
5123
+ var DASHBOARD_PATH = "/api/v2/workspace/dashboard";
5124
+ var PREVIEW_TYPES = ["bar", "row", "line", "area", "pie", "doughnut", "table"];
5125
+ var RENDER_TYPES = [...PREVIEW_TYPES, "single value"];
5126
+ async function previewPage(client, draft, rows, chartType) {
5127
+ const page = await client.request({
5128
+ method: "POST",
5129
+ path: PREVIEW_PATH,
5130
+ auth: "service",
5131
+ skipDataUnwrap: true,
5132
+ body: {
5133
+ rows: firstRows(rows),
5134
+ chartType,
5135
+ title: draft.title,
5136
+ dimensions: draft.dimensions,
5137
+ measures: draft.measures,
5138
+ width: 600,
5139
+ height: 320
5140
+ }
5141
+ });
5142
+ return pageSchema.parse(page);
5143
+ }
5144
+ function tablePage(draft, rows) {
5145
+ const page = firstRows(rows);
5146
+ return {
5147
+ kind: "table",
5148
+ title: draft.title,
5149
+ chartSpec: { type: "table" },
5150
+ rows: page,
5151
+ columns: Object.keys(page[0] ?? {})
5152
+ };
5153
+ }
5154
+ async function draftPage(client, draft, rows, chartType) {
5155
+ const data = rowsSchema.parse(firstRows(rows));
5156
+ const carry = { draft: { ...draft, chartType }, previewTypes: PREVIEW_TYPES };
5157
+ if (chartType === "table" || !data.length) return { ...tablePage(draft, data), ...carry };
5158
+ try {
5159
+ return { ...await previewPage(client, draft, data, chartType), ...carry };
5160
+ } catch (error) {
5161
+ const previewError = error instanceof DatabrainApiError ? error.code : "PREVIEW_UNAVAILABLE";
5162
+ return { ...tablePage(draft, data), ...carry, draft: { ...draft, chartType: "table" }, previewError };
5163
+ }
5164
+ }
5165
+ var sameName = (a, b) => typeof a === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
5166
+ async function workspacesForDatamart(client, datamartName, mode = "hint") {
5167
+ const cached = (key) => mode === "hint" ? { cacheKey: key, cacheTtlMs: 5 * 60 * 1e3 } : {};
5168
+ try {
5169
+ const [workspaces, datamarts] = await Promise.all([
5170
+ client.request({ method: "GET", path: "/api/v2/workspace", auth: "service", ...cached("list_workspaces") }),
5171
+ client.request({ method: "GET", path: "/api/v2/data-app/datamarts", auth: "service", ...cached("list_datamarts") })
5172
+ ]);
5173
+ const datasource = (Array.isArray(datamarts) ? datamarts : []).find((d) => sameName(d?.name ?? d?.datamartName, datamartName))?.companyIntegration?.id;
5174
+ const all = (Array.isArray(workspaces) ? workspaces : []).filter((w) => typeof w?.name === "string");
5175
+ const onDatamart = all.filter((w) => sameName(w.datamart?.name ?? w.datamartName, datamartName));
5176
+ const onDatasource = datasource ? all.filter((w) => !w.datamart && w.companyIntegration?.id === datasource) : [];
5177
+ return [...new Set([...onDatamart, ...onDatasource].map((w) => w.name))];
5178
+ } catch {
5179
+ return [];
5180
+ }
5181
+ }
5182
+ function saveKey(draft, input) {
5183
+ const h = createHash("sha256").update(JSON.stringify({
5184
+ sql: draft.sql,
5185
+ datamartName: draft.datamartName,
5186
+ workspaceName: input.workspaceName,
5187
+ dashboardTitle: input.dashboardTitle,
5188
+ chartType: input.chartType,
5189
+ // One attempt, many retries: the tile mints an id when the form opens and re-sends it on
5190
+ // retry, so a retry replays and a fresh Save is a fresh key. Without one, content is the key.
5191
+ attemptId: input.attemptId ?? null
5192
+ })).digest();
5193
+ h[6] = h[6] & 15 | 64;
5194
+ h[8] = h[8] & 63 | 128;
5195
+ const x = h.subarray(0, 16).toString("hex");
5196
+ return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20, 32)}`;
5197
+ }
5198
+ async function saveDraftAsDashboard(client, draft, input) {
5199
+ const metric = await client.request({
5200
+ method: "POST",
5201
+ path: METRIC_PATH,
5202
+ auth: "service",
5203
+ body: {
5204
+ workspaceName: input.workspaceName,
5205
+ name: input.dashboardTitle,
5206
+ query: draft.sql,
5207
+ description: draft.question,
5208
+ isPublished: false,
5209
+ validateOnly: false
5210
+ }
5211
+ });
5212
+ const metricId = metric?.metricId || metric?.id;
5213
+ if (!metricId) throw new DatabrainApiError(502, "METRIC_ID_MISSING", "The metric was created but returned no id.");
5214
+ const slug = input.dashboardTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63) || "chart";
5215
+ const body = {
5216
+ schemaVersion: 1,
5217
+ title: input.dashboardTitle,
5218
+ description: draft.question,
5219
+ layout: { columns: 24 },
5220
+ tiles: [{
5221
+ id: slug,
5222
+ kind: "databrain-chart",
5223
+ title: input.dashboardTitle,
5224
+ position: { x: 0, y: 0, w: 12, h: 8 },
5225
+ chart: { type: input.chartType, metricRef: metricId }
5226
+ }]
5227
+ };
5228
+ const dashboard = await client.request({
5229
+ method: "POST",
5230
+ path: DASHBOARD_PATH,
5231
+ auth: "service",
5232
+ query: { workspaceName: input.workspaceName },
5233
+ headers: { "Idempotency-Key": saveKey(draft, input) },
5234
+ body,
5235
+ invalidateCache: ["list_dashboards"],
5236
+ // A metadata insert: 15s keeps the attempt and its retries inside the host's 60s window.
5237
+ timeoutMs: 15e3
5238
+ });
5239
+ if (!dashboard?.dashboardId) {
5240
+ throw new DatabrainApiError(502, "DASHBOARD_ID_MISSING", `Metric ${metricId} was saved but the dashboard returned no id.`);
5241
+ }
5242
+ return { dashboardId: dashboard.dashboardId, metricId, workspaceName: input.workspaceName, title: input.dashboardTitle };
5243
+ }
5244
+
5269
5245
  // src/tools/query/ask-question.ts
5270
- import { z as z25 } from "zod";
5246
+ import { z as z28 } from "zod";
5247
+
5248
+ // src/tools/query/answer-format.ts
5249
+ function formatConfidence(value) {
5250
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
5251
+ const percent = value > 1 ? value : value * 100;
5252
+ return `${Math.round(Math.min(Math.max(percent, 0), 100))}%`;
5253
+ }
5254
+ function formatProse(value) {
5255
+ if (Array.isArray(value)) {
5256
+ const items = value.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => `- ${item.trim()}`);
5257
+ return items.length ? items.join("\n") : null;
5258
+ }
5259
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
5260
+ return null;
5261
+ }
5262
+
5263
+ // src/tools/query/ask-question.ts
5264
+ function normalizePreviewType(type) {
5265
+ const t = (type || "").toLowerCase().trim();
5266
+ if (RENDER_TYPES.includes(t)) return t;
5267
+ if (t === "single value" || t === "kpi" || t === "number") return "single value";
5268
+ if (t === "time series" || t === "stepped") return "line";
5269
+ if (t === "stack" || t === "column") return "bar";
5270
+ if (t === "horizontal stack" || t === "horizontal bar") return "row";
5271
+ if (t === "stacked area") return "area";
5272
+ return "table";
5273
+ }
5271
5274
  var MAX_DISPLAY_ROWS = 50;
5272
5275
  function formatDataTable(data, columns) {
5273
5276
  if (!Array.isArray(data) || data.length === 0) return "_No data returned._";
@@ -5297,16 +5300,16 @@ function registerAskQuestion(server, client) {
5297
5300
  "ask_question",
5298
5301
  "Ask a natural language question \u2014 converts to SQL, returns data + chart suggestion. Requires datamartName with a populated semantic layer.",
5299
5302
  {
5300
- question: z25.string().describe(
5303
+ question: z28.string().describe(
5301
5304
  'The natural language question to ask (e.g. "What was total revenue last month?")'
5302
5305
  ),
5303
- datamartName: z25.string().optional().describe(
5306
+ datamartName: z28.string().optional().describe(
5304
5307
  "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
5308
  ),
5306
- sessionId: z25.string().optional().describe(
5309
+ sessionId: z28.string().optional().describe(
5307
5310
  "Session ID from a previous ask_question response, for follow-up questions."
5308
5311
  ),
5309
- timezone: z25.string().optional().describe('IANA timezone (e.g. "America/New_York"). Defaults to UTC.')
5312
+ timezone: z28.string().optional().describe('IANA timezone (e.g. "America/New_York"). Defaults to UTC.')
5310
5313
  },
5311
5314
  WRITE_CREATE,
5312
5315
  async (params) => {
@@ -5411,15 +5414,26 @@ You can verify with \`get_semantic_layer\` using datamartName "${datamartName}".
5411
5414
  });
5412
5415
  const data = Array.isArray(result.data) ? result.data : [];
5413
5416
  if (hasChartApps(server)) {
5417
+ const chartType = normalizePreviewType(result.suggestedChartType);
5418
+ const draft = {
5419
+ question: params.question,
5420
+ sql: result.query || "",
5421
+ datamartName,
5422
+ dimensions: result.columns?.dimensions || [],
5423
+ measures: result.columns?.measures || [],
5424
+ chartType,
5425
+ title: params.question.slice(0, 200),
5426
+ sessionId: result.sessionId || void 0,
5427
+ workspaces: await workspacesForDatamart(client, datamartName)
5428
+ };
5414
5429
  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
- });
5430
+ const page = draft.sql ? await draftPage(client, draft, rows, chartType) : { ...tablePage(draft, rows), previewTypes: [] };
5431
+ const shown = rows.length === data.length ? `${data.length} rows` : `first ${rows.length} of ${data.length} rows`;
5432
+ try {
5433
+ return chartResult(`${page.kind === "table" ? "Table" : "Chart"} ready (${page.chartSpec && page.chartSpec.type}): ${shown}.` + (result.summary ? ` ${result.summary}` : ""), { ...page, rowsTotal: data.length });
5434
+ } catch (error) {
5435
+ if (!(error instanceof Error) || error.message !== "Chart page exceeds payload budget") throw error;
5436
+ }
5423
5437
  }
5424
5438
  const table = formatDataTable(data, result.columns);
5425
5439
  const parts = [];
@@ -5431,23 +5445,24 @@ You can verify with \`get_semantic_layer\` using datamartName "${datamartName}".
5431
5445
  parts.push(
5432
5446
  `**Suggested chart type:** ${result.suggestedChartType}`
5433
5447
  );
5434
- if (result.confidence !== null) {
5435
- parts.push(
5436
- `**Confidence:** ${Math.round(result.confidence * 100)}%`
5437
- );
5448
+ const confidence = formatConfidence(result.confidence);
5449
+ if (confidence) {
5450
+ parts.push(`**Confidence:** ${confidence}`);
5438
5451
  }
5439
5452
  parts.push(
5440
5453
  `**Rows returned:** ${data.length} | **Time:** ${result.timeTaken}ms`
5441
5454
  );
5442
- if (result.reasoning) {
5455
+ const reasoning = formatProse(result.reasoning);
5456
+ if (reasoning) {
5443
5457
  parts.push(`
5444
5458
  ### Reasoning
5445
- ${result.reasoning}`);
5459
+ ${reasoning}`);
5446
5460
  }
5447
- if (result.summary) {
5461
+ const summary = formatProse(result.summary);
5462
+ if (summary) {
5448
5463
  parts.push(`
5449
5464
  ### Summary
5450
- ${result.summary}`);
5465
+ ${summary}`);
5451
5466
  }
5452
5467
  parts.push(`
5453
5468
  ### Data
@@ -5494,14 +5509,14 @@ To fix this:
5494
5509
  }
5495
5510
 
5496
5511
  // src/tools/query/get-semantic-layer.ts
5497
- import { z as z26 } from "zod";
5512
+ import { z as z29 } from "zod";
5498
5513
  function registerGetSemanticLayer(server, client) {
5499
5514
  const services = createServiceBundle(client);
5500
5515
  server.tool(
5501
5516
  "get_semantic_layer",
5502
5517
  "Get semantic layer metadata for a datamart: table/column descriptions, synonyms, completion score. Omit datamartName to auto-resolve.",
5503
5518
  {
5504
- datamartName: z26.string().optional().describe(
5519
+ datamartName: z29.string().optional().describe(
5505
5520
  "Name of the datamart to inspect. Omit to list available datamarts."
5506
5521
  )
5507
5522
  },
@@ -5601,25 +5616,405 @@ ${result.feedback}`);
5601
5616
  "\n> **Tip:** The completion score is below 50%. Consider using `update_semantic_layer` to add more descriptions, synonyms, and example questions for better AI query accuracy."
5602
5617
  );
5603
5618
  }
5604
- return toolSuccess(parts.join("\n"));
5605
- } catch (err) {
5606
- if (err instanceof DatabrainApiError) return err.toToolResponse();
5607
- return toolError(`Unexpected error: ${err.message}`);
5619
+ return toolSuccess(parts.join("\n"));
5620
+ } catch (err) {
5621
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
5622
+ return toolError(`Unexpected error: ${err.message}`);
5623
+ }
5624
+ }
5625
+ );
5626
+ }
5627
+
5628
+ // src/tools/query/semantic-model.ts
5629
+ import { z as z30 } from "zod";
5630
+
5631
+ // src/services/semantic-source.ts
5632
+ import { parseDocument, stringify as stringify2 } from "yaml";
5633
+ var ENTITY_COLLECTIONS = {
5634
+ table: ["tables"],
5635
+ column: ["columns"],
5636
+ measure: ["measures"],
5637
+ metric: ["metrics"],
5638
+ join: ["joins"],
5639
+ entity: ["entities"],
5640
+ glossary: ["glossary"],
5641
+ saved_query: ["saved_queries", "verified_queries"],
5642
+ instruction: ["instructions", "agent_instructions"],
5643
+ access_rule: ["access", "policies"]
5644
+ };
5645
+ function isObject(value) {
5646
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5647
+ }
5648
+ function assertSafe(value, seen = /* @__PURE__ */ new Set(), depth = 0) {
5649
+ if (depth > 100) throw new Error("Semantic source nesting exceeds 100 levels.");
5650
+ if (!value || typeof value !== "object") return;
5651
+ if (seen.has(value)) throw new Error("Recursive YAML aliases are not supported.");
5652
+ seen.add(value);
5653
+ for (const [key, child] of Object.entries(value)) {
5654
+ if (["__proto__", "constructor", "prototype"].includes(key)) throw new Error(`Unsupported source key: ${key}.`);
5655
+ assertSafe(child, seen, depth + 1);
5656
+ }
5657
+ seen.delete(value);
5658
+ }
5659
+ function parseSemanticSource(source2) {
5660
+ const document = parseDocument(source2, { prettyErrors: false, uniqueKeys: true });
5661
+ if (document.errors.length || document.warnings.length) {
5662
+ throw new Error((document.errors[0] ?? document.warnings[0]).message);
5663
+ }
5664
+ const value = document.toJS({ maxAliasCount: 100 });
5665
+ if (!isObject(value)) throw new Error("Semantic source must be a YAML mapping.");
5666
+ assertSafe(value);
5667
+ return value;
5668
+ }
5669
+ function semanticEntities(source2, visibleOnly = false) {
5670
+ const out = [];
5671
+ for (const [kind, keys] of Object.entries(ENTITY_COLLECTIONS)) {
5672
+ if (kind === "column") continue;
5673
+ const key = keys.find((key2) => Array.isArray(source2[key2]));
5674
+ const entries = key ? source2[key] : [];
5675
+ for (const entry of entries) {
5676
+ if (!isObject(entry) || typeof entry.name !== "string" || visibleOnly && entry.hidden === true) continue;
5677
+ const columns = Array.isArray(entry.columns) ? entry.columns.filter(isObject) : [];
5678
+ const entity = visibleOnly && kind === "table" ? { ...entry, columns: columns.filter((column) => column.hidden !== true) } : entry;
5679
+ out.push({ id: `${kind}:${entry.name}`, kind, name: entry.name, entity });
5680
+ if (kind === "table") for (const column of columns) {
5681
+ if (typeof column.name !== "string" || visibleOnly && column.hidden === true) continue;
5682
+ const name = `${entry.name}.${column.name}`;
5683
+ out.push({ id: `column:${name}`, kind: "column", name, entity: column });
5684
+ }
5685
+ }
5686
+ }
5687
+ return out;
5688
+ }
5689
+ function mergePatch(base, patch) {
5690
+ if (isObject(patch)) {
5691
+ const next = isObject(base) ? { ...base } : {};
5692
+ for (const [key, value] of Object.entries(patch)) {
5693
+ if (value === null) delete next[key];
5694
+ else next[key] = mergePatch(next[key], value);
5695
+ }
5696
+ return next;
5697
+ }
5698
+ if (Array.isArray(patch) && patch.length && patch.every((item) => isObject(item) && typeof item.name === "string")) {
5699
+ const next = Array.isArray(base) ? [...base] : [];
5700
+ const names = /* @__PURE__ */ new Set();
5701
+ for (const item of patch) {
5702
+ const name = item.name;
5703
+ if (names.has(name)) throw new Error(`Duplicate patch entity: ${name}.`);
5704
+ names.add(name);
5705
+ const index = next.findIndex((entry) => isObject(entry) && entry.name === name);
5706
+ if (index < 0) next.push(mergePatch({}, item));
5707
+ else next[index] = mergePatch(next[index], item);
5708
+ }
5709
+ return next;
5710
+ }
5711
+ return patch;
5712
+ }
5713
+ function changeSemanticSource(source2, input) {
5714
+ const model = parseSemanticSource(source2);
5715
+ if (input.yaml_patch !== void 0) return stringify2(mergePatch(model, parseSemanticSource(input.yaml_patch)));
5716
+ const change = input.change;
5717
+ assertSafe(change);
5718
+ let parent = model;
5719
+ if (change.kind === "column") {
5720
+ if (!change.table) throw new Error("A column change requires table and an unqualified column name.");
5721
+ const table = (Array.isArray(model.tables) ? model.tables : []).find(
5722
+ (item) => isObject(item) && item.name === change.table
5723
+ );
5724
+ if (!isObject(table)) throw new Error(`Table not found: ${change.table}.`);
5725
+ parent = table;
5726
+ } else if (change.table) throw new Error("table is only valid for a column change.");
5727
+ const keys = ENTITY_COLLECTIONS[change.kind];
5728
+ const key = keys.find((key2) => Array.isArray(parent[key2])) ?? keys[0];
5729
+ const entries = Array.isArray(parent[key]) ? [...parent[key]] : [];
5730
+ const index = entries.findIndex((entry) => isObject(entry) && entry.name === change.name);
5731
+ if (change.operation === "remove") {
5732
+ if (change.value !== void 0) throw new Error("A remove change must not include value.");
5733
+ if (index < 0) throw new Error(`Entity not found: ${change.kind} ${change.name}.`);
5734
+ entries.splice(index, 1);
5735
+ } else {
5736
+ if (!change.value) throw new Error("An upsert change requires value.");
5737
+ if (change.value.name !== void 0 && change.value.name !== change.name) throw new Error("value.name must match name.");
5738
+ const value = mergePatch(index < 0 ? {} : entries[index], { ...change.value, name: change.name });
5739
+ if (index < 0) entries.push(value);
5740
+ else entries[index] = value;
5741
+ }
5742
+ parent[key] = entries;
5743
+ return stringify2(model);
5744
+ }
5745
+
5746
+ // src/services/semantic-retrieval.ts
5747
+ function tokens(text2) {
5748
+ return text2.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
5749
+ }
5750
+ function text(value) {
5751
+ return typeof value === "string" ? value : Array.isArray(value) ? value.map(text).join(" ") : "";
5752
+ }
5753
+ function retrieveSemanticSource(source2, question, k) {
5754
+ const nodes = semanticEntities(parseSemanticSource(source2), true);
5755
+ const query = new Set(tokens(question));
5756
+ const score = (node) => {
5757
+ const fields = [
5758
+ [node.name, 8],
5759
+ [node.entity.alias ?? node.entity.term, 6],
5760
+ [node.entity.synonyms, 4],
5761
+ [node.entity.description ?? node.entity.definition ?? node.entity.question, 2]
5762
+ ];
5763
+ return fields.reduce((sum, [value, weight]) => {
5764
+ const words = new Set(tokens(text(value)));
5765
+ return sum + [...query].filter((word) => words.has(word)).length * weight;
5766
+ }, 0);
5767
+ };
5768
+ 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);
5769
+ const edges = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
5770
+ const byName = /* @__PURE__ */ new Map();
5771
+ for (const node of nodes) byName.set(node.name, [...byName.get(node.name) ?? [], node.id]);
5772
+ const link = (a, b) => {
5773
+ if (a === b || !edges.has(b)) return;
5774
+ edges.get(a).add(b);
5775
+ edges.get(b).add(a);
5776
+ };
5777
+ const refKeys = /* @__PURE__ */ new Set([
5778
+ "table",
5779
+ "column",
5780
+ "columns",
5781
+ "references",
5782
+ "measure",
5783
+ "numerator",
5784
+ "denominator",
5785
+ "metrics",
5786
+ "measures",
5787
+ "glossary_refs",
5788
+ "left",
5789
+ "right",
5790
+ "from",
5791
+ "to",
5792
+ "on",
5793
+ "bindings",
5794
+ "scope",
5795
+ "targets",
5796
+ "tables",
5797
+ "grain",
5798
+ "identifiers",
5799
+ "default_time_dimension",
5800
+ "expression",
5801
+ "sql",
5802
+ "on_sql",
5803
+ "predicate",
5804
+ "type_config"
5805
+ ]);
5806
+ const refs = (value) => {
5807
+ if (typeof value === "string") return value.match(/[\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)?/gu) ?? [];
5808
+ if (Array.isArray(value)) return value.flatMap(refs);
5809
+ if (isObject(value)) return Object.entries(value).flatMap(([key, child]) => refKeys.has(key) ? refs(child) : []);
5810
+ return [];
5811
+ };
5812
+ for (const node of nodes) {
5813
+ if (node.kind === "column") link(node.id, `table:${node.name.split(".")[0]}`);
5814
+ for (const ref of refs(node.entity)) for (const id2 of byName.get(ref) ?? []) link(node.id, id2);
5815
+ }
5816
+ const chosen = new Map(ranked.map((node) => [node.id, node]));
5817
+ for (const node of ranked) {
5818
+ for (const id2 of [...edges.get(node.id)].sort()) {
5819
+ if (!chosen.has(id2)) {
5820
+ const adjacent = nodes.find((node2) => node2.id === id2);
5821
+ chosen.set(id2, { ...adjacent, score: 0, match: "adjacency" });
5822
+ }
5823
+ }
5824
+ }
5825
+ return { results: [...chosen.values()], k, lexical_matches: ranked.length, retrieval: "local_lexical" };
5826
+ }
5827
+
5828
+ // src/tools/query/semantic-model.ts
5829
+ var datamartId = z30.string().trim().min(1).max(256).describe("Datamart ID from list_datamarts.");
5830
+ var source = z30.string().min(1).max(2e6).describe("Complete proposed semantic model as YAML.");
5831
+ var entityKind = z30.enum(["table", "column", "measure", "metric", "join", "entity", "glossary", "saved_query", "instruction", "access_rule"]);
5832
+ var changeInputs = {
5833
+ source: source.optional(),
5834
+ yaml_patch: z30.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."),
5835
+ change: z30.object({
5836
+ operation: z30.enum(["upsert", "remove"]),
5837
+ kind: entityKind,
5838
+ name: z30.string().trim().min(1),
5839
+ table: z30.string().trim().min(1).optional().describe("Required for columns. Use the table name and an unqualified column name."),
5840
+ value: z30.record(z30.unknown()).optional().describe("Fields to merge for upsert. Omit for remove.")
5841
+ }).strict().optional().describe("One named entity change. Supply exactly one of source, yaml_patch or change.")
5842
+ };
5843
+ var expectedHash = z30.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.");
5844
+ function errorCode(error) {
5845
+ const nested = isObject(error.details) && isObject(error.details.error) ? error.details.error : void 0;
5846
+ return typeof nested?.code === "string" ? nested.code : error.code;
5847
+ }
5848
+ function failure(error) {
5849
+ if (error instanceof DatabrainApiError) {
5850
+ if (error.status === 409 || error.status === 422) {
5851
+ return toolError(stringify({
5852
+ status: error.status,
5853
+ code: errorCode(error),
5854
+ reason: error.message,
5855
+ details: error.details,
5856
+ recovery: error.status === 409 ? "Pull the current model and review a new proposal before applying." : "Correct the validation errors and propose the change again."
5857
+ }));
5858
+ }
5859
+ return error.toToolResponse();
5860
+ }
5861
+ return toolError(`Semantic model error: ${error instanceof Error ? error.message : String(error)}`);
5862
+ }
5863
+ async function candidate(api, id2, input) {
5864
+ if ([input.source, input.yaml_patch, input.change].filter((value) => value !== void 0).length !== 1) {
5865
+ throw new Error("Supply exactly one of source, yaml_patch or change.");
5866
+ }
5867
+ let stored;
5868
+ try {
5869
+ stored = await api.pull(id2);
5870
+ } catch (error) {
5871
+ if (!(error instanceof DatabrainApiError && error.status === 404 && errorCode(error) === "layer_not_found" && input.source !== void 0)) throw error;
5872
+ }
5873
+ const hash = stored?.content_hash ?? null;
5874
+ if (input.expected_content_hash !== void 0 && input.expected_content_hash !== hash) {
5875
+ throw new DatabrainApiError(409, "content_hash_mismatch", "The model changed since the proposal was reviewed.", { current_hash: hash });
5876
+ }
5877
+ return {
5878
+ source: input.source ?? changeSemanticSource(stored.source, input),
5879
+ expected_content_hash: input.expected_content_hash === void 0 ? hash : input.expected_content_hash
5880
+ };
5881
+ }
5882
+ async function proposal(api, id2, input) {
5883
+ const next = await candidate(api, id2, input);
5884
+ const validation = await api.validate(id2, next.source);
5885
+ let plan = null;
5886
+ let plan_note;
5887
+ if (validation.ok) {
5888
+ try {
5889
+ plan = await api.plan(id2, next.source, next.expected_content_hash);
5890
+ } catch (error) {
5891
+ if (!(error instanceof DatabrainApiError && [404, 405, 501].includes(error.status))) throw error;
5892
+ plan_note = "This Databrain API has no plan step. The validation above is the server check; apply runs it again.";
5893
+ }
5894
+ }
5895
+ return { ...next, validation, errors: validation.errors, plan, ...plan_note ? { plan_note } : {}, dry_run: true };
5896
+ }
5897
+ function registerSearchSemanticModel(server, client) {
5898
+ const api = createServiceBundle(client).semanticModelApi;
5899
+ server.tool(
5900
+ "search_semantic_model",
5901
+ "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.",
5902
+ { datamartId, question: z30.string().trim().min(1).max(1e4), k: z30.number().int().min(1).max(100).default(10) },
5903
+ READ_ONLY,
5904
+ async ({ datamartId: datamartId2, question, k }) => {
5905
+ try {
5906
+ try {
5907
+ return toolSuccess(stringify(await api.retrieve(datamartId2, question, k)));
5908
+ } catch (error) {
5909
+ if (!(error instanceof DatabrainApiError && [404, 405, 501].includes(error.status))) throw error;
5910
+ if (["datamart_not_found", "layer_not_found", "forbidden", "unauthorized"].includes(errorCode(error))) throw error;
5911
+ }
5912
+ const stored = await api.pull(datamartId2);
5913
+ return toolSuccess(stringify({ ...retrieveSemanticSource(stored.source, question, k), content_hash: stored.content_hash }));
5914
+ } catch (error) {
5915
+ return failure(error);
5916
+ }
5917
+ }
5918
+ );
5919
+ }
5920
+ function registerGetSemanticEntity(server, client) {
5921
+ const api = createServiceBundle(client).semanticModelApi;
5922
+ server.tool(
5923
+ "get_semantic_entity",
5924
+ "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.",
5925
+ { datamartId, kind: entityKind, name: z30.string().trim().min(1) },
5926
+ READ_ONLY,
5927
+ async ({ datamartId: datamartId2, kind, name }) => {
5928
+ try {
5929
+ const stored = await api.pull(datamartId2);
5930
+ const found = semanticEntities(parseSemanticSource(stored.source)).filter((entity) => entity.kind === kind && entity.name === name);
5931
+ if (found.length !== 1) return toolError(`Expected one ${kind} named ${name}; found ${found.length}. Use search_semantic_model to find its exact name.`);
5932
+ return toolSuccess(stringify({ ...found[0], content_hash: stored.content_hash }));
5933
+ } catch (error) {
5934
+ return failure(error);
5935
+ }
5936
+ }
5937
+ );
5938
+ }
5939
+ function registerProposeSemanticChange(server, client) {
5940
+ const api = createServiceBundle(client).semanticModelApi;
5941
+ server.tool(
5942
+ "propose_semantic_change",
5943
+ "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.",
5944
+ { datamartId, ...changeInputs },
5945
+ READ_ONLY,
5946
+ async (params) => {
5947
+ try {
5948
+ return toolSuccess(stringify(await proposal(api, params.datamartId, params)));
5949
+ } catch (error) {
5950
+ return failure(error);
5951
+ }
5952
+ }
5953
+ );
5954
+ }
5955
+ function registerApplySemanticChange(server, client) {
5956
+ const api = createServiceBundle(client).semanticModelApi;
5957
+ server.tool(
5958
+ "apply_semantic_change",
5959
+ "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.",
5960
+ {
5961
+ datamartId,
5962
+ ...changeInputs,
5963
+ dry_run: z30.boolean().default(true),
5964
+ expected_content_hash: expectedHash,
5965
+ confirm: z30.string().optional().describe("User supplied APPLY_TO_PRODUCTION after reviewing the plan. Never invent confirmation.")
5966
+ },
5967
+ DESTRUCTIVE,
5968
+ async (params) => {
5969
+ try {
5970
+ if (params.dry_run !== false) return toolSuccess(stringify(await proposal(api, params.datamartId, params)));
5971
+ const refusal = validateProductionApply(params);
5972
+ if (refusal) return toolError(refusal);
5973
+ 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.");
5974
+ const next = await candidate(api, params.datamartId, params);
5975
+ const validation = await api.validate(params.datamartId, next.source);
5976
+ if (!validation.ok) return failure(new DatabrainApiError(422, "validation_failed", "Apply refused because the model does not validate.", { validation }));
5977
+ return toolSuccess(stringify(await api.apply(params.datamartId, next.source, next.expected_content_hash)));
5978
+ } catch (error) {
5979
+ return failure(error);
5980
+ }
5981
+ }
5982
+ );
5983
+ }
5984
+ function registerEvalSemanticModel(server, client) {
5985
+ const api = createServiceBundle(client).semanticModelApi;
5986
+ server.tool(
5987
+ "eval_semantic_model",
5988
+ "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.",
5989
+ { datamartId, source: source.optional(), questions: z30.array(z30.union([z30.string().trim().min(1), z30.object({
5990
+ name: z30.string().optional(),
5991
+ question: z30.string().min(1),
5992
+ sql: z30.string().optional(),
5993
+ expected_columns: z30.array(z30.string()).optional(),
5994
+ expected_empty: z30.boolean().optional()
5995
+ }).strict()])).max(100).optional() },
5996
+ READ_ONLY,
5997
+ async ({ datamartId: datamartId2, source: source2, questions }) => {
5998
+ const shaped = questions?.map((entry) => typeof entry === "string" ? { question: entry } : entry);
5999
+ try {
6000
+ return toolSuccess(stringify(await api.evaluate(datamartId2, { source: source2, questions: shaped })));
6001
+ } catch (error) {
6002
+ return failure(error);
5608
6003
  }
5609
6004
  }
5610
6005
  );
5611
6006
  }
5612
6007
 
5613
6008
  // 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(
6009
+ import { z as z31 } from "zod";
6010
+ var columnSchema2 = z31.object({
6011
+ name: z31.string().describe("Column name (must exist in the datamart)"),
6012
+ description: z31.string().optional().describe("Business-friendly description of what this column represents"),
6013
+ synonyms: z31.array(z31.string()).optional().describe(
5619
6014
  'Alternative names users might use for this column (e.g. ["revenue", "sales amount"])'
5620
6015
  ),
5621
- miscellaneousInfo: z27.string().optional().describe("Additional context such as valid values, units, or caveats"),
5622
- columnType: z27.enum([
6016
+ miscellaneousInfo: z31.string().optional().describe("Additional context such as valid values, units, or caveats"),
6017
+ columnType: z31.enum([
5623
6018
  "String",
5624
6019
  "Long String",
5625
6020
  "String (Custom)",
@@ -5630,18 +6025,25 @@ var columnSchema2 = z27.object({
5630
6025
  "Identifier",
5631
6026
  "Number",
5632
6027
  "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.')
6028
+ ]).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.'),
6029
+ // The public PUT accepts these three as well (docs: Update Semantic Layer).
6030
+ // They were missing here, and zod strips unknown keys, so a caller who set
6031
+ // `isIdentifier: true` got a success message and a row with `false`.
6032
+ columnTypeConfig: z31.union([z31.record(z31.string(), z31.string()), z31.string(), z31.object({ lowerLimit: z31.number(), upperLimit: z31.number() }), z31.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."),
6033
+ isIdentifier: z31.boolean().optional().describe("Mark the column as an identifier (primary or foreign key)."),
6034
+ isNotIndexed: z31.boolean().optional().describe("Exclude the column from AI indexing.")
5634
6035
  });
5635
- var tableSchema2 = z27.object({
5636
- name: z27.string().describe("Table name (must exist in the datamart)"),
5637
- description: z27.string().optional().describe(
6036
+ var tableSchema2 = z31.object({
6037
+ name: z31.string().describe("Table name (must exist in the datamart)"),
6038
+ schemaName: z31.string().optional().describe("Schema the table lives in, when the datamart has the same table name in more than one schema."),
6039
+ description: z31.string().optional().describe(
5638
6040
  "Business-friendly description of what this table represents"
5639
6041
  ),
5640
- synonyms: z27.array(z27.string()).optional().describe("Alternative names for this table"),
5641
- miscellaneousInfo: z27.string().optional().describe(
6042
+ synonyms: z31.array(z31.string()).optional().describe("Alternative names for this table"),
6043
+ miscellaneousInfo: z31.string().optional().describe(
5642
6044
  "Additional context such as how the table relates to other tables"
5643
6045
  ),
5644
- columns: z27.array(columnSchema2).optional().describe("Column-level semantic definitions to update")
6046
+ columns: z31.array(columnSchema2).optional().describe("Column-level semantic definitions to update")
5645
6047
  });
5646
6048
  function columnKey(tableName, columnName) {
5647
6049
  return `${tableName.toLowerCase()}\0${columnName.toLowerCase()}`;
@@ -5673,6 +6075,7 @@ function normalizeTablesForApi(tables, datatypesByColumn) {
5673
6075
  if (!tables) return void 0;
5674
6076
  return tables.map((table) => {
5675
6077
  const normalized = { name: table.name };
6078
+ if (table.schemaName !== void 0) normalized.schemaName = table.schemaName;
5676
6079
  if (table.description !== void 0) normalized.description = table.description;
5677
6080
  if (table.synonyms !== void 0) normalized.synonyms = table.synonyms;
5678
6081
  if (table.miscellaneousInfo !== void 0) {
@@ -5696,6 +6099,15 @@ function normalizeTablesForApi(tables, datatypesByColumn) {
5696
6099
  normalizedColumn.columnType = column.columnType;
5697
6100
  }
5698
6101
  }
6102
+ if (column.columnTypeConfig !== void 0) {
6103
+ normalizedColumn.columnTypeConfig = column.columnTypeConfig;
6104
+ }
6105
+ if (column.isIdentifier !== void 0) {
6106
+ normalizedColumn.isIdentifier = column.isIdentifier;
6107
+ }
6108
+ if (column.isNotIndexed !== void 0) {
6109
+ normalizedColumn.isNotIndexed = column.isNotIndexed;
6110
+ }
5699
6111
  return normalizedColumn;
5700
6112
  });
5701
6113
  }
@@ -5708,13 +6120,13 @@ function registerUpdateSemanticLayer(server, client) {
5708
6120
  "update_semantic_layer",
5709
6121
  "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
6122
  {
5711
- datamartName: z27.string().optional().describe(
6123
+ datamartName: z31.string().optional().describe(
5712
6124
  "Name of the datamart to update. Omit to list available datamarts."
5713
6125
  ),
5714
- tables: z27.array(tableSchema2).optional().describe(
6126
+ tables: z31.array(tableSchema2).optional().describe(
5715
6127
  "Table and column definitions to update. Only include tables/columns you want to change."
5716
6128
  ),
5717
- feedback: z27.string().optional().describe(
6129
+ feedback: z31.string().optional().describe(
5718
6130
  'General feedback or context about the datamart (e.g. "This datamart contains e-commerce data for the US market").'
5719
6131
  )
5720
6132
  },
@@ -5776,7 +6188,7 @@ You can now:
5776
6188
  }
5777
6189
 
5778
6190
  // src/tools/query/semantic-generation.ts
5779
- import { z as z28 } from "zod";
6191
+ import { z as z32 } from "zod";
5780
6192
  function nonEmptyString(value) {
5781
6193
  return typeof value === "string" && value.trim().length > 0 ? value : null;
5782
6194
  }
@@ -5808,9 +6220,9 @@ async function resolveDatamartFromWorkspace(params) {
5808
6220
  });
5809
6221
  if (resolution.kind !== "resolved") return { ok: false, response: resolution.response };
5810
6222
  const selected = resolution.selected;
5811
- const datamartId = workspaceDatamartIdFrom(selected);
6223
+ const datamartId2 = workspaceDatamartIdFrom(selected);
5812
6224
  const workspaceDatamartName = workspaceDatamartNameFrom(selected);
5813
- if (!datamartId) {
6225
+ if (!datamartId2) {
5814
6226
  return {
5815
6227
  ok: false,
5816
6228
  response: toolError(
@@ -5828,40 +6240,11 @@ async function resolveDatamartFromWorkspace(params) {
5828
6240
  }
5829
6241
  return {
5830
6242
  ok: true,
5831
- datamartName: workspaceDatamartName || params.datamartName || datamartId,
5832
- datamartId,
6243
+ datamartName: workspaceDatamartName || params.datamartName || datamartId2,
6244
+ datamartId: datamartId2,
5833
6245
  workspaceName: resolution.name
5834
6246
  };
5835
6247
  }
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
6248
  async function resolveDatamart(params) {
5866
6249
  if (params.datamartId) {
5867
6250
  return {
@@ -5890,39 +6273,19 @@ async function resolveDatamart(params) {
5890
6273
  toolName: params.toolName
5891
6274
  });
5892
6275
  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
- }
6276
+ const datamartId2 = datamartIdFrom(resolution.selected);
6277
+ if (!datamartId2) {
5915
6278
  return {
5916
6279
  ok: false,
5917
6280
  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.`
6281
+ `Datamart "${resolution.name}" did not include an ID in the datamart list. Refresh the list and try again, or pass datamartId directly.`
5919
6282
  )
5920
6283
  };
5921
6284
  }
5922
6285
  return {
5923
6286
  ok: true,
5924
6287
  datamartName: resolution.name,
5925
- datamartId
6288
+ datamartId: datamartId2
5926
6289
  };
5927
6290
  }
5928
6291
  function registerStartSemanticLayerGeneration(server, client) {
@@ -5931,9 +6294,9 @@ function registerStartSemanticLayerGeneration(server, client) {
5931
6294
  "start_semantic_layer_generation",
5932
6295
  "Start backend semantic-layer auto-generation for a datamart. Returns an async job id to poll.",
5933
6296
  {
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.")
6297
+ datamartName: z32.string().optional().describe("Datamart name. Omit to list available datamarts"),
6298
+ workspaceName: z32.string().optional().describe("Workspace linked to the datamart. Use when datamartName resolution needs a dataMartId bridge."),
6299
+ datamartId: z32.string().optional().describe("Datamart UUID. Use only when already known.")
5937
6300
  },
5938
6301
  WRITE_CREATE,
5939
6302
  async (params) => {
@@ -5975,9 +6338,9 @@ function registerGetSemanticLayerGenerationStatus(server, client) {
5975
6338
  "get_semantic_layer_generation_status",
5976
6339
  "Get semantic-layer auto-generation job status for a datamart.",
5977
6340
  {
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.")
6341
+ datamartName: z32.string().optional().describe("Datamart name. Omit to list available datamarts"),
6342
+ workspaceName: z32.string().optional().describe("Workspace linked to the datamart. Use when datamartName resolution needs a dataMartId bridge."),
6343
+ datamartId: z32.string().optional().describe("Datamart UUID. Use only when already known.")
5981
6344
  },
5982
6345
  READ_ONLY,
5983
6346
  async (params) => {
@@ -6014,17 +6377,23 @@ function registerGetSemanticLayerGenerationStatus(server, client) {
6014
6377
  }
6015
6378
 
6016
6379
  // src/tools/widgets/list-widgets.ts
6017
- import { z as z29 } from "zod";
6380
+ import { z as z33 } from "zod";
6381
+ function widgetReferenceId(widget) {
6382
+ for (const candidate2 of [widget.externalMetricId, widget.id, widget._id, widget.metricId]) {
6383
+ if (typeof candidate2 === "string" && candidate2.trim().length > 0) return candidate2.trim();
6384
+ }
6385
+ return void 0;
6386
+ }
6018
6387
  function registerListWidgets(server, client) {
6019
6388
  const services = createServiceBundle(client);
6020
6389
  const tool = server.tool(
6021
6390
  "list_widgets",
6022
6391
  "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
6392
  {
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.")
6393
+ clientId: z33.string().describe("Client/tenant identifier for this dashboard"),
6394
+ dashboardName: z33.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6395
+ workspaceName: z33.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6396
+ embedId: z33.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier.")
6028
6397
  },
6029
6398
  READ_ONLY,
6030
6399
  async (params) => {
@@ -6043,17 +6412,18 @@ function registerListWidgets(server, client) {
6043
6412
  );
6044
6413
  if (hasChartApps(server)) {
6045
6414
  const visible = (Array.isArray(widgets) ? widgets : []).slice(0, 50);
6046
- return chartResult(`${visible.length} widgets available in the first dashboard page.`, {
6415
+ const references = visible.flatMap((w) => {
6416
+ const widgetId = widgetReferenceId(w);
6417
+ return widgetId ? [{
6418
+ name: typeof w.name === "string" ? w.name.slice(0, 500) : "Untitled widget",
6419
+ reference: { widgetId, embedId, clientId: params.clientId }
6420
+ }] : [];
6421
+ });
6422
+ return chartResult(`${references.length} widgets available in the first dashboard page.`, {
6047
6423
  kind: "dashboard",
6048
6424
  title: "Dashboard",
6049
6425
  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
- }),
6426
+ widgets: references,
6057
6427
  description: Array.isArray(widgets) && widgets.length > 50 ? "Showing the first 50 widgets." : ""
6058
6428
  });
6059
6429
  }
@@ -6087,29 +6457,29 @@ function registerListWidgets(server, client) {
6087
6457
  }
6088
6458
 
6089
6459
  // src/tools/widgets/add-widget.ts
6090
- import { z as z30 } from "zod";
6460
+ import { z as z34 } from "zod";
6091
6461
  function registerAddWidget(server, client) {
6092
6462
  const services = createServiceBundle(client);
6093
6463
  server.tool(
6094
6464
  "add_widget",
6095
6465
  "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
6466
  {
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.")
6467
+ clientId: z34.string().describe("Client/tenant identifier (widget is created in their scope)"),
6468
+ dashboardName: z34.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6469
+ workspaceName: z34.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6470
+ embedId: z34.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6471
+ dataAppName: z34.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
6472
+ templateMetricId: z34.string().optional().describe("Source widget metricId to clone from. Copies query, chartOptions, and datasource fields; caller can override via other params."),
6473
+ name: z34.string().optional().describe("Widget name (overrides template)"),
6474
+ metricId: z34.string().optional().describe("Unique slug for the new widget (auto-generated if omitted)"),
6475
+ query: z34.string().optional().describe("SQL query body (overrides template)"),
6476
+ chartOptions: z34.record(z34.unknown()).optional().describe("Chart config JSON (chart type, colors, legend, axes). Overrides template."),
6477
+ description: z34.string().optional(),
6478
+ companyIntegrationId: z34.string().optional().describe("Datasource UUID override. Auto-resolved from `embedId` or sole active datasource; leave blank unless the user explicitly picked one by ID."),
6479
+ integrationName: z34.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.'),
6480
+ isPublished: z34.boolean().optional().describe("Publish immediately (default true)"),
6481
+ body: z34.record(z34.unknown()).optional().describe("Raw createMetric body passthrough. If provided, used as-is (after defaults). Overrides all other params."),
6482
+ widgets: z34.array(z34.record(z34.unknown())).min(1).max(50).optional().describe("Batch of raw createMetric bodies. Dashboard attachment is added to each body.")
6113
6483
  },
6114
6484
  WRITE_CREATE,
6115
6485
  async (params) => {
@@ -6257,20 +6627,20 @@ Use \`list_widgets\` to verify, or \`update_widget\`/\`remove_widget\` to adjust
6257
6627
  }
6258
6628
 
6259
6629
  // src/tools/widgets/update-widget.ts
6260
- import { z as z31 } from "zod";
6630
+ import { z as z35 } from "zod";
6261
6631
  function registerUpdateWidget(server, client) {
6262
6632
  const services = createServiceBundle(client);
6263
6633
  server.tool(
6264
6634
  "update_widget",
6265
6635
  "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
6636
  {
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.")
6637
+ externalMetricId: z35.string().describe("Internal uuid of the widget (externalMetrics.id). Not the user-facing metricId slug."),
6638
+ clientId: z35.string().describe("Client/tenant identifier"),
6639
+ dashboardName: z35.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6640
+ workspaceName: z35.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6641
+ embedId: z35.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6642
+ dataAppName: z35.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank."),
6643
+ set: z35.record(z35.unknown()).describe("Partial patch. Keys can include: name, query, chartOptions, description, isPublished, isLive, selectedGroupBy, drillDownSettings, limit, etc.")
6274
6644
  },
6275
6645
  WRITE_IDEMPOTENT,
6276
6646
  async (params) => {
@@ -6333,19 +6703,19 @@ Use \`list_widgets\` to verify.`
6333
6703
  }
6334
6704
 
6335
6705
  // src/tools/widgets/remove-widget.ts
6336
- import { z as z32 } from "zod";
6706
+ import { z as z36 } from "zod";
6337
6707
  function registerRemoveWidget(server, client) {
6338
6708
  const services = createServiceBundle(client);
6339
6709
  server.tool(
6340
6710
  "remove_widget",
6341
6711
  "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
6712
  {
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.")
6713
+ externalMetricId: z36.string().describe("Internal uuid of the widget (externalMetrics.id)"),
6714
+ clientId: z36.string().describe("Client/tenant identifier \u2014 the widget will be hidden for this client only"),
6715
+ dashboardName: z36.string().optional().describe("Dashboard name (non-technical). Preferred input \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6716
+ workspaceName: z36.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6717
+ embedId: z36.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6718
+ dataAppName: z36.string().optional().describe("Technical override. Auto-resolved from the dashboard; non-technical callers should leave this blank.")
6349
6719
  },
6350
6720
  DESTRUCTIVE,
6351
6721
  async (params) => {
@@ -6401,21 +6771,21 @@ Note: this is a per-client soft delete. The widget remains available for other c
6401
6771
  }
6402
6772
 
6403
6773
  // src/tools/widgets/generate-widget.ts
6404
- import { z as z33 } from "zod";
6774
+ import { z as z37 } from "zod";
6405
6775
  function registerGenerateWidget(server, client) {
6406
6776
  const services = createServiceBundle(client);
6407
6777
  const tool = server.tool(
6408
6778
  "generate_widget",
6409
6779
  "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
6780
  {
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.")
6781
+ clientId: z37.string().describe("Client/tenant identifier"),
6782
+ userInputs: z37.record(z37.unknown()).describe('Prompt + context object. Typical shape: { prompt: "calls per day over last 30 days", schema: {...}, examples: [...] }. Backend decides which keys it honors.'),
6783
+ dashboardName: z37.string().optional().describe("Dashboard name (non-technical). Preferred input for CS/CX flows \u2014 pass this with optional `workspaceName` instead of `embedId`."),
6784
+ workspaceName: z37.string().optional().describe("Workspace name (non-technical). Only needed if `dashboardName` is ambiguous across workspaces."),
6785
+ embedId: z37.string().optional().describe("Technical alternative to `dashboardName`. Pass only if you already have the embed identifier."),
6786
+ dataAppName: z37.string().optional().describe("Technical override. Auto-resolved from the embed; non-technical callers should leave this blank."),
6787
+ integrationId: z37.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."),
6788
+ integrationName: z37.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
6789
  },
6420
6790
  READ_ONLY,
6421
6791
  async (params) => {
@@ -6500,9 +6870,9 @@ function registerGenerateWidget(server, client) {
6500
6870
  }
6501
6871
 
6502
6872
  // 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");
6873
+ import { z as z38 } from "zod";
6874
+ var modeSchema = z38.enum(["dry_run", "apply"]).default("dry_run");
6875
+ var conflictStrategySchema = z38.enum(["fail", "update", "create_new"]).default("fail");
6506
6876
  function makePlanText(plan) {
6507
6877
  return JSON.stringify(plan, null, 2);
6508
6878
  }
@@ -6519,22 +6889,22 @@ function registerCreateOrUpdateMetricFromQuery(server, client) {
6519
6889
  "[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
6890
  {
6521
6891
  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}`),
6892
+ confirm: z38.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
6523
6893
  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")
6894
+ clientId: z38.string().describe("Client/tenant identifier for the dashboard widget scope"),
6895
+ workspaceName: z38.string().describe("Target workspace name"),
6896
+ dashboardName: z38.string().optional().describe("Dashboard name to attach to"),
6897
+ embedId: z38.string().optional().describe("Technical embed id alternative to dashboardName"),
6898
+ dataAppName: z38.string().optional().describe("Optional data app override"),
6899
+ name: z38.string().describe("Metric/widget display name"),
6900
+ metricId: z38.string().describe("Stable metric slug to create or update"),
6901
+ query: z38.string().describe("SQL query already validated against the warehouse"),
6902
+ description: z38.string().optional().describe("Metric description"),
6903
+ chartOptions: z38.record(z38.unknown()).optional().describe("Chart configuration JSON"),
6904
+ companyIntegrationId: z38.string().optional().describe("Datasource UUID override"),
6905
+ integrationName: z38.string().optional().describe("Datasource display name override"),
6906
+ isPublished: z38.boolean().optional().describe("Publish after apply. Defaults to false for production safety"),
6907
+ requestedBy: z38.string().optional().describe("Human operator/request context to store on the metric when supported")
6538
6908
  },
6539
6909
  WRITE_IDEMPOTENT,
6540
6910
  async (params) => {
@@ -6694,8 +7064,8 @@ function registerCreateOrUpdateMetricFromQuery(server, client) {
6694
7064
  }
6695
7065
 
6696
7066
  // 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");
7067
+ import { z as z39 } from "zod";
7068
+ var modeSchema2 = z39.enum(["dry_run", "apply"]).default("dry_run");
6699
7069
  function registerCreateWorkspaceMetric(server, client) {
6700
7070
  server.tool(
6701
7071
  "create_workspace_metric",
@@ -6704,16 +7074,18 @@ function registerCreateWorkspaceMetric(server, client) {
6704
7074
  mode: modeSchema2.describe(
6705
7075
  "dry_run validates the SQL only (no write); apply creates or updates the metric after confirmation"
6706
7076
  ),
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(
7077
+ confirm: z39.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
7078
+ workspaceName: z39.string().describe("Target workspace name"),
7079
+ name: z39.string().describe("Metric display name"),
7080
+ query: z39.string().describe(
7081
+ "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)."
7082
+ ),
7083
+ metricId: z39.string().optional().describe(
6712
7084
  "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
7085
  ),
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(
7086
+ description: z39.string().optional().describe("Metric description"),
7087
+ datasourceId: z39.string().optional().describe("Datasource UUID override; defaults to the workspace datasource"),
7088
+ isPublished: z39.boolean().optional().describe(
6717
7089
  "Publish after apply. Defaults to false. Applies to creates; on updates the existing publish state is preserved."
6718
7090
  )
6719
7091
  },
@@ -6769,25 +7141,25 @@ function registerCreateWorkspaceMetric(server, client) {
6769
7141
  }
6770
7142
 
6771
7143
  // src/tools/widgets/validate-metric-spec.ts
6772
- import { z as z36 } from "zod";
7144
+ import { z as z40 } from "zod";
6773
7145
  function registerValidateMetricSpec(server, client) {
6774
7146
  const services = createServiceBundle(client);
6775
7147
  server.tool(
6776
7148
  "validate_metric_spec",
6777
7149
  "Validate a production metric spec from SQL without saving it. Resolves dashboard, data app, datasource, conflicts, and required fields.",
6778
7150
  {
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")
7151
+ clientId: z40.string().describe("Client/tenant identifier used to validate dashboard widget scope"),
7152
+ workspaceName: z40.string().describe("Target production-backed workspace name"),
7153
+ dashboardName: z40.string().optional().describe("Dashboard name to attach to"),
7154
+ embedId: z40.string().optional().describe("Technical embed id alternative to dashboardName"),
7155
+ dataAppName: z40.string().optional().describe("Optional data app override"),
7156
+ name: z40.string().describe("Metric/widget display name"),
7157
+ metricId: z40.string().describe("Stable metric slug to create or update"),
7158
+ query: z40.string().describe("SQL query already validated against the warehouse"),
7159
+ description: z40.string().optional().describe("Metric description"),
7160
+ chartOptions: z40.record(z40.unknown()).optional().describe("Chart configuration JSON"),
7161
+ companyIntegrationId: z40.string().optional().describe("Datasource UUID override"),
7162
+ integrationName: z40.string().optional().describe("Datasource display name override")
6791
7163
  },
6792
7164
  READ_ONLY,
6793
7165
  async (params) => {
@@ -6878,20 +7250,20 @@ function registerValidateMetricSpec(server, client) {
6878
7250
  }
6879
7251
 
6880
7252
  // src/tools/reports/list-scheduled-reports.ts
6881
- import { z as z37 } from "zod";
7253
+ import { z as z41 } from "zod";
6882
7254
  function registerListScheduledReports(server, client) {
6883
7255
  const services = createServiceBundle(client);
6884
7256
  server.tool(
6885
7257
  "list_scheduled_reports",
6886
7258
  "List configured scheduled reports for a specific client/dashboard embed. Shows schedule metadata, not delivery success history.",
6887
7259
  {
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")
7260
+ clientId: z41.string().describe("Client/tenant identifier. Required by the current backend API for non-empty results"),
7261
+ dashboardName: z41.string().optional().describe("Dashboard name to inspect"),
7262
+ workspaceName: z41.string().optional().describe("Workspace name if dashboardName is ambiguous"),
7263
+ embedId: z41.string().optional().describe("Technical embed id alternative to dashboardName"),
7264
+ userIdentifier: z41.string().optional().describe("Optional creator/user filter"),
7265
+ pageNumber: z41.number().int().positive().optional().describe("Page number when pagination is enabled"),
7266
+ isPagination: z41.boolean().optional().describe("Enable backend pagination")
6895
7267
  },
6896
7268
  READ_ONLY,
6897
7269
  async (params) => {
@@ -6965,7 +7337,7 @@ function registerWhoami(server, client) {
6965
7337
  status: "authenticated",
6966
7338
  activeBearer: "service-token",
6967
7339
  ...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."
7340
+ 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
7341
  },
6970
7342
  null,
6971
7343
  2
@@ -7000,6 +7372,400 @@ function registerWhoami(server, client) {
7000
7372
  );
7001
7373
  }
7002
7374
 
7375
+ // src/tools/custom-chart/scaffold.ts
7376
+ import { transformSync } from "esbuild";
7377
+ import { z as z42 } from "zod";
7378
+ function registerCustomChartScaffold(server, _client) {
7379
+ server.tool(
7380
+ "custom_chart_scaffold",
7381
+ "Return the files of a new custom chart project (manifest.json, chart.ts, chart.css, icon.svg, sample data, guide). Nothing is persisted. For a hosted AI app, edit the returned draft and pass chartTs to validate, preview and upload without local files.",
7382
+ {
7383
+ key: z42.string().describe("Chart key, becomes chart type custom:<key>. Lowercase letters, digits, _ and -, 64 max."),
7384
+ name: z42.string().describe("Display name shown in the chart picker, 1 to 256 characters."),
7385
+ description: z42.string().optional().describe("Optional description, up to 500 characters."),
7386
+ slots: z42.array(slotInputSchema).optional().describe("Data slots. Defaults to category (dimension), value (measure), target (optional measure)."),
7387
+ options: z42.array(optionInputSchema).optional().describe("Chart options. Defaults to the template options (labels, subtitle, radius, colour, sort).")
7388
+ },
7389
+ READ_ONLY,
7390
+ async (params) => {
7391
+ try {
7392
+ const result = scaffoldProject({
7393
+ key: params.key,
7394
+ name: params.name,
7395
+ description: params.description,
7396
+ slots: params.slots,
7397
+ options: params.options
7398
+ });
7399
+ if (!result.ok) {
7400
+ return toolError(`The requested manifest is not valid:
7401
+ ${formatProblems(result.problems)}`);
7402
+ }
7403
+ return toolSuccess(
7404
+ JSON.stringify(
7405
+ {
7406
+ status: "scaffold_only",
7407
+ key: params.key,
7408
+ chartType: `custom:${params.key}`,
7409
+ files: result.files,
7410
+ draft: {
7411
+ code: transformSync(result.files["chart.ts"], { loader: "ts", format: "esm" }).code.replace(/export\s*\{[^}]*\};?\s*$/, ""),
7412
+ css: result.files["chart.css"],
7413
+ manifest: JSON.parse(result.files["manifest.json"]),
7414
+ sampleData: (() => {
7415
+ const sample = JSON.parse(result.files["sample-data.json"]);
7416
+ return { columns: sample.columns.map(({ id: id2, label, type, slotId }) => ({ id: id2, label, type, ...slotId ? { slotId } : {} })), rows: sample.rows };
7417
+ })(),
7418
+ name: params.name,
7419
+ description: params.description ?? "",
7420
+ iconSvg: result.files["icon.svg"]
7421
+ },
7422
+ nextStep: "Hosted: pass draft to custom_chart_edit with a description and workspaceId. Then validate/preview/upload using chartTs=draft.code, manifest, bundleCss=draft.css and iconSvg. Upload saves a version; publishing requires explicit confirmation. Local project: " + result.nextStep
7423
+ },
7424
+ null,
7425
+ 2
7426
+ )
7427
+ );
7428
+ } catch (err) {
7429
+ return toolError(`Unexpected error: ${err.message}`);
7430
+ }
7431
+ }
7432
+ );
7433
+ }
7434
+
7435
+ // src/tools/custom-chart/validate.ts
7436
+ function registerCustomChartValidate(server, client) {
7437
+ server.tool(
7438
+ "custom_chart_validate",
7439
+ "Validate a custom chart manifest and bundle with the local rules and, when a service token is configured, the server rules. Never persists anything.",
7440
+ bundleInputShape,
7441
+ READ_ONLY,
7442
+ async (params) => {
7443
+ try {
7444
+ const resolved = await resolveBundleInput(params, { build: true });
7445
+ if (resolved.error) return toolError(resolved.error);
7446
+ if (!resolved.bundle) {
7447
+ return toolSuccess(JSON.stringify({ ok: false, problems: resolved.problems, source: resolved.source }, null, 2));
7448
+ }
7449
+ const { bundle } = resolved;
7450
+ const problems = validateBundle(bundle);
7451
+ const complete = isCompleteBundle(bundle);
7452
+ const result = {
7453
+ ok: problems.length === 0,
7454
+ problems,
7455
+ source: resolved.source,
7456
+ contentHash: complete && problems.length === 0 ? contentHash(bundle) : void 0,
7457
+ sizeBytes: complete && problems.length === 0 ? bundleSizeBytes(bundle) : void 0
7458
+ };
7459
+ if (!complete) {
7460
+ result.note = "Only the manifest was checked. Pass bundleJs and iconSvg (or projectDir) to check the whole package.";
7461
+ } else if (problems.length === 0) {
7462
+ const remote = await remoteValidate(client, bundle);
7463
+ if (remote.status === "ok") {
7464
+ result.remote = remote.result;
7465
+ if (!remote.result.ok) {
7466
+ result.ok = false;
7467
+ result.problems = remote.result.problems;
7468
+ }
7469
+ if (remote.result.contentHash) result.contentHash = remote.result.contentHash;
7470
+ if (typeof remote.result.sizeBytes === "number") result.sizeBytes = remote.result.sizeBytes;
7471
+ } else if (remote.status === "skipped") {
7472
+ result.remote = { status: "skipped", reason: remote.reason };
7473
+ } else {
7474
+ result.remote = { status: "failed", reason: remote.reason };
7475
+ if (remote.problems.length > 0) {
7476
+ result.ok = false;
7477
+ result.problems = remote.problems;
7478
+ }
7479
+ }
7480
+ }
7481
+ 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.";
7482
+ return toolSuccess(JSON.stringify(result, null, 2));
7483
+ } catch (err) {
7484
+ return toolError(`Unexpected error: ${err.message}`);
7485
+ }
7486
+ }
7487
+ );
7488
+ }
7489
+
7490
+ // src/tools/custom-chart/upload.ts
7491
+ import { z as z43 } from "zod";
7492
+ function registerCustomChartUpload(server, client) {
7493
+ const api = createCustomChartsApi(client);
7494
+ server.tool(
7495
+ "custom_chart_upload",
7496
+ "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.",
7497
+ {
7498
+ mode: z43.enum(["dry_run", "apply"]).default("dry_run").describe("dry_run validates and returns the plan; apply uploads."),
7499
+ confirm: z43.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`),
7500
+ publish: z43.boolean().default(false).describe("After a successful apply, publish the uploaded version so every builder can use it."),
7501
+ name: z43.string().optional().describe("Display name override. Defaults to manifest.name."),
7502
+ description: z43.string().optional().describe("Description override. Defaults to manifest.description."),
7503
+ ...bundleInputShape
7504
+ },
7505
+ WRITE_CREATE,
7506
+ async (params) => {
7507
+ try {
7508
+ const mode = params.mode || "dry_run";
7509
+ const resolved = await resolveBundleInput(params, { build: false });
7510
+ if (resolved.error) return toolError(resolved.error);
7511
+ if (!resolved.bundle) {
7512
+ return toolError(`The project could not be read:
7513
+ ${formatProblems(resolved.problems)}`);
7514
+ }
7515
+ const { bundle } = resolved;
7516
+ if (!isCompleteBundle(bundle)) {
7517
+ 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).");
7518
+ }
7519
+ const problems = validateBundle(bundle);
7520
+ if (problems.length > 0) {
7521
+ return toolError(`The bundle is not valid. Fix these and try again:
7522
+ ${formatProblems(problems)}`);
7523
+ }
7524
+ const manifest = bundle.manifest;
7525
+ const key = manifest.id;
7526
+ const body = {
7527
+ manifest,
7528
+ bundleJs: bundle.bundleJs,
7529
+ bundleCss: bundle.bundleCss,
7530
+ iconSvg: bundle.iconSvg,
7531
+ ...params.name ? { name: params.name } : {},
7532
+ ...params.description ? { description: params.description } : {}
7533
+ };
7534
+ const localHash = contentHash(bundle);
7535
+ const localSize = bundleSizeBytes(bundle);
7536
+ if (mode === "dry_run") {
7537
+ const remote = await remoteValidate(client, bundle);
7538
+ let existing;
7539
+ let planNote;
7540
+ try {
7541
+ existing = (await api.list()).find((chart) => chart.key === key);
7542
+ } catch (err) {
7543
+ 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}`;
7544
+ }
7545
+ const plan = planUpload(key, existing);
7546
+ const remoteOk = remote.status !== "ok" || remote.result.ok;
7547
+ return toolSuccess(
7548
+ JSON.stringify(
7549
+ {
7550
+ status: "dry_run_only",
7551
+ safety: genericProductionSafetyNote(),
7552
+ ok: remoteOk,
7553
+ key,
7554
+ chartType: `custom:${key}`,
7555
+ contentHash: remote.status === "ok" && remote.result.contentHash ? remote.result.contentHash : localHash,
7556
+ sizeBytes: remote.status === "ok" && typeof remote.result.sizeBytes === "number" ? remote.result.sizeBytes : localSize,
7557
+ problems: remote.status === "ok" ? remote.result.problems : remote.status === "failed" ? remote.problems : [],
7558
+ remote: remote.status === "ok" ? "validated by the server" : remote.reason,
7559
+ plan: plan.summary,
7560
+ ...planNote ? { planNote } : {},
7561
+ publishAfterUpload: params.publish === true,
7562
+ requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
7563
+ 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."
7564
+ },
7565
+ null,
7566
+ 2
7567
+ )
7568
+ );
7569
+ }
7570
+ const guardError = validateProductionApply({ confirm: params.confirm });
7571
+ if (guardError) return toolError(guardError);
7572
+ const uploaded = await api.upload(body);
7573
+ const versionNumber = uploaded.version?.versionNumber;
7574
+ const lines = [];
7575
+ if (uploaded.created === "none") {
7576
+ lines.push(`This exact bundle is already v${versionNumber ?? "?"} of custom:${key}. Nothing was created.`);
7577
+ } else if (uploaded.created === "chart") {
7578
+ lines.push(`Created custom chart custom:${key} (v${versionNumber ?? 1}, status private).`);
7579
+ } else {
7580
+ lines.push(`Uploaded v${versionNumber ?? "?"} of custom:${key}. It is in preview until you publish it.`);
7581
+ }
7582
+ lines.push(`contentHash: ${uploaded.version?.contentHash ?? localHash}`);
7583
+ if (params.publish) {
7584
+ const published = await api.publish(key, uploaded.version?.id);
7585
+ lines.push(`Published v${versionNumber ?? "?"} of custom:${key}. Status: ${published.chart?.status ?? "published"}. Every dashboard using custom:${key} now renders this version.`);
7586
+ } else if (uploaded.created !== "none" || uploaded.chart?.status !== "published") {
7587
+ 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.`);
7588
+ }
7589
+ return toolSuccess(lines.join("\n"));
7590
+ } catch (err) {
7591
+ if (err instanceof DatabrainApiError) {
7592
+ const problems = apiProblems(err);
7593
+ if (problems.length > 0) {
7594
+ return toolError(`The server refused the bundle: ${err.message}
7595
+ ${formatProblems(problems)}`);
7596
+ }
7597
+ return err.toToolResponse();
7598
+ }
7599
+ return toolError(`Unexpected error: ${err.message}`);
7600
+ }
7601
+ }
7602
+ );
7603
+ }
7604
+
7605
+ // src/tools/custom-chart/list.ts
7606
+ function registerListCustomCharts(server, client) {
7607
+ server.tool(
7608
+ "list_custom_charts",
7609
+ "List the custom chart types of the company (key, name, status, published and preview versions). Uses the service token.",
7610
+ {},
7611
+ READ_ONLY,
7612
+ async () => {
7613
+ try {
7614
+ const charts = await createCustomChartsApi(client).list();
7615
+ const rows = charts.map((chart) => ({
7616
+ key: chart.key,
7617
+ chartType: `custom:${chart.key}`,
7618
+ name: chart.name,
7619
+ status: chart.status,
7620
+ publishedVersionId: chart.publishedVersionId ?? null,
7621
+ publishedVersionNumber: typeof chart.publishedVersionNumber === "number" ? chart.publishedVersionNumber : void 0,
7622
+ previewVersionId: chart.previewVersionId ?? null,
7623
+ previewVersionNumber: typeof chart.previewVersionNumber === "number" ? chart.previewVersionNumber : void 0,
7624
+ versionsCount: typeof chart.versionsCount === "number" ? chart.versionsCount : void 0,
7625
+ updatedAt: chart.updatedAt,
7626
+ createdBy: chart.createdBy ?? null
7627
+ }));
7628
+ if (rows.length === 0) {
7629
+ return toolSuccess(
7630
+ "No custom charts yet. Scaffold one with custom_chart_scaffold, build it with `databrain custom-chart build`, then upload with custom_chart_upload."
7631
+ );
7632
+ }
7633
+ return toolSuccess(JSON.stringify({ count: rows.length, charts: rows }, null, 2));
7634
+ } catch (err) {
7635
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
7636
+ return toolError(`Unexpected error: ${err.message}`);
7637
+ }
7638
+ }
7639
+ );
7640
+ }
7641
+
7642
+ // src/tools/custom-chart/publish.ts
7643
+ import { z as z44 } from "zod";
7644
+ function registerPublishCustomChart(server, client) {
7645
+ const api = createCustomChartsApi(client);
7646
+ server.tool(
7647
+ "publish_custom_chart",
7648
+ "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.",
7649
+ {
7650
+ key: z44.string().describe("The chart key (manifest.id), for example revenue-bars."),
7651
+ versionId: z44.string().optional().describe("A specific version id to publish. Defaults to the preview version."),
7652
+ mode: z44.enum(["dry_run", "apply"]).default("dry_run").describe("dry_run shows what would be published; apply publishes."),
7653
+ confirm: z44.string().optional().describe(`Required for apply: ${APPLY_TO_PRODUCTION_CONFIRMATION}`)
7654
+ },
7655
+ WRITE_IDEMPOTENT,
7656
+ async (params) => {
7657
+ try {
7658
+ const mode = params.mode || "dry_run";
7659
+ const detail = await api.get(params.key);
7660
+ const versions = Array.isArray(detail.versions) ? detail.versions : [];
7661
+ const targetId = params.versionId ?? detail.previewVersionId ?? null;
7662
+ const target = versions.find((version) => version.id === targetId);
7663
+ if (!targetId) {
7664
+ return toolError(
7665
+ `custom:${params.key} has no preview version to publish${detail.publishedVersionId ? ` (v published: ${detail.publishedVersionId})` : ""}. Upload a new version first, or pass versionId.`
7666
+ );
7667
+ }
7668
+ if (params.versionId && versions.length > 0 && !target) {
7669
+ return toolError(`custom:${params.key} has no version with id ${params.versionId}. Versions: ${versions.map((v) => `${v.id} (v${v.versionNumber})`).join(", ")}.`);
7670
+ }
7671
+ if (detail.publishedVersionId === targetId) {
7672
+ return toolSuccess(`custom:${params.key} already has ${target ? `v${target.versionNumber}` : targetId} published. Nothing to do.`);
7673
+ }
7674
+ if (mode === "dry_run") {
7675
+ return toolSuccess(
7676
+ JSON.stringify(
7677
+ {
7678
+ status: "dry_run_only",
7679
+ safety: genericProductionSafetyNote(),
7680
+ key: params.key,
7681
+ chartType: `custom:${params.key}`,
7682
+ name: detail.name,
7683
+ currentStatus: detail.status,
7684
+ currentPublishedVersionId: detail.publishedVersionId ?? null,
7685
+ willPublish: { versionId: targetId, versionNumber: target?.versionNumber, contentHash: target?.contentHash },
7686
+ impact: `Every dashboard using custom:${params.key} will render this version.`,
7687
+ requiredApplyConfirmation: APPLY_TO_PRODUCTION_CONFIRMATION,
7688
+ nextStep: `Ask the user to confirm, then call again with mode="apply" and confirm="${APPLY_TO_PRODUCTION_CONFIRMATION}".`
7689
+ },
7690
+ null,
7691
+ 2
7692
+ )
7693
+ );
7694
+ }
7695
+ const guardError = validateProductionApply({ confirm: params.confirm });
7696
+ if (guardError) return toolError(guardError);
7697
+ const result = await api.publish(params.key, targetId);
7698
+ return toolSuccess(
7699
+ `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.`
7700
+ );
7701
+ } catch (err) {
7702
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
7703
+ return toolError(`Unexpected error: ${err.message}`);
7704
+ }
7705
+ }
7706
+ );
7707
+ }
7708
+
7709
+ // src/tools/tool-titles.ts
7710
+ var TOOL_TITLES = {
7711
+ whoami: "Show active credential",
7712
+ list_datasources: "List data sources",
7713
+ discover_datasource_schema: "Discover data source schema",
7714
+ list_datamarts: "List datamarts",
7715
+ list_workspaces: "List workspaces",
7716
+ sync_datasource: "Sync data source",
7717
+ create_datamart: "Create datamart",
7718
+ create_workspace: "Create workspace",
7719
+ list_data_apps: "List data apps",
7720
+ list_tenants: "List tenants",
7721
+ create_data_app: "Create data app",
7722
+ list_api_tokens: "List API tokens",
7723
+ create_api_token: "Create API token",
7724
+ rotate_api_token: "Rotate API token",
7725
+ create_embed: "Create embed",
7726
+ list_embeds: "List embeds",
7727
+ get_embed_details: "Get embed details",
7728
+ update_embed: "Update embed",
7729
+ customize_embed_theme: "Customize embed theme",
7730
+ get_demo_link: "Get demo link",
7731
+ generate_guest_token: "Generate guest token",
7732
+ generate_embed_code: "Generate embed code",
7733
+ list_dashboards: "List dashboards",
7734
+ create_dashboard: "Create dashboard",
7735
+ get_dashboard: "Get dashboard",
7736
+ dashboard_snapshot: "Dashboard snapshot (picture)",
7737
+ generate_dashboard_tiles: "Generate dashboard tiles",
7738
+ ask_question: "Ask a data question",
7739
+ get_semantic_layer: "Get semantic layer",
7740
+ update_semantic_layer: "Update semantic layer",
7741
+ start_semantic_layer_generation: "Start semantic layer generation",
7742
+ get_semantic_layer_generation_status: "Get semantic layer generation status",
7743
+ search_semantic_model: "Search semantic model",
7744
+ get_semantic_entity: "Get semantic entity",
7745
+ propose_semantic_change: "Propose semantic model change",
7746
+ apply_semantic_change: "Apply semantic model change",
7747
+ eval_semantic_model: "Evaluate semantic model",
7748
+ list_widgets: "List widgets",
7749
+ validate_metric_spec: "Validate metric spec",
7750
+ create_or_update_metric_from_query: "Create or update metric from SQL",
7751
+ create_workspace_metric: "Create workspace metric",
7752
+ add_widget: "Add widget",
7753
+ update_widget: "Update widget",
7754
+ remove_widget: "Remove widget",
7755
+ generate_widget: "Generate widget",
7756
+ list_scheduled_reports: "List scheduled reports",
7757
+ custom_chart_scaffold: "Scaffold custom chart",
7758
+ custom_chart_validate: "Validate custom chart",
7759
+ custom_chart_upload: "Upload custom chart",
7760
+ list_custom_charts: "List custom charts",
7761
+ publish_custom_chart: "Publish custom chart"
7762
+ };
7763
+ function toolTitle(name) {
7764
+ if (TOOL_TITLES[name]) return TOOL_TITLES[name];
7765
+ const words = name.split("_").filter(Boolean);
7766
+ return words.map((word, index) => index === 0 ? word[0].toUpperCase() + word.slice(1) : word).join(" ");
7767
+ }
7768
+
7003
7769
  // src/tools/registry.ts
7004
7770
  function instrumentToolRegistration(server) {
7005
7771
  const original = server.tool.bind(server);
@@ -7020,7 +7786,15 @@ function instrumentToolRegistration(server) {
7020
7786
  () => handler(...handlerArgs)
7021
7787
  );
7022
7788
  }
7023
- return original(...args);
7789
+ const title = toolTitle(name);
7790
+ const annotationIndex = cbIndex - 1;
7791
+ const annotations = args[annotationIndex];
7792
+ if (annotationIndex > 1 && annotations && typeof annotations === "object" && ("readOnlyHint" in annotations || "destructiveHint" in annotations) && !("title" in annotations)) {
7793
+ args[annotationIndex] = { title, ...annotations };
7794
+ }
7795
+ const registered = original(...args);
7796
+ if (registered && typeof registered === "object" && !registered.title) registered.title = title;
7797
+ return registered;
7024
7798
  };
7025
7799
  }
7026
7800
  function registerAllTools(server, client) {
@@ -7049,12 +7823,18 @@ function registerAllTools(server, client) {
7049
7823
  registerListDashboards(server, client);
7050
7824
  registerCreateDashboard(server, client);
7051
7825
  registerGetDashboard(server, client);
7826
+ registerDashboardSnapshot(server, client);
7052
7827
  registerGenerateDashboardTiles(server, client);
7053
7828
  registerAskQuestion(server, client);
7054
7829
  registerGetSemanticLayer(server, client);
7055
7830
  registerUpdateSemanticLayer(server, client);
7056
7831
  registerStartSemanticLayerGeneration(server, client);
7057
7832
  registerGetSemanticLayerGenerationStatus(server, client);
7833
+ registerSearchSemanticModel(server, client);
7834
+ registerGetSemanticEntity(server, client);
7835
+ registerProposeSemanticChange(server, client);
7836
+ registerApplySemanticChange(server, client);
7837
+ registerEvalSemanticModel(server, client);
7058
7838
  registerListWidgets(server, client);
7059
7839
  registerValidateMetricSpec(server, client);
7060
7840
  registerCreateOrUpdateMetricFromQuery(server, client);
@@ -7064,6 +7844,12 @@ function registerAllTools(server, client) {
7064
7844
  registerRemoveWidget(server, client);
7065
7845
  registerGenerateWidget(server, client);
7066
7846
  registerListScheduledReports(server, client);
7847
+ registerCustomChartScaffold(server, client);
7848
+ registerCustomChartAuthor(server, client);
7849
+ registerCustomChartValidate(server, client);
7850
+ registerCustomChartUpload(server, client);
7851
+ registerListCustomCharts(server, client);
7852
+ registerPublishCustomChart(server, client);
7067
7853
  }
7068
7854
 
7069
7855
  // src/resources/content/getting-started.ts
@@ -8944,6 +9730,20 @@ When the same flag is set at multiple levels, the highest precedence wins:
8944
9730
 
8945
9731
  // src/resources/content/semantic-layer-guide.ts
8946
9732
  var semanticLayerGuideContent = `
9733
+
9734
+ ## Semantic models v2
9735
+
9736
+ The v2 source is one YAML document. Use the configured service credential and a datamart ID from \`list_datamarts\`.
9737
+
9738
+ 1. \`search_semantic_model\` finds entities by question. \`get_semantic_entity\` reads one entity by kind and name. Column names use table.column.
9739
+ 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.
9740
+ 3. \`eval_semantic_model\` evaluates the proposed source.
9741
+ 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.
9742
+
9743
+ 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.
9744
+
9745
+ The legacy maintenance workflow below applies to datamarts that have not moved to v2. Do not call \`update_semantic_layer\` for v2.
9746
+
8947
9747
  # Databrain Semantic Layer Guide
8948
9748
 
8949
9749
  ## What Is the Semantic Layer?
@@ -9930,6 +10730,9 @@ If **no datamarts exist**, tell the user they need to create one in the **Databr
9930
10730
 
9931
10731
  If **no datasources exist either**, the user must first connect their database in the **Databrain UI**.
9932
10732
 
10733
+ **For a v2 semantic model**
10734
+ 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.
10735
+
9933
10736
  **Step 1b \u2014 Verify the Semantic Layer**
9934
10737
  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
10738
 
@@ -10011,6 +10814,15 @@ If the confidence is low or results seem wrong:
10011
10814
  // src/prompts/populate-semantic-layer.ts
10012
10815
  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
10816
 
10817
+ **For a v2 semantic model**
10818
+ 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:
10819
+ 1. Inspect relevant entities with \`search_semantic_model\` and \`get_semantic_entity\`, using the datamart ID.
10820
+ 2. Call \`propose_semantic_change\` with a YAML patch or structured change. Show its errors and plan. Correct errors before continuing.
10821
+ 3. Call \`eval_semantic_model\` with the returned source. Show the verdicts and let the user review the candidate.
10822
+ 4. Stop for the user's explicit \`APPLY_TO_PRODUCTION\` confirmation. Never supply it yourself.
10823
+ 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.
10824
+ 6. Call \`get_semantic_entity\` and inspect the changed entity. Do not call \`update_semantic_layer\` for a v2 model.
10825
+
10014
10826
  **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
10827
 
10016
10828
  **Step 1 \u2014 Identify the Datamart**
@@ -10372,27 +11184,31 @@ function registerAllPrompts(server) {
10372
11184
  }
10373
11185
 
10374
11186
  export {
10375
- oauthHttpsUrl,
10376
- oauthConfigSchema,
10377
- DatabrainApiError,
10378
- toolError,
10379
- toolSuccess,
10380
- logger,
10381
- MCP_SERVER_VERSION,
10382
- toolContext,
10383
- DatabrainClient,
10384
- createServiceBundle,
10385
11187
  READ_ONLY,
11188
+ WRITE_CREATE,
10386
11189
  CHART_URI,
10387
11190
  KPI_URI,
10388
11191
  APP_MIME,
11192
+ MAX_CUSTOM_BUNDLE_CHARS,
11193
+ CUSTOM_BUNDLE_CHUNK_CHARS,
11194
+ rowsSchema,
10389
11195
  referenceSchema,
10390
11196
  viewSchema,
10391
11197
  pageSchema,
11198
+ bounded,
11199
+ appMeta,
10392
11200
  enableChartApps,
10393
11201
  chartResult,
11202
+ createServiceBundle,
11203
+ APPLY_TO_PRODUCTION_CONFIRMATION,
11204
+ draftSchema,
11205
+ PREVIEW_PATH,
11206
+ RENDER_TYPES,
11207
+ draftPage,
11208
+ workspacesForDatamart,
11209
+ saveDraftAsDashboard,
10394
11210
  registerAllTools,
10395
11211
  registerAllResources,
10396
11212
  registerAllPrompts
10397
11213
  };
10398
- //# sourceMappingURL=chunk-7DS7OLWP.js.map
11214
+ //# sourceMappingURL=chunk-ES2LUP4U.js.map