@databrainhq/mcp-server 0.2.17 → 0.3.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +21 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/.mcp.json +11 -0
  4. package/README.md +171 -7
  5. package/dist/apps/server.d.ts +323 -0
  6. package/dist/apps/server.js +11 -0
  7. package/dist/apps/server.js.map +1 -0
  8. package/dist/apps/stdio.d.ts +2 -0
  9. package/dist/apps/stdio.js +16 -0
  10. package/dist/apps/stdio.js.map +1 -0
  11. package/dist/chunk-5IGP6A6T.js +496 -0
  12. package/dist/chunk-5IGP6A6T.js.map +1 -0
  13. package/dist/chunk-CUPL7CAN.js +75 -0
  14. package/dist/chunk-CUPL7CAN.js.map +1 -0
  15. package/dist/chunk-ERTAKB2H.js +11093 -0
  16. package/dist/chunk-ERTAKB2H.js.map +1 -0
  17. package/dist/chunk-N7X762VU.js +34 -0
  18. package/dist/chunk-N7X762VU.js.map +1 -0
  19. package/dist/chunk-OCBTNAAO.js +1946 -0
  20. package/dist/chunk-OCBTNAAO.js.map +1 -0
  21. package/dist/chunk-Z3ZHN4UW.js +130 -0
  22. package/dist/chunk-Z3ZHN4UW.js.map +1 -0
  23. package/dist/cli.d.ts +15 -0
  24. package/dist/cli.js +495 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/custom-chart/runtime-shim.js +146 -0
  27. package/dist/custom-chart/template/AGENTS.md +374 -0
  28. package/dist/custom-chart/template/README.md +374 -0
  29. package/dist/custom-chart/template/chart.css +28 -0
  30. package/dist/custom-chart/template/chart.ts +192 -0
  31. package/dist/custom-chart/template/custom-chart.d.ts +78 -0
  32. package/dist/custom-chart/template/icon.svg +1 -0
  33. package/dist/custom-chart/template/manifest.json +23 -0
  34. package/dist/custom-chart/template/package.json +14 -0
  35. package/dist/custom-chart/template/sample-data.json +18 -0
  36. package/dist/custom-chart/template/tsconfig.json +12 -0
  37. package/dist/index.js +65 -9787
  38. package/dist/index.js.map +1 -1
  39. package/dist/main-KZRHWE3K.js +857 -0
  40. package/dist/main-KZRHWE3K.js.map +1 -0
  41. package/dist/test-only-K7QTSVO4.js +38 -0
  42. package/dist/test-only-K7QTSVO4.js.map +1 -0
  43. package/package.json +17 -5
  44. package/skills/author-dashboard/SKILL.md +30 -0
  45. package/skills/embed-dashboard/SKILL.md +23 -0
  46. package/skills/style-dashboard/SKILL.md +58 -0
