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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +158 -7
  2. package/dist/apps/server.d.ts +82 -44
  3. package/dist/apps/server.js +5 -2
  4. package/dist/apps/stdio.js +6 -3
  5. package/dist/apps/stdio.js.map +1 -1
  6. package/dist/bundle-TDQXWIPM.js +24 -0
  7. package/dist/bundle-TDQXWIPM.js.map +1 -0
  8. package/dist/{chunk-RF3HTNO7.js → chunk-DM7S4R57.js} +2 -2
  9. package/dist/{chunk-7DS7OLWP.js → chunk-ES2LUP4U.js} +2030 -1214
  10. package/dist/chunk-ES2LUP4U.js.map +1 -0
  11. package/dist/chunk-IBYBK5ZE.js +34 -0
  12. package/dist/chunk-IBYBK5ZE.js.map +1 -0
  13. package/dist/chunk-NBFUKVWP.js +603 -0
  14. package/dist/chunk-NBFUKVWP.js.map +1 -0
  15. package/dist/chunk-Q5EYDKSA.js +1224 -0
  16. package/dist/chunk-Q5EYDKSA.js.map +1 -0
  17. package/dist/chunk-RHK62ZJ6.js +884 -0
  18. package/dist/chunk-RHK62ZJ6.js.map +1 -0
  19. package/dist/{chunk-JCXUZ3QZ.js → chunk-Z3ZHN4UW.js} +34 -6
  20. package/dist/chunk-Z3ZHN4UW.js.map +1 -0
  21. package/dist/cli.d.ts +15 -0
  22. package/dist/cli.js +497 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/custom-chart/custom-runtime.js +63 -0
  25. package/dist/custom-chart/runtime-shim.js +146 -0
  26. package/dist/custom-chart/template/AGENTS.md +374 -0
  27. package/dist/custom-chart/template/README.md +374 -0
  28. package/dist/custom-chart/template/chart.css +28 -0
  29. package/dist/custom-chart/template/chart.ts +192 -0
  30. package/dist/custom-chart/template/custom-chart.d.ts +78 -0
  31. package/dist/custom-chart/template/icon.svg +1 -0
  32. package/dist/custom-chart/template/manifest.json +23 -0
  33. package/dist/custom-chart/template/package.json +14 -0
  34. package/dist/custom-chart/template/sample-data.json +18 -0
  35. package/dist/custom-chart/template/tsconfig.json +12 -0
  36. package/dist/index.js +15 -8
  37. package/dist/index.js.map +1 -1
  38. package/dist/main-JEYYFGJM.js +872 -0
  39. package/dist/main-JEYYFGJM.js.map +1 -0
  40. package/dist/{test-only-EJ5O4WN5.js → test-only-K7QTSVO4.js} +7 -6
  41. package/dist/test-only-K7QTSVO4.js.map +1 -0
  42. package/package.json +9 -4
  43. package/dist/chunk-7DS7OLWP.js.map +0 -1
  44. package/dist/chunk-JCXUZ3QZ.js.map +0 -1
  45. package/dist/chunk-NDVW3ZSJ.js +0 -310
  46. package/dist/chunk-NDVW3ZSJ.js.map +0 -1
  47. package/dist/main-7QNRH6X4.js +0 -438
  48. package/dist/main-7QNRH6X4.js.map +0 -1
  49. package/dist/test-only-EJ5O4WN5.js.map +0 -1
  50. /package/dist/{chunk-RF3HTNO7.js.map → chunk-DM7S4R57.js.map} +0 -0
