@databrainhq/mcp-server 0.3.0-rc.1 → 0.3.0-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -7
- package/dist/apps/server.d.ts +82 -44
- package/dist/apps/server.js +5 -2
- package/dist/apps/stdio.js +6 -3
- package/dist/apps/stdio.js.map +1 -1
- package/dist/bundle-TDQXWIPM.js +24 -0
- package/dist/bundle-TDQXWIPM.js.map +1 -0
- package/dist/{chunk-RF3HTNO7.js → chunk-DM7S4R57.js} +2 -2
- package/dist/{chunk-7DS7OLWP.js → chunk-ES2LUP4U.js} +2030 -1214
- package/dist/chunk-ES2LUP4U.js.map +1 -0
- package/dist/chunk-IBYBK5ZE.js +34 -0
- package/dist/chunk-IBYBK5ZE.js.map +1 -0
- package/dist/chunk-NBFUKVWP.js +603 -0
- package/dist/chunk-NBFUKVWP.js.map +1 -0
- package/dist/chunk-Q5EYDKSA.js +1224 -0
- package/dist/chunk-Q5EYDKSA.js.map +1 -0
- package/dist/chunk-RHK62ZJ6.js +884 -0
- package/dist/chunk-RHK62ZJ6.js.map +1 -0
- package/dist/{chunk-JCXUZ3QZ.js → chunk-Z3ZHN4UW.js} +34 -6
- package/dist/chunk-Z3ZHN4UW.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +497 -0
- package/dist/cli.js.map +1 -0
- package/dist/custom-chart/custom-runtime.js +63 -0
- package/dist/custom-chart/runtime-shim.js +146 -0
- package/dist/custom-chart/template/AGENTS.md +374 -0
- package/dist/custom-chart/template/README.md +374 -0
- package/dist/custom-chart/template/chart.css +28 -0
- package/dist/custom-chart/template/chart.ts +192 -0
- package/dist/custom-chart/template/custom-chart.d.ts +78 -0
- package/dist/custom-chart/template/icon.svg +1 -0
- package/dist/custom-chart/template/manifest.json +23 -0
- package/dist/custom-chart/template/package.json +14 -0
- package/dist/custom-chart/template/sample-data.json +18 -0
- package/dist/custom-chart/template/tsconfig.json +12 -0
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/dist/main-JEYYFGJM.js +872 -0
- package/dist/main-JEYYFGJM.js.map +1 -0
- package/dist/{test-only-EJ5O4WN5.js → test-only-K7QTSVO4.js} +7 -6
- package/dist/test-only-K7QTSVO4.js.map +1 -0
- package/package.json +9 -4
- package/dist/chunk-7DS7OLWP.js.map +0 -1
- package/dist/chunk-JCXUZ3QZ.js.map +0 -1
- package/dist/chunk-NDVW3ZSJ.js +0 -310
- package/dist/chunk-NDVW3ZSJ.js.map +0 -1
- package/dist/main-7QNRH6X4.js +0 -438
- package/dist/main-7QNRH6X4.js.map +0 -1
- package/dist/test-only-EJ5O4WN5.js.map +0 -1
- /package/dist/{chunk-RF3HTNO7.js.map → chunk-DM7S4R57.js.map} +0 -0
|
@@ -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
|