@@ -0,0 +1,857 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ registerChartApps
4
+ } from "./chunk-5IGP6A6T.js";
5
+ import {
6
+ PREVIEW_PATH,
7
+ WRITE_CREATE,
8
+ enableChartApps,
9
+ registerAllPrompts,
10
+ registerAllResources,
11
+ registerAllTools
12
+ } from "./chunk-ERTAKB2H.js";
13
+ import {
14
+ DatabrainApiError,
15
+ DatabrainClient,
16
+ MCP_SERVER_VERSION,
17
+ parseErrorBody,
18
+ stringify,
19
+ toolContext,
20
+ toolError,
21
+ toolSuccess
22
+ } from "./chunk-OCBTNAAO.js";
23
+ import {
24
+ FilePatStore,
25
+ HOSTED_SCOPES,
26
+ HostedAuthError,
27
+ createBearerVerifier,
28
+ principalSchema
29
+ } from "./chunk-Z3ZHN4UW.js";
30
+
31
+ // src/hosted/main.ts
32
+ import { isAbsolute } from "path";
33
+ import { pathToFileURL } from "url";
34
+
35
+ // src/hosted/config.ts
36
+ function loadHostedConfig(env = process.env) {
37
+ const testOnly = env.DATABRAIN_HOSTED_TEST_ONLY === "1";
38
+ if (testOnly && env.NODE_ENV === "production") throw new Error("Hosted test mode is forbidden in production.");
39
+ if (env.DATABRAIN_HOSTED_TEST_ONLY && !testOnly) throw new Error("Invalid DATABRAIN_HOSTED_TEST_ONLY.");
40
+ const validUrl = (value, field) => {
41
+ try {
42
+ if (!value) throw new Error();
43
+ const url = new URL(value);
44
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:") throw new Error();
45
+ return url;
46
+ } catch {
47
+ throw new Error(`Configure ${field} as an HTTPS URL without credentials, query or fragment.`);
48
+ }
49
+ };
50
+ let publicValue = env.DATABRAIN_HOSTED_PUBLIC_URL;
51
+ let issuerValue = env.DATABRAIN_HOSTED_ISSUER;
52
+ if (!publicValue && !issuerValue && env.HOSTED_APP_URL) {
53
+ const origin = validUrl(env.HOSTED_APP_URL, "HOSTED_APP_URL").origin;
54
+ publicValue = `${origin}/mcp`;
55
+ issuerValue = origin;
56
+ }
57
+ const publicUrl = validUrl(publicValue, "DATABRAIN_HOSTED_PUBLIC_URL");
58
+ validUrl(issuerValue, "DATABRAIN_HOSTED_ISSUER");
59
+ if (publicUrl.pathname !== "/mcp") throw new Error("DATABRAIN_HOSTED_PUBLIC_URL must end in /mcp.");
60
+ const port = Number(env.DATABRAIN_HOSTED_PORT || "3144");
61
+ const bind = env.DATABRAIN_HOSTED_BIND || "127.0.0.1";
62
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("Invalid DATABRAIN_HOSTED_PORT.");
63
+ return {
64
+ publicUrl: publicValue,
65
+ issuer: issuerValue,
66
+ port,
67
+ bind,
68
+ testOnly
69
+ };
70
+ }
71
+
72
+ // src/hosted/http.ts
73
+ import { createServer } from "http";
74
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
75
+
76
+ // src/hosted/server.ts
77
+ import { AsyncLocalStorage } from "async_hooks";
78
+ import { z as z2 } from "zod";
79
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
80
+ import { instrument } from "@posthog/mcp";
81
+ import { PostHog } from "posthog-node";
82
+
83
+ // src/tools/infrastructure/create-workspace.ts
84
+ import { z } from "zod";
85
+ function registerCreateWorkspace(server, client) {
86
+ server.tool(
87
+ "create_workspace",
88
+ 'Create a new workspace to connect a datasource or datamart for dashboard building. Works only when this sign-in granted "All workspaces I can access" and your role may create workspaces; the new workspace is usable on the next call.',
89
+ {
90
+ name: z.string().describe("Unique workspace name"),
91
+ connectionType: z.enum(["DATASOURCE", "DATAMART", "MULTI_DATASOURCE"]).optional().describe("Connection type \u2014 DATASOURCE, DATAMART, or MULTI_DATASOURCE"),
92
+ datasourceName: z.string().optional().describe("Datasource name (required when connectionType is DATASOURCE)"),
93
+ datamartName: z.string().optional().describe("Datamart name (required when connectionType is DATAMART)"),
94
+ body: z.record(z.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/workspace")
95
+ },
96
+ WRITE_CREATE,
97
+ async (params) => {
98
+ try {
99
+ const body = params.body ? { ...params.body } : {
100
+ name: params.name,
101
+ connectionType: params.connectionType,
102
+ datasourceName: params.datasourceName,
103
+ datamartName: params.datamartName
104
+ };
105
+ const result = await client.request({
106
+ method: "POST",
107
+ path: "/api/v2/workspace",
108
+ body,
109
+ auth: "service",
110
+ invalidateCache: ["list_workspaces"]
111
+ });
112
+ if (result.error) {
113
+ return toolError(`Workspace creation failed: ${stringify(result.error)}`);
114
+ }
115
+ return toolSuccess(
116
+ `Workspace "${result.name || params.name}" created successfully.
117
+
118
+ Next steps:
119
+ 1. Use \`create_data_app\` to create a data app linked to this workspace
120
+ 2. Build dashboards in the Databrain UI using this workspace
121
+ 3. Use \`create_embed\` to embed dashboards`
122
+ );
123
+ } catch (err) {
124
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
125
+ return toolError(`Unexpected error: ${err.message}`);
126
+ }
127
+ }
128
+ );
129
+ }
130
+
131
+ // src/hosted/authorization.ts
132
+ var TOOL_SCOPES = {
133
+ whoami: [],
134
+ list_datasources: ["mcp:datamarts:read"],
135
+ discover_datasource_schema: ["mcp:datamarts:read"],
136
+ list_datamarts: ["mcp:datamarts:read"],
137
+ list_workspaces: ["mcp:datamarts:read"],
138
+ sync_datasource: ["mcp:datamarts:write"],
139
+ create_datamart: ["mcp:datamarts:write"],
140
+ list_data_apps: ["mcp:dashboards:read"],
141
+ list_tenants: ["mcp:dashboards:read"],
142
+ create_data_app: ["mcp:embeds:write"],
143
+ list_api_tokens: ["mcp:embeds:write"],
144
+ create_api_token: ["mcp:embeds:write"],
145
+ rotate_api_token: ["mcp:embeds:write"],
146
+ create_embed: ["mcp:embeds:write"],
147
+ list_embeds: ["mcp:dashboards:read"],
148
+ get_embed_details: ["mcp:dashboards:read"],
149
+ update_embed: ["mcp:embeds:write"],
150
+ customize_embed_theme: ["mcp:embeds:write"],
151
+ get_demo_link: ["mcp:embeds:write"],
152
+ generate_guest_token: ["mcp:embeds:write"],
153
+ generate_embed_code: ["mcp:dashboards:read"],
154
+ create_workspace: ["mcp:datamarts:write"],
155
+ list_dashboards: ["mcp:dashboards:read"],
156
+ create_dashboard: ["mcp:dashboards:write"],
157
+ get_dashboard: ["mcp:dashboards:read"],
158
+ dashboard_snapshot: ["mcp:dashboards:read"],
159
+ generate_dashboard_tiles: ["mcp:dashboards:write", "mcp:query"],
160
+ ask_question: ["mcp:query"],
161
+ get_semantic_layer: ["mcp:datamarts:read"],
162
+ update_semantic_layer: ["mcp:datamarts:write"],
163
+ start_semantic_layer_generation: ["mcp:datamarts:write"],
164
+ get_semantic_layer_generation_status: ["mcp:datamarts:read"],
165
+ search_semantic_model: ["mcp:datamarts:read"],
166
+ get_semantic_entity: ["mcp:datamarts:read"],
167
+ propose_semantic_change: ["mcp:datamarts:read"],
168
+ apply_semantic_change: ["mcp:datamarts:write"],
169
+ eval_semantic_model: ["mcp:datamarts:read", "mcp:query"],
170
+ list_widgets: ["mcp:dashboards:read"],
171
+ validate_metric_spec: ["mcp:dashboards:read", "mcp:query"],
172
+ create_or_update_metric_from_query: ["mcp:dashboards:write", "mcp:query"],
173
+ create_workspace_metric: ["mcp:dashboards:write", "mcp:query"],
174
+ add_widget: ["mcp:dashboards:write"],
175
+ update_widget: ["mcp:dashboards:write"],
176
+ remove_widget: ["mcp:dashboards:write"],
177
+ generate_widget: ["mcp:dashboards:write", "mcp:query"],
178
+ list_scheduled_reports: ["mcp:dashboards:read"],
179
+ custom_chart_scaffold: [],
180
+ custom_chart_validate: ["mcp:custom-charts:read"],
181
+ custom_chart_upload: ["mcp:custom-charts:write"],
182
+ list_custom_charts: ["mcp:custom-charts:read"],
183
+ publish_custom_chart: ["mcp:custom-charts:write"],
184
+ // Chart tile controls (the chart-apps composition). The same routes as ask_question and a
185
+ // dashboard save; listed so a hosted composition is not refused for want of a policy.
186
+ preview_chart: ["mcp:query"],
187
+ save_chart_as_dashboard: ["mcp:dashboards:write", "mcp:query"],
188
+ // Saved-widget rendering mints a guest token; retain the issuer's embed-write boundary.
189
+ render_chart: ["mcp:dashboards:read", "mcp:query", "mcp:embeds:write"],
190
+ refresh_widget: ["mcp:dashboards:read", "mcp:query", "mcp:embeds:write"],
191
+ drill_down: ["mcp:dashboards:read", "mcp:query", "mcp:embeds:write"],
192
+ change_date_range: ["mcp:dashboards:read", "mcp:query", "mcp:embeds:write"]
193
+ };
194
+ function requiredScopes(name) {
195
+ if (!Object.hasOwn(TOOL_SCOPES, name)) throw new HostedAuthError(403, "tool_not_authorized");
196
+ return TOOL_SCOPES[name];
197
+ }
198
+ function authorizeTool(name, principal) {
199
+ if (principal.expiresAt !== void 0 && principal.expiresAt <= Date.now() / 1e3) throw new HostedAuthError(401, "invalid_token");
200
+ if (requiredScopes(name).some((scope) => !principal.scopes.includes(scope))) throw new HostedAuthError(403, "insufficient_scope");
201
+ }
202
+ var TOOL_ARGUMENT_VALUES = {
203
+ list_dashboards: { scope: ["auto", "data_app", "global"] }
204
+ };
205
+ var TOOL_OPAQUE_ARGUMENTS = {
206
+ preview_chart: ["rows"]
207
+ };
208
+ function assertToolArguments(name, value, principal, delegated = false) {
209
+ const allowed = Object.hasOwn(TOOL_ARGUMENT_VALUES, name) ? TOOL_ARGUMENT_VALUES[name] : {};
210
+ const opaque = Object.hasOwn(TOOL_OPAQUE_ARGUMENTS, name) ? TOOL_OPAQUE_ARGUMENTS[name] : void 0;
211
+ const target = selectWorkspace(value, principal);
212
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
213
+ assertBoundArguments(value, principal, { target, delegated });
214
+ return target;
215
+ }
216
+ const rest = {};
217
+ for (const [key, child] of Object.entries(value)) {
218
+ if (!Object.hasOwn(allowed, key)) {
219
+ rest[key] = child;
220
+ continue;
221
+ }
222
+ if (typeof child !== "string" || !allowed[key].includes(child)) throw new HostedAuthError(403, "identity_argument_forbidden");
223
+ }
224
+ assertBoundArguments(rest, principal, { target, delegated, opaque });
225
+ return target;
226
+ }
227
+ var WORKSPACE_FREE_TOOLS = /* @__PURE__ */ new Set([
228
+ "whoami",
229
+ "list_workspaces",
230
+ "create_workspace",
231
+ "generate_embed_code",
232
+ "custom_chart_scaffold",
233
+ "custom_chart_validate",
234
+ "custom_chart_upload",
235
+ "list_custom_charts",
236
+ "publish_custom_chart"
237
+ ]);
238
+ var WORKSPACE_SPANNING_TOOLS = /* @__PURE__ */ new Set(["list_dashboards"]);
239
+ var WORKSPACE_INPUT = "workspace";
240
+ var WorkspaceSelectionError = class extends HostedAuthError {
241
+ constructor(status, code, reason) {
242
+ super(status, code);
243
+ this.reason = reason;
244
+ }
245
+ };
246
+ function grantedWorkspaces(principal) {
247
+ if (principal.grantedWorkspaces) return principal.grantedWorkspaces;
248
+ return principal.workspaceId ? [{ id: principal.workspaceId, name: principal.workspaceName }] : [];
249
+ }
250
+ function grantedNames(principal) {
251
+ const names = grantedWorkspaces(principal).map((item) => item.name ?? item.id);
252
+ return names.length > 20 ? `${names.slice(0, 20).join(", ")} and ${names.length - 20} more` : names.join(", ");
253
+ }
254
+ function findGranted(principal, key, value) {
255
+ const granted = grantedWorkspaces(principal);
256
+ const matches = typeof value !== "string" ? [] : key === "workspaceId" ? granted.filter((item) => item.id === value) : key === "workspaceName" ? granted.filter((item) => item.name === value) : granted.some((item) => item.id === value) ? granted.filter((item) => item.id === value) : granted.filter((item) => item.name === value);
257
+ if (matches.length !== 1) {
258
+ throw new WorkspaceSelectionError(
259
+ 404,
260
+ "NOT_FOUND_IN_GRANT",
261
+ `Workspace ${JSON.stringify(String(value).slice(0, 100))} is not one this sign-in granted. Granted: ${grantedNames(principal)}.`
262
+ );
263
+ }
264
+ return matches[0];
265
+ }
266
+ function selectWorkspace(args, principal, keys = [WORKSPACE_INPUT, "workspaceName", "workspaceId"]) {
267
+ let target;
268
+ if (args && typeof args === "object" && !Array.isArray(args)) {
269
+ for (const key of keys) {
270
+ const value = args[key];
271
+ if (value === void 0) continue;
272
+ const found = findGranted(principal, key, value);
273
+ if (target && target.id !== found.id) {
274
+ throw new WorkspaceSelectionError(
275
+ 400,
276
+ "WORKSPACE_CONFLICT",
277
+ "These inputs name different workspaces. Name one workspace for this call."
278
+ );
279
+ }
280
+ target = found;
281
+ }
282
+ }
283
+ const granted = grantedWorkspaces(principal);
284
+ return target ?? (granted.length === 1 ? granted[0] : void 0);
285
+ }
286
+ function namesWorkspace(value) {
287
+ if (!value || typeof value !== "object") return false;
288
+ return Object.entries(value).some(([key, child]) => [WORKSPACE_INPUT, "workspaceName", "workspaceId"].includes(key) && child !== void 0 || namesWorkspace(child));
289
+ }
290
+ function tenantAllowed(child, principal, delegated) {
291
+ if (typeof child !== "string") return false;
292
+ if (principal.tenantIds) return principal.tenantIds.includes(child);
293
+ return delegated && principal.credentialType === "oauth";
294
+ }
295
+ function assertBoundArguments(value, principal, binding = {}) {
296
+ if (!value || typeof value !== "object") return;
297
+ const { opaque, ...nested } = binding;
298
+ for (const [key, child] of Object.entries(value)) {
299
+ if (opaque?.includes(key) && !Array.isArray(value)) continue;
300
+ if (key === "workspaceId" || key === "workspaceName") {
301
+ const target = binding.target;
302
+ if (target ? child !== (key === "workspaceId" ? target.id : target.name) : !findGranted(principal, key, child)) {
303
+ throw new HostedAuthError(403, "principal_mismatch");
304
+ }
305
+ }
306
+ if (["companyId", "userId"].includes(key) && child !== principal[key]) {
307
+ throw new HostedAuthError(403, "principal_mismatch");
308
+ }
309
+ if ([
310
+ "scope",
311
+ "scopes",
312
+ "authUser",
313
+ "actorKind",
314
+ "credentialType",
315
+ "oauthClientId",
316
+ "operatorEmail",
317
+ "session_variables",
318
+ "isAllClient",
319
+ "isAdmin",
320
+ "serviceToken",
321
+ "apiToken",
322
+ "authorization"
323
+ ].includes(key)) {
324
+ throw new HostedAuthError(403, "identity_argument_forbidden");
325
+ }
326
+ if (["clientId", "tenantId"].includes(key) && !tenantAllowed(child, principal, binding.delegated === true)) {
327
+ throw new HostedAuthError(403, "tenant_not_authorized");
328
+ }
329
+ assertBoundArguments(child, principal, nested);
330
+ }
331
+ }
332
+
333
+ // src/hosted/server.ts
334
+ var schemaInstrumentation = new PostHog("hosted-contract-only", { disabled: true });
335
+ var callWorkspace = new AsyncLocalStorage();
336
+ var WORKSPACE_HINT = "Call whoami to see the workspaces this sign-in granted, then name one.";
337
+ function workspaceError(error) {
338
+ return toolError(`Error: ${error.reason} (${error.code})
339
+
340
+ Hint: ${WORKSPACE_HINT}`);
341
+ }
342
+ function scopeError(missing) {
343
+ return toolError(`Error: This connection cannot do this. It needs ${missing.join(", ")}, which your role did not allow when you connected. (INSUFFICIENT_SCOPE)
344
+
345
+ Hint: Ask a Databrain admin for a role that allows it, then connect again. If your role allows it in a workspace you did not choose, connect again and choose that workspace.`);
346
+ }
347
+ function onlyWorkspace(raw, target) {
348
+ const keep = (items) => Array.isArray(items) ? items.filter((item) => {
349
+ const record = item && typeof item === "object" ? item : {};
350
+ return record.id === target.id || record._id === target.id || record.id === void 0 && record._id === void 0 && target.name !== void 0 && record.name === target.name;
351
+ }) : items;
352
+ if (Array.isArray(raw)) return keep(raw);
353
+ if (raw && typeof raw === "object" && "data" in raw) return { ...raw, data: keep(raw.data) };
354
+ return raw;
355
+ }
356
+ var HostedClient = class extends DatabrainClient {
357
+ constructor(adapter, principal, delegation) {
358
+ super({ apiUrl: "https://api.usedatabrain.com", demoDomain: "https://demo.usedatabrain.com" });
359
+ this.adapter = adapter;
360
+ this.principal = principal;
361
+ this.delegation = delegation;
362
+ }
363
+ // A generated API credential must never replace the authorizing hosted principal.
364
+ setRuntimeApiToken(_token) {
365
+ }
366
+ async request(options) {
367
+ try {
368
+ const name = toolContext.getStore()?.tool;
369
+ if (!name) throw new HostedAuthError(403, "tool_not_authorized");
370
+ authorizeTool(name, this.principal);
371
+ const named = callWorkspace.getStore()?.target;
372
+ const target = named ?? (selectWorkspace(options.body, this.principal, ["workspaceName", "workspaceId"]) ?? selectWorkspace(options.query, this.principal, ["workspaceName", "workspaceId"]));
373
+ const delegated = this.delegation !== void 0;
374
+ const opaque = options.method === "POST" && options.path === PREVIEW_PATH ? ["rows"] : void 0;
375
+ assertBoundArguments(options.body, this.principal, { target, delegated, opaque });
376
+ assertBoundArguments(options.query, this.principal, { target, delegated });
377
+ let raw = await this.adapter.executeRequest(
378
+ structuredClone(options),
379
+ structuredClone(this.principal),
380
+ this.delegation,
381
+ target && structuredClone(target)
382
+ );
383
+ if (named && options.method === "GET" && options.path === "/api/v2/workspace") raw = onlyWorkspace(raw, named);
384
+ if (!options.skipDataUnwrap && raw && typeof raw === "object" && !Array.isArray(raw) && "data" in raw && raw.data !== null && raw.data !== void 0) return raw.data;
385
+ return raw;
386
+ } catch (error) {
387
+ if (error instanceof WorkspaceSelectionError) {
388
+ throw new DatabrainApiError(error.status, error.code, `${error.reason} (${error.code})`, void 0, WORKSPACE_HINT);
389
+ }
390
+ const status = error instanceof HostedAuthError ? error.status : 503;
391
+ const detail = error instanceof HostedAuthError ? error.detail : void 0;
392
+ if (status === 403 && detail?.code) {
393
+ throw new DatabrainApiError(
394
+ 0,
395
+ detail.code,
396
+ `${detail.message ?? "Your role does not allow this here."} (${detail.code})`,
397
+ void 0,
398
+ "Ask a Databrain admin for access, or pick a workspace where your role allows it."
399
+ );
400
+ }
401
+ if (status === 401 || status === 403) {
402
+ throw new DatabrainApiError(
403
+ 0,
404
+ "HOSTED_REQUEST_DENIED",
405
+ "The signed-in Databrain user cannot do this: the sign-in expired or was revoked, or the granted scopes, role or workspace do not allow it. Sign in again or ask an admin."
406
+ );
407
+ }
408
+ if (detail && (detail.code || detail.message)) {
409
+ const message = detail.message ?? (status === 404 ? "Not found in the workspace this sign-in approved." : "The backend refused this request.");
410
+ throw new DatabrainApiError(
411
+ detail.status,
412
+ detail.code ?? "HOSTED_REQUEST_FAILED",
413
+ detail.code && !message.includes(detail.code) ? `${message} (${detail.code})` : message,
414
+ { error: { code: detail.code, message: detail.message } }
415
+ );
416
+ }
417
+ throw new DatabrainApiError(
418
+ status,
419
+ "HOSTED_REQUEST_DENIED",
420
+ status === 404 ? "Not found in the workspace this sign-in approved." : "The authorized backend could not complete this request. Check workspace access and hosted integration."
421
+ );
422
+ }
423
+ }
424
+ };
425
+ function workspaceLine(name, input) {
426
+ return WORKSPACE_SPANNING_TOOLS.has(name) ? ` With more than one granted workspace, name one in \`${input}\` to list only that one; leave it out to list them all.` : ` Needed when you granted more than one workspace: name it in \`${input}\`.`;
427
+ }
428
+ function whoami(principal) {
429
+ const granted = grantedWorkspaces(principal);
430
+ return toolSuccess(JSON.stringify({
431
+ status: "authenticated",
432
+ activeBearer: principal.credentialType,
433
+ actorKind: principal.actorKind,
434
+ userId: principal.userId,
435
+ companyId: principal.companyId,
436
+ workspaceGrant: principal.workspaces?.mode === "all" ? "All workspaces I can access (new ones are included)" : "Only the workspaces chosen at sign-in",
437
+ grantedWorkspaces: granted.map((item) => ({ id: item.id, name: item.name })),
438
+ ...granted.length === 1 ? { workspaceId: granted[0].id, workspaceName: granted[0].name } : {},
439
+ note: granted.length === 1 ? "Every call works in this workspace." : `Name the workspace on each call that works in one (the \`${WORKSPACE_INPUT}\` or \`workspaceName\` input): ${grantedNames(principal)}.`,
440
+ oauthClientId: principal.clientId,
441
+ scopes: principal.scopes
442
+ }, null, 2));
443
+ }
444
+ function createHostedToolServer(principal, adapter, delegation) {
445
+ const bound = structuredClone(principal);
446
+ const delegated = delegation !== void 0;
447
+ const server = new McpServer({ name: "databrain", version: MCP_SERVER_VERSION }, {
448
+ instructions: "Hosted Databrain: each request is authenticated as the signed-in user, with that user's own role in each workspace. Use whoami to see the verified identity, scopes and the workspaces this sign-in granted. When more than one workspace is granted, name the workspace on each call (the workspace or workspaceName input). Never ask for a service token or put credentials in tool arguments. Workspace and tenant access come from the bearer principal. Existing dry-run and production-confirmation rules still apply. Use ask_question for inline chart or KPI answers. Chart controls and save_chart_as_dashboard use the same workspace and role; saving requires the existing production confirmation. Hosts without MCP Apps can read the text answer."
449
+ });
450
+ const register = server.tool.bind(server);
451
+ server.tool = ((...args) => {
452
+ const name = args[0];
453
+ const index = args.length - 1;
454
+ const handler = args[index];
455
+ const shape = typeof args[1] === "string" && args[2] && typeof args[2] === "object" && !("readOnlyHint" in args[2]) ? args[2] : void 0;
456
+ const ownSelector = shape && ["workspaceName", "workspaceId"].find((key) => Object.hasOwn(shape, key));
457
+ const addsInput = shape !== void 0 && !ownSelector && !WORKSPACE_FREE_TOOLS.has(name) && !Object.hasOwn(shape, WORKSPACE_INPUT);
458
+ if (shape && !WORKSPACE_FREE_TOOLS.has(name)) {
459
+ if (addsInput) {
460
+ args[2] = { ...shape, [WORKSPACE_INPUT]: z2.string().min(1).max(512).optional().describe(WORKSPACE_SPANNING_TOOLS.has(name) ? "Workspace id or name. Leave it out to cover every granted workspace." : "Workspace id or name. Needed when you granted more than one workspace.") };
461
+ }
462
+ args[1] = `${args[1]}${workspaceLine(name, addsInput ? WORKSPACE_INPUT : ownSelector)}`;
463
+ }
464
+ args[index] = async (...values) => {
465
+ try {
466
+ authorizeTool(name, bound);
467
+ const target = assertToolArguments(name, values[0], bound, delegated);
468
+ if (name === "whoami") return whoami(bound);
469
+ const input = values[0] && typeof values[0] === "object" ? values[0] : void 0;
470
+ if (!target && !WORKSPACE_FREE_TOOLS.has(name) && !WORKSPACE_SPANNING_TOOLS.has(name) && !namesWorkspace(input)) {
471
+ throw new WorkspaceSelectionError(
472
+ 400,
473
+ "WORKSPACE_REQUIRED",
474
+ `Name the workspace for this call. Granted: ${grantedNames(bound)}.`
475
+ );
476
+ }
477
+ if (input) {
478
+ if (addsInput) delete input[WORKSPACE_INPUT];
479
+ if (shape && target) {
480
+ if (Object.hasOwn(shape, "workspaceId")) input.workspaceId = target.id;
481
+ if (Object.hasOwn(shape, "workspaceName") && target.name !== void 0) input.workspaceName = target.name;
482
+ }
483
+ }
484
+ const result = await callWorkspace.run({ target }, () => handler(...values));
485
+ if (target && result?._meta?.["ui/resourceUri"]) {
486
+ return { ...result, _meta: { ...result._meta, "databrain/workspace": target } };
487
+ }
488
+ return result;
489
+ } catch (error) {
490
+ if (error instanceof WorkspaceSelectionError) return workspaceError(error);
491
+ return toolError("Hosted authorization denied this tool call. Check the granted scopes, workspace and tenant access.");
492
+ }
493
+ };
494
+ return register(...args);
495
+ });
496
+ const client = new HostedClient(adapter, bound, delegation);
497
+ enableChartApps(server);
498
+ registerAllTools(server, client);
499
+ registerCreateWorkspace(server, client);
500
+ registerAllResources(server);
501
+ registerAllPrompts(server);
502
+ registerChartApps(server, client);
503
+ instrument(server, schemaInstrumentation, { beforeSend: () => null });
504
+ return server;
505
+ }
506
+
507
+ // src/hosted/http.ts
508
+ var HEALTH_BODY = JSON.stringify({ status: "ok" });
509
+ function healthPath(url) {
510
+ const path = (url || "/").split("?")[0];
511
+ return path === "/health";
512
+ }
513
+ function reply(res, status, body) {
514
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
515
+ res.end(JSON.stringify(body));
516
+ }
517
+ async function readBody(req) {
518
+ let size = 0;
519
+ const chunks = [];
520
+ for await (const chunk of req) {
521
+ size += chunk.length;
522
+ if (size > 1048576) throw new Error("body_too_large");
523
+ chunks.push(Buffer.from(chunk));
524
+ }
525
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
526
+ }
527
+ function createHostedHttpServer(config, adapter, pats) {
528
+ const publicUrl = new URL(config.publicUrl);
529
+ const metadataPath = `/.well-known/oauth-protected-resource${publicUrl.pathname === "/" ? "" : publicUrl.pathname}`;
530
+ const metadataUrl = `${publicUrl.origin}${metadataPath}`;
531
+ const verify = createBearerVerifier(adapter, config.publicUrl, pats);
532
+ let active = 0;
533
+ const server = createServer(async (req, res) => {
534
+ if (healthPath(req.url)) {
535
+ if (req.method !== "GET" && req.method !== "HEAD") {
536
+ res.writeHead(405, { Allow: "GET, HEAD", "Cache-Control": "no-store" });
537
+ res.end();
538
+ return;
539
+ }
540
+ res.writeHead(200, {
541
+ "Content-Type": "application/json",
542
+ "Cache-Control": "no-store",
543
+ "Content-Length": Buffer.byteLength(HEALTH_BODY)
544
+ });
545
+ res.end(req.method === "HEAD" ? void 0 : HEALTH_BODY);
546
+ return;
547
+ }
548
+ res.setHeader("X-Content-Type-Options", "nosniff");
549
+ if (config.testOnly) res.setHeader("X-Databrain-Test-Only", "true");
550
+ try {
551
+ const localPort = server.address()?.port;
552
+ if (![publicUrl.host, `127.0.0.1:${localPort}`, `localhost:${localPort}`].includes(req.headers.host || "")) {
553
+ reply(res, 403, { error: "invalid_host" });
554
+ return;
555
+ }
556
+ if (req.headers.origin && req.headers.origin !== publicUrl.origin) {
557
+ reply(res, 403, { error: "invalid_origin" });
558
+ return;
559
+ }
560
+ if (req.headers.origin) {
561
+ res.setHeader("Access-Control-Allow-Origin", publicUrl.origin);
562
+ res.setHeader("Vary", "Origin");
563
+ res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate, MCP-Protocol-Version");
564
+ }
565
+ const url = new URL(req.url || "/", publicUrl.origin);
566
+ if (req.method === "GET" && [metadataPath, "/.well-known/oauth-protected-resource"].includes(url.pathname)) {
567
+ reply(res, 200, {
568
+ resource: config.publicUrl,
569
+ authorization_servers: [config.issuer],
570
+ scopes_supported: HOSTED_SCOPES,
571
+ bearer_methods_supported: ["header"],
572
+ resource_name: config.testOnly ? "Databrain (TEST ONLY; fixture identity)" : "Databrain"
573
+ });
574
+ return;
575
+ }
576
+ if (url.pathname !== publicUrl.pathname) {
577
+ reply(res, 404, { error: "not_found" });
578
+ return;
579
+ }
580
+ if (url.search) {
581
+ reply(res, 400, { error: "query_parameters_not_supported" });
582
+ return;
583
+ }
584
+ if (req.method === "OPTIONS") {
585
+ res.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS");
586
+ res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, MCP-Protocol-Version, MCP-Session-Id");
587
+ res.writeHead(204);
588
+ res.end();
589
+ return;
590
+ }
591
+ const authFields = req.rawHeaders.filter((_, i) => i % 2 === 0 && req.rawHeaders[i].toLowerCase() === "authorization");
592
+ const match = /^Bearer ([^\s]+)$/i.exec(req.headers.authorization || "");
593
+ if (authFields.length !== 1 || !match) throw new HostedAuthError(401, "invalid_token");
594
+ const principal = await verify(match[1]);
595
+ if (req.method !== "POST") {
596
+ res.setHeader("Allow", "POST, OPTIONS");
597
+ reply(res, 405, { error: "method_not_allowed" });
598
+ return;
599
+ }
600
+ if (!req.headers["content-type"]?.toLowerCase().startsWith("application/json")) {
601
+ reply(res, 415, { error: "unsupported_media_type" });
602
+ return;
603
+ }
604
+ if (active >= 32) {
605
+ reply(res, 503, { error: "server_busy" });
606
+ return;
607
+ }
608
+ active++;
609
+ let body;
610
+ try {
611
+ body = await readBody(req);
612
+ } catch {
613
+ active--;
614
+ reply(res, 400, { error: "invalid_request" });
615
+ return;
616
+ }
617
+ try {
618
+ if (Array.isArray(body)) {
619
+ reply(res, 400, { error: "batch_not_supported" });
620
+ return;
621
+ }
622
+ const delegation = match[1].startsWith("dbp_") ? void 0 : adapter.delegate?.(match[1], principal);
623
+ if (body?.method === "tools/call" && typeof body.params?.name === "string") {
624
+ try {
625
+ authorizeTool(body.params.name, principal);
626
+ assertToolArguments(body.params.name, body.params.arguments, principal, delegation !== void 0);
627
+ } catch (error) {
628
+ if (error instanceof WorkspaceSelectionError) {
629
+ reply(res, 200, { jsonrpc: "2.0", id: body.id ?? null, result: workspaceError(error) });
630
+ return;
631
+ }
632
+ if (error instanceof HostedAuthError && error.code === "insufficient_scope") {
633
+ const missing = requiredScopes(body.params.name).filter((scope) => !principal.scopes.includes(scope));
634
+ reply(res, 200, { jsonrpc: "2.0", id: body.id ?? null, result: scopeError(missing) });
635
+ return;
636
+ }
637
+ throw error;
638
+ }
639
+ }
640
+ const mcp = createHostedToolServer(principal, adapter, delegation);
641
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true });
642
+ let closed = false;
643
+ const close = () => {
644
+ if (!closed) {
645
+ closed = true;
646
+ void mcp.close();
647
+ }
648
+ };
649
+ res.once("close", close);
650
+ try {
651
+ await mcp.connect(transport);
652
+ await transport.handleRequest(req, res, body);
653
+ } finally {
654
+ if (res.writableEnded) close();
655
+ }
656
+ } finally {
657
+ active--;
658
+ }
659
+ } catch (error) {
660
+ if (res.headersSent) {
661
+ res.end();
662
+ return;
663
+ }
664
+ const status = error instanceof HostedAuthError ? error.status : 503;
665
+ if (status === 401) res.setHeader("WWW-Authenticate", `Bearer error="invalid_token", resource_metadata="${metadataUrl}"`);
666
+ reply(res, status, { error: error instanceof HostedAuthError ? error.code : "temporarily_unavailable" });
667
+ }
668
+ });
669
+ server.requestTimeout = 3e4;
670
+ server.headersTimeout = 1e4;
671
+ server.keepAliveTimeout = 5e3;
672
+ return server;
673
+ }
674
+
675
+ // src/hosted/provider.ts
676
+ import { z as z3 } from "zod";
677
+ var USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION} (hosted)`;
678
+ var REQUEST_TIMEOUT_MS = 3e4;
679
+ var FORWARDED_HEADERS = /* @__PURE__ */ new Set(["idempotency-key"]);
680
+ var WORKSPACE_HEADER = "X-Databrain-Workspace";
681
+ var GRANT_DENIALS = /* @__PURE__ */ new Set(["ROLE_NOT_ALLOWED", "WORKSPACE_CREATE_NOT_ALLOWED"]);
682
+ function errorDetail(status, text, bearer) {
683
+ let parsed;
684
+ try {
685
+ parsed = JSON.parse(text);
686
+ } catch {
687
+ return { status };
688
+ }
689
+ const envelope = parseErrorBody(parsed);
690
+ const tiles = tileProblems(parsed);
691
+ const code = envelope.code ?? (tiles ? "TILE_VALIDATION_FAILED" : void 0);
692
+ const message = envelope.message ?? tiles;
693
+ const clean = (value, max) => {
694
+ if (!value) return void 0;
695
+ let out = value.replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
696
+ if (bearer) out = out.split(bearer).join("[redacted]");
697
+ return out.slice(0, max) || void 0;
698
+ };
699
+ return {
700
+ status,
701
+ code: code && /^[A-Za-z0-9_.:-]{1,64}$/.test(code) ? code : void 0,
702
+ message: clean(message, 1200)
703
+ };
704
+ }
705
+ function tileProblems(body) {
706
+ const verdicts = body?.tileVerdicts;
707
+ if (!Array.isArray(verdicts)) return void 0;
708
+ const lines = verdicts.flatMap((verdict) => {
709
+ const { id, ok, problems } = verdict ?? {};
710
+ if (ok !== false || !Array.isArray(problems)) return [];
711
+ const words = problems.filter((p) => typeof p === "string");
712
+ return words.length ? [`Tile ${typeof id === "string" ? id : "?"}: ${words.join(" ")}`] : [];
713
+ });
714
+ return lines.length ? `The dashboard was not saved. ${lines.join(" ")}` : void 0;
715
+ }
716
+ function createProviderAdapter(config) {
717
+ const issuer = new URL(config.issuer);
718
+ if (issuer.protocol !== "https:" || issuer.username || issuer.password || issuer.search || issuer.hash) {
719
+ throw new Error("Invalid hosted authorization server configuration.");
720
+ }
721
+ const apiBase = config.issuer.replace(/\/$/, "");
722
+ const endpoint = `${apiBase}/oauth/principal`;
723
+ const publicOrigin = new URL(config.publicUrl).origin;
724
+ const bearers = /* @__PURE__ */ new WeakMap();
725
+ const responseSchema = principalSchema.extend({ issuer: z3.literal(config.issuer) }).strip();
726
+ return {
727
+ async verifyOAuth(token, resource) {
728
+ try {
729
+ const response = await fetch(endpoint, {
730
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
731
+ redirect: "error",
732
+ signal: AbortSignal.timeout(1e4)
733
+ });
734
+ if (!response.ok) throw new HostedAuthError(response.status >= 500 ? 503 : 401, "invalid_token");
735
+ const { issuer: _issuer, ...principal } = responseSchema.parse(await response.json());
736
+ if (principal.resource !== resource || principal.credentialType !== "oauth") throw new HostedAuthError(401, "invalid_token");
737
+ return principal;
738
+ } catch (error) {
739
+ if (error instanceof HostedAuthError) throw error;
740
+ throw new HostedAuthError(401, "invalid_token");
741
+ }
742
+ },
743
+ async resolvePatGrant() {
744
+ throw new HostedAuthError(401, "pat_membership_adapter_required");
745
+ },
746
+ delegate(token) {
747
+ const handle = Object.freeze({ kind: "hosted-delegation" });
748
+ bearers.set(handle, token);
749
+ return handle;
750
+ },
751
+ async executeRequest(options, principal, delegation, workspace) {
752
+ const bearer = delegation ? bearers.get(delegation) : void 0;
753
+ if (options.auth !== "none" && !bearer) throw new HostedAuthError(503, "delegated_backend_adapter_required");
754
+ if (options.auth === "none" && !options.token) throw new HostedAuthError(403, "tool_not_authorized");
755
+ if (!options.path.startsWith("/") || options.path.startsWith("//")) throw new HostedAuthError(403, "tool_not_authorized");
756
+ const url = new URL(`${apiBase}${options.path}`);
757
+ if (url.origin !== issuer.origin) throw new HostedAuthError(403, "tool_not_authorized");
758
+ for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);
759
+ const headers = {
760
+ Accept: "application/json",
761
+ "Content-Type": "application/json",
762
+ "User-Agent": USER_AGENT,
763
+ "X-Databrain-Client": USER_AGENT
764
+ };
765
+ for (const [key, value] of Object.entries(options.headers ?? {})) {
766
+ if (FORWARDED_HEADERS.has(key.toLowerCase())) headers[key] = value;
767
+ }
768
+ const tool = toolContext.getStore();
769
+ if (tool) {
770
+ headers["X-Databrain-MCP-Tool"] = tool.tool;
771
+ headers["X-Databrain-MCP-Tool-Category"] = tool.category;
772
+ headers["X-Databrain-MCP-Workflow-Intent"] = tool.workflowIntent;
773
+ headers["X-Databrain-MCP-Input-Mode"] = tool.inputMode;
774
+ }
775
+ if (options.auth === "none") {
776
+ headers["x-plugin-token"] = options.token;
777
+ headers.Origin = publicOrigin;
778
+ } else {
779
+ headers.Authorization = `Bearer ${bearer}`;
780
+ if (workspace) {
781
+ if (!principal.grantedWorkspaces?.some((item) => item.id === workspace.id) && principal.workspaceId !== workspace.id) throw new HostedAuthError(403, "principal_mismatch");
782
+ headers[WORKSPACE_HEADER] = workspace.id;
783
+ }
784
+ }
785
+ const response = await fetch(url, {
786
+ method: options.method,
787
+ headers,
788
+ redirect: "error",
789
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
790
+ signal: AbortSignal.timeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS)
791
+ });
792
+ const text = await response.text();
793
+ if (!response.ok) {
794
+ const forbidden = response.status === 403 ? errorDetail(403, text, bearer) : void 0;
795
+ if (forbidden?.code && GRANT_DENIALS.has(forbidden.code)) {
796
+ throw new HostedAuthError(403, "delegated_request_denied", forbidden);
797
+ }
798
+ const status = response.status === 401 ? 401 : response.status >= 500 ? 503 : response.status === 403 || response.status === 404 ? 404 : 400;
799
+ const detail = status === 401 || status === 503 || response.status === 403 ? void 0 : errorDetail(response.status, text, bearer);
800
+ throw new HostedAuthError(status, "delegated_request_denied", detail);
801
+ }
802
+ try {
803
+ return text ? JSON.parse(text) : null;
804
+ } catch {
805
+ throw new HostedAuthError(503, "delegated_response_invalid");
806
+ }
807
+ }
808
+ };
809
+ }
810
+
811
+ // src/hosted/main.ts
812
+ async function startHosted() {
813
+ const config = loadHostedConfig();
814
+ let adapter;
815
+ if (config.testOnly) {
816
+ const file = process.env.DATABRAIN_HOSTED_TEST_FIXTURES;
817
+ if (!file || !isAbsolute(file)) throw new Error("Hosted test mode requires an absolute fixture store path.");
818
+ const { createTestOnlyAdapter } = await import("./test-only-K7QTSVO4.js");
819
+ adapter = createTestOnlyAdapter(file);
820
+ } else {
821
+ const file = process.env.DATABRAIN_HOSTED_ADAPTER_MODULE;
822
+ if (!file) {
823
+ adapter = createProviderAdapter(config);
824
+ process.stderr.write("[databrain] OAuth configured: data tools run as the signed-in user through the issuer; PAT membership requires a trusted adapter.\n");
825
+ } else {
826
+ if (!isAbsolute(file)) throw new Error("DATABRAIN_HOSTED_ADAPTER_MODULE must be an absolute path.");
827
+ try {
828
+ adapter = (await import(pathToFileURL(file).href)).adapter;
829
+ } catch {
830
+ throw new Error("Could not load the trusted hosted adapter.");
831
+ }
832
+ }
833
+ if (!adapter || ["verifyOAuth", "resolvePatGrant", "executeRequest"].some((key) => typeof adapter[key] !== "function")) throw new Error("Invalid trusted hosted adapter.");
834
+ }
835
+ const patPath = process.env.DATABRAIN_HOSTED_PAT_STORE;
836
+ if (patPath && !isAbsolute(patPath)) throw new Error("Hosted PAT store must be an absolute path.");
837
+ const server = createHostedHttpServer(config, adapter, patPath ? new FilePatStore(patPath) : void 0);
838
+ await new Promise((resolve, reject) => {
839
+ server.once("error", reject);
840
+ server.listen(config.port, config.bind, resolve);
841
+ });
842
+ process.stderr.write(`[databrain] hosted HTTP ready on ${config.bind}:${config.port}/mcp${config.testOnly ? " \u2014 TEST ONLY: synthetic identity and data; no OAuth consent" : ""}
843
+ `);
844
+ const shutdown = () => {
845
+ server.close(() => process.exit(0));
846
+ setTimeout(() => {
847
+ server.closeAllConnections();
848
+ process.exit(0);
849
+ }, 5e3).unref();
850
+ };
851
+ process.once("SIGINT", shutdown);
852
+ process.once("SIGTERM", shutdown);
853
+ }
854
+ export {
855
+ startHosted
856
+ };
857
+ //# sourceMappingURL=main-KZRHWE3K.js.map