@@ -0,0 +1,884 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isId,
4
+ readTemplateFiles,
5
+ validateManifest
6
+ } from "./chunk-Q5EYDKSA.js";
7
+
8
+ // src/utils/errors.ts
9
+ var DatabrainApiError = class extends Error {
10
+ constructor(status, code, message, details, serverHint, retryable) {
11
+ super(message);
12
+ this.status = status;
13
+ this.code = code;
14
+ this.details = details;
15
+ this.serverHint = serverHint;
16
+ this.retryable = retryable;
17
+ this.name = "DatabrainApiError";
18
+ }
19
+ toToolResponse() {
20
+ const parts = [`Error: ${this.message}`];
21
+ const detailDump = this.formatDetails();
22
+ if (detailDump) parts.push(detailDump);
23
+ const hint = this.serverHint ? `Hint: ${this.serverHint}` : this.hint();
24
+ if (hint) parts.push(hint);
25
+ return {
26
+ content: [{ type: "text", text: parts.join("\n\n") }],
27
+ isError: true
28
+ };
29
+ }
30
+ /**
31
+ * Surface API-returned details for 4xx errors so the LLM can recover or
32
+ * report the real reason (validation messages, field names, etc.). Skips
33
+ * fields already collapsed into `message`/`code`, and skips 5xx where the
34
+ * payload usually isn't actionable.
35
+ */
36
+ formatDetails() {
37
+ if (this.status < 400 || this.status >= 500) return "";
38
+ if (!this.details || typeof this.details !== "object") return "";
39
+ const skipKeys = /* @__PURE__ */ new Set(["message", "detail", "error", "code", "data", "hint", "retryable"]);
40
+ const extra = {};
41
+ for (const [k, v] of Object.entries(this.details)) {
42
+ if (skipKeys.has(k)) continue;
43
+ if (v === null || v === void 0) continue;
44
+ extra[k] = v;
45
+ }
46
+ if (Object.keys(extra).length === 0) return "";
47
+ try {
48
+ return `API details: ${JSON.stringify(extra)}`;
49
+ } catch {
50
+ return "";
51
+ }
52
+ }
53
+ hint() {
54
+ if (this.code === "OAUTH_SIGN_IN_REQUIRED") {
55
+ return "Hint: Snapshots draw as a signed-in Databrain user. Connect this AI app with a Databrain sign-in (the hosted connector, or /mcp in Claude Code); a service token cannot take them.";
56
+ }
57
+ if (this.code === "RENDER_IN_PROGRESS") {
58
+ return "Hint: Your other snapshot is still rendering. Wait a few seconds, then call the tool again.";
59
+ }
60
+ if (this.code === "SNAPSHOT_TIMEOUT" || this.code === "SNAPSHOT_TOO_LARGE") {
61
+ return "Hint: Ask for one chart with metricId, or a smaller width.";
62
+ }
63
+ if (this.code === "UNAUTHORIZED_ORIGIN") {
64
+ return "Hint: The embed origin is not on the allowed domains list. Add the domain to the allowed domains in the Databrain settings; the token itself is fine.";
65
+ }
66
+ if (this.status === 401 || this.status === 403) {
67
+ if (this.code === "OAUTH_API_ERROR") {
68
+ 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.";
69
+ }
70
+ if (this.code === "INVALID_SERVICE_TOKEN" || this.code === "MISSING_SERVICE_TOKEN" || this.message.toLowerCase().includes("service token")) {
71
+ 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.";
72
+ }
73
+ 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.";
74
+ }
75
+ if (this.status === 400) {
76
+ return "Hint: The request was invalid. Check parameter values match expected types and formats. Use the corresponding list tool to verify IDs and names.";
77
+ }
78
+ if (this.status === 404) {
79
+ return "Hint: The requested resource was not found. Verify the ID or name is correct using the corresponding list tool.";
80
+ }
81
+ if (this.status === 409) {
82
+ return "Hint: A resource with this name already exists. Use a different name or list existing resources first.";
83
+ }
84
+ if (this.status === 422) {
85
+ return "Hint: The request parameters are invalid. Check the required fields and their formats.";
86
+ }
87
+ if (this.status === 429) {
88
+ return "Hint: API rate limit reached. Wait a moment and try again.";
89
+ }
90
+ if (this.status >= 500) {
91
+ return "Hint: The Databrain API is experiencing issues. Try again in a few moments.";
92
+ }
93
+ return "";
94
+ }
95
+ };
96
+ function toolError(message) {
97
+ return {
98
+ content: [{ type: "text", text: message }],
99
+ isError: true
100
+ };
101
+ }
102
+ function stringify(value) {
103
+ if (typeof value === "string") return value;
104
+ try {
105
+ return JSON.stringify(value, null, 2);
106
+ } catch {
107
+ return String(value);
108
+ }
109
+ }
110
+ function toolSuccess(text) {
111
+ return {
112
+ content: [{ type: "text", text }]
113
+ };
114
+ }
115
+
116
+ // src/utils/logger.ts
117
+ var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
118
+ var currentLevel = process.env.DATABRAIN_LOG_LEVEL || "warn";
119
+ var secrets = /* @__PURE__ */ new Set();
120
+ function registerSecret(value) {
121
+ if (typeof value === "string" && value.length >= 8) secrets.add(value);
122
+ }
123
+ function redactSecrets(text) {
124
+ let out = text;
125
+ for (const secret of secrets) out = out.split(secret).join("[redacted]");
126
+ return out;
127
+ }
128
+ function log(level, message, data) {
129
+ if (LOG_LEVELS[level] < LOG_LEVELS[currentLevel]) return;
130
+ const line = `[databrain-mcp] [${level.toUpperCase()}] ${message}`;
131
+ const text = data !== void 0 ? `${line} ${JSON.stringify(data)}` : line;
132
+ process.stderr.write(`${redactSecrets(text)}
133
+ `);
134
+ }
135
+ var logger = {
136
+ debug: (msg, data) => log("debug", msg, data),
137
+ info: (msg, data) => log("info", msg, data),
138
+ warn: (msg, data) => log("warn", msg, data),
139
+ error: (msg, data) => log("error", msg, data)
140
+ };
141
+
142
+ // package.json
143
+ var package_default = {
144
+ name: "@databrainhq/mcp-server",
145
+ mcpName: "com.usedatabrain/mcp-server",
146
+ version: "0.3.0-rc.11",
147
+ description: "MCP server for Databrain embedded analytics - connect databases, create datamarts, configure embeds, and generate frontend code",
148
+ type: "module",
149
+ bin: {
150
+ "mcp-server": "dist/index.js",
151
+ "databrain-mcp-server": "dist/index.js",
152
+ "databrain-mcp-chart-apps": "dist/apps/stdio.js",
153
+ databrain: "dist/cli.js"
154
+ },
155
+ main: "dist/index.js",
156
+ types: "dist/index.d.ts",
157
+ files: [
158
+ "dist",
159
+ "skills",
160
+ ".claude-plugin",
161
+ ".mcp.json"
162
+ ],
163
+ scripts: {
164
+ build: "tsup && node scripts/copy-custom-chart-assets.mjs",
165
+ dev: "tsx src/index.ts",
166
+ test: "vitest run",
167
+ "test:watch": "vitest",
168
+ "test:live": "vitest run tests/integration/prompt-flows.test.ts",
169
+ lint: "tsc --noEmit",
170
+ prepublishOnly: "npm run build"
171
+ },
172
+ keywords: [
173
+ "mcp",
174
+ "databrain",
175
+ "embedded-analytics",
176
+ "ai",
177
+ "model-context-protocol"
178
+ ],
179
+ license: "MIT",
180
+ dependencies: {
181
+ "@modelcontextprotocol/sdk": "^1.32.1",
182
+ "@posthog/mcp": "^0.10.7",
183
+ esbuild: "0.27.3",
184
+ fflate: "0.8.3",
185
+ "posthog-node": "^5.21.2",
186
+ yaml: "^2.9.1",
187
+ zod: "^3.24.0"
188
+ },
189
+ devDependencies: {
190
+ "@types/node": "^25.3.5",
191
+ tsup: "^8.4.0",
192
+ tsx: "^4.19.0",
193
+ typescript: "^5.7.0",
194
+ vitest: "^3.0.0"
195
+ },
196
+ publishConfig: {
197
+ access: "public"
198
+ },
199
+ repository: {
200
+ type: "git",
201
+ url: "git+https://github.com/databrainhq/mcp-server.git"
202
+ },
203
+ engines: {
204
+ node: ">=18.0.0"
205
+ }
206
+ };
207
+
208
+ // src/version.ts
209
+ var MCP_SERVER_VERSION = package_default.version;
210
+
211
+ // src/custom-chart/studio.ts
212
+ import { z } from "zod";
213
+ var studioCell = z.union([z.string().max(4096), z.number().finite(), z.boolean(), z.null()]);
214
+ var studioDraftSchema = z.object({
215
+ code: z.string().max(64e3),
216
+ css: z.string().max(32e3),
217
+ manifest: z.record(z.unknown()),
218
+ sampleData: z.object({
219
+ columns: z.array(z.object({
220
+ id: z.string(),
221
+ label: z.string(),
222
+ type: z.enum(["string", "number", "boolean"]),
223
+ slotId: z.string().optional()
224
+ }).strict()).max(64),
225
+ rows: z.array(z.array(studioCell)).max(200)
226
+ }).strict(),
227
+ name: z.string().max(256),
228
+ description: z.string().max(500),
229
+ iconSvg: z.string().max(16384)
230
+ }).strict();
231
+ function parseStudioStream(text) {
232
+ if (text.length > 4 * 1024 * 1024) throw new Error("Studio stream too large");
233
+ let draft;
234
+ let sessionId;
235
+ let failure = false;
236
+ for (const frame of text.split(/\r?\n\r?\n/)) {
237
+ const line = frame.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart()).join("\n");
238
+ if (!line || line === "[DONE]") continue;
239
+ const event = JSON.parse(line);
240
+ if (event.type === "session" && typeof event.sessionId === "string") sessionId = event.sessionId;
241
+ if (event.type === "error") failure = true;
242
+ if (event.type === "tool_execution_end" && event.name === "update_custom_chart" && event.status === "completed" && event.result?.kind === "applied") {
243
+ draft = studioDraftSchema.parse(event.result.draft);
244
+ }
245
+ }
246
+ if (failure && !draft) throw new Error("Bryn did not complete an edit");
247
+ return { draft, sessionId, applied: !!draft };
248
+ }
249
+ async function editWithBryn(client, input) {
250
+ if (!client.isHosted()) return { available: false, draft: input.draft, applied: false };
251
+ try {
252
+ const result = await client.request({
253
+ method: "POST",
254
+ path: `/api/v2/custom-charts/${encodeURIComponent(input.key)}/bryn/turn`,
255
+ auth: "service",
256
+ body: {
257
+ workspaceId: input.workspaceId,
258
+ message: input.message,
259
+ draft: input.draft,
260
+ sessionId: input.sessionId,
261
+ mode: "build",
262
+ viewingOldVersion: false
263
+ },
264
+ responseFormat: "studio-sse",
265
+ timeoutMs: 18e4,
266
+ noRetryOnTimeout: true
267
+ });
268
+ return { available: true, draft: result.draft ?? input.draft, applied: result.applied, sessionId: result.sessionId };
269
+ } catch (error) {
270
+ if (error instanceof DatabrainApiError && error.status === 404) {
271
+ return { available: false, draft: input.draft, applied: false };
272
+ }
273
+ throw error;
274
+ }
275
+ }
276
+
277
+ // src/utils/cache.ts
278
+ var TtlCache = class {
279
+ store = /* @__PURE__ */ new Map();
280
+ get(key) {
281
+ const entry = this.store.get(key);
282
+ if (!entry) return void 0;
283
+ if (Date.now() > entry.expiresAt) {
284
+ this.store.delete(key);
285
+ return void 0;
286
+ }
287
+ return entry.value;
288
+ }
289
+ set(key, value, ttlMs) {
290
+ this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
291
+ }
292
+ invalidateByPrefix(prefix) {
293
+ for (const key of this.store.keys()) {
294
+ if (key.startsWith(prefix)) {
295
+ this.store.delete(key);
296
+ }
297
+ }
298
+ }
299
+ clear() {
300
+ this.store.clear();
301
+ }
302
+ };
303
+
304
+ // src/client/error-body.ts
305
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
306
+ var str = (v) => typeof v === "string" && v.trim() ? v : void 0;
307
+ var fromEnvelope = (v) => {
308
+ if (!isRecord(v)) return void 0;
309
+ const out = {
310
+ code: str(v.code),
311
+ message: str(v.message) ?? str(v.detail),
312
+ hint: str(v.hint),
313
+ retryable: typeof v.retryable === "boolean" ? v.retryable : void 0,
314
+ details: v.details
315
+ };
316
+ return out.code || out.message || out.hint ? out : void 0;
317
+ };
318
+ function parseErrorBody(body) {
319
+ if (!isRecord(body)) return {};
320
+ const nested = fromEnvelope(isRecord(body.data) ? body.data.error : void 0) ?? fromEnvelope(body.error) ?? fromEnvelope(body);
321
+ if (nested) {
322
+ return { ...nested, message: nested.message ?? str(body.error) };
323
+ }
324
+ return { message: str(body.error) };
325
+ }
326
+
327
+ // src/utils/tool-context.ts
328
+ import { AsyncLocalStorage } from "async_hooks";
329
+ var toolContext = new AsyncLocalStorage();
330
+
331
+ // src/client/oauth-client-credentials.ts
332
+ import { z as z2 } from "zod";
333
+ var oauthHttpsUrl = z2.string().refine((value) => {
334
+ try {
335
+ const url = new URL(value);
336
+ return url.protocol === "https:" && !url.username && !url.password && !url.hash;
337
+ } catch {
338
+ return false;
339
+ }
340
+ }, "OAuth URLs must use HTTPS without userinfo or fragments");
341
+ var oauthConfigSchema = z2.object({
342
+ issuer: oauthHttpsUrl.refine((value) => {
343
+ try {
344
+ return !new URL(value).search;
345
+ } catch {
346
+ return false;
347
+ }
348
+ }, "OAuth issuer must not contain a query"),
349
+ clientId: z2.string().min(1),
350
+ clientSecret: z2.string().min(1),
351
+ resource: oauthHttpsUrl,
352
+ scope: z2.string().min(1).optional(),
353
+ authMethod: z2.enum(["client_secret_basic", "client_secret_post"]).default("client_secret_basic")
354
+ });
355
+ var metadataSchema = z2.object({
356
+ issuer: z2.string(),
357
+ token_endpoint: oauthHttpsUrl,
358
+ token_endpoint_auth_methods_supported: z2.array(z2.string()).optional(),
359
+ grant_types_supported: z2.array(z2.string()).optional()
360
+ });
361
+ var tokenSchema = z2.object({
362
+ access_token: z2.string().regex(/^[A-Za-z0-9\-._~+/]+=*$/),
363
+ token_type: z2.string().refine((value) => value.toLowerCase() === "bearer"),
364
+ expires_in: z2.number().finite().positive().optional()
365
+ });
366
+ var TOKEN_TIMEOUT_MS = 1e4;
367
+ var FAILURE_COOLDOWN_MS = 3e4;
368
+ var MAX_REFRESH_SKEW_MS = 6e4;
369
+ function authError() {
370
+ return new Error("OAuth token acquisition failed. Check the configured issuer, client credentials, resource and scopes.");
371
+ }
372
+ var OAuthClientCredentials = class {
373
+ config;
374
+ tokenEndpoint;
375
+ cached;
376
+ pending;
377
+ retryAt = 0;
378
+ constructor(config) {
379
+ const parsed = oauthConfigSchema.safeParse(config);
380
+ if (!parsed.success) throw authError();
381
+ this.config = Object.freeze(parsed.data);
382
+ }
383
+ getStatus() {
384
+ if (this.cached && Date.now() < this.cached.renewAt) return "ready";
385
+ if (this.retryAt > 0) return "unavailable";
386
+ return "configured";
387
+ }
388
+ async getToken() {
389
+ if (this.cached && Date.now() < this.cached.renewAt) return this.cached.value;
390
+ if (this.pending) return this.pending;
391
+ if (Date.now() < this.retryAt) throw authError();
392
+ this.cached = void 0;
393
+ this.pending = this.acquire().catch(() => {
394
+ this.retryAt = Date.now() + FAILURE_COOLDOWN_MS;
395
+ throw authError();
396
+ }).finally(() => {
397
+ this.pending = void 0;
398
+ });
399
+ return this.pending;
400
+ }
401
+ async discoverTokenEndpoint(signal) {
402
+ if (this.tokenEndpoint) return this.tokenEndpoint;
403
+ const issuer = new URL(this.config.issuer);
404
+ const path = issuer.pathname.replace(/\/$/, "");
405
+ const candidates = [.../* @__PURE__ */ new Set([
406
+ `${issuer.origin}/.well-known/oauth-authorization-server${path}`,
407
+ `${issuer.origin}/.well-known/openid-configuration${path}`,
408
+ `${issuer.origin}${path}/.well-known/openid-configuration`
409
+ ])];
410
+ for (const url of candidates) {
411
+ const response = await fetch(url, {
412
+ headers: { Accept: "application/json" },
413
+ redirect: "error",
414
+ signal
415
+ });
416
+ if (response.status === 404 || response.status === 405) continue;
417
+ if (!response.ok) throw authError();
418
+ const metadata = metadataSchema.parse(await response.json());
419
+ if (metadata.issuer !== this.config.issuer) throw authError();
420
+ const methods = metadata.token_endpoint_auth_methods_supported ?? ["client_secret_basic"];
421
+ if (!methods.includes(this.config.authMethod)) throw authError();
422
+ if (metadata.grant_types_supported && !metadata.grant_types_supported.includes("client_credentials")) {
423
+ throw authError();
424
+ }
425
+ this.tokenEndpoint = metadata.token_endpoint;
426
+ return this.tokenEndpoint;
427
+ }
428
+ throw authError();
429
+ }
430
+ async acquire() {
431
+ const signal = AbortSignal.timeout(TOKEN_TIMEOUT_MS);
432
+ const endpoint = await this.discoverTokenEndpoint(signal);
433
+ const body = new URLSearchParams({
434
+ grant_type: "client_credentials",
435
+ resource: this.config.resource
436
+ });
437
+ if (this.config.scope) body.set("scope", this.config.scope);
438
+ const headers = {
439
+ Accept: "application/json",
440
+ "Content-Type": "application/x-www-form-urlencoded"
441
+ };
442
+ if (this.config.authMethod === "client_secret_post") {
443
+ body.set("client_id", this.config.clientId);
444
+ body.set("client_secret", this.config.clientSecret);
445
+ } else {
446
+ const encode = (value) => new URLSearchParams({ value }).toString().slice(6);
447
+ headers.Authorization = `Basic ${Buffer.from(`${encode(this.config.clientId)}:${encode(this.config.clientSecret)}`).toString("base64")}`;
448
+ }
449
+ const startedAt = Date.now();
450
+ const response = await fetch(endpoint, {
451
+ method: "POST",
452
+ headers,
453
+ body: body.toString(),
454
+ redirect: "error",
455
+ signal
456
+ });
457
+ if (!response.ok) throw authError();
458
+ const token = tokenSchema.parse(await response.json());
459
+ if (token.expires_in !== void 0) {
460
+ const lifetimeMs = token.expires_in * 1e3;
461
+ const expiresAt = startedAt + lifetimeMs;
462
+ if (!Number.isFinite(expiresAt) || Date.now() >= expiresAt) throw authError();
463
+ this.cached = {
464
+ value: token.access_token,
465
+ renewAt: expiresAt - Math.min(MAX_REFRESH_SKEW_MS, lifetimeMs * 0.1)
466
+ };
467
+ }
468
+ this.retryAt = 0;
469
+ return token.access_token;
470
+ }
471
+ };
472
+
473
+ // src/client/databrain-client.ts
474
+ var MCP_USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION}`;
475
+ var MAX_RETRIES = 3;
476
+ var REQUEST_TIMEOUT_MS = 3e4;
477
+ var NON_TRANSIENT_PATTERNS = [
478
+ "out of context",
479
+ "Definition not found",
480
+ "not a data question",
481
+ "Query generation failed",
482
+ "not found",
483
+ "not authenticated",
484
+ "Invalid Service Token",
485
+ "service token"
486
+ ];
487
+ var DatabrainClient = class {
488
+ constructor(config) {
489
+ this.config = config;
490
+ registerSecret(config.serviceToken);
491
+ registerSecret(config.apiToken);
492
+ if (config.oauth) {
493
+ if (!oauthHttpsUrl.safeParse(config.apiUrl).success) {
494
+ throw new Error("OAuth requires an HTTPS Databrain API URL without userinfo or fragments.");
495
+ }
496
+ this.oauth = new OAuthClientCredentials(config.oauth);
497
+ this.config = Object.freeze({ ...config });
498
+ }
499
+ }
500
+ cache = new TtlCache();
501
+ runtimeApiToken;
502
+ oauth;
503
+ oauthFallback = false;
504
+ hasApiToken() {
505
+ return !!(this.runtimeApiToken || this.config.apiToken);
506
+ }
507
+ /** The hosted composition supplies a signed-in identity, unlike local API credentials. */
508
+ isHosted() {
509
+ return false;
510
+ }
511
+ setRuntimeApiToken(token) {
512
+ registerSecret(token);
513
+ this.runtimeApiToken = token;
514
+ logger.info("Runtime API token set for this session");
515
+ }
516
+ getServiceToken() {
517
+ return this.config.serviceToken;
518
+ }
519
+ getApiUrl() {
520
+ return this.config.apiUrl;
521
+ }
522
+ getDemoDomain() {
523
+ return this.config.demoDomain;
524
+ }
525
+ /**
526
+ * Describes which credential is currently used as the bearer for outgoing
527
+ * API calls. Used by `whoami` so the LLM can give the user a truthful answer.
528
+ */
529
+ getActiveAuth() {
530
+ if (this.oauth) {
531
+ if (this.oauthFallback) return { source: "service-token", oauthFallback: true };
532
+ return { source: "oauth-client-credentials", status: this.oauth.getStatus() };
533
+ }
534
+ if (this.config.serviceToken) return { source: "service-token" };
535
+ if (this.runtimeApiToken || this.config.apiToken) return { source: "api-token" };
536
+ return { source: "none" };
537
+ }
538
+ async getToken(auth) {
539
+ if (auth === "none") return void 0;
540
+ if (auth === "service") {
541
+ if (this.oauth) {
542
+ try {
543
+ const token = await this.oauth.getToken();
544
+ this.oauthFallback = false;
545
+ return token;
546
+ } catch {
547
+ if (!this.config.serviceToken) {
548
+ throw new DatabrainApiError(
549
+ 0,
550
+ "OAUTH_TOKEN_ERROR",
551
+ "OAuth token acquisition failed. Check the configured issuer, client credentials, resource and scopes."
552
+ );
553
+ }
554
+ if (!this.oauthFallback) logger.warn("OAuth unavailable; using the configured static service token.");
555
+ this.oauthFallback = true;
556
+ }
557
+ }
558
+ return this.config.serviceToken;
559
+ }
560
+ return this.runtimeApiToken || this.config.apiToken;
561
+ }
562
+ async request(options) {
563
+ return this.requestInternal(options);
564
+ }
565
+ async requestInternal(options) {
566
+ const usesOAuth = !!this.oauth && options.auth === "service" && !options.token;
567
+ const cacheAllowed = !usesOAuth;
568
+ if (options.invalidateCache) {
569
+ for (const prefix of options.invalidateCache) {
570
+ this.cache.invalidateByPrefix(prefix);
571
+ }
572
+ }
573
+ if (cacheAllowed && options.cacheKey && options.cacheTtlMs) {
574
+ const cached = this.cache.get(options.cacheKey);
575
+ if (cached !== void 0) {
576
+ logger.debug(`Cache hit: ${options.cacheKey}`);
577
+ return cached;
578
+ }
579
+ }
580
+ let token = options.token ?? await this.getToken(options.auth);
581
+ if (options.auth !== "none" && !options.token && !token) {
582
+ if (options.auth === "service") {
583
+ throw new DatabrainApiError(
584
+ 401,
585
+ "MISSING_SERVICE_TOKEN",
586
+ "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."
587
+ );
588
+ }
589
+ throw new DatabrainApiError(
590
+ 401,
591
+ "MISSING_API_TOKEN",
592
+ "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."
593
+ );
594
+ }
595
+ let url = `${this.config.apiUrl}${options.path}`;
596
+ if (options.query) {
597
+ const params = new URLSearchParams(options.query);
598
+ url += `?${params.toString()}`;
599
+ }
600
+ let lastError;
601
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
602
+ try {
603
+ if (usesOAuth && attempt > 1) token = await this.getToken(options.auth);
604
+ const headers = {
605
+ "Content-Type": "application/json",
606
+ Accept: "application/json",
607
+ "User-Agent": MCP_USER_AGENT,
608
+ "X-Databrain-Client": MCP_USER_AGENT,
609
+ ...options.headers
610
+ };
611
+ const ctx = toolContext.getStore();
612
+ if (ctx) {
613
+ headers["X-Databrain-MCP-Tool"] = ctx.tool;
614
+ headers["X-Databrain-MCP-Tool-Category"] = ctx.category;
615
+ headers["X-Databrain-MCP-Workflow-Intent"] = ctx.workflowIntent;
616
+ headers["X-Databrain-MCP-Input-Mode"] = ctx.inputMode;
617
+ }
618
+ if (token) {
619
+ if (options.auth === "none") {
620
+ headers["x-plugin-token"] = token;
621
+ headers["Origin"] = "https://mcp.usedatabrain.com";
622
+ } else {
623
+ headers["Authorization"] = `Bearer ${token}`;
624
+ }
625
+ }
626
+ const response = await fetch(url, {
627
+ method: options.method,
628
+ headers,
629
+ body: options.body ? JSON.stringify(options.body) : void 0,
630
+ signal: AbortSignal.timeout(options.timeoutMs || REQUEST_TIMEOUT_MS),
631
+ ...usesOAuth ? { redirect: "error" } : {}
632
+ });
633
+ if (!response.ok) {
634
+ if (usesOAuth) {
635
+ throw new DatabrainApiError(
636
+ response.status,
637
+ "OAUTH_API_ERROR",
638
+ `Databrain API request failed with status ${response.status} using configured service credentials.`
639
+ );
640
+ }
641
+ const errorBody = await response.text();
642
+ logger.warn(`[API Error] ${options.method} ${options.path} status=${response.status} body=${errorBody.slice(0, 500)}`);
643
+ let parsed = {};
644
+ try {
645
+ parsed = JSON.parse(errorBody);
646
+ } catch {
647
+ }
648
+ const envelope = parseErrorBody(parsed);
649
+ const errorMsg = envelope.message || `API request failed with status ${response.status}`;
650
+ const error = new DatabrainApiError(
651
+ response.status,
652
+ envelope.code || `HTTP_${response.status}`,
653
+ errorMsg,
654
+ parsed,
655
+ envelope.hint,
656
+ envelope.retryable
657
+ );
658
+ const isTransient = envelope.retryable !== void 0 ? envelope.retryable && (response.status === 429 || response.status >= 500) : response.status === 429 || response.status >= 500 && !NON_TRANSIENT_PATTERNS.some((p) => errorMsg.includes(p));
659
+ if (isTransient && attempt < MAX_RETRIES) {
660
+ const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 8e3);
661
+ logger.warn(`Retrying in ${delay}ms (attempt ${attempt}/${MAX_RETRIES})`, {
662
+ status: response.status,
663
+ path: options.path
664
+ });
665
+ await new Promise((r) => setTimeout(r, delay));
666
+ lastError = error;
667
+ continue;
668
+ }
669
+ throw error;
670
+ }
671
+ const text = await response.text();
672
+ let data = options.responseFormat === "studio-sse" ? parseStudioStream(text) : text ? JSON.parse(text) : {};
673
+ if (!options.skipDataUnwrap && data && typeof data === "object" && !Array.isArray(data) && "data" in data) {
674
+ const inner = data.data;
675
+ if (inner !== null && inner !== void 0) {
676
+ data = inner;
677
+ }
678
+ }
679
+ if (cacheAllowed && options.cacheKey && options.cacheTtlMs) {
680
+ this.cache.set(options.cacheKey, data, options.cacheTtlMs);
681
+ }
682
+ return data;
683
+ } catch (err) {
684
+ if (err instanceof DatabrainApiError) throw err;
685
+ if (attempt < MAX_RETRIES && err?.name === "TimeoutError" && !options.noRetryOnTimeout) {
686
+ const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 8e3);
687
+ logger.warn(`Request timed out, retrying in ${delay}ms`, { path: options.path });
688
+ await new Promise((r) => setTimeout(r, delay));
689
+ lastError = err;
690
+ continue;
691
+ }
692
+ throw new DatabrainApiError(
693
+ 0,
694
+ "NETWORK_ERROR",
695
+ usesOAuth ? "Failed to connect to Databrain API using configured service credentials." : `Failed to connect to Databrain API: ${err.message}`
696
+ );
697
+ }
698
+ }
699
+ throw lastError || new DatabrainApiError(0, "UNKNOWN", "Request failed after retries");
700
+ }
701
+ };
702
+
703
+ // src/api/custom-charts-api.ts
704
+ var CUSTOM_CHARTS_PATH = "/api/mcp/custom-charts";
705
+ var LIST_CACHE_KEY = "list_custom_charts";
706
+ var encodeKey = (key) => encodeURIComponent(key);
707
+ var CustomChartsApi = class {
708
+ constructor(client) {
709
+ this.client = client;
710
+ }
711
+ async list() {
712
+ const response = await this.client.request({
713
+ method: "GET",
714
+ path: CUSTOM_CHARTS_PATH,
715
+ auth: "service",
716
+ cacheKey: LIST_CACHE_KEY,
717
+ cacheTtlMs: 3e4
718
+ });
719
+ if (Array.isArray(response)) return response;
720
+ return Array.isArray(response?.charts) ? response.charts : [];
721
+ }
722
+ async get(key) {
723
+ const response = await this.client.request({
724
+ method: "GET",
725
+ path: `${CUSTOM_CHARTS_PATH}/${encodeKey(key)}`,
726
+ auth: "service"
727
+ });
728
+ const wrapped = response;
729
+ return wrapped.chart ? wrapped.chart : response;
730
+ }
731
+ /** Never persists. The server repeats every client side check. */
732
+ async validate(body) {
733
+ const response = await this.client.request({
734
+ method: "POST",
735
+ path: `${CUSTOM_CHARTS_PATH}/validate`,
736
+ auth: "service",
737
+ body,
738
+ timeoutMs: 6e4
739
+ });
740
+ return {
741
+ ok: response.ok === true,
742
+ problems: Array.isArray(response.problems) ? response.problems : [],
743
+ contentHash: response.contentHash,
744
+ sizeBytes: response.sizeBytes
745
+ };
746
+ }
747
+ /** Creates the chart (first upload) or a preview version; idempotent on contentHash. */
748
+ async upload(body) {
749
+ return this.client.request({
750
+ method: "POST",
751
+ path: CUSTOM_CHARTS_PATH,
752
+ auth: "service",
753
+ body,
754
+ timeoutMs: 6e4,
755
+ invalidateCache: [LIST_CACHE_KEY]
756
+ });
757
+ }
758
+ async bundle(key, versionId) {
759
+ return this.client.request({
760
+ method: "GET",
761
+ path: `${CUSTOM_CHARTS_PATH}/${encodeKey(key)}/versions/${encodeURIComponent(versionId)}/bundle`,
762
+ auth: "service"
763
+ });
764
+ }
765
+ async publish(key, versionId) {
766
+ return this.client.request({
767
+ method: "POST",
768
+ path: `${CUSTOM_CHARTS_PATH}/${encodeKey(key)}/publish`,
769
+ auth: "service",
770
+ body: versionId ? { versionId } : {},
771
+ invalidateCache: [LIST_CACHE_KEY]
772
+ });
773
+ }
774
+ async discardPreview(key) {
775
+ return this.client.request({
776
+ method: "POST",
777
+ path: `${CUSTOM_CHARTS_PATH}/${encodeKey(key)}/discard-preview`,
778
+ auth: "service",
779
+ body: {},
780
+ invalidateCache: [LIST_CACHE_KEY]
781
+ });
782
+ }
783
+ };
784
+
785
+ // src/custom-chart/api.ts
786
+ function createCustomChartsApi(client) {
787
+ return new CustomChartsApi(client);
788
+ }
789
+ function apiProblems(err) {
790
+ if (!(err instanceof DatabrainApiError)) return [];
791
+ const details = err.details;
792
+ const list = details?.error?.problems ?? details?.problems;
793
+ if (!Array.isArray(list)) return [];
794
+ return list.filter(
795
+ (item) => !!item && typeof item === "object" && typeof item.code === "string" && typeof item.message === "string"
796
+ );
797
+ }
798
+ function planUpload(key, existing, versionsKnown) {
799
+ if (!existing) {
800
+ return { action: "create chart", key, nextVersionNumber: 1, summary: `will create chart custom:${key} (version 1, status private)` };
801
+ }
802
+ const count = typeof versionsKnown === "number" ? versionsKnown : typeof existing.versionsCount === "number" ? existing.versionsCount : void 0;
803
+ const next = count !== void 0 ? count + 1 : NaN;
804
+ const label = Number.isFinite(next) ? `version ${next}` : "the next version";
805
+ return {
806
+ action: "create version",
807
+ key,
808
+ existing,
809
+ nextVersionNumber: Number.isFinite(next) ? next : 0,
810
+ summary: `will create ${label} of custom:${key} in preview (status stays ${existing.status}; the published version keeps serving until you publish)`
811
+ };
812
+ }
813
+
814
+ // src/custom-chart/scaffold.ts
815
+ var DEFAULT_SLOTS = [
816
+ { id: "category", label: "Category", role: "dimension", types: ["string"], required: true, description: "One bar per value." },
817
+ { id: "value", label: "Value", role: "measure", types: ["number"], required: true },
818
+ { id: "target", label: "Target", role: "measure", types: ["number"], required: false }
819
+ ];
820
+ function scaffoldProject(input) {
821
+ const template = readTemplateFiles();
822
+ const templateManifest = JSON.parse(template["manifest.json"]);
823
+ const problems = [];
824
+ if (!isId(input.key)) {
825
+ problems.push({
826
+ code: "manifest.id.invalid",
827
+ path: "/id",
828
+ message: "key must match ^[a-z][a-z0-9_-]{0,63}$ (lowercase letters, digits, _ and -)."
829
+ });
830
+ }
831
+ const name = typeof input.name === "string" ? input.name.trim() : "";
832
+ if (name.length === 0 || name.length > 256) {
833
+ problems.push({ code: "manifest.name.invalid", path: "/name", message: "name must be 1 to 256 characters." });
834
+ }
835
+ const manifest = {
836
+ schemaVersion: 2,
837
+ id: input.key,
838
+ name,
839
+ version: "1.0.0",
840
+ ...input.description ? { description: input.description } : {},
841
+ slots: input.slots && input.slots.length > 0 ? input.slots : templateManifest.slots ?? DEFAULT_SLOTS,
842
+ options: input.options ?? templateManifest.options
843
+ };
844
+ problems.push(...validateManifest(manifest));
845
+ const packageJson = JSON.parse(template["package.json"]);
846
+ packageJson.name = input.key;
847
+ const files = {};
848
+ for (const [file, content] of Object.entries(template)) {
849
+ if (file === "manifest.json") files[file] = JSON.stringify(manifest, null, 2) + "\n";
850
+ else if (file === "package.json") files[file] = JSON.stringify(packageJson, null, 2) + "\n";
851
+ else files[file] = content;
852
+ }
853
+ return {
854
+ ok: problems.length === 0,
855
+ problems,
856
+ files,
857
+ nextStep: "Write these files into a folder, edit chart.ts, then run `databrain custom-chart validate` and `databrain custom-chart build`. Upload with `databrain custom-chart upload` or the custom_chart_upload tool."
858
+ };
859
+ }
860
+
861
+ export {
862
+ oauthHttpsUrl,
863
+ oauthConfigSchema,
864
+ DatabrainApiError,
865
+ toolError,
866
+ stringify,
867
+ toolSuccess,
868
+ studioDraftSchema,
869
+ parseStudioStream,
870
+ editWithBryn,
871
+ registerSecret,
872
+ redactSecrets,
873
+ logger,
874
+ parseErrorBody,
875
+ MCP_SERVER_VERSION,
876
+ toolContext,
877
+ DatabrainClient,
878
+ CustomChartsApi,
879
+ createCustomChartsApi,
880
+ apiProblems,
881
+ planUpload,
882
+ scaffoldProject
883
+ };
884
+ //# sourceMappingURL=chunk-RHK62ZJ6.js.map