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