@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.
- package/README.md +158 -7
- package/dist/apps/server.d.ts +82 -44
- package/dist/apps/server.js +5 -2
- package/dist/apps/stdio.js +6 -3
- package/dist/apps/stdio.js.map +1 -1
- package/dist/bundle-TDQXWIPM.js +24 -0
- package/dist/bundle-TDQXWIPM.js.map +1 -0
- package/dist/{chunk-RF3HTNO7.js → chunk-DM7S4R57.js} +2 -2
- package/dist/{chunk-7DS7OLWP.js → chunk-ES2LUP4U.js} +2030 -1214
- package/dist/chunk-ES2LUP4U.js.map +1 -0
- package/dist/chunk-IBYBK5ZE.js +34 -0
- package/dist/chunk-IBYBK5ZE.js.map +1 -0
- package/dist/chunk-NBFUKVWP.js +603 -0
- package/dist/chunk-NBFUKVWP.js.map +1 -0
- package/dist/chunk-Q5EYDKSA.js +1224 -0
- package/dist/chunk-Q5EYDKSA.js.map +1 -0
- package/dist/chunk-RHK62ZJ6.js +884 -0
- package/dist/chunk-RHK62ZJ6.js.map +1 -0
- package/dist/{chunk-JCXUZ3QZ.js → chunk-Z3ZHN4UW.js} +34 -6
- package/dist/chunk-Z3ZHN4UW.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +497 -0
- package/dist/cli.js.map +1 -0
- package/dist/custom-chart/custom-runtime.js +63 -0
- package/dist/custom-chart/runtime-shim.js +146 -0
- package/dist/custom-chart/template/AGENTS.md +374 -0
- package/dist/custom-chart/template/README.md +374 -0
- package/dist/custom-chart/template/chart.css +28 -0
- package/dist/custom-chart/template/chart.ts +192 -0
- package/dist/custom-chart/template/custom-chart.d.ts +78 -0
- package/dist/custom-chart/template/icon.svg +1 -0
- package/dist/custom-chart/template/manifest.json +23 -0
- package/dist/custom-chart/template/package.json +14 -0
- package/dist/custom-chart/template/sample-data.json +18 -0
- package/dist/custom-chart/template/tsconfig.json +12 -0
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/dist/main-JEYYFGJM.js +872 -0
- package/dist/main-JEYYFGJM.js.map +1 -0
- package/dist/{test-only-EJ5O4WN5.js → test-only-K7QTSVO4.js} +7 -6
- package/dist/test-only-K7QTSVO4.js.map +1 -0
- package/package.json +9 -4
- package/dist/chunk-7DS7OLWP.js.map +0 -1
- package/dist/chunk-JCXUZ3QZ.js.map +0 -1
- package/dist/chunk-NDVW3ZSJ.js +0 -310
- package/dist/chunk-NDVW3ZSJ.js.map +0 -1
- package/dist/main-7QNRH6X4.js +0 -438
- package/dist/main-7QNRH6X4.js.map +0 -1
- package/dist/test-only-EJ5O4WN5.js.map +0 -1
- /package/dist/{chunk-RF3HTNO7.js.map → chunk-DM7S4R57.js.map} +0 -0
|
@@ -1,621 +1,87 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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 =
|
|
582
|
-
var
|
|
583
|
-
var
|
|
584
|
-
var
|
|
585
|
-
var
|
|
586
|
-
var
|
|
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:
|
|
48
|
+
label: z.string().max(2e3),
|
|
589
49
|
value: cell,
|
|
590
|
-
x:
|
|
591
|
-
y:
|
|
592
|
-
width:
|
|
593
|
-
height:
|
|
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 =
|
|
597
|
-
from:
|
|
598
|
-
to:
|
|
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 =
|
|
601
|
-
cursor:
|
|
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 =
|
|
606
|
-
kind:
|
|
607
|
-
title:
|
|
608
|
-
chartSpec:
|
|
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:
|
|
611
|
-
svg:
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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:
|
|
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:
|
|
119
|
+
if (hasChartApps(server)) tool.update({ _meta: appMeta(["model", "app"]) });
|
|
651
120
|
}
|
|
652
121
|
function chartResult(summary, payload) {
|
|
653
|
-
return bounded({
|
|
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
|
-
|
|
670
|
-
|
|
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
|
-
|
|
684
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
|
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(
|
|
712
|
+
async startSemanticLayerGeneration(datamartId2) {
|
|
1015
713
|
return this.client.request({
|
|
1016
714
|
method: "POST",
|
|
1017
|
-
path: `/
|
|
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(
|
|
720
|
+
async getSemanticLayerGenerationStatus(datamartId2) {
|
|
1023
721
|
return this.client.request({
|
|
1024
722
|
method: "GET",
|
|
1025
|
-
path: `/
|
|
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
|
|
1049
|
-
if (Array.isArray(
|
|
1050
|
-
return
|
|
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
|
|
1262
|
-
if (Object.keys(
|
|
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:
|
|
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
|
|
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:
|
|
2077
|
-
workspaceId:
|
|
2078
|
-
searchValue:
|
|
2079
|
-
limit:
|
|
2080
|
-
datasourceId:
|
|
2081
|
-
datamartId:
|
|
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
|
|
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:
|
|
2182
|
-
workspaceName:
|
|
2183
|
-
body:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
2339
|
-
name:
|
|
2340
|
-
body:
|
|
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
|
-
|
|
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
|
|
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:
|
|
2400
|
-
tokenId:
|
|
2401
|
-
body:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
2724
|
-
name:
|
|
2725
|
-
alias:
|
|
2726
|
-
label:
|
|
2727
|
-
isHide:
|
|
2728
|
-
isCustomColumn:
|
|
2729
|
-
sql:
|
|
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 =
|
|
2732
|
-
name:
|
|
2733
|
-
schemaName:
|
|
2734
|
-
clientColumn:
|
|
2735
|
-
label:
|
|
2736
|
-
columns:
|
|
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 =
|
|
2739
|
-
parentTableName:
|
|
2740
|
-
parentColumnName:
|
|
2741
|
-
childTableName:
|
|
2742
|
-
childColumnName:
|
|
2743
|
-
cardinality:
|
|
2744
|
-
join:
|
|
2745
|
-
relationshipName:
|
|
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:
|
|
2767
|
-
confirm:
|
|
2768
|
-
name:
|
|
2769
|
-
datasourceName:
|
|
2770
|
-
tableList:
|
|
2771
|
-
relationships:
|
|
2772
|
-
tenancySettings:
|
|
2773
|
-
tenancyLevel:
|
|
2774
|
-
clientColumnType:
|
|
2775
|
-
schemaName:
|
|
2776
|
-
tableName:
|
|
2777
|
-
tableClientNameColumn:
|
|
2778
|
-
tablePrimaryKeyColumn:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
2945
|
-
datamartName:
|
|
2946
|
-
isAllowMetricCreation:
|
|
2947
|
-
metricCreationMode:
|
|
2948
|
-
isAllowMetricDeletion:
|
|
2949
|
-
isAllowMetricUpdate:
|
|
2950
|
-
isAllowMetricLayoutChange:
|
|
2951
|
-
isAllowManageMetrics:
|
|
2952
|
-
isAllowUnderlyingData:
|
|
2953
|
-
isAllowAiPilot:
|
|
2954
|
-
isAllowEmailReports:
|
|
2955
|
-
isAllowCreateDashboardView:
|
|
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:
|
|
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:
|
|
2967
|
-
workspaceName:
|
|
2968
|
-
embedType:
|
|
2969
|
-
metricId:
|
|
2970
|
-
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:
|
|
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:
|
|
2977
|
-
isImportMetrics:
|
|
2978
|
-
metricImportMode:
|
|
2979
|
-
isRenameDashboard:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
3201
|
-
body:
|
|
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:
|
|
3205
|
-
accessSettings:
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
3401
|
-
fontFamily:
|
|
3402
|
-
fontSize:
|
|
3403
|
-
fontWeight:
|
|
3404
|
-
fontColor:
|
|
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 =
|
|
3407
|
-
fontSize:
|
|
3408
|
-
fontFamily:
|
|
3409
|
-
fontWeight:
|
|
3410
|
-
color:
|
|
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 =
|
|
3422
|
-
hideAxisLines:
|
|
3423
|
-
hideSplitLines:
|
|
3424
|
-
hideAxisLabels:
|
|
3425
|
-
hideAxisTicks:
|
|
3426
|
-
axisName:
|
|
3427
|
-
axisNameOffset:
|
|
3428
|
-
axisLabelMargin:
|
|
3429
|
-
fontSize:
|
|
3430
|
-
fontFamily:
|
|
3431
|
-
fontWeight:
|
|
3432
|
-
color:
|
|
3433
|
-
axisColor:
|
|
3434
|
-
axisNameFontConfig:
|
|
3435
|
-
fontFamily:
|
|
3436
|
-
fontSize:
|
|
3437
|
-
fontWeight:
|
|
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:
|
|
3594
|
-
body:
|
|
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:
|
|
3599
|
-
accessPreset:
|
|
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:
|
|
3384
|
+
datePickerFormat: z18.string().optional().describe("Date picker display format"),
|
|
3603
3385
|
// --- Colors ---
|
|
3604
|
-
primaryColor:
|
|
3605
|
-
backgroundColor:
|
|
3606
|
-
textColor:
|
|
3607
|
-
ctaColor:
|
|
3608
|
-
ctaTextColor:
|
|
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:
|
|
3611
|
-
selectBoxSize:
|
|
3612
|
-
selectBoxVariant:
|
|
3613
|
-
selectBoxBorderRadius:
|
|
3614
|
-
selectBoxTextColor:
|
|
3615
|
-
metricCardColor:
|
|
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:
|
|
3618
|
-
cardTitleFontWeight:
|
|
3619
|
-
cardTitleColor:
|
|
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:
|
|
3622
|
-
cardDescriptionFontWeight:
|
|
3623
|
-
cardDescriptionColor:
|
|
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:
|
|
3626
|
-
cardBorderRadius:
|
|
3627
|
-
cardShadow:
|
|
3628
|
-
disableShadowOnHover:
|
|
3629
|
-
disableCardStroke:
|
|
3630
|
-
cardStrokeColor:
|
|
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:
|
|
3633
|
-
chartFontColor:
|
|
3634
|
-
chartColors:
|
|
3635
|
-
chartPaletteName:
|
|
3636
|
-
chartPaletteColors:
|
|
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:
|
|
3643
|
-
buttonPrimaryTextColor:
|
|
3644
|
-
buttonSecondaryColor:
|
|
3645
|
-
buttonSecondaryTextColor:
|
|
3646
|
-
checkboxCheckedColor:
|
|
3647
|
-
checkboxUncheckedColor:
|
|
3648
|
-
switchEnabledColor:
|
|
3649
|
-
switchDisabledColor:
|
|
3650
|
-
breadcrumbFontFamily:
|
|
3651
|
-
breadcrumbFontColor:
|
|
3652
|
-
breadcrumbActiveColor:
|
|
3653
|
-
filterBadgeColor:
|
|
3654
|
-
filterBadgeTextColor:
|
|
3655
|
-
datePickerColor:
|
|
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:
|
|
3658
|
-
breakpointLg:
|
|
3659
|
-
breakpointMd:
|
|
3660
|
-
breakpointSm:
|
|
3661
|
-
breakpointXs:
|
|
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:
|
|
3664
|
-
layoutColsLg:
|
|
3665
|
-
layoutColsMd:
|
|
3666
|
-
layoutColsSm:
|
|
3667
|
-
layoutColsXs:
|
|
3668
|
-
layoutColsXxs:
|
|
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:
|
|
3674
|
-
radialLabelPosition:
|
|
3675
|
-
marginTop:
|
|
3676
|
-
marginLeft:
|
|
3677
|
-
marginRight:
|
|
3678
|
-
marginBottom:
|
|
3679
|
-
legendShow:
|
|
3680
|
-
legendPosition:
|
|
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:
|
|
3691
|
-
legendShape:
|
|
3692
|
-
legendFontSize:
|
|
3693
|
-
legendFontWeight:
|
|
3694
|
-
legendFontFamily:
|
|
3695
|
-
legendColor:
|
|
3696
|
-
legendTruncateLength:
|
|
3697
|
-
disableLegendScrolling:
|
|
3698
|
-
enableVariableLegendPosition:
|
|
3699
|
-
legendTop:
|
|
3700
|
-
legendLeft:
|
|
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
|
|
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:
|
|
3786
|
-
dashboardName:
|
|
3787
|
-
workspaceName:
|
|
3788
|
-
embedId:
|
|
3789
|
-
expiryHours:
|
|
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
|
|
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:
|
|
3853
|
-
dataAppName:
|
|
3854
|
-
body:
|
|
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:
|
|
3858
|
-
allowedEmbeds:
|
|
3859
|
-
rlsSettings:
|
|
3860
|
-
metricId:
|
|
3861
|
-
values:
|
|
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:
|
|
3864
|
-
metricId:
|
|
3865
|
-
values:
|
|
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:
|
|
3868
|
-
dashboardId:
|
|
3869
|
-
values:
|
|
3870
|
-
isShowOnUrl:
|
|
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:
|
|
3873
|
-
userIdentifier:
|
|
3874
|
-
timezone:
|
|
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:
|
|
3878
|
-
allowedEmbeds:
|
|
3879
|
-
rlsSettings:
|
|
3880
|
-
metricId:
|
|
3881
|
-
values:
|
|
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:
|
|
3884
|
-
metricId:
|
|
3885
|
-
values:
|
|
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:
|
|
3888
|
-
dashboardId:
|
|
3889
|
-
values:
|
|
3890
|
-
isShowOnUrl:
|
|
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:
|
|
3893
|
-
isEnableArchiveMetrics:
|
|
3894
|
-
isEnableManageMetrics:
|
|
3895
|
-
isEnableCreateDashboardView:
|
|
3896
|
-
isEnableMetricUpdation:
|
|
3897
|
-
isEnableCustomizeLayout:
|
|
3898
|
-
isEnableUnderlyingData:
|
|
3899
|
-
isEnableDownloadMetrics:
|
|
3900
|
-
isShowSideBar:
|
|
3901
|
-
isShowDashboardName:
|
|
3902
|
-
isDisableMetricCreation:
|
|
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:
|
|
3905
|
-
hideDashboardFilters:
|
|
3906
|
-
userIdentifier:
|
|
3907
|
-
expiryTime:
|
|
3908
|
-
datasourceName:
|
|
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
|
|
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:
|
|
4958
|
-
framework:
|
|
4959
|
-
embedType:
|
|
4960
|
-
options:
|
|
4961
|
-
enableDownloadCsv:
|
|
4962
|
-
enableEmailCsv:
|
|
4963
|
-
enableDownloadAllMetrics:
|
|
4964
|
-
enableDownloadAllPdf:
|
|
4965
|
-
disableFullscreen:
|
|
4966
|
-
isHideTablePreview:
|
|
4967
|
-
isHideChartSettings:
|
|
4968
|
-
isStickyDashboardFilters:
|
|
4969
|
-
enableMultiMetricFilters:
|
|
4970
|
-
themeName:
|
|
4971
|
-
customTheme:
|
|
4972
|
-
optionsIcon:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
5058
|
-
var dashboardPositionSchema =
|
|
5059
|
-
x:
|
|
5060
|
-
y:
|
|
5061
|
-
w:
|
|
5062
|
-
h:
|
|
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 =
|
|
5065
|
-
type:
|
|
5066
|
-
metricRef:
|
|
5067
|
-
query:
|
|
5068
|
-
companyIntegrationId:
|
|
5069
|
-
chartOptions:
|
|
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 =
|
|
5072
|
-
id:
|
|
5073
|
-
kind:
|
|
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:
|
|
4857
|
+
title: z23.string().min(1).max(200),
|
|
5076
4858
|
chart: dashboardChartSchema.optional()
|
|
5077
4859
|
}).strict();
|
|
5078
4860
|
var dashboardDocumentShape = {
|
|
5079
|
-
schemaVersion:
|
|
5080
|
-
title:
|
|
5081
|
-
description:
|
|
5082
|
-
layout:
|
|
5083
|
-
tiles:
|
|
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 =
|
|
5086
|
-
var idempotencyKeySchema =
|
|
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
|
|
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:
|
|
5148
|
-
integrationId:
|
|
5149
|
-
integrationName:
|
|
5150
|
-
tiles:
|
|
5151
|
-
|
|
5152
|
-
id:
|
|
5153
|
-
title:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
5309
|
+
sessionId: z28.string().optional().describe(
|
|
5307
5310
|
"Session ID from a previous ask_question response, for follow-up questions."
|
|
5308
5311
|
),
|
|
5309
|
-
timezone:
|
|
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
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
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
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
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
|
-
|
|
5455
|
+
const reasoning = formatProse(result.reasoning);
|
|
5456
|
+
if (reasoning) {
|
|
5443
5457
|
parts.push(`
|
|
5444
5458
|
### Reasoning
|
|
5445
|
-
${
|
|
5459
|
+
${reasoning}`);
|
|
5446
5460
|
}
|
|
5447
|
-
|
|
5461
|
+
const summary = formatProse(result.summary);
|
|
5462
|
+
if (summary) {
|
|
5448
5463
|
parts.push(`
|
|
5449
5464
|
### Summary
|
|
5450
|
-
${
|
|
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
|
|
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:
|
|
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
|
|
5615
|
-
var columnSchema2 =
|
|
5616
|
-
name:
|
|
5617
|
-
description:
|
|
5618
|
-
synonyms:
|
|
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:
|
|
5622
|
-
columnType:
|
|
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 =
|
|
5636
|
-
name:
|
|
5637
|
-
|
|
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:
|
|
5641
|
-
miscellaneousInfo:
|
|
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:
|
|
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:
|
|
6123
|
+
datamartName: z31.string().optional().describe(
|
|
5712
6124
|
"Name of the datamart to update. Omit to list available datamarts."
|
|
5713
6125
|
),
|
|
5714
|
-
tables:
|
|
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:
|
|
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
|
|
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
|
|
6223
|
+
const datamartId2 = workspaceDatamartIdFrom(selected);
|
|
5812
6224
|
const workspaceDatamartName = workspaceDatamartNameFrom(selected);
|
|
5813
|
-
if (!
|
|
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 ||
|
|
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
|
|
5894
|
-
if (!
|
|
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}"
|
|
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:
|
|
5935
|
-
workspaceName:
|
|
5936
|
-
datamartId:
|
|
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:
|
|
5979
|
-
workspaceName:
|
|
5980
|
-
datamartId:
|
|
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
|
|
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:
|
|
6025
|
-
dashboardName:
|
|
6026
|
-
workspaceName:
|
|
6027
|
-
embedId:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
6098
|
-
dashboardName:
|
|
6099
|
-
workspaceName:
|
|
6100
|
-
embedId:
|
|
6101
|
-
dataAppName:
|
|
6102
|
-
templateMetricId:
|
|
6103
|
-
name:
|
|
6104
|
-
metricId:
|
|
6105
|
-
query:
|
|
6106
|
-
chartOptions:
|
|
6107
|
-
description:
|
|
6108
|
-
companyIntegrationId:
|
|
6109
|
-
integrationName:
|
|
6110
|
-
isPublished:
|
|
6111
|
-
body:
|
|
6112
|
-
widgets:
|
|
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
|
|
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:
|
|
6268
|
-
clientId:
|
|
6269
|
-
dashboardName:
|
|
6270
|
-
workspaceName:
|
|
6271
|
-
embedId:
|
|
6272
|
-
dataAppName:
|
|
6273
|
-
set:
|
|
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
|
|
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:
|
|
6344
|
-
clientId:
|
|
6345
|
-
dashboardName:
|
|
6346
|
-
workspaceName:
|
|
6347
|
-
embedId:
|
|
6348
|
-
dataAppName:
|
|
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
|
|
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:
|
|
6412
|
-
userInputs:
|
|
6413
|
-
dashboardName:
|
|
6414
|
-
workspaceName:
|
|
6415
|
-
embedId:
|
|
6416
|
-
dataAppName:
|
|
6417
|
-
integrationId:
|
|
6418
|
-
integrationName:
|
|
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
|
|
6504
|
-
var modeSchema =
|
|
6505
|
-
var conflictStrategySchema =
|
|
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:
|
|
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:
|
|
6525
|
-
workspaceName:
|
|
6526
|
-
dashboardName:
|
|
6527
|
-
embedId:
|
|
6528
|
-
dataAppName:
|
|
6529
|
-
name:
|
|
6530
|
-
metricId:
|
|
6531
|
-
query:
|
|
6532
|
-
description:
|
|
6533
|
-
chartOptions:
|
|
6534
|
-
companyIntegrationId:
|
|
6535
|
-
integrationName:
|
|
6536
|
-
isPublished:
|
|
6537
|
-
requestedBy:
|
|
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
|
|
6698
|
-
var modeSchema2 =
|
|
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:
|
|
6708
|
-
workspaceName:
|
|
6709
|
-
name:
|
|
6710
|
-
query:
|
|
6711
|
-
|
|
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:
|
|
6715
|
-
datasourceId:
|
|
6716
|
-
isPublished:
|
|
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
|
|
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:
|
|
6780
|
-
workspaceName:
|
|
6781
|
-
dashboardName:
|
|
6782
|
-
embedId:
|
|
6783
|
-
dataAppName:
|
|
6784
|
-
name:
|
|
6785
|
-
metricId:
|
|
6786
|
-
query:
|
|
6787
|
-
description:
|
|
6788
|
-
chartOptions:
|
|
6789
|
-
companyIntegrationId:
|
|
6790
|
-
integrationName:
|
|
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
|
|
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:
|
|
6889
|
-
dashboardName:
|
|
6890
|
-
workspaceName:
|
|
6891
|
-
embedId:
|
|
6892
|
-
userIdentifier:
|
|
6893
|
-
pageNumber:
|
|
6894
|
-
isPagination:
|
|
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.
|
|
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
|
-
|
|
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-
|
|
11214
|
+
//# sourceMappingURL=chunk-ES2LUP4U.js.map
|