@databrainhq/mcp-server 0.2.10 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -10
- package/dist/index.js +652 -239
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,9 +8,11 @@ import { z, ZodError } from "zod";
|
|
|
8
8
|
var configSchema = z.object({
|
|
9
9
|
serviceToken: z.string().min(1).optional(),
|
|
10
10
|
apiToken: z.string().min(1).optional(),
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
pat: z.string().min(1).optional(),
|
|
12
|
+
apiUrl: z.string().url().default("https://api.usedatabrain.com"),
|
|
13
|
+
demoDomain: z.string().url().default("https://demo.usedatabrain.com")
|
|
14
|
+
}).refine((c) => c.serviceToken || c.apiToken || c.pat, {
|
|
15
|
+
message: "At least one of DATABRAIN_PAT, DATABRAIN_SERVICE_TOKEN, or DATABRAIN_API_TOKEN must be set"
|
|
14
16
|
});
|
|
15
17
|
function formatConfigError(err) {
|
|
16
18
|
const issues = err.issues.map((i) => i.message);
|
|
@@ -19,8 +21,12 @@ function formatConfigError(err) {
|
|
|
19
21
|
lines.push(` - ${issue}`);
|
|
20
22
|
}
|
|
21
23
|
lines.push("");
|
|
22
|
-
lines.push("
|
|
23
|
-
lines.push(
|
|
24
|
+
lines.push(" Either:");
|
|
25
|
+
lines.push(" \u2022 Set DATABRAIN_PAT to a Personal Access Token (Settings > Personal Access Tokens) \u2014 preferred, binds this MCP session to your user.");
|
|
26
|
+
lines.push(" \u2022 OR set DATABRAIN_SERVICE_TOKEN (Settings > Service Tokens) \u2014 org-level, no per-user audit; the `connect` tool can mint a PAT on top.");
|
|
27
|
+
lines.push(' Then add to your MCP config: { "env": { "DATABRAIN_PAT": "..." } }');
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push(" Self-hosted? Also set DATABRAIN_API_URL to your instance's API origin (e.g. https://api.your-databrain.example.com). Tokens minted on cloud do not work against a self-hosted instance and vice versa.");
|
|
24
30
|
return lines.join("\n");
|
|
25
31
|
}
|
|
26
32
|
function loadConfig() {
|
|
@@ -28,7 +34,9 @@ function loadConfig() {
|
|
|
28
34
|
return configSchema.parse({
|
|
29
35
|
serviceToken: process.env.DATABRAIN_SERVICE_TOKEN || void 0,
|
|
30
36
|
apiToken: process.env.DATABRAIN_API_TOKEN || void 0,
|
|
31
|
-
|
|
37
|
+
pat: process.env.DATABRAIN_PAT || void 0,
|
|
38
|
+
apiUrl: process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com",
|
|
39
|
+
demoDomain: process.env.DATABRAIN_DEMO_DOMAIN || "https://demo.usedatabrain.com"
|
|
32
40
|
});
|
|
33
41
|
} catch (err) {
|
|
34
42
|
if (err instanceof ZodError) {
|
|
@@ -78,13 +86,39 @@ var DatabrainApiError = class extends Error {
|
|
|
78
86
|
this.name = "DatabrainApiError";
|
|
79
87
|
}
|
|
80
88
|
toToolResponse() {
|
|
89
|
+
const parts = [`Error: ${this.message}`];
|
|
90
|
+
const detailDump = this.formatDetails();
|
|
91
|
+
if (detailDump) parts.push(detailDump);
|
|
92
|
+
const hint = this.hint();
|
|
93
|
+
if (hint) parts.push(hint);
|
|
81
94
|
return {
|
|
82
|
-
content: [{ type: "text", text:
|
|
83
|
-
|
|
84
|
-
${this.hint()}` }],
|
|
95
|
+
content: [{ type: "text", text: parts.join("\n\n") }],
|
|
85
96
|
isError: true
|
|
86
97
|
};
|
|
87
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Surface API-returned details for 4xx errors so the LLM can recover or
|
|
101
|
+
* report the real reason (validation messages, field names, etc.). Skips
|
|
102
|
+
* fields already collapsed into `message`/`code`, and skips 5xx where the
|
|
103
|
+
* payload usually isn't actionable.
|
|
104
|
+
*/
|
|
105
|
+
formatDetails() {
|
|
106
|
+
if (this.status < 400 || this.status >= 500) return "";
|
|
107
|
+
if (!this.details || typeof this.details !== "object") return "";
|
|
108
|
+
const skipKeys = /* @__PURE__ */ new Set(["message", "detail", "error", "code"]);
|
|
109
|
+
const extra = {};
|
|
110
|
+
for (const [k, v] of Object.entries(this.details)) {
|
|
111
|
+
if (skipKeys.has(k)) continue;
|
|
112
|
+
if (v === null || v === void 0) continue;
|
|
113
|
+
extra[k] = v;
|
|
114
|
+
}
|
|
115
|
+
if (Object.keys(extra).length === 0) return "";
|
|
116
|
+
try {
|
|
117
|
+
return `API details: ${JSON.stringify(extra)}`;
|
|
118
|
+
} catch {
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
88
122
|
hint() {
|
|
89
123
|
if (this.status === 401 || this.status === 403) {
|
|
90
124
|
if (this.code === "INVALID_SERVICE_TOKEN" || this.code === "MISSING_SERVICE_TOKEN" || this.message.toLowerCase().includes("service token")) {
|
|
@@ -154,6 +188,122 @@ var logger = {
|
|
|
154
188
|
error: (msg, data) => log("error", msg, data)
|
|
155
189
|
};
|
|
156
190
|
|
|
191
|
+
// src/auth/credential-store.ts
|
|
192
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, chmodSync, unlinkSync, existsSync } from "fs";
|
|
193
|
+
import { homedir } from "os";
|
|
194
|
+
import { dirname, join } from "path";
|
|
195
|
+
function defaultStorePath() {
|
|
196
|
+
const override = process.env.DATABRAIN_CREDENTIAL_PATH;
|
|
197
|
+
if (override) return override;
|
|
198
|
+
return join(homedir(), ".databrain", "mcp", "credentials.json");
|
|
199
|
+
}
|
|
200
|
+
function readStore(path) {
|
|
201
|
+
if (!existsSync(path)) return {};
|
|
202
|
+
try {
|
|
203
|
+
const raw = readFileSync(path, "utf8");
|
|
204
|
+
if (!raw.trim()) return {};
|
|
205
|
+
const parsed = JSON.parse(raw);
|
|
206
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
207
|
+
return parsed;
|
|
208
|
+
}
|
|
209
|
+
return {};
|
|
210
|
+
} catch (err) {
|
|
211
|
+
logger.warn("Credential store unreadable, starting fresh", {
|
|
212
|
+
error: err.message
|
|
213
|
+
});
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function writeStore(path, store) {
|
|
218
|
+
const dir = dirname(path);
|
|
219
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
220
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
221
|
+
writeFileSync(tmp, JSON.stringify(store, null, 2), { mode: 384 });
|
|
222
|
+
renameSync(tmp, path);
|
|
223
|
+
try {
|
|
224
|
+
chmodSync(path, 384);
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
var CredentialStore = class {
|
|
229
|
+
constructor(path = defaultStorePath()) {
|
|
230
|
+
this.path = path;
|
|
231
|
+
}
|
|
232
|
+
get(baseUrl) {
|
|
233
|
+
const store = readStore(this.path);
|
|
234
|
+
const cred = store[normalizeUrl(baseUrl)];
|
|
235
|
+
return cred ?? null;
|
|
236
|
+
}
|
|
237
|
+
set(baseUrl, cred) {
|
|
238
|
+
const store = readStore(this.path);
|
|
239
|
+
store[normalizeUrl(baseUrl)] = cred;
|
|
240
|
+
writeStore(this.path, store);
|
|
241
|
+
}
|
|
242
|
+
clear(baseUrl) {
|
|
243
|
+
const store = readStore(this.path);
|
|
244
|
+
const key = normalizeUrl(baseUrl);
|
|
245
|
+
if (!(key in store)) return false;
|
|
246
|
+
delete store[key];
|
|
247
|
+
if (Object.keys(store).length === 0 && existsSync(this.path)) {
|
|
248
|
+
unlinkSync(this.path);
|
|
249
|
+
} else {
|
|
250
|
+
writeStore(this.path, store);
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
getPath() {
|
|
255
|
+
return this.path;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
function normalizeUrl(url) {
|
|
259
|
+
return url.replace(/\/+$/, "");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/auth/operator.ts
|
|
263
|
+
var OPERATOR_PAT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
264
|
+
var storeSingleton;
|
|
265
|
+
function getCredentialStore() {
|
|
266
|
+
if (!storeSingleton) storeSingleton = new CredentialStore();
|
|
267
|
+
return storeSingleton;
|
|
268
|
+
}
|
|
269
|
+
function loadCachedOperator(baseUrl) {
|
|
270
|
+
let cred;
|
|
271
|
+
try {
|
|
272
|
+
cred = getCredentialStore().get(baseUrl);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
logger.warn("Credential store unreadable, treating as empty", {
|
|
275
|
+
error: err.message
|
|
276
|
+
});
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
if (!cred) return null;
|
|
280
|
+
if (cred.expiresAt && cred.expiresAt < Date.now()) return null;
|
|
281
|
+
return cred;
|
|
282
|
+
}
|
|
283
|
+
function persistOperator(baseUrl, response) {
|
|
284
|
+
const cred = {
|
|
285
|
+
pat: response.pat,
|
|
286
|
+
email: response.email,
|
|
287
|
+
userId: response.userId,
|
|
288
|
+
role: response.role,
|
|
289
|
+
workspaces: response.workspaces,
|
|
290
|
+
expiresAt: response.expiresAt
|
|
291
|
+
};
|
|
292
|
+
try {
|
|
293
|
+
getCredentialStore().set(baseUrl, cred);
|
|
294
|
+
return { cred, persisted: true };
|
|
295
|
+
} catch (err) {
|
|
296
|
+
logger.warn(
|
|
297
|
+
"Could not write operator credential to disk \u2014 using in-memory only for this session",
|
|
298
|
+
{ error: err.message }
|
|
299
|
+
);
|
|
300
|
+
return { cred, persisted: false };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function clearOperator(baseUrl) {
|
|
304
|
+
return getCredentialStore().clear(baseUrl);
|
|
305
|
+
}
|
|
306
|
+
|
|
157
307
|
// src/client/databrain-client.ts
|
|
158
308
|
var MCP_SERVER_VERSION = "0.2.0";
|
|
159
309
|
var MCP_USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION}`;
|
|
@@ -175,6 +325,8 @@ var DatabrainClient = class {
|
|
|
175
325
|
}
|
|
176
326
|
cache = new TtlCache();
|
|
177
327
|
runtimeApiToken;
|
|
328
|
+
operator = null;
|
|
329
|
+
operatorLoaded = false;
|
|
178
330
|
hasApiToken() {
|
|
179
331
|
return !!(this.runtimeApiToken || this.config.apiToken);
|
|
180
332
|
}
|
|
@@ -188,12 +340,90 @@ var DatabrainClient = class {
|
|
|
188
340
|
getApiUrl() {
|
|
189
341
|
return this.config.apiUrl;
|
|
190
342
|
}
|
|
343
|
+
getDemoDomain() {
|
|
344
|
+
return this.config.demoDomain;
|
|
345
|
+
}
|
|
346
|
+
getOperator() {
|
|
347
|
+
if (!this.operatorLoaded) {
|
|
348
|
+
this.operator = loadCachedOperator(this.config.apiUrl);
|
|
349
|
+
this.operatorLoaded = true;
|
|
350
|
+
}
|
|
351
|
+
return this.operator;
|
|
352
|
+
}
|
|
353
|
+
setOperator(cred) {
|
|
354
|
+
this.operator = cred;
|
|
355
|
+
this.operatorLoaded = true;
|
|
356
|
+
this.cache.invalidateByPrefix("");
|
|
357
|
+
}
|
|
358
|
+
clearOperator() {
|
|
359
|
+
this.operator = null;
|
|
360
|
+
this.operatorLoaded = true;
|
|
361
|
+
this.cache.invalidateByPrefix("");
|
|
362
|
+
return clearOperator(this.config.apiUrl);
|
|
363
|
+
}
|
|
364
|
+
async bootstrapOperator(email) {
|
|
365
|
+
if (!this.config.serviceToken) {
|
|
366
|
+
throw new DatabrainApiError(
|
|
367
|
+
401,
|
|
368
|
+
"MISSING_SERVICE_TOKEN",
|
|
369
|
+
"Cannot connect \u2014 DATABRAIN_SERVICE_TOKEN is not configured. Set it in your MCP server config first."
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const response = await this.request({
|
|
373
|
+
method: "POST",
|
|
374
|
+
path: "/api/mcp/bootstrap",
|
|
375
|
+
body: { email },
|
|
376
|
+
auth: "service",
|
|
377
|
+
// Bootstrap uses the service token explicitly — not the operator PAT.
|
|
378
|
+
token: this.config.serviceToken
|
|
379
|
+
});
|
|
380
|
+
const result = persistOperator(this.config.apiUrl, response);
|
|
381
|
+
this.setOperator(result.cred);
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Describes which credential is currently used as the bearer for outgoing
|
|
386
|
+
* API calls. Used by `whoami` so the LLM can give the user a truthful answer.
|
|
387
|
+
*/
|
|
388
|
+
getActiveAuth() {
|
|
389
|
+
if (this.config.pat) return { source: "env-pat" };
|
|
390
|
+
const op = this.getOperator();
|
|
391
|
+
if (op?.pat) return { source: "cached-pat", operator: op };
|
|
392
|
+
if (this.config.serviceToken) return { source: "service-token" };
|
|
393
|
+
if (this.runtimeApiToken || this.config.apiToken) return { source: "api-token" };
|
|
394
|
+
return { source: "none" };
|
|
395
|
+
}
|
|
191
396
|
getToken(auth) {
|
|
192
397
|
if (auth === "none") return void 0;
|
|
398
|
+
if (this.config.pat) return this.config.pat;
|
|
399
|
+
const op = this.getOperator();
|
|
400
|
+
if (op?.pat) return op.pat;
|
|
193
401
|
if (auth === "service") return this.config.serviceToken;
|
|
194
402
|
return this.runtimeApiToken || this.config.apiToken;
|
|
195
403
|
}
|
|
196
404
|
async request(options) {
|
|
405
|
+
try {
|
|
406
|
+
return await this.requestInternal(options);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
if (err instanceof DatabrainApiError && err.code === "PAT_EXPIRED" && !options.token && !this.config.pat && options.path !== "/api/mcp/bootstrap") {
|
|
409
|
+
const op = this.getOperator();
|
|
410
|
+
if (op?.email) {
|
|
411
|
+
logger.info("Operator PAT expired \u2014 silently re-bootstrapping");
|
|
412
|
+
try {
|
|
413
|
+
await this.bootstrapOperator(op.email);
|
|
414
|
+
} catch (bootstrapErr) {
|
|
415
|
+
logger.warn("Silent re-bootstrap failed", {
|
|
416
|
+
error: bootstrapErr.message
|
|
417
|
+
});
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
return this.requestInternal(options);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
throw err;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async requestInternal(options) {
|
|
197
427
|
if (options.invalidateCache) {
|
|
198
428
|
for (const prefix of options.invalidateCache) {
|
|
199
429
|
this.cache.invalidateByPrefix(prefix);
|
|
@@ -1990,79 +2220,8 @@ function registerListWorkspaces(server, client) {
|
|
|
1990
2220
|
);
|
|
1991
2221
|
}
|
|
1992
2222
|
|
|
1993
|
-
// src/tools/infrastructure/link-datamart-to-workspace.ts
|
|
1994
|
-
import { z as z9 } from "zod";
|
|
1995
|
-
function registerLinkDatamartToWorkspace(server, client) {
|
|
1996
|
-
server.tool(
|
|
1997
|
-
"link_datamart_to_workspace",
|
|
1998
|
-
"Link a datamart to an existing workspace. ALWAYS ask the user which workspace to use \u2014 do not auto-select.",
|
|
1999
|
-
{
|
|
2000
|
-
workspaceName: z9.string().optional().describe(
|
|
2001
|
-
"Name of the workspace to update. Omit to list available workspaces and let the user choose."
|
|
2002
|
-
),
|
|
2003
|
-
datamartName: z9.string().describe(
|
|
2004
|
-
"Name of the datamart to link. Use `list_datamarts` to find it."
|
|
2005
|
-
)
|
|
2006
|
-
},
|
|
2007
|
-
WRITE_IDEMPOTENT,
|
|
2008
|
-
async ({ workspaceName, datamartName }) => {
|
|
2009
|
-
try {
|
|
2010
|
-
if (!workspaceName) {
|
|
2011
|
-
const workspaces = await client.request({
|
|
2012
|
-
method: "GET",
|
|
2013
|
-
path: "/api/v2/workspace",
|
|
2014
|
-
auth: "service",
|
|
2015
|
-
cacheKey: "list_workspaces",
|
|
2016
|
-
cacheTtlMs: 5 * 60 * 1e3
|
|
2017
|
-
});
|
|
2018
|
-
if (!Array.isArray(workspaces) || workspaces.length === 0) {
|
|
2019
|
-
return toolError(
|
|
2020
|
-
"No workspaces found. Create a workspace in the **Databrain UI** first, then come back here to link the datamart."
|
|
2021
|
-
);
|
|
2022
|
-
}
|
|
2023
|
-
const list = workspaces.map((ws) => `- **${ws.name}** (ID: ${ws._id || ws.id})`).join("\n");
|
|
2024
|
-
return toolSuccess(
|
|
2025
|
-
`Found ${workspaces.length} workspace(s):
|
|
2026
|
-
|
|
2027
|
-
${list}
|
|
2028
|
-
|
|
2029
|
-
**Ask the user** which workspace they want to link to datamart **${datamartName}**, then call \`link_datamart_to_workspace\` with both \`workspaceName\` and \`datamartName\`.`
|
|
2030
|
-
);
|
|
2031
|
-
}
|
|
2032
|
-
const result = await client.request({
|
|
2033
|
-
method: "PUT",
|
|
2034
|
-
path: "/api/v2/workspace",
|
|
2035
|
-
body: {
|
|
2036
|
-
name: workspaceName,
|
|
2037
|
-
connectionType: "DATAMART",
|
|
2038
|
-
datamartName
|
|
2039
|
-
},
|
|
2040
|
-
auth: "service",
|
|
2041
|
-
invalidateCache: ["list_workspaces"]
|
|
2042
|
-
});
|
|
2043
|
-
if (result.error) {
|
|
2044
|
-
return toolError(
|
|
2045
|
-
`Failed to link datamart: ${typeof result.error === "object" ? JSON.stringify(result.error) : result.error}`
|
|
2046
|
-
);
|
|
2047
|
-
}
|
|
2048
|
-
return toolSuccess(
|
|
2049
|
-
`Workspace **${workspaceName}** is now linked to datamart **${datamartName}**.
|
|
2050
|
-
|
|
2051
|
-
Next steps:
|
|
2052
|
-
- Build dashboards in the Databrain UI using this workspace
|
|
2053
|
-
- Use \`list_dashboards\` to find dashboards in this workspace
|
|
2054
|
-
- Use \`create_embed\` to embed dashboards into your app`
|
|
2055
|
-
);
|
|
2056
|
-
} catch (err) {
|
|
2057
|
-
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
2058
|
-
return toolError(`Unexpected error: ${err.message}`);
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
);
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
2223
|
// src/tools/embed/create-embed.ts
|
|
2065
|
-
import { z as
|
|
2224
|
+
import { z as z9 } from "zod";
|
|
2066
2225
|
var DEFAULT_ACCESS_SETTINGS2 = {
|
|
2067
2226
|
isAllowMetricCreation: false,
|
|
2068
2227
|
isAllowMetricDeletion: false,
|
|
@@ -2075,18 +2234,18 @@ var DEFAULT_ACCESS_SETTINGS2 = {
|
|
|
2075
2234
|
isAllowCreateDashboardView: false,
|
|
2076
2235
|
metricCreationMode: "DRAG_DROP"
|
|
2077
2236
|
};
|
|
2078
|
-
var accessSettingsSchema =
|
|
2079
|
-
datamartName:
|
|
2080
|
-
isAllowMetricCreation:
|
|
2081
|
-
metricCreationMode:
|
|
2082
|
-
isAllowMetricDeletion:
|
|
2083
|
-
isAllowMetricUpdate:
|
|
2084
|
-
isAllowMetricLayoutChange:
|
|
2085
|
-
isAllowManageMetrics:
|
|
2086
|
-
isAllowUnderlyingData:
|
|
2087
|
-
isAllowAiPilot:
|
|
2088
|
-
isAllowEmailReports:
|
|
2089
|
-
isAllowCreateDashboardView:
|
|
2237
|
+
var accessSettingsSchema = z9.object({
|
|
2238
|
+
datamartName: z9.string().describe("Name of the datamart (required)"),
|
|
2239
|
+
isAllowMetricCreation: z9.boolean().optional().describe("Allow end-users to create metrics"),
|
|
2240
|
+
metricCreationMode: z9.enum(["DRAG_DROP", "CHAT"]).optional().describe("Metric creation mode"),
|
|
2241
|
+
isAllowMetricDeletion: z9.boolean().optional().describe("Allow metric deletion"),
|
|
2242
|
+
isAllowMetricUpdate: z9.boolean().optional().describe("Allow metric editing"),
|
|
2243
|
+
isAllowMetricLayoutChange: z9.boolean().optional().describe("Allow layout changes"),
|
|
2244
|
+
isAllowManageMetrics: z9.boolean().optional().describe("Show manage metrics UI"),
|
|
2245
|
+
isAllowUnderlyingData: z9.boolean().optional().describe("Allow viewing underlying data"),
|
|
2246
|
+
isAllowAiPilot: z9.boolean().optional().describe("Enable AI pilot features"),
|
|
2247
|
+
isAllowEmailReports: z9.boolean().optional().describe("Allow email report scheduling"),
|
|
2248
|
+
isAllowCreateDashboardView: z9.boolean().optional().describe("Allow creating dashboard views")
|
|
2090
2249
|
}).describe("Embed access settings. Only datamartName is required \u2014 all flags default to false.");
|
|
2091
2250
|
function registerCreateEmbed(server, client) {
|
|
2092
2251
|
const services = createServiceBundle(client);
|
|
@@ -2094,23 +2253,23 @@ function registerCreateEmbed(server, client) {
|
|
|
2094
2253
|
"create_embed",
|
|
2095
2254
|
"Create an embed. Provide dashboardId, workspaceName, accessSettings.datamartName. Add clientId for multi-tenant per-client dashboards. All isAllow* flags default to false.",
|
|
2096
2255
|
{
|
|
2097
|
-
body:
|
|
2256
|
+
body: z9.record(z9.unknown()).optional().describe(
|
|
2098
2257
|
"Raw API body passthrough. If provided, all other fields are ignored."
|
|
2099
2258
|
),
|
|
2100
|
-
dashboardId:
|
|
2101
|
-
workspaceName:
|
|
2102
|
-
embedType:
|
|
2103
|
-
metricId:
|
|
2104
|
-
name:
|
|
2259
|
+
dashboardId: z9.string().optional().describe("Dashboard identifier for embed creation"),
|
|
2260
|
+
workspaceName: z9.string().optional().describe("Workspace name. Omit to auto-resolve if only one exists."),
|
|
2261
|
+
embedType: z9.enum(["dashboard", "metric"]).optional().describe("Embed type"),
|
|
2262
|
+
metricId: z9.string().optional().describe("Required for metric embed type"),
|
|
2263
|
+
name: z9.string().optional().describe("Human-readable embed config name"),
|
|
2105
2264
|
accessSettings: accessSettingsSchema.optional(),
|
|
2106
2265
|
// Multi-tenant (dashboard embed) params
|
|
2107
|
-
clientId:
|
|
2266
|
+
clientId: z9.string().optional().describe(
|
|
2108
2267
|
"Client identifier for multi-tenant isolation. When provided, creates a dashboard embed (each client gets their own dashboard copy)."
|
|
2109
2268
|
),
|
|
2110
|
-
templateDashboardId:
|
|
2111
|
-
isImportMetrics:
|
|
2112
|
-
metricImportMode:
|
|
2113
|
-
isRenameDashboard:
|
|
2269
|
+
templateDashboardId: z9.string().optional().describe("Template dashboard ID to clone metrics from"),
|
|
2270
|
+
isImportMetrics: z9.boolean().optional().describe("Import metrics from template dashboard"),
|
|
2271
|
+
metricImportMode: z9.enum(["COPY", "LINK"]).optional().describe("COPY duplicates metrics, LINK shares them"),
|
|
2272
|
+
isRenameDashboard: z9.boolean().optional().describe("Rename the dashboard for this client")
|
|
2114
2273
|
},
|
|
2115
2274
|
WRITE_CREATE,
|
|
2116
2275
|
async (params) => {
|
|
@@ -2212,14 +2371,14 @@ Next: run \`get_embed_details\` to verify the embed configuration.`
|
|
|
2212
2371
|
}
|
|
2213
2372
|
|
|
2214
2373
|
// src/tools/embed/list-embeds.ts
|
|
2215
|
-
import { z as
|
|
2374
|
+
import { z as z10 } from "zod";
|
|
2216
2375
|
function registerListEmbeds(server, client) {
|
|
2217
2376
|
const services = createServiceBundle(client);
|
|
2218
2377
|
server.tool(
|
|
2219
2378
|
"list_embeds",
|
|
2220
|
-
"List embed configurations. Requires an API token (use create_api_token first).",
|
|
2379
|
+
"List embed configurations. Requires an API token (use create_api_token first). Response includes `distinctClientIds` \u2014 a deduped list of end-customer client IDs that have multi-tenant dashboard embeds, useful for discovering which customers exist when only the workspace operator persona knows them.",
|
|
2221
2380
|
{
|
|
2222
|
-
dataAppName:
|
|
2381
|
+
dataAppName: z10.string().optional().describe("Filter by data app name")
|
|
2223
2382
|
},
|
|
2224
2383
|
READ_ONLY,
|
|
2225
2384
|
async (params) => {
|
|
@@ -2230,19 +2389,28 @@ function registerListEmbeds(server, client) {
|
|
|
2230
2389
|
"No embeds found. Create an embed in the Databrain UI, then run `list_embeds` again."
|
|
2231
2390
|
);
|
|
2232
2391
|
}
|
|
2392
|
+
const embeds = result.map((e) => {
|
|
2393
|
+
const n = services.discoveryService.normalizeEmbed(e);
|
|
2394
|
+
const clientId = typeof e.clientId === "string" && e.clientId.trim().length > 0 ? e.clientId : null;
|
|
2395
|
+
return {
|
|
2396
|
+
embedId: n.embedId || null,
|
|
2397
|
+
internalId: !n.embedId && n.internalId ? n.internalId : void 0,
|
|
2398
|
+
name: n.name,
|
|
2399
|
+
type: n.embedType || "dashboard",
|
|
2400
|
+
dashboardId: n.dashboardId || null,
|
|
2401
|
+
dataApp: n.dataAppName || null,
|
|
2402
|
+
clientId
|
|
2403
|
+
};
|
|
2404
|
+
});
|
|
2405
|
+
const distinctClientIds = Array.from(
|
|
2406
|
+
new Set(
|
|
2407
|
+
embeds.map((e) => e.clientId).filter((id) => typeof id === "string" && id.length > 0)
|
|
2408
|
+
)
|
|
2409
|
+
).sort();
|
|
2233
2410
|
return toolSuccess(JSON.stringify({
|
|
2234
|
-
count:
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
return {
|
|
2238
|
-
embedId: n.embedId || null,
|
|
2239
|
-
internalId: !n.embedId && n.internalId ? n.internalId : void 0,
|
|
2240
|
-
name: n.name,
|
|
2241
|
-
type: n.embedType || "dashboard",
|
|
2242
|
-
dashboardId: n.dashboardId || null,
|
|
2243
|
-
dataApp: n.dataAppName || null
|
|
2244
|
-
};
|
|
2245
|
-
})
|
|
2411
|
+
count: embeds.length,
|
|
2412
|
+
distinctClientIds,
|
|
2413
|
+
embeds
|
|
2246
2414
|
}));
|
|
2247
2415
|
} catch (err) {
|
|
2248
2416
|
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
@@ -2253,14 +2421,14 @@ function registerListEmbeds(server, client) {
|
|
|
2253
2421
|
}
|
|
2254
2422
|
|
|
2255
2423
|
// src/tools/embed/get-embed-details.ts
|
|
2256
|
-
import { z as
|
|
2424
|
+
import { z as z11 } from "zod";
|
|
2257
2425
|
function registerGetEmbedDetails(server, client) {
|
|
2258
2426
|
const services = createServiceBundle(client);
|
|
2259
2427
|
server.tool(
|
|
2260
2428
|
"get_embed_details",
|
|
2261
2429
|
"Get full embed configuration including access settings and theme.",
|
|
2262
2430
|
{
|
|
2263
|
-
embedId:
|
|
2431
|
+
embedId: z11.string().describe("ID of the embed")
|
|
2264
2432
|
},
|
|
2265
2433
|
READ_ONLY,
|
|
2266
2434
|
async (params) => {
|
|
@@ -2276,7 +2444,7 @@ function registerGetEmbedDetails(server, client) {
|
|
|
2276
2444
|
}
|
|
2277
2445
|
|
|
2278
2446
|
// src/tools/embed/update-embed.ts
|
|
2279
|
-
import { z as
|
|
2447
|
+
import { z as z12 } from "zod";
|
|
2280
2448
|
|
|
2281
2449
|
// src/tools/embed/helpers.ts
|
|
2282
2450
|
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.";
|
|
@@ -2314,15 +2482,15 @@ function registerUpdateEmbed(server, client) {
|
|
|
2314
2482
|
"update_embed",
|
|
2315
2483
|
"Update embed access settings, permissions, theme, or name.",
|
|
2316
2484
|
{
|
|
2317
|
-
embedId:
|
|
2318
|
-
body:
|
|
2485
|
+
embedId: z12.string().describe("ID of the embed to update"),
|
|
2486
|
+
body: z12.record(z12.unknown()).optional().describe(
|
|
2319
2487
|
"Raw API body for PUT /api/v2/data-app/embeds. If provided, it is sent directly (passthrough mode)."
|
|
2320
2488
|
),
|
|
2321
|
-
name:
|
|
2322
|
-
accessSettings:
|
|
2489
|
+
name: z12.string().optional().describe("New name for the embed"),
|
|
2490
|
+
accessSettings: z12.record(z12.unknown()).optional().describe(
|
|
2323
2491
|
"Partial update of access settings. See `get_embed_details` for current settings."
|
|
2324
2492
|
),
|
|
2325
|
-
theme:
|
|
2493
|
+
theme: z12.record(z12.unknown()).optional().describe(
|
|
2326
2494
|
"Theme configuration object. See databrain://theme-reference for the full schema."
|
|
2327
2495
|
)
|
|
2328
2496
|
},
|
|
@@ -2368,7 +2536,7 @@ function registerUpdateEmbed(server, client) {
|
|
|
2368
2536
|
}
|
|
2369
2537
|
|
|
2370
2538
|
// src/tools/embed/customize-theme.ts
|
|
2371
|
-
import { z as
|
|
2539
|
+
import { z as z13 } from "zod";
|
|
2372
2540
|
var THEME_PRESETS = {
|
|
2373
2541
|
light: {
|
|
2374
2542
|
general: {
|
|
@@ -2502,7 +2670,7 @@ var ACCESS_PRESETS = {
|
|
|
2502
2670
|
metricCreationMode: "CHAT"
|
|
2503
2671
|
}
|
|
2504
2672
|
};
|
|
2505
|
-
var fontFamilyEnum =
|
|
2673
|
+
var fontFamilyEnum = z13.enum([
|
|
2506
2674
|
"Inter",
|
|
2507
2675
|
"Roboto",
|
|
2508
2676
|
"Open Sans",
|
|
@@ -2514,17 +2682,17 @@ var fontFamilyEnum = z14.enum([
|
|
|
2514
2682
|
"Nunito",
|
|
2515
2683
|
"system-ui"
|
|
2516
2684
|
]).describe("Font family");
|
|
2517
|
-
var tooltipFontSchema =
|
|
2518
|
-
fontFamily:
|
|
2519
|
-
fontSize:
|
|
2520
|
-
fontWeight:
|
|
2521
|
-
fontColor:
|
|
2685
|
+
var tooltipFontSchema = z13.object({
|
|
2686
|
+
fontFamily: z13.string().optional(),
|
|
2687
|
+
fontSize: z13.number().optional(),
|
|
2688
|
+
fontWeight: z13.number().optional(),
|
|
2689
|
+
fontColor: z13.string().optional()
|
|
2522
2690
|
}).optional();
|
|
2523
|
-
var fontConfigInputSchema =
|
|
2524
|
-
fontSize:
|
|
2525
|
-
fontFamily:
|
|
2526
|
-
fontWeight:
|
|
2527
|
-
color:
|
|
2691
|
+
var fontConfigInputSchema = z13.object({
|
|
2692
|
+
fontSize: z13.number().optional(),
|
|
2693
|
+
fontFamily: z13.string().optional(),
|
|
2694
|
+
fontWeight: z13.number().optional(),
|
|
2695
|
+
color: z13.string().optional()
|
|
2528
2696
|
}).optional();
|
|
2529
2697
|
function mapToFrontendFontConfig(input) {
|
|
2530
2698
|
if (!input) return void 0;
|
|
@@ -2535,23 +2703,23 @@ function mapToFrontendFontConfig(input) {
|
|
|
2535
2703
|
...input.color && { color: input.color }
|
|
2536
2704
|
};
|
|
2537
2705
|
}
|
|
2538
|
-
var axisSchema =
|
|
2539
|
-
hideAxisLines:
|
|
2540
|
-
hideSplitLines:
|
|
2541
|
-
hideAxisLabels:
|
|
2542
|
-
hideAxisTicks:
|
|
2543
|
-
axisName:
|
|
2544
|
-
axisNameOffset:
|
|
2545
|
-
axisLabelMargin:
|
|
2546
|
-
fontSize:
|
|
2547
|
-
fontFamily:
|
|
2548
|
-
fontWeight:
|
|
2549
|
-
color:
|
|
2550
|
-
axisColor:
|
|
2551
|
-
axisNameFontConfig:
|
|
2552
|
-
fontFamily:
|
|
2553
|
-
fontSize:
|
|
2554
|
-
fontWeight:
|
|
2706
|
+
var axisSchema = z13.object({
|
|
2707
|
+
hideAxisLines: z13.boolean().optional().describe("Hide axis lines"),
|
|
2708
|
+
hideSplitLines: z13.boolean().optional().describe("Hide grid/split lines"),
|
|
2709
|
+
hideAxisLabels: z13.boolean().optional().describe("Hide axis labels"),
|
|
2710
|
+
hideAxisTicks: z13.boolean().optional().describe("Hide axis tick marks"),
|
|
2711
|
+
axisName: z13.string().optional().describe("Axis title text"),
|
|
2712
|
+
axisNameOffset: z13.number().optional().describe("Axis title offset from axis"),
|
|
2713
|
+
axisLabelMargin: z13.number().optional().describe("Margin between axis labels and chart area in pixels"),
|
|
2714
|
+
fontSize: z13.number().optional().describe("Axis label font size"),
|
|
2715
|
+
fontFamily: z13.string().optional().describe("Axis label font family"),
|
|
2716
|
+
fontWeight: z13.number().optional().describe("Axis label font weight"),
|
|
2717
|
+
color: z13.string().optional().describe("Axis label color (hex)"),
|
|
2718
|
+
axisColor: z13.string().optional().describe("Axis line color (hex)"),
|
|
2719
|
+
axisNameFontConfig: z13.object({
|
|
2720
|
+
fontFamily: z13.string().optional(),
|
|
2721
|
+
fontSize: z13.number().optional(),
|
|
2722
|
+
fontWeight: z13.number().optional()
|
|
2555
2723
|
}).optional().describe("Axis title font configuration")
|
|
2556
2724
|
}).optional();
|
|
2557
2725
|
function buildAdminTheme(p, presetBase) {
|
|
@@ -2707,94 +2875,94 @@ function registerCustomizeTheme(server, client) {
|
|
|
2707
2875
|
"customize_embed_theme",
|
|
2708
2876
|
`Theme an embed: presets (${Object.keys(THEME_PRESETS).join("/")}), colors, fonts, cards, chart appearance, responsive layout, or access presets (${Object.keys(ACCESS_PRESETS).join("/")}).`,
|
|
2709
2877
|
{
|
|
2710
|
-
embedId:
|
|
2711
|
-
body:
|
|
2878
|
+
embedId: z13.string().describe("ID of the embed to theme"),
|
|
2879
|
+
body: z13.record(z13.unknown()).optional().describe(
|
|
2712
2880
|
"Raw API body for PUT /api/v2/data-app/embeds. If provided, sent directly and all convenience fields are ignored."
|
|
2713
2881
|
),
|
|
2714
2882
|
// --- Presets ---
|
|
2715
|
-
themePreset:
|
|
2716
|
-
accessPreset:
|
|
2883
|
+
themePreset: z13.enum(["light", "dark", "corporate", "minimal"]).optional().describe("Apply a theme preset as the base. Granular params below override preset values."),
|
|
2884
|
+
accessPreset: z13.enum(["view-only", "power-user", "export-only", "ai-enabled"]).optional().describe("Apply an access settings preset"),
|
|
2717
2885
|
// --- General ---
|
|
2718
2886
|
fontFamily: fontFamilyEnum.optional().describe("Global font family"),
|
|
2719
|
-
datePickerFormat:
|
|
2887
|
+
datePickerFormat: z13.string().optional().describe("Date picker display format"),
|
|
2720
2888
|
// --- Colors ---
|
|
2721
|
-
primaryColor:
|
|
2722
|
-
backgroundColor:
|
|
2723
|
-
textColor:
|
|
2724
|
-
ctaColor:
|
|
2725
|
-
ctaTextColor:
|
|
2889
|
+
primaryColor: z13.string().optional().describe("Primary accent color (hex)"),
|
|
2890
|
+
backgroundColor: z13.string().optional().describe("Overall background color (hex)"),
|
|
2891
|
+
textColor: z13.string().optional().describe("Default text color (hex)"),
|
|
2892
|
+
ctaColor: z13.string().optional().describe("Call-to-action button color (hex)"),
|
|
2893
|
+
ctaTextColor: z13.string().optional().describe("CTA button text color (hex)"),
|
|
2726
2894
|
// --- Dashboard ---
|
|
2727
|
-
dashboardBackgroundColor:
|
|
2728
|
-
selectBoxSize:
|
|
2729
|
-
selectBoxVariant:
|
|
2730
|
-
selectBoxBorderRadius:
|
|
2731
|
-
selectBoxTextColor:
|
|
2732
|
-
metricCardColor:
|
|
2895
|
+
dashboardBackgroundColor: z13.string().optional().describe("Dashboard container background (hex)"),
|
|
2896
|
+
selectBoxSize: z13.enum(["small", "medium", "large"]).optional().describe("Filter select box size"),
|
|
2897
|
+
selectBoxVariant: z13.enum(["floating", "outlined", "standard"]).optional().describe("Filter select box style"),
|
|
2898
|
+
selectBoxBorderRadius: z13.string().optional().describe('Select box border radius (e.g. "20px")'),
|
|
2899
|
+
selectBoxTextColor: z13.string().optional().describe("Select box text color (hex)"),
|
|
2900
|
+
metricCardColor: z13.string().optional().describe("Metric card background color (hex)"),
|
|
2733
2901
|
// --- Card Title ---
|
|
2734
|
-
cardTitleFontSize:
|
|
2735
|
-
cardTitleFontWeight:
|
|
2736
|
-
cardTitleColor:
|
|
2902
|
+
cardTitleFontSize: z13.string().optional().describe('Card title font size (e.g. "16px")'),
|
|
2903
|
+
cardTitleFontWeight: z13.string().optional().describe('Card title font weight (e.g. "600")'),
|
|
2904
|
+
cardTitleColor: z13.string().optional().describe("Card title text color (hex)"),
|
|
2737
2905
|
// --- Card Description ---
|
|
2738
|
-
cardDescriptionFontSize:
|
|
2739
|
-
cardDescriptionFontWeight:
|
|
2740
|
-
cardDescriptionColor:
|
|
2906
|
+
cardDescriptionFontSize: z13.string().optional().describe("Card description font size"),
|
|
2907
|
+
cardDescriptionFontWeight: z13.string().optional().describe("Card description font weight"),
|
|
2908
|
+
cardDescriptionColor: z13.string().optional().describe("Card description text color (hex)"),
|
|
2741
2909
|
// --- Card Customization ---
|
|
2742
|
-
cardPadding:
|
|
2743
|
-
cardBorderRadius:
|
|
2744
|
-
cardShadow:
|
|
2745
|
-
disableShadowOnHover:
|
|
2746
|
-
disableCardStroke:
|
|
2747
|
-
cardStrokeColor:
|
|
2910
|
+
cardPadding: z13.string().optional().describe("Card internal padding"),
|
|
2911
|
+
cardBorderRadius: z13.string().optional().describe("Card border radius"),
|
|
2912
|
+
cardShadow: z13.string().optional().describe("Card box-shadow CSS value"),
|
|
2913
|
+
disableShadowOnHover: z13.boolean().optional().describe("Disable shadow on card hover"),
|
|
2914
|
+
disableCardStroke: z13.boolean().optional().describe("Remove card border stroke"),
|
|
2915
|
+
cardStrokeColor: z13.string().optional().describe("Card border stroke color (hex)"),
|
|
2748
2916
|
// --- Chart Colors (admin theme level) ---
|
|
2749
|
-
chartFontFamily:
|
|
2750
|
-
chartFontColor:
|
|
2751
|
-
chartColors:
|
|
2752
|
-
chartPaletteName:
|
|
2753
|
-
chartPaletteColors:
|
|
2917
|
+
chartFontFamily: z13.string().optional().describe("Font family for chart text"),
|
|
2918
|
+
chartFontColor: z13.string().optional().describe("Font color for chart text (hex)"),
|
|
2919
|
+
chartColors: z13.array(z13.string()).optional().describe("Array of chart series colors (hex)"),
|
|
2920
|
+
chartPaletteName: z13.string().optional().describe("Name for a custom palette"),
|
|
2921
|
+
chartPaletteColors: z13.array(z13.string()).optional().describe("9 colors for a custom palette"),
|
|
2754
2922
|
// --- Chart Tooltip (admin theme level) ---
|
|
2755
2923
|
tooltipHeader: tooltipFontSchema.describe("Tooltip header font config"),
|
|
2756
2924
|
tooltipLabel: tooltipFontSchema.describe("Tooltip label font config"),
|
|
2757
2925
|
tooltipValue: tooltipFontSchema.describe("Tooltip value font config"),
|
|
2758
2926
|
// --- Component Theme ---
|
|
2759
|
-
buttonPrimaryColor:
|
|
2760
|
-
buttonPrimaryTextColor:
|
|
2761
|
-
buttonSecondaryColor:
|
|
2762
|
-
buttonSecondaryTextColor:
|
|
2763
|
-
checkboxCheckedColor:
|
|
2764
|
-
checkboxUncheckedColor:
|
|
2765
|
-
switchEnabledColor:
|
|
2766
|
-
switchDisabledColor:
|
|
2767
|
-
breadcrumbFontFamily:
|
|
2768
|
-
breadcrumbFontColor:
|
|
2769
|
-
breadcrumbActiveColor:
|
|
2770
|
-
filterBadgeColor:
|
|
2771
|
-
filterBadgeTextColor:
|
|
2772
|
-
datePickerColor:
|
|
2927
|
+
buttonPrimaryColor: z13.string().optional().describe("Primary button background (hex)"),
|
|
2928
|
+
buttonPrimaryTextColor: z13.string().optional().describe("Primary button text (hex)"),
|
|
2929
|
+
buttonSecondaryColor: z13.string().optional().describe("Secondary button background (hex)"),
|
|
2930
|
+
buttonSecondaryTextColor: z13.string().optional().describe("Secondary button text (hex)"),
|
|
2931
|
+
checkboxCheckedColor: z13.string().optional().describe("Checked checkbox color (hex)"),
|
|
2932
|
+
checkboxUncheckedColor: z13.string().optional().describe("Unchecked checkbox color (hex)"),
|
|
2933
|
+
switchEnabledColor: z13.string().optional().describe("Enabled switch color (hex)"),
|
|
2934
|
+
switchDisabledColor: z13.string().optional().describe("Disabled switch color (hex)"),
|
|
2935
|
+
breadcrumbFontFamily: z13.string().optional().describe("Drill breadcrumb font family"),
|
|
2936
|
+
breadcrumbFontColor: z13.string().optional().describe("Drill breadcrumb font color (hex)"),
|
|
2937
|
+
breadcrumbActiveColor: z13.string().optional().describe("Active breadcrumb color (hex)"),
|
|
2938
|
+
filterBadgeColor: z13.string().optional().describe("Multi-select filter badge color (hex)"),
|
|
2939
|
+
filterBadgeTextColor: z13.string().optional().describe("Filter badge text color (hex)"),
|
|
2940
|
+
datePickerColor: z13.string().optional().describe("Date picker accent color (hex)"),
|
|
2773
2941
|
// --- Responsive Breakpoints ---
|
|
2774
|
-
breakpointXl:
|
|
2775
|
-
breakpointLg:
|
|
2776
|
-
breakpointMd:
|
|
2777
|
-
breakpointSm:
|
|
2778
|
-
breakpointXs:
|
|
2942
|
+
breakpointXl: z13.number().optional().describe("XL breakpoint width in px"),
|
|
2943
|
+
breakpointLg: z13.number().optional().describe("LG breakpoint width in px"),
|
|
2944
|
+
breakpointMd: z13.number().optional().describe("MD breakpoint width in px"),
|
|
2945
|
+
breakpointSm: z13.number().optional().describe("SM breakpoint width in px"),
|
|
2946
|
+
breakpointXs: z13.number().optional().describe("XS breakpoint width in px"),
|
|
2779
2947
|
// --- Metric Layout Columns ---
|
|
2780
|
-
layoutColsXl:
|
|
2781
|
-
layoutColsLg:
|
|
2782
|
-
layoutColsMd:
|
|
2783
|
-
layoutColsSm:
|
|
2784
|
-
layoutColsXs:
|
|
2785
|
-
layoutColsXxs:
|
|
2948
|
+
layoutColsXl: z13.number().optional().describe("Grid columns at XL breakpoint"),
|
|
2949
|
+
layoutColsLg: z13.number().optional().describe("Grid columns at LG breakpoint"),
|
|
2950
|
+
layoutColsMd: z13.number().optional().describe("Grid columns at MD breakpoint"),
|
|
2951
|
+
layoutColsSm: z13.number().optional().describe("Grid columns at SM breakpoint"),
|
|
2952
|
+
layoutColsXs: z13.number().optional().describe("Grid columns at XS breakpoint"),
|
|
2953
|
+
layoutColsXxs: z13.number().optional().describe("Grid columns at XXS breakpoint"),
|
|
2786
2954
|
// --- Chart Appearance (detailed) ---
|
|
2787
2955
|
tooltipHeaderStyle: fontConfigInputSchema.describe("Tooltip header: fontSize, fontFamily, fontWeight, color"),
|
|
2788
2956
|
tooltipLabelStyle: fontConfigInputSchema.describe("Tooltip label: fontSize, fontFamily, fontWeight, color"),
|
|
2789
2957
|
tooltipValueStyle: fontConfigInputSchema.describe("Tooltip value: fontSize, fontFamily, fontWeight, color"),
|
|
2790
|
-
labelPosition:
|
|
2791
|
-
radialLabelPosition:
|
|
2792
|
-
marginTop:
|
|
2793
|
-
marginLeft:
|
|
2794
|
-
marginRight:
|
|
2795
|
-
marginBottom:
|
|
2796
|
-
legendShow:
|
|
2797
|
-
legendPosition:
|
|
2958
|
+
labelPosition: z13.enum(["hidden", "top", "left", "right", "bottom", "inside"]).optional().describe("Data label position on charts"),
|
|
2959
|
+
radialLabelPosition: z13.enum(["outside", "inside"]).optional().describe("Label position for radial/pie charts"),
|
|
2960
|
+
marginTop: z13.number().optional().describe("Chart top margin in pixels"),
|
|
2961
|
+
marginLeft: z13.number().optional().describe("Chart left margin in pixels"),
|
|
2962
|
+
marginRight: z13.number().optional().describe("Chart right margin in pixels"),
|
|
2963
|
+
marginBottom: z13.number().optional().describe("Chart bottom margin in pixels"),
|
|
2964
|
+
legendShow: z13.boolean().optional().describe("Show or hide chart legend"),
|
|
2965
|
+
legendPosition: z13.enum([
|
|
2798
2966
|
"top-left",
|
|
2799
2967
|
"top-center",
|
|
2800
2968
|
"top-right",
|
|
@@ -2804,17 +2972,17 @@ function registerCustomizeTheme(server, client) {
|
|
|
2804
2972
|
"bottom-center",
|
|
2805
2973
|
"bottom-right"
|
|
2806
2974
|
]).optional().describe("Legend position"),
|
|
2807
|
-
legendLayout:
|
|
2808
|
-
legendShape:
|
|
2809
|
-
legendFontSize:
|
|
2810
|
-
legendFontWeight:
|
|
2811
|
-
legendFontFamily:
|
|
2812
|
-
legendColor:
|
|
2813
|
-
legendTruncateLength:
|
|
2814
|
-
disableLegendScrolling:
|
|
2815
|
-
enableVariableLegendPosition:
|
|
2816
|
-
legendTop:
|
|
2817
|
-
legendLeft:
|
|
2975
|
+
legendLayout: z13.enum(["horizontal", "vertical"]).optional().describe("Legend layout direction"),
|
|
2976
|
+
legendShape: z13.enum(["circle", "rect", "roundRect", "triangle", "diamond", "arrow", "none"]).optional().describe("Legend icon shape"),
|
|
2977
|
+
legendFontSize: z13.number().optional().describe("Legend font size"),
|
|
2978
|
+
legendFontWeight: z13.number().optional().describe("Legend font weight"),
|
|
2979
|
+
legendFontFamily: z13.string().optional().describe("Legend font family"),
|
|
2980
|
+
legendColor: z13.string().optional().describe("Legend text color (hex)"),
|
|
2981
|
+
legendTruncateLength: z13.number().optional().describe("Max characters before truncating legend"),
|
|
2982
|
+
disableLegendScrolling: z13.boolean().optional().describe("Disable scrolling in legend"),
|
|
2983
|
+
enableVariableLegendPosition: z13.boolean().optional().describe("Allow user to drag legend"),
|
|
2984
|
+
legendTop: z13.number().optional().describe("Custom legend top offset"),
|
|
2985
|
+
legendLeft: z13.number().optional().describe("Custom legend left offset"),
|
|
2818
2986
|
verticalAxis: axisSchema.describe("Vertical (Y) axis configuration"),
|
|
2819
2987
|
horizontalAxis: axisSchema.describe("Horizontal (X) axis configuration")
|
|
2820
2988
|
},
|
|
@@ -2888,6 +3056,76 @@ Use \`get_embed_details\` to view the full current theme configuration.` + GUEST
|
|
|
2888
3056
|
);
|
|
2889
3057
|
}
|
|
2890
3058
|
|
|
3059
|
+
// src/tools/embed/get-demo-link.ts
|
|
3060
|
+
import { z as z14 } from "zod";
|
|
3061
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
3062
|
+
var DEFAULT_EXPIRY_HOURS = 24;
|
|
3063
|
+
var MAX_EXPIRY_HOURS = 24 * 30;
|
|
3064
|
+
function registerGetDemoLink(server, client) {
|
|
3065
|
+
const services = createServiceBundle(client);
|
|
3066
|
+
server.tool(
|
|
3067
|
+
"get_demo_link",
|
|
3068
|
+
`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.`,
|
|
3069
|
+
{
|
|
3070
|
+
clientId: z14.string().describe("End-customer identifier to scope the preview to."),
|
|
3071
|
+
dashboardName: z14.string().optional().describe("Dashboard name (non-technical). Preferred \u2014 pass with optional `workspaceName`."),
|
|
3072
|
+
workspaceName: z14.string().optional().describe("Workspace name. Only needed if `dashboardName` is ambiguous."),
|
|
3073
|
+
embedId: z14.string().optional().describe("Technical alternative to `dashboardName`."),
|
|
3074
|
+
expiryHours: z14.number().min(1).max(MAX_EXPIRY_HOURS).optional().describe(`Token TTL in hours. Defaults to ${DEFAULT_EXPIRY_HOURS}. Max ${MAX_EXPIRY_HOURS} (30 days).`)
|
|
3075
|
+
},
|
|
3076
|
+
READ_ONLY,
|
|
3077
|
+
async (params) => {
|
|
3078
|
+
try {
|
|
3079
|
+
const embed = await services.widgetService.resolveEmbedId({
|
|
3080
|
+
embedId: params.embedId,
|
|
3081
|
+
dashboardName: params.dashboardName,
|
|
3082
|
+
workspaceName: params.workspaceName,
|
|
3083
|
+
required: true
|
|
3084
|
+
});
|
|
3085
|
+
if (!embed.ok) return toolError(embed.error);
|
|
3086
|
+
const embedId = embed.value.embedId;
|
|
3087
|
+
const dashboardName = embed.value.resolvedFrom?.dashboardName || null;
|
|
3088
|
+
const ctx = await services.widgetService.resolveEmbedContext(embedId);
|
|
3089
|
+
if (!ctx.dataAppName) {
|
|
3090
|
+
return toolError(
|
|
3091
|
+
`Could not resolve the data app for embed "${embedId}". The demo link needs a data app to mint a guest token against. Run \`list_data_apps\` to confirm one exists, or pass \`embedId\` for an embed with metadata.dataAppName populated.`
|
|
3092
|
+
);
|
|
3093
|
+
}
|
|
3094
|
+
const dashboardSlug = ctx.externalDashboardId || ctx.dashboardId;
|
|
3095
|
+
if (!dashboardSlug) {
|
|
3096
|
+
return toolError(
|
|
3097
|
+
`Embed "${embedId}" has no dashboardId/externalDashboardId \u2014 the demo URL requires one to load the dashboard. This is unexpected; verify the embed in the Databrain UI.`
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
const expiryHours = params.expiryHours ?? DEFAULT_EXPIRY_HOURS;
|
|
3101
|
+
const { token } = await services.guestTokenService.generateToken({
|
|
3102
|
+
clientId: params.clientId,
|
|
3103
|
+
dataAppName: ctx.dataAppName,
|
|
3104
|
+
allowedEmbeds: [embedId],
|
|
3105
|
+
expiryTime: expiryHours * HOUR_MS
|
|
3106
|
+
});
|
|
3107
|
+
const baseUrl = client.getDemoDomain().replace(/\/+$/, "");
|
|
3108
|
+
const demoUrl = `${baseUrl}/?token=${encodeURIComponent(token)}&dashboardId=${encodeURIComponent(dashboardSlug)}`;
|
|
3109
|
+
return toolSuccess(
|
|
3110
|
+
JSON.stringify({
|
|
3111
|
+
demoUrl,
|
|
3112
|
+
clientId: params.clientId,
|
|
3113
|
+
dashboardName,
|
|
3114
|
+
embedId,
|
|
3115
|
+
dashboardId: dashboardSlug,
|
|
3116
|
+
dataAppName: ctx.dataAppName,
|
|
3117
|
+
expiresInHours: expiryHours,
|
|
3118
|
+
note: `Open this URL in a browser to preview the dashboard exactly as the end-customer sees it. This matches the UI's "Share Demo Link" flow. Override the host with DATABRAIN_DEMO_DOMAIN for self-hosted / custom preview domains.`
|
|
3119
|
+
})
|
|
3120
|
+
);
|
|
3121
|
+
} catch (err) {
|
|
3122
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
3123
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
|
|
2891
3129
|
// src/tools/frontend/generate-guest-token.ts
|
|
2892
3130
|
import { z as z15 } from "zod";
|
|
2893
3131
|
function registerGenerateGuestToken(server, client) {
|
|
@@ -4918,13 +5156,173 @@ function registerGenerateWidget(server, client) {
|
|
|
4918
5156
|
);
|
|
4919
5157
|
}
|
|
4920
5158
|
|
|
5159
|
+
// src/tools/auth/connect.ts
|
|
5160
|
+
import { z as z26 } from "zod";
|
|
5161
|
+
function registerConnect(server, client) {
|
|
5162
|
+
server.tool(
|
|
5163
|
+
"connect",
|
|
5164
|
+
"Bind this MCP session to a Databrain user. Pass the user's Databrain login email; the server mints a personal token cached locally so subsequent actions are role-checked and audited under that user. One-time per machine.",
|
|
5165
|
+
{
|
|
5166
|
+
email: z26.string().email().describe("The Databrain login email for the user running this MCP session.")
|
|
5167
|
+
},
|
|
5168
|
+
WRITE_IDEMPOTENT,
|
|
5169
|
+
async ({ email }) => {
|
|
5170
|
+
try {
|
|
5171
|
+
const { cred, persisted } = await client.bootstrapOperator(email);
|
|
5172
|
+
const expiresInDays = Math.max(
|
|
5173
|
+
0,
|
|
5174
|
+
Math.round((cred.expiresAt - Date.now()) / (24 * 60 * 60 * 1e3))
|
|
5175
|
+
);
|
|
5176
|
+
const envPatActive = client.getActiveAuth().source === "env-pat";
|
|
5177
|
+
const notes = [];
|
|
5178
|
+
if (envPatActive) {
|
|
5179
|
+
notes.push(
|
|
5180
|
+
"DATABRAIN_PAT is set in your MCP env, so that token continues to be used as the bearer for API calls. The cached identity is now available for `whoami`. Unset DATABRAIN_PAT if you want this freshly minted PAT to be used instead."
|
|
5181
|
+
);
|
|
5182
|
+
} else {
|
|
5183
|
+
notes.push(
|
|
5184
|
+
"Mutations from this MCP session are now role-checked and audit-logged under this user."
|
|
5185
|
+
);
|
|
5186
|
+
}
|
|
5187
|
+
if (!persisted) {
|
|
5188
|
+
notes.push(
|
|
5189
|
+
"Could not persist the credential to disk \u2014 likely a sandboxed or read-only home directory. The PAT is held in memory for this session only; you'll be asked to connect again next time the MCP server restarts. To avoid this, generate a PAT in Databrain Settings and paste it into your MCP config as DATABRAIN_PAT."
|
|
5190
|
+
);
|
|
5191
|
+
}
|
|
5192
|
+
return toolSuccess(
|
|
5193
|
+
JSON.stringify(
|
|
5194
|
+
{
|
|
5195
|
+
status: "connected",
|
|
5196
|
+
email: cred.email,
|
|
5197
|
+
userId: cred.userId,
|
|
5198
|
+
role: cred.role,
|
|
5199
|
+
workspaces: cred.workspaces,
|
|
5200
|
+
expiresInDays,
|
|
5201
|
+
persisted,
|
|
5202
|
+
activeBearer: envPatActive ? "env-pat" : "cached-pat",
|
|
5203
|
+
notes
|
|
5204
|
+
},
|
|
5205
|
+
null,
|
|
5206
|
+
2
|
|
5207
|
+
)
|
|
5208
|
+
);
|
|
5209
|
+
} catch (err) {
|
|
5210
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
5211
|
+
return toolError(`Unexpected error during connect: ${err.message}`);
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
);
|
|
5215
|
+
}
|
|
5216
|
+
|
|
5217
|
+
// src/tools/auth/whoami.ts
|
|
5218
|
+
function registerWhoami(server, client) {
|
|
5219
|
+
server.tool(
|
|
5220
|
+
"whoami",
|
|
5221
|
+
"Return the credential currently used for API calls and (if known) the Databrain user it belongs to. Use to confirm operator identity before mutating actions.",
|
|
5222
|
+
{},
|
|
5223
|
+
READ_ONLY,
|
|
5224
|
+
async () => {
|
|
5225
|
+
const auth = client.getActiveAuth();
|
|
5226
|
+
const cached = client.getOperator();
|
|
5227
|
+
if (auth.source === "env-pat") {
|
|
5228
|
+
const body = {
|
|
5229
|
+
status: "connected",
|
|
5230
|
+
activeBearer: "env-pat",
|
|
5231
|
+
note: "Using DATABRAIN_PAT from your MCP env. The backend resolves the user behind this PAT \u2014 actions are role-checked and audited under their account."
|
|
5232
|
+
};
|
|
5233
|
+
if (cached) {
|
|
5234
|
+
body.cachedIdentity = {
|
|
5235
|
+
email: cached.email,
|
|
5236
|
+
userId: cached.userId,
|
|
5237
|
+
role: cached.role,
|
|
5238
|
+
workspaces: cached.workspaces,
|
|
5239
|
+
note: "Local cache from a prior `connect` \u2014 informational only; the env PAT is the active bearer."
|
|
5240
|
+
};
|
|
5241
|
+
} else {
|
|
5242
|
+
body.cachedIdentity = null;
|
|
5243
|
+
body.hint = "Run `connect <your-databrain-email>` to populate identity metadata locally for richer whoami output. The env PAT will continue to be the active bearer regardless.";
|
|
5244
|
+
}
|
|
5245
|
+
return toolSuccess(JSON.stringify(body, null, 2));
|
|
5246
|
+
}
|
|
5247
|
+
if (auth.source === "cached-pat") {
|
|
5248
|
+
const op = auth.operator;
|
|
5249
|
+
const expiresInDays = Math.max(
|
|
5250
|
+
0,
|
|
5251
|
+
Math.round((op.expiresAt - Date.now()) / (24 * 60 * 60 * 1e3))
|
|
5252
|
+
);
|
|
5253
|
+
return toolSuccess(
|
|
5254
|
+
JSON.stringify(
|
|
5255
|
+
{
|
|
5256
|
+
status: "connected",
|
|
5257
|
+
activeBearer: "cached-pat",
|
|
5258
|
+
email: op.email,
|
|
5259
|
+
userId: op.userId,
|
|
5260
|
+
role: op.role,
|
|
5261
|
+
workspaces: op.workspaces,
|
|
5262
|
+
expiresInDays
|
|
5263
|
+
},
|
|
5264
|
+
null,
|
|
5265
|
+
2
|
|
5266
|
+
)
|
|
5267
|
+
);
|
|
5268
|
+
}
|
|
5269
|
+
return toolSuccess(
|
|
5270
|
+
JSON.stringify(
|
|
5271
|
+
{
|
|
5272
|
+
status: "not_connected",
|
|
5273
|
+
activeBearer: auth.source,
|
|
5274
|
+
note: "No operator bound to this MCP session. Calls fall back to the org-level service token (no per-user audit). Either run `connect <email>` to bootstrap interactively, or paste a Personal Access Token into your MCP config as DATABRAIN_PAT."
|
|
5275
|
+
},
|
|
5276
|
+
null,
|
|
5277
|
+
2
|
|
5278
|
+
)
|
|
5279
|
+
);
|
|
5280
|
+
}
|
|
5281
|
+
);
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
// src/tools/auth/disconnect.ts
|
|
5285
|
+
function registerDisconnect(server, client) {
|
|
5286
|
+
server.tool(
|
|
5287
|
+
"disconnect",
|
|
5288
|
+
"Wipe the locally cached operator credential for this Databrain instance. Use when switching Databrain accounts or revoking access on a shared machine. Subsequent calls fall back to the service token until `connect` is run again.",
|
|
5289
|
+
{},
|
|
5290
|
+
DESTRUCTIVE,
|
|
5291
|
+
async () => {
|
|
5292
|
+
const wasConnected = client.getOperator() !== null;
|
|
5293
|
+
const envPatActive = client.getActiveAuth().source === "env-pat";
|
|
5294
|
+
client.clearOperator();
|
|
5295
|
+
const notes = [];
|
|
5296
|
+
if (wasConnected) {
|
|
5297
|
+
notes.push("Local operator credential cleared. Run `connect` again to rebind.");
|
|
5298
|
+
} else {
|
|
5299
|
+
notes.push("No cached operator to clear.");
|
|
5300
|
+
}
|
|
5301
|
+
if (envPatActive) {
|
|
5302
|
+
notes.push(
|
|
5303
|
+
"DATABRAIN_PAT is still set in your MCP env, so API calls continue using that token. Unset it in your MCP config to fully disconnect."
|
|
5304
|
+
);
|
|
5305
|
+
}
|
|
5306
|
+
return toolSuccess(
|
|
5307
|
+
JSON.stringify(
|
|
5308
|
+
{ status: "disconnected", wasConnected, notes },
|
|
5309
|
+
null,
|
|
5310
|
+
2
|
|
5311
|
+
)
|
|
5312
|
+
);
|
|
5313
|
+
}
|
|
5314
|
+
);
|
|
5315
|
+
}
|
|
5316
|
+
|
|
4921
5317
|
// src/tools/registry.ts
|
|
4922
5318
|
function registerAllTools(server, client) {
|
|
5319
|
+
registerConnect(server, client);
|
|
5320
|
+
registerWhoami(server, client);
|
|
5321
|
+
registerDisconnect(server, client);
|
|
4923
5322
|
registerListDatasources(server, client);
|
|
4924
5323
|
registerDiscoverDatasourceSchema(server, client);
|
|
4925
5324
|
registerListDatamarts(server, client);
|
|
4926
5325
|
registerListWorkspaces(server, client);
|
|
4927
|
-
registerLinkDatamartToWorkspace(server, client);
|
|
4928
5326
|
registerListDataApps(server, client);
|
|
4929
5327
|
registerCreateDataApp(server, client);
|
|
4930
5328
|
registerListApiTokens(server, client);
|
|
@@ -4935,6 +5333,7 @@ function registerAllTools(server, client) {
|
|
|
4935
5333
|
registerGetEmbedDetails(server, client);
|
|
4936
5334
|
registerUpdateEmbed(server, client);
|
|
4937
5335
|
registerCustomizeTheme(server, client);
|
|
5336
|
+
registerGetDemoLink(server, client);
|
|
4938
5337
|
registerGenerateGuestToken(server, client);
|
|
4939
5338
|
registerGenerateEmbedCode(server, client);
|
|
4940
5339
|
registerListDashboards(server, client);
|
|
@@ -8018,8 +8417,10 @@ Wait for explicit confirmation before calling \`remove_widget({ externalMetricId
|
|
|
8018
8417
|
**Step 4 \u2014 Verify**
|
|
8019
8418
|
Call \`list_widgets({ clientId, dashboardName, workspaceName? })\` again. Present the updated list and confirm the changes match what the user asked for.
|
|
8020
8419
|
|
|
8021
|
-
**Step 5 \u2014
|
|
8022
|
-
|
|
8420
|
+
**Step 5 \u2014 Hand off the demo link**
|
|
8421
|
+
Always close with a demo link so the user can verify visually. Call \`get_demo_link({ clientId, dashboardName, workspaceName? })\` \u2014 it returns the same URL the Databrain UI's "Share Demo Link" button generates: a hosted preview page that loads the dashboard exactly as that \`clientId\` will see it. Default expiry is 24h; pass \`expiryHours\` to override.
|
|
8422
|
+
|
|
8423
|
+
Present the URL to the user and remind them: (a) only \`isPublished: true\` widgets appear in the preview, so flag any unpublished widgets from the Step 4 \`list_widgets\` output; (b) if they already have the embed wired up in their own app, the cached frontend guest token there must be refreshed before changes appear.
|
|
8023
8424
|
|
|
8024
8425
|
**Important rules:**
|
|
8025
8426
|
- STOP after each step. Present results and wait for user input before proceeding.
|
|
@@ -8080,6 +8481,19 @@ function registerAllPrompts(server) {
|
|
|
8080
8481
|
|
|
8081
8482
|
// src/server.ts
|
|
8082
8483
|
function createServer(config) {
|
|
8484
|
+
const client = new DatabrainClient(config);
|
|
8485
|
+
const auth = client.getActiveAuth();
|
|
8486
|
+
let operatorBlurb;
|
|
8487
|
+
if (auth.source === "env-pat") {
|
|
8488
|
+
operatorBlurb = "OPERATOR IDENTITY: a Personal Access Token is active via DATABRAIN_PAT. The backend resolves the user behind it \u2014 all calls are role-checked and audited under their account. Do NOT ask the user to connect; their identity is already bound. Use `whoami` if you need to confirm details.\n\n";
|
|
8489
|
+
} else if (auth.source === "cached-pat") {
|
|
8490
|
+
const op = auth.operator;
|
|
8491
|
+
operatorBlurb = `OPERATOR IDENTITY: connected as ${op.email} (${op.role}) via locally cached PAT. All mutations are role-checked and audit-logged under this user. Use \`whoami\` to confirm, \`disconnect\` to switch users.
|
|
8492
|
+
|
|
8493
|
+
`;
|
|
8494
|
+
} else {
|
|
8495
|
+
operatorBlurb = "OPERATOR IDENTITY: not connected. On the user's first request, ask once: \"What's your Databrain login email? I'll bind this MCP session to your account so actions are role-checked and audited.\" Then call `connect` with their email. If `connect` reports it could not persist the credential (sandboxed host), suggest the user generate a PAT in Databrain Settings and paste it into their MCP config as DATABRAIN_PAT for a stable bind. If they decline to connect, fall back to service-token mode (calls still work, but mutations are tagged actor=system in the audit log). Do not pester after one ask.\n\n";
|
|
8496
|
+
}
|
|
8083
8497
|
const server = new McpServer(
|
|
8084
8498
|
{
|
|
8085
8499
|
name: "databrain",
|
|
@@ -8087,8 +8501,8 @@ function createServer(config) {
|
|
|
8087
8501
|
description: "Databrain MCP Server \u2014 embed analytics dashboards and charts into your app. Focused on embed discovery, deterministic embed updates, theming, filters, localization, permissions, and guest token generation."
|
|
8088
8502
|
},
|
|
8089
8503
|
{
|
|
8090
|
-
instructions: `Databrain uses two token types \u2014 never confuse them:
|
|
8091
|
-
\u2022 SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, list_datamarts, list_workspaces,
|
|
8504
|
+
instructions: operatorBlurb + `Databrain uses two token types \u2014 never confuse them:
|
|
8505
|
+
\u2022 SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, list_datamarts, list_workspaces, list_data_apps, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer. You already have this \u2014 never ask the user for it.
|
|
8092
8506
|
\u2022 API TOKEN (per data app): scoped to one data app, used for embedding operations (list_embeds, get_embed_details, create_embed, update_embed, generate_guest_token) and data-app scoped dashboard listing. Created/rotated via create_api_token or rotate_api_token during setup.
|
|
8093
8507
|
|
|
8094
8508
|
3 core use cases \u2014 match the user's scenario to the right prompt:
|
|
@@ -8114,7 +8528,6 @@ IMPORTANT \u2014 Be interactive. Present results from each tool call and ask the
|
|
|
8114
8528
|
Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, filters, permissions, datasource, datamart, workspace, metric creation.`
|
|
8115
8529
|
}
|
|
8116
8530
|
);
|
|
8117
|
-
const client = new DatabrainClient(config);
|
|
8118
8531
|
registerAllTools(server, client);
|
|
8119
8532
|
registerAllResources(server);
|
|
8120
8533
|
registerAllPrompts(server);
|