@databrainhq/mcp-server 0.3.0-rc.13 → 0.3.0-rc.15

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/.mcp.json +6 -1
  2. package/dist/apps/server.d.ts +102 -389
  3. package/dist/apps/server.js +5 -5
  4. package/dist/apps/stdio.d.ts +1 -2
  5. package/dist/apps/stdio.js +6 -6
  6. package/dist/{bundle-TDQXWIPM.js → bundle-QJNCUMNY.js} +2 -2
  7. package/dist/{chunk-Q5EYDKSA.js → chunk-2F7V6RZR.js} +511 -107
  8. package/dist/chunk-2F7V6RZR.js.map +1 -0
  9. package/dist/chunk-3AH56XX7.js +28 -0
  10. package/dist/chunk-3AH56XX7.js.map +1 -0
  11. package/dist/{chunk-AEQTEE5C.js → chunk-4E57ZRZO.js} +302 -63
  12. package/dist/chunk-4E57ZRZO.js.map +1 -0
  13. package/dist/{chunk-PSJVYD4W.js → chunk-6MD7KOAU.js} +11723 -10560
  14. package/dist/chunk-6MD7KOAU.js.map +1 -0
  15. package/dist/{chunk-HCYZN7I3.js → chunk-FJLRZJ4A.js} +10 -7
  16. package/dist/chunk-FJLRZJ4A.js.map +1 -0
  17. package/dist/{chunk-X44C5GW2.js → chunk-MUZ3KEJX.js} +63 -40
  18. package/dist/chunk-MUZ3KEJX.js.map +1 -0
  19. package/dist/{chunk-DTFYHLYN.js → chunk-NXE2A4WV.js} +26 -11
  20. package/dist/chunk-NXE2A4WV.js.map +1 -0
  21. package/dist/{chunk-233VGFHY.js → chunk-QQ6KNULX.js} +317 -147
  22. package/dist/chunk-QQ6KNULX.js.map +1 -0
  23. package/dist/chunk-V5JINLOA.js +506 -0
  24. package/dist/chunk-V5JINLOA.js.map +1 -0
  25. package/dist/cli.d.ts +16 -15
  26. package/dist/cli.js +134 -36
  27. package/dist/cli.js.map +1 -1
  28. package/dist/custom-chart/custom-runtime.js +6 -6
  29. package/dist/custom-chart/template/chart.ts +116 -28
  30. package/dist/custom-chart/template/icon.svg +1 -1
  31. package/dist/custom-chart/template/manifest.json +66 -12
  32. package/dist/custom-chart/template/sample-data.json +21 -3
  33. package/dist/index.d.ts +1 -2
  34. package/dist/index.js +67 -13
  35. package/dist/index.js.map +1 -1
  36. package/dist/{main-VEKNKWL5.js → main-2FKQQLWX.js} +489 -174
  37. package/dist/main-2FKQQLWX.js.map +1 -0
  38. package/dist/{test-only-LJGOY2QA.js → test-only-VH3FF5ML.js} +15 -9
  39. package/dist/test-only-VH3FF5ML.js.map +1 -0
  40. package/package.json +20 -9
  41. package/dist/chunk-233VGFHY.js.map +0 -1
  42. package/dist/chunk-AEQTEE5C.js.map +0 -1
  43. package/dist/chunk-DTFYHLYN.js.map +0 -1
  44. package/dist/chunk-HCYZN7I3.js.map +0 -1
  45. package/dist/chunk-PSJVYD4W.js.map +0 -1
  46. package/dist/chunk-Q5EYDKSA.js.map +0 -1
  47. package/dist/chunk-X44C5GW2.js.map +0 -1
  48. package/dist/main-VEKNKWL5.js.map +0 -1
  49. package/dist/test-only-LJGOY2QA.js.map +0 -1
  50. /package/dist/{bundle-TDQXWIPM.js.map → bundle-QJNCUMNY.js.map} +0 -0
@@ -1,7 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ catalogueTurnEntry,
4
+ isExecutableBrynTool,
5
+ parseBrynCatalogue,
6
+ registerBrynTools,
7
+ registerBrynTurns,
8
+ registerCatalogueTurns
9
+ } from "./chunk-V5JINLOA.js";
2
10
  import {
3
11
  registerChartApps
4
- } from "./chunk-233VGFHY.js";
12
+ } from "./chunk-QQ6KNULX.js";
5
13
  import {
6
14
  PREVIEW_PATH,
7
15
  WRITE_CREATE,
@@ -9,26 +17,33 @@ import {
9
17
  registerAllPrompts,
10
18
  registerAllResources,
11
19
  registerAllTools
12
- } from "./chunk-PSJVYD4W.js";
20
+ } from "./chunk-6MD7KOAU.js";
13
21
  import {
22
+ BrynCatalogueCache,
14
23
  DatabrainApiError,
15
24
  DatabrainClient,
16
25
  MCP_SERVER_VERSION,
26
+ RateLimitedError,
27
+ parseBrynStream,
17
28
  parseErrorBody,
29
+ parseRetryAfter,
18
30
  parseStudioStream,
31
+ readBrynStream,
19
32
  stringify,
20
33
  toolContext,
21
34
  toolError,
22
35
  toolSuccess
23
- } from "./chunk-AEQTEE5C.js";
24
- import "./chunk-Q5EYDKSA.js";
36
+ } from "./chunk-4E57ZRZO.js";
37
+ import "./chunk-2F7V6RZR.js";
25
38
  import {
26
39
  FilePatStore,
27
- HOSTED_SCOPES,
28
- HostedAuthError,
29
40
  createBearerVerifier,
30
41
  principalSchema
31
- } from "./chunk-X44C5GW2.js";
42
+ } from "./chunk-MUZ3KEJX.js";
43
+ import {
44
+ HOSTED_SCOPES,
45
+ HostedAuthError
46
+ } from "./chunk-3AH56XX7.js";
32
47
 
33
48
  // src/hosted/main.ts
34
49
  import { isAbsolute } from "path";
@@ -37,16 +52,21 @@ import { pathToFileURL } from "url";
37
52
  // src/hosted/config.ts
38
53
  function loadHostedConfig(env = process.env) {
39
54
  const testOnly = env.DATABRAIN_HOSTED_TEST_ONLY === "1";
40
- if (testOnly && env.NODE_ENV === "production") throw new Error("Hosted test mode is forbidden in production.");
41
- if (env.DATABRAIN_HOSTED_TEST_ONLY && !testOnly) throw new Error("Invalid DATABRAIN_HOSTED_TEST_ONLY.");
55
+ if (testOnly && env.NODE_ENV === "production")
56
+ throw new Error("Hosted test mode is forbidden in production.");
57
+ if (env.DATABRAIN_HOSTED_TEST_ONLY && !testOnly)
58
+ throw new Error("Invalid DATABRAIN_HOSTED_TEST_ONLY.");
42
59
  const validUrl = (value, field) => {
43
60
  try {
44
61
  if (!value) throw new Error();
45
62
  const url = new URL(value);
46
- if (url.username || url.password || url.search || url.hash || url.protocol !== "https:") throw new Error();
63
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:")
64
+ throw new Error();
47
65
  return url;
48
66
  } catch {
49
- throw new Error(`Configure ${field} as an HTTPS URL without credentials, query or fragment.`);
67
+ throw new Error(
68
+ `Configure ${field} as an HTTPS URL without credentials, query or fragment.`
69
+ );
50
70
  }
51
71
  };
52
72
  let publicValue = env.DATABRAIN_HOSTED_PUBLIC_URL;
@@ -58,10 +78,12 @@ function loadHostedConfig(env = process.env) {
58
78
  }
59
79
  const publicUrl = validUrl(publicValue, "DATABRAIN_HOSTED_PUBLIC_URL");
60
80
  validUrl(issuerValue, "DATABRAIN_HOSTED_ISSUER");
61
- if (publicUrl.pathname !== "/mcp") throw new Error("DATABRAIN_HOSTED_PUBLIC_URL must end in /mcp.");
81
+ if (publicUrl.pathname !== "/mcp")
82
+ throw new Error("DATABRAIN_HOSTED_PUBLIC_URL must end in /mcp.");
62
83
  const port = Number(env.DATABRAIN_HOSTED_PORT || "3144");
63
84
  const bind = env.DATABRAIN_HOSTED_BIND || "127.0.0.1";
64
- if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("Invalid DATABRAIN_HOSTED_PORT.");
85
+ if (!Number.isInteger(port) || port < 1024 || port > 65535)
86
+ throw new Error("Invalid DATABRAIN_HOSTED_PORT.");
65
87
  let apiUrl;
66
88
  if (env.DATABRAIN_HOSTED_API_URL) {
67
89
  let url;
@@ -71,7 +93,9 @@ function loadHostedConfig(env = process.env) {
71
93
  url = void 0;
72
94
  }
73
95
  if (!url || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
74
- throw new Error("Configure DATABRAIN_HOSTED_API_URL as an origin without credentials, path, query or fragment.");
96
+ throw new Error(
97
+ "Configure DATABRAIN_HOSTED_API_URL as an origin without credentials, path, query or fragment."
98
+ );
75
99
  }
76
100
  apiUrl = url.origin;
77
101
  }
@@ -86,65 +110,17 @@ function loadHostedConfig(env = process.env) {
86
110
  }
87
111
 
88
112
  // src/hosted/http.ts
89
- import { createServer } from "http";
113
+ import {
114
+ createServer
115
+ } from "http";
90
116
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
91
117
 
92
- // src/hosted/server.ts
93
- import { AsyncLocalStorage } from "async_hooks";
94
- import { z as z2 } from "zod";
95
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
96
- import { instrument } from "@posthog/mcp";
97
- import { PostHog } from "posthog-node";
98
-
99
- // src/tools/infrastructure/create-workspace.ts
100
- import { z } from "zod";
101
- function registerCreateWorkspace(server, client) {
102
- server.tool(
103
- "create_workspace",
104
- '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.',
105
- {
106
- name: z.string().describe("Unique workspace name"),
107
- connectionType: z.enum(["DATASOURCE", "DATAMART", "MULTI_DATASOURCE"]).optional().describe("Connection type \u2014 DATASOURCE, DATAMART, or MULTI_DATASOURCE"),
108
- datasourceName: z.string().optional().describe("Datasource name (required when connectionType is DATASOURCE)"),
109
- datamartName: z.string().optional().describe("Datamart name (required when connectionType is DATAMART)"),
110
- body: z.record(z.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/workspace")
111
- },
112
- WRITE_CREATE,
113
- async (params) => {
114
- try {
115
- const body = params.body ? { ...params.body } : {
116
- name: params.name,
117
- connectionType: params.connectionType,
118
- datasourceName: params.datasourceName,
119
- datamartName: params.datamartName
120
- };
121
- const result = await client.request({
122
- method: "POST",
123
- path: "/api/v2/workspace",
124
- body,
125
- auth: "service",
126
- invalidateCache: ["list_workspaces"]
127
- });
128
- if (result.error) {
129
- return toolError(`Workspace creation failed: ${stringify(result.error)}`);
130
- }
131
- return toolSuccess(
132
- `Workspace "${result.name || params.name}" created successfully.
133
-
134
- Next steps:
135
- 1. Use \`create_data_app\` to create a data app linked to this workspace
136
- 2. Build dashboards in the Databrain UI using this workspace
137
- 3. Use \`create_embed\` to embed dashboards`
138
- );
139
- } catch (err) {
140
- if (err instanceof DatabrainApiError) return err.toToolResponse();
141
- return toolError(`Unexpected error: ${err.message}`);
142
- }
143
- }
118
+ // src/hosted/authorization.ts
119
+ function brynEntry(name, catalogue) {
120
+ return catalogue?.tools.find(
121
+ (entry) => entry.name === name && isExecutableBrynTool(entry)
144
122
  );
145
123
  }
146
-
147
- // src/hosted/authorization.ts
148
124
  var TOOL_SCOPES = {
149
125
  whoami: [],
150
126
  list_datasources: ["mcp:datamarts:read"],
@@ -172,7 +148,11 @@ var TOOL_SCOPES = {
172
148
  create_dashboard: ["mcp:dashboards:write"],
173
149
  get_dashboard: ["mcp:dashboards:read"],
174
150
  dashboard_snapshot: ["mcp:dashboards:read"],
175
- generate_dashboard_tiles: ["mcp:dashboards:write", "mcp:query", "mcp:embeds:write"],
151
+ generate_dashboard_tiles: [
152
+ "mcp:dashboards:write",
153
+ "mcp:query",
154
+ "mcp:embeds:write"
155
+ ],
176
156
  ask_question: ["mcp:query"],
177
157
  get_semantic_layer: ["mcp:datamarts:read"],
178
158
  update_semantic_layer: ["mcp:datamarts:write"],
@@ -184,8 +164,16 @@ var TOOL_SCOPES = {
184
164
  apply_semantic_change: ["mcp:datamarts:write"],
185
165
  eval_semantic_model: ["mcp:datamarts:read", "mcp:query"],
186
166
  list_widgets: ["mcp:dashboards:read"],
187
- validate_metric_spec: ["mcp:dashboards:read", "mcp:query", "mcp:embeds:write"],
188
- create_or_update_metric_from_query: ["mcp:dashboards:write", "mcp:query", "mcp:embeds:write"],
167
+ validate_metric_spec: [
168
+ "mcp:dashboards:read",
169
+ "mcp:query",
170
+ "mcp:embeds:write"
171
+ ],
172
+ create_or_update_metric_from_query: [
173
+ "mcp:dashboards:write",
174
+ "mcp:query",
175
+ "mcp:embeds:write"
176
+ ],
189
177
  create_workspace_metric: ["mcp:dashboards:write", "mcp:query"],
190
178
  add_widget: ["mcp:dashboards:write", "mcp:embeds:write"],
191
179
  update_widget: ["mcp:dashboards:write", "mcp:embeds:write"],
@@ -212,25 +200,51 @@ var TOOL_SCOPES = {
212
200
  drill_down: ["mcp:dashboards:read", "mcp:query"],
213
201
  change_date_range: ["mcp:dashboards:read", "mcp:query"]
214
202
  };
215
- function requiredScopes(name) {
216
- if (!Object.hasOwn(TOOL_SCOPES, name)) throw new HostedAuthError(403, "tool_not_authorized");
203
+ var BRYN_TURN_SCOPES = {
204
+ ask_bryn_metric: ["mcp:query"],
205
+ ask_bryn_semantic_model: ["mcp:datamarts:read", "mcp:datamarts:write"],
206
+ ask_bryn_theme: ["mcp:embeds:write"],
207
+ ask_bryn_custom_chart: ["mcp:custom-charts:write"]
208
+ };
209
+ function requiredScopes(name, catalogue) {
210
+ if ((catalogue?.tools.length || catalogue?.turns.length) && Object.hasOwn(BRYN_TURN_SCOPES, name))
211
+ return BRYN_TURN_SCOPES[name];
212
+ const turn = Object.hasOwn(TOOL_SCOPES, name) ? void 0 : catalogueTurnEntry(name, catalogue);
213
+ if (turn) return turn.requiredScopes;
214
+ const bryn = brynEntry(name, catalogue);
215
+ if (bryn) {
216
+ if (bryn.requiredScopes) return bryn.requiredScopes;
217
+ if (bryn.surface === "custom-chart-studio")
218
+ return ["mcp:custom-charts:write"];
219
+ return bryn.effect === "read" ? ["mcp:dashboards:read"] : ["mcp:embeds:write"];
220
+ }
221
+ if (!Object.hasOwn(TOOL_SCOPES, name))
222
+ throw new HostedAuthError(403, "tool_not_authorized");
217
223
  return TOOL_SCOPES[name];
218
224
  }
219
- function authorizeTool(name, principal) {
220
- if (principal.expiresAt !== void 0 && principal.expiresAt <= Date.now() / 1e3) throw new HostedAuthError(401, "invalid_token");
221
- if (requiredScopes(name).some((scope) => !principal.scopes.includes(scope))) throw new HostedAuthError(403, "insufficient_scope");
225
+ function authorizeTool(name, principal, catalogue) {
226
+ if (principal.expiresAt !== void 0 && principal.expiresAt <= Date.now() / 1e3)
227
+ throw new HostedAuthError(401, "invalid_token");
228
+ if (requiredScopes(name, catalogue).some(
229
+ (scope) => !principal.scopes.includes(scope)
230
+ ))
231
+ throw new HostedAuthError(403, "insufficient_scope");
222
232
  }
223
233
  var TOOL_ARGUMENT_VALUES = {
224
234
  list_dashboards: { scope: ["auto", "data_app", "global"] }
225
235
  };
226
236
  var TOOL_OPAQUE_ARGUMENTS = {
237
+ ask_bryn_theme: ["theme", "dark", "schema", "contrast"],
238
+ ask_bryn_custom_chart: ["draft"],
227
239
  preview_chart: ["rows"],
228
240
  custom_chart_preview: ["rows", "manifest"],
229
241
  custom_chart_edit: ["draft"]
230
242
  };
231
- function assertToolArguments(name, value, principal, delegated = false) {
243
+ function assertToolArguments(name, value, principal, delegated = false, catalogue) {
232
244
  const allowed = Object.hasOwn(TOOL_ARGUMENT_VALUES, name) ? TOOL_ARGUMENT_VALUES[name] : {};
233
- const opaque = Object.hasOwn(TOOL_OPAQUE_ARGUMENTS, name) ? TOOL_OPAQUE_ARGUMENTS[name] : void 0;
245
+ const bryn = brynEntry(name, catalogue);
246
+ const turn = Object.hasOwn(TOOL_SCOPES, name) ? void 0 : catalogueTurnEntry(name, catalogue);
247
+ const opaque = turn ? turn.opaqueContextFields : bryn ? bryn.opaqueContextFields ? ["input", ...bryn.opaqueContextFields] : ["input", "draft", "theme", "dark", "schema", "contrast", "followUp"] : Object.hasOwn(TOOL_OPAQUE_ARGUMENTS, name) ? TOOL_OPAQUE_ARGUMENTS[name] : void 0;
234
248
  const target = selectWorkspace(value, principal);
235
249
  if (!value || typeof value !== "object" || Array.isArray(value)) {
236
250
  assertBoundArguments(value, principal, { target, delegated });
@@ -242,7 +256,8 @@ function assertToolArguments(name, value, principal, delegated = false) {
242
256
  rest[key] = child;
243
257
  continue;
244
258
  }
245
- if (typeof child !== "string" || !allowed[key].includes(child)) throw new HostedAuthError(403, "identity_argument_forbidden");
259
+ if (typeof child !== "string" || !allowed[key].includes(child))
260
+ throw new HostedAuthError(403, "identity_argument_forbidden");
246
261
  }
247
262
  assertBoundArguments(rest, principal, { target, delegated, opaque });
248
263
  return target;
@@ -261,7 +276,9 @@ var WORKSPACE_FREE_TOOLS = /* @__PURE__ */ new Set([
261
276
  "list_custom_charts",
262
277
  "publish_custom_chart"
263
278
  ]);
264
- var WORKSPACE_SPANNING_TOOLS = /* @__PURE__ */ new Set(["list_dashboards"]);
279
+ var WORKSPACE_SPANNING_TOOLS = /* @__PURE__ */ new Set([
280
+ "list_dashboards"
281
+ ]);
265
282
  var WORKSPACE_INPUT = "workspace";
266
283
  var WorkspaceSelectionError = class extends HostedAuthError {
267
284
  constructor(status, code, reason) {
@@ -274,7 +291,9 @@ function grantedWorkspaces(principal) {
274
291
  return principal.workspaceId ? [{ id: principal.workspaceId, name: principal.workspaceName }] : [];
275
292
  }
276
293
  function grantedNames(principal) {
277
- const names = grantedWorkspaces(principal).map((item) => item.name ?? item.id);
294
+ const names = grantedWorkspaces(principal).map(
295
+ (item) => item.name ?? item.id
296
+ );
278
297
  return names.length > 20 ? `${names.slice(0, 20).join(", ")} and ${names.length - 20} more` : names.join(", ");
279
298
  }
280
299
  function findGranted(principal, key, value) {
@@ -311,7 +330,9 @@ function selectWorkspace(args, principal, keys = [WORKSPACE_INPUT, "workspaceNam
311
330
  }
312
331
  function namesWorkspace(value) {
313
332
  if (!value || typeof value !== "object") return false;
314
- return Object.entries(value).some(([key, child]) => [WORKSPACE_INPUT, "workspaceName", "workspaceId"].includes(key) && child !== void 0 || namesWorkspace(child));
333
+ return Object.entries(value).some(
334
+ ([key, child]) => [WORKSPACE_INPUT, "workspaceName", "workspaceId"].includes(key) && child !== void 0 || namesWorkspace(child)
335
+ );
315
336
  }
316
337
  function tenantAllowed(child, principal, delegated) {
317
338
  if (typeof child !== "string") return false;
@@ -357,18 +378,85 @@ function assertBoundArguments(value, principal, binding = {}) {
357
378
  }
358
379
 
359
380
  // src/hosted/server.ts
360
- var schemaInstrumentation = new PostHog("hosted-contract-only", { disabled: true });
381
+ import { AsyncLocalStorage } from "async_hooks";
382
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
383
+ import { instrument } from "@posthog/mcp";
384
+ import { PostHog } from "posthog-node";
385
+ import { z as z2 } from "zod";
386
+
387
+ // src/tools/infrastructure/create-workspace.ts
388
+ import { z } from "zod";
389
+ function registerCreateWorkspace(server, client) {
390
+ server.tool(
391
+ "create_workspace",
392
+ '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.',
393
+ {
394
+ name: z.string().describe("Unique workspace name"),
395
+ connectionType: z.enum(["DATASOURCE", "DATAMART", "MULTI_DATASOURCE"]).optional().describe(
396
+ "Connection type \u2014 DATASOURCE, DATAMART, or MULTI_DATASOURCE"
397
+ ),
398
+ datasourceName: z.string().optional().describe(
399
+ "Datasource name (required when connectionType is DATASOURCE)"
400
+ ),
401
+ datamartName: z.string().optional().describe("Datamart name (required when connectionType is DATAMART)"),
402
+ body: z.record(z.string(), z.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/workspace")
403
+ },
404
+ WRITE_CREATE,
405
+ async (params) => {
406
+ try {
407
+ const body = params.body ? { ...params.body } : {
408
+ name: params.name,
409
+ connectionType: params.connectionType,
410
+ datasourceName: params.datasourceName,
411
+ datamartName: params.datamartName
412
+ };
413
+ const result = await client.request({
414
+ method: "POST",
415
+ path: "/api/v2/workspace",
416
+ body,
417
+ auth: "service",
418
+ invalidateCache: ["list_workspaces"]
419
+ });
420
+ if (result.error) {
421
+ return toolError(
422
+ `Workspace creation failed: ${stringify(result.error)}`
423
+ );
424
+ }
425
+ return toolSuccess(
426
+ `Workspace "${result.name || params.name}" created successfully.
427
+
428
+ Next steps:
429
+ 1. Use \`create_data_app\` to create a data app linked to this workspace
430
+ 2. Build dashboards in the Databrain UI using this workspace
431
+ 3. Use \`create_embed\` to embed dashboards`
432
+ );
433
+ } catch (err) {
434
+ if (err instanceof DatabrainApiError) return err.toToolResponse();
435
+ return toolError(`Unexpected error: ${err.message}`);
436
+ }
437
+ }
438
+ );
439
+ }
440
+
441
+ // src/hosted/server.ts
442
+ var schemaInstrumentation = new PostHog("hosted-contract-only", {
443
+ disabled: true
444
+ });
361
445
  var callWorkspace = new AsyncLocalStorage();
362
446
  var WORKSPACE_HINT = "Call whoami to see the workspaces this sign-in granted, then name one.";
363
447
  function workspaceError(error) {
364
- return toolError(`Error: ${error.reason} (${error.code})
448
+ return toolError(
449
+ `Error: ${error.reason} (${error.code})
365
450
 
366
- Hint: ${WORKSPACE_HINT}`);
451
+ Hint: ${WORKSPACE_HINT}`
452
+ );
367
453
  }
368
454
  function scopeError(missing) {
369
- return toolError(`Error: This connection cannot do this. It needs ${missing.join(", ")}, which your role did not allow when you connected. (INSUFFICIENT_SCOPE)
455
+ return toolError(
456
+ `Error: This connection cannot do this. It needs ${missing.join(", ")}, which your role did not allow when you connected. (INSUFFICIENT_SCOPE)
370
457
 
371
- 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.`);
458
+ 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.`
459
+ );
372
460
  }
373
461
  function onlyWorkspace(raw, target) {
374
462
  const keep = (items) => Array.isArray(items) ? items.filter((item) => {
@@ -376,15 +464,20 @@ function onlyWorkspace(raw, target) {
376
464
  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;
377
465
  }) : items;
378
466
  if (Array.isArray(raw)) return keep(raw);
379
- if (raw && typeof raw === "object" && "data" in raw) return { ...raw, data: keep(raw.data) };
467
+ if (raw && typeof raw === "object" && "data" in raw)
468
+ return { ...raw, data: keep(raw.data) };
380
469
  return raw;
381
470
  }
382
471
  var HostedClient = class extends DatabrainClient {
383
- constructor(adapter, principal, delegation) {
384
- super({ apiUrl: "https://api.usedatabrain.com", demoDomain: "https://demo.usedatabrain.com" });
472
+ constructor(adapter, principal, delegation, brynCatalogue) {
473
+ super({
474
+ apiUrl: "https://api.usedatabrain.com",
475
+ demoDomain: "https://demo.usedatabrain.com"
476
+ });
385
477
  this.adapter = adapter;
386
478
  this.principal = principal;
387
479
  this.delegation = delegation;
480
+ this.brynCatalogue = brynCatalogue;
388
481
  }
389
482
  // A generated API credential must never replace the authorizing hosted principal.
390
483
  setRuntimeApiToken(_token) {
@@ -396,25 +489,58 @@ var HostedClient = class extends DatabrainClient {
396
489
  try {
397
490
  const name = toolContext.getStore()?.tool;
398
491
  if (!name) throw new HostedAuthError(403, "tool_not_authorized");
399
- authorizeTool(name, this.principal);
492
+ authorizeTool(name, this.principal, this.brynCatalogue);
400
493
  const named = callWorkspace.getStore()?.target;
401
- const target = named ?? (selectWorkspace(options.body, this.principal, ["workspaceName", "workspaceId"]) ?? selectWorkspace(options.query, this.principal, ["workspaceName", "workspaceId"]));
494
+ const target = named ?? selectWorkspace(options.body, this.principal, [
495
+ "workspaceName",
496
+ "workspaceId"
497
+ ]) ?? selectWorkspace(options.query, this.principal, [
498
+ "workspaceName",
499
+ "workspaceId"
500
+ ]);
402
501
  const delegated = this.delegation !== void 0;
403
- const opaque = options.method === "POST" && options.path === PREVIEW_PATH ? ["rows"] : options.method === "POST" && /^\/api\/v2\/custom-charts\/[a-z][a-z0-9_-]*\/bryn\/turn$/.test(options.path) ? ["draft"] : void 0;
404
- assertBoundArguments(options.body, this.principal, { target, delegated, opaque });
405
- assertBoundArguments(options.query, this.principal, { target, delegated });
502
+ const opaque = options.method === "POST" && options.path === PREVIEW_PATH ? ["rows"] : options.method === "POST" && /^\/api\/v2\/custom-charts\/[a-z][a-z0-9_-]*\/bryn\/turn$/.test(
503
+ options.path
504
+ ) ? ["draft"] : options.method === "POST" && options.path === "/api/v2/theme-bryn/turn" ? ["theme", "dark", "schema", "contrast"] : options.method === "POST" && /^\/api\/mcp\/bryn\/tools\/[a-z][a-z0-9_]*$/.test(
505
+ options.path
506
+ ) ? [
507
+ "input",
508
+ "draft",
509
+ "theme",
510
+ "dark",
511
+ "schema",
512
+ "contrast",
513
+ "followUp"
514
+ ] : void 0;
515
+ assertBoundArguments(options.body, this.principal, {
516
+ target,
517
+ delegated,
518
+ opaque
519
+ });
520
+ assertBoundArguments(options.query, this.principal, {
521
+ target,
522
+ delegated
523
+ });
406
524
  let raw = await this.adapter.executeRequest(
407
525
  structuredClone(options),
408
526
  structuredClone(this.principal),
409
527
  this.delegation,
410
528
  target && structuredClone(target)
411
529
  );
412
- if (named && options.method === "GET" && options.path === "/api/v2/workspace") raw = onlyWorkspace(raw, named);
413
- if (!options.skipDataUnwrap && raw && typeof raw === "object" && !Array.isArray(raw) && "data" in raw && raw.data !== null && raw.data !== void 0) return raw.data;
530
+ if (named && options.method === "GET" && options.path === "/api/v2/workspace")
531
+ raw = onlyWorkspace(raw, named);
532
+ if (!options.skipDataUnwrap && raw && typeof raw === "object" && !Array.isArray(raw) && "data" in raw && raw.data !== null && raw.data !== void 0)
533
+ return raw.data;
414
534
  return raw;
415
535
  } catch (error) {
416
536
  if (error instanceof WorkspaceSelectionError) {
417
- throw new DatabrainApiError(error.status, error.code, `${error.reason} (${error.code})`, void 0, WORKSPACE_HINT);
537
+ throw new DatabrainApiError(
538
+ error.status,
539
+ error.code,
540
+ `${error.reason} (${error.code})`,
541
+ void 0,
542
+ WORKSPACE_HINT
543
+ );
418
544
  }
419
545
  const status = error instanceof HostedAuthError ? error.status : 503;
420
546
  const detail = error instanceof HostedAuthError ? error.detail : void 0;
@@ -456,47 +582,75 @@ function workspaceLine(name, input) {
456
582
  }
457
583
  function whoami(principal) {
458
584
  const granted = grantedWorkspaces(principal);
459
- return toolSuccess(JSON.stringify({
460
- status: "authenticated",
461
- activeBearer: principal.credentialType,
462
- actorKind: principal.actorKind,
463
- userId: principal.userId,
464
- companyId: principal.companyId,
465
- workspaceGrant: principal.workspaces?.mode === "all" ? "All workspaces I can access (new ones are included)" : "Only the workspaces chosen at sign-in",
466
- grantedWorkspaces: granted.map((item) => ({ id: item.id, name: item.name })),
467
- ...granted.length === 1 ? { workspaceId: granted[0].id, workspaceName: granted[0].name } : {},
468
- 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)}.`,
469
- oauthClientId: principal.clientId,
470
- scopes: principal.scopes
471
- }, null, 2));
585
+ return toolSuccess(
586
+ JSON.stringify(
587
+ {
588
+ status: "authenticated",
589
+ activeBearer: principal.credentialType,
590
+ actorKind: principal.actorKind,
591
+ userId: principal.userId,
592
+ companyId: principal.companyId,
593
+ workspaceGrant: principal.workspaces?.mode === "all" ? "All workspaces I can access (new ones are included)" : "Only the workspaces chosen at sign-in",
594
+ grantedWorkspaces: granted.map((item) => ({
595
+ id: item.id,
596
+ name: item.name
597
+ })),
598
+ ...granted.length === 1 ? { workspaceId: granted[0].id, workspaceName: granted[0].name } : {},
599
+ 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)}.`,
600
+ oauthClientId: principal.clientId,
601
+ scopes: principal.scopes
602
+ },
603
+ null,
604
+ 2
605
+ )
606
+ );
472
607
  }
473
- function createHostedToolServer(principal, adapter, delegation) {
608
+ function createHostedToolServer(principal, adapter, delegation, brynCatalogue) {
474
609
  const bound = structuredClone(principal);
475
610
  const delegated = delegation !== void 0;
476
- const server = new McpServer({ name: "databrain", version: MCP_SERVER_VERSION }, {
477
- 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. For existing saved charts, call list_widgets with dashboardId or dashboardName and no clientId; no embed, data app or datamart is needed. Use render_metric_tile to draw a metricRef directly. Use ask_question for new data questions. 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."
478
- });
611
+ const server = new McpServer(
612
+ { name: "databrain", version: MCP_SERVER_VERSION },
613
+ {
614
+ 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. For existing saved charts, call list_widgets with dashboardId or dashboardName and no clientId; no embed, data app or datamart is needed. Use render_metric_tile to draw a metricRef directly. Use ask_question for new data questions. 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."
615
+ }
616
+ );
479
617
  const register = server.tool.bind(server);
480
618
  server.tool = ((...args) => {
481
619
  const name = args[0];
482
620
  const index = args.length - 1;
483
621
  const handler = args[index];
484
622
  const shape = typeof args[1] === "string" && args[2] && typeof args[2] === "object" && !("readOnlyHint" in args[2]) ? args[2] : void 0;
485
- const ownSelector = shape && ["workspaceName", "workspaceId"].find((key) => Object.hasOwn(shape, key));
623
+ const ownSelector = shape && ["workspaceName", "workspaceId"].find(
624
+ (key) => Object.hasOwn(shape, key)
625
+ );
486
626
  const addsInput = shape !== void 0 && !ownSelector && !WORKSPACE_FREE_TOOLS.has(name) && !Object.hasOwn(shape, WORKSPACE_INPUT);
487
627
  if (shape && !WORKSPACE_FREE_TOOLS.has(name)) {
488
628
  if (addsInput) {
489
- 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.") };
629
+ args[2] = {
630
+ ...shape,
631
+ [WORKSPACE_INPUT]: z2.string().min(1).max(512).optional().describe(
632
+ 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."
633
+ )
634
+ };
490
635
  }
491
636
  args[1] = `${args[1]}${workspaceLine(name, addsInput ? WORKSPACE_INPUT : ownSelector)}`;
492
637
  }
493
638
  args[index] = async (...values) => {
494
639
  try {
495
- authorizeTool(name, bound);
496
- const target = assertToolArguments(name, values[0], bound, delegated);
640
+ authorizeTool(name, bound, brynCatalogue);
641
+ const target = assertToolArguments(
642
+ name,
643
+ values[0],
644
+ bound,
645
+ delegated,
646
+ brynCatalogue
647
+ );
497
648
  if (name === "whoami") return whoami(bound);
498
649
  const input = values[0] && typeof values[0] === "object" ? values[0] : void 0;
499
- if (input?.projectDir !== void 0) return toolError("projectDir is not available on the hosted server. Pass chartTs, manifest, bundleCss and iconSvg to compile in memory.");
650
+ if (input?.projectDir !== void 0)
651
+ return toolError(
652
+ "projectDir is not available on the hosted server. Pass chartTs, manifest, bundleCss and iconSvg to compile in memory."
653
+ );
500
654
  if (!target && !WORKSPACE_FREE_TOOLS.has(name) && !WORKSPACE_SPANNING_TOOLS.has(name) && !namesWorkspace(input)) {
501
655
  throw new WorkspaceSelectionError(
502
656
  400,
@@ -507,25 +661,41 @@ function createHostedToolServer(principal, adapter, delegation) {
507
661
  if (input) {
508
662
  if (addsInput) delete input[WORKSPACE_INPUT];
509
663
  if (shape && target) {
510
- if (Object.hasOwn(shape, "workspaceId")) input.workspaceId = target.id;
511
- if (Object.hasOwn(shape, "workspaceName") && target.name !== void 0) input.workspaceName = target.name;
664
+ if (Object.hasOwn(shape, "workspaceId"))
665
+ input.workspaceId = target.id;
666
+ if (Object.hasOwn(shape, "workspaceName") && target.name !== void 0)
667
+ input.workspaceName = target.name;
512
668
  }
513
669
  }
514
- const result = await callWorkspace.run({ target }, () => handler(...values));
670
+ const result = await callWorkspace.run(
671
+ { target },
672
+ () => handler(...values)
673
+ );
515
674
  if (target && result?._meta?.["ui/resourceUri"]) {
516
- return { ...result, _meta: { ...result._meta, "databrain/workspace": target } };
675
+ return {
676
+ ...result,
677
+ _meta: { ...result._meta, "databrain/workspace": target }
678
+ };
517
679
  }
518
680
  return result;
519
681
  } catch (error) {
520
- if (error instanceof WorkspaceSelectionError) return workspaceError(error);
521
- return toolError("Hosted authorization denied this tool call. Check the granted scopes, workspace and tenant access.");
682
+ if (error instanceof WorkspaceSelectionError)
683
+ return workspaceError(error);
684
+ return toolError(
685
+ "Hosted authorization denied this tool call. Check the granted scopes, workspace and tenant access."
686
+ );
522
687
  }
523
688
  };
524
689
  return register(...args);
525
690
  });
526
- const client = new HostedClient(adapter, bound, delegation);
691
+ const client = new HostedClient(adapter, bound, delegation, brynCatalogue);
527
692
  enableChartApps(server);
528
693
  registerAllTools(server, client);
694
+ if (brynCatalogue?.tools.length || brynCatalogue?.turns.length) {
695
+ registerBrynTools(server, client, brynCatalogue);
696
+ registerBrynTurns(server, client);
697
+ registerCatalogueTurns(server, client, brynCatalogue);
698
+ }
529
699
  registerCreateWorkspace(server, client);
530
700
  registerAllResources(server);
531
701
  registerAllPrompts(server);
@@ -536,12 +706,16 @@ function createHostedToolServer(principal, adapter, delegation) {
536
706
 
537
707
  // src/hosted/http.ts
538
708
  var HEALTH_BODY = JSON.stringify({ status: "ok" });
709
+ var MAX_MCP_BODY_BYTES = 4 * 1024 * 1024 + 64 * 1024;
539
710
  function healthPath(url) {
540
711
  const path = (url || "/").split("?")[0];
541
712
  return path === "/health";
542
713
  }
543
714
  function reply(res, status, body) {
544
- res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
715
+ res.writeHead(status, {
716
+ "Content-Type": "application/json",
717
+ "Cache-Control": "no-store"
718
+ });
545
719
  res.end(JSON.stringify(body));
546
720
  }
547
721
  async function readBody(req) {
@@ -549,7 +723,7 @@ async function readBody(req) {
549
723
  const chunks = [];
550
724
  for await (const chunk of req) {
551
725
  size += chunk.length;
552
- if (size > 1048576) throw new Error("body_too_large");
726
+ if (size > MAX_MCP_BODY_BYTES) throw new Error("body_too_large");
553
727
  chunks.push(Buffer.from(chunk));
554
728
  }
555
729
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
@@ -560,6 +734,7 @@ function createHostedHttpServer(config, adapter, pats) {
560
734
  const metadataUrl = `${publicUrl.origin}${metadataPath}`;
561
735
  const verify = createBearerVerifier(adapter, config.publicUrl, pats);
562
736
  let active = 0;
737
+ const catalogueCache = new BrynCatalogueCache();
563
738
  const server = createServer(async (req, res) => {
564
739
  if (healthPath(req.url)) {
565
740
  if (req.method !== "GET" && req.method !== "HEAD") {
@@ -579,7 +754,11 @@ function createHostedHttpServer(config, adapter, pats) {
579
754
  if (config.testOnly) res.setHeader("X-Databrain-Test-Only", "true");
580
755
  try {
581
756
  const localPort = server.address()?.port;
582
- if (![publicUrl.host, `127.0.0.1:${localPort}`, `localhost:${localPort}`].includes(req.headers.host || "")) {
757
+ if (![
758
+ publicUrl.host,
759
+ `127.0.0.1:${localPort}`,
760
+ `localhost:${localPort}`
761
+ ].includes(req.headers.host || "")) {
583
762
  reply(res, 403, { error: "invalid_host" });
584
763
  return;
585
764
  }
@@ -590,10 +769,15 @@ function createHostedHttpServer(config, adapter, pats) {
590
769
  if (req.headers.origin) {
591
770
  res.setHeader("Access-Control-Allow-Origin", publicUrl.origin);
592
771
  res.setHeader("Vary", "Origin");
593
- res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate, MCP-Protocol-Version");
772
+ res.setHeader(
773
+ "Access-Control-Expose-Headers",
774
+ "WWW-Authenticate, MCP-Protocol-Version"
775
+ );
594
776
  }
595
777
  const url = new URL(req.url || "/", publicUrl.origin);
596
- if (req.method === "GET" && [metadataPath, "/.well-known/oauth-protected-resource"].includes(url.pathname)) {
778
+ if (req.method === "GET" && [metadataPath, "/.well-known/oauth-protected-resource"].includes(
779
+ url.pathname
780
+ )) {
597
781
  reply(res, 200, {
598
782
  resource: config.publicUrl,
599
783
  authorization_servers: [config.issuer],
@@ -612,15 +796,24 @@ function createHostedHttpServer(config, adapter, pats) {
612
796
  return;
613
797
  }
614
798
  if (req.method === "OPTIONS") {
615
- res.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS");
616
- res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, MCP-Protocol-Version, MCP-Session-Id");
799
+ res.setHeader(
800
+ "Access-Control-Allow-Methods",
801
+ "POST, GET, DELETE, OPTIONS"
802
+ );
803
+ res.setHeader(
804
+ "Access-Control-Allow-Headers",
805
+ "Authorization, Content-Type, MCP-Protocol-Version, MCP-Session-Id"
806
+ );
617
807
  res.writeHead(204);
618
808
  res.end();
619
809
  return;
620
810
  }
621
- const authFields = req.rawHeaders.filter((_, i) => i % 2 === 0 && req.rawHeaders[i].toLowerCase() === "authorization");
811
+ const authFields = req.rawHeaders.filter(
812
+ (_, i) => i % 2 === 0 && req.rawHeaders[i].toLowerCase() === "authorization"
813
+ );
622
814
  const match = /^Bearer ([^\s]+)$/i.exec(req.headers.authorization || "");
623
- if (authFields.length !== 1 || !match) throw new HostedAuthError(401, "invalid_token");
815
+ if (authFields.length !== 1 || !match)
816
+ throw new HostedAuthError(401, "invalid_token");
624
817
  const principal = await verify(match[1]);
625
818
  if (req.method !== "POST") {
626
819
  res.setHeader("Allow", "POST, OPTIONS");
@@ -650,25 +843,87 @@ function createHostedHttpServer(config, adapter, pats) {
650
843
  return;
651
844
  }
652
845
  const delegation = match[1].startsWith("dbp_") ? void 0 : adapter.delegate?.(match[1], principal);
846
+ let brynCatalogue = { tools: [], turns: [] };
847
+ let catalogueRetryAfter;
848
+ if (adapter.getBrynCatalogue) {
849
+ const read = await catalogueCache.get(
850
+ `${principal.companyId}:${principal.userId}:${principal.credentialType}`,
851
+ async () => {
852
+ let raw;
853
+ try {
854
+ raw = await adapter.getBrynCatalogue(principal, delegation);
855
+ } catch (error) {
856
+ if (error instanceof HostedAuthError) {
857
+ if (error.detail?.status === 429)
858
+ throw new RateLimitedError(error.retryAfterMs);
859
+ if (error.status === 404) return { tools: [], turns: [] };
860
+ }
861
+ throw error;
862
+ }
863
+ try {
864
+ return parseBrynCatalogue(raw);
865
+ } catch {
866
+ return { tools: [], turns: [] };
867
+ }
868
+ }
869
+ );
870
+ brynCatalogue = read.catalogue;
871
+ if (read.degraded) catalogueRetryAfter = read.retryAfterSeconds;
872
+ }
653
873
  if (body?.method === "tools/call" && typeof body.params?.name === "string") {
654
874
  try {
655
- authorizeTool(body.params.name, principal);
656
- assertToolArguments(body.params.name, body.params.arguments, principal, delegation !== void 0);
875
+ authorizeTool(body.params.name, principal, brynCatalogue);
876
+ assertToolArguments(
877
+ body.params.name,
878
+ body.params.arguments,
879
+ principal,
880
+ delegation !== void 0,
881
+ brynCatalogue
882
+ );
657
883
  } catch (error) {
658
884
  if (error instanceof WorkspaceSelectionError) {
659
- reply(res, 200, { jsonrpc: "2.0", id: body.id ?? null, result: workspaceError(error) });
885
+ reply(res, 200, {
886
+ jsonrpc: "2.0",
887
+ id: body.id ?? null,
888
+ result: workspaceError(error)
889
+ });
660
890
  return;
661
891
  }
662
892
  if (error instanceof HostedAuthError && error.code === "insufficient_scope") {
663
- const missing = requiredScopes(body.params.name).filter((scope) => !principal.scopes.includes(scope));
664
- reply(res, 200, { jsonrpc: "2.0", id: body.id ?? null, result: scopeError(missing) });
893
+ const missing = requiredScopes(
894
+ body.params.name,
895
+ brynCatalogue
896
+ ).filter((scope) => !principal.scopes.includes(scope));
897
+ reply(res, 200, {
898
+ jsonrpc: "2.0",
899
+ id: body.id ?? null,
900
+ result: scopeError(missing)
901
+ });
902
+ return;
903
+ }
904
+ if (catalogueRetryAfter !== void 0 && error instanceof HostedAuthError && error.code === "tool_not_authorized") {
905
+ reply(res, 200, {
906
+ jsonrpc: "2.0",
907
+ id: body.id ?? null,
908
+ result: toolError(
909
+ `Error: Bryn tools are temporarily unavailable because the tool list could not be read (rate limited or busy). Retry in ${catalogueRetryAfter} seconds. (TEMPORARILY_UNAVAILABLE)`
910
+ )
911
+ });
665
912
  return;
666
913
  }
667
914
  throw error;
668
915
  }
669
916
  }
670
- const mcp = createHostedToolServer(principal, adapter, delegation);
671
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true });
917
+ const mcp = createHostedToolServer(
918
+ principal,
919
+ adapter,
920
+ delegation,
921
+ brynCatalogue
922
+ );
923
+ const transport = new StreamableHTTPServerTransport({
924
+ sessionIdGenerator: void 0,
925
+ enableJsonResponse: true
926
+ });
672
927
  let closed = false;
673
928
  const close = () => {
674
929
  if (!closed) {
@@ -692,8 +947,14 @@ function createHostedHttpServer(config, adapter, pats) {
692
947
  return;
693
948
  }
694
949
  const status = error instanceof HostedAuthError ? error.status : 503;
695
- if (status === 401) res.setHeader("WWW-Authenticate", `Bearer error="invalid_token", resource_metadata="${metadataUrl}"`);
696
- reply(res, status, { error: error instanceof HostedAuthError ? error.code : "temporarily_unavailable" });
950
+ if (status === 401)
951
+ res.setHeader(
952
+ "WWW-Authenticate",
953
+ `Bearer error="invalid_token", resource_metadata="${metadataUrl}"`
954
+ );
955
+ reply(res, status, {
956
+ error: error instanceof HostedAuthError ? error.code : "temporarily_unavailable"
957
+ });
697
958
  }
698
959
  });
699
960
  server.requestTimeout = 3e4;
@@ -708,7 +969,12 @@ var USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION} (hosted)`;
708
969
  var REQUEST_TIMEOUT_MS = 3e4;
709
970
  var FORWARDED_HEADERS = /* @__PURE__ */ new Set(["idempotency-key"]);
710
971
  var WORKSPACE_HEADER = "X-Databrain-Workspace";
711
- var GRANT_DENIALS = /* @__PURE__ */ new Set(["ROLE_NOT_ALLOWED", "WORKSPACE_CREATE_NOT_ALLOWED", "forbidden_permission", "insufficient_scope"]);
972
+ var GRANT_DENIALS = /* @__PURE__ */ new Set([
973
+ "ROLE_NOT_ALLOWED",
974
+ "WORKSPACE_CREATE_NOT_ALLOWED",
975
+ "forbidden_permission",
976
+ "insufficient_scope"
977
+ ]);
712
978
  function errorDetail(status, text, bearer) {
713
979
  let parsed;
714
980
  try {
@@ -754,20 +1020,27 @@ function createProviderAdapter(config) {
754
1020
  const publicOrigin = new URL(config.publicUrl).origin;
755
1021
  const bearers = /* @__PURE__ */ new WeakMap();
756
1022
  const responseSchema = principalSchema.extend({ issuer: z3.literal(config.issuer) }).strip();
757
- return {
1023
+ const adapter = {
758
1024
  async verifyOAuth(token, resource) {
759
1025
  try {
760
1026
  const response = await fetch(endpoint, {
761
- headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
1027
+ headers: {
1028
+ Authorization: `Bearer ${token}`,
1029
+ Accept: "application/json"
1030
+ },
762
1031
  redirect: "error",
763
1032
  signal: AbortSignal.timeout(1e4)
764
1033
  });
765
1034
  if (!response.ok) {
766
- if (response.status === 429 || response.status >= 500) throw new HostedAuthError(503, "temporarily_unavailable");
1035
+ if (response.status === 429 || response.status >= 500)
1036
+ throw new HostedAuthError(503, "temporarily_unavailable");
767
1037
  throw new HostedAuthError(401, "invalid_token");
768
1038
  }
769
- const { issuer: _issuer, ...principal } = responseSchema.parse(await response.json());
770
- if (principal.resource !== resource || principal.credentialType !== "oauth") throw new HostedAuthError(401, "invalid_token");
1039
+ const { issuer: _issuer, ...principal } = responseSchema.parse(
1040
+ await response.json()
1041
+ );
1042
+ if (principal.resource !== resource || principal.credentialType !== "oauth")
1043
+ throw new HostedAuthError(401, "invalid_token");
771
1044
  return principal;
772
1045
  } catch (error) {
773
1046
  if (error instanceof HostedAuthError) throw error;
@@ -778,18 +1051,25 @@ function createProviderAdapter(config) {
778
1051
  throw new HostedAuthError(401, "pat_membership_adapter_required");
779
1052
  },
780
1053
  delegate(token) {
781
- const handle = Object.freeze({ kind: "hosted-delegation" });
1054
+ const handle = Object.freeze({
1055
+ kind: "hosted-delegation"
1056
+ });
782
1057
  bearers.set(handle, token);
783
1058
  return handle;
784
1059
  },
785
1060
  async executeRequest(options, principal, delegation, workspace) {
786
1061
  const bearer = delegation ? bearers.get(delegation) : void 0;
787
- if (options.auth !== "none" && !bearer) throw new HostedAuthError(503, "delegated_backend_adapter_required");
788
- if (options.auth === "none" && !options.token) throw new HostedAuthError(403, "tool_not_authorized");
789
- if (!options.path.startsWith("/") || options.path.startsWith("//")) throw new HostedAuthError(403, "tool_not_authorized");
1062
+ if (options.auth !== "none" && !bearer)
1063
+ throw new HostedAuthError(503, "delegated_backend_adapter_required");
1064
+ if (options.auth === "none" && !options.token)
1065
+ throw new HostedAuthError(403, "tool_not_authorized");
1066
+ if (!options.path.startsWith("/") || options.path.startsWith("//"))
1067
+ throw new HostedAuthError(403, "tool_not_authorized");
790
1068
  const url = new URL(`${apiBase}${options.path}`);
791
- if (url.origin !== apiOrigin) throw new HostedAuthError(403, "tool_not_authorized");
792
- for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);
1069
+ if (url.origin !== apiOrigin)
1070
+ throw new HostedAuthError(403, "tool_not_authorized");
1071
+ for (const [key, value] of Object.entries(options.query ?? {}))
1072
+ url.searchParams.set(key, value);
793
1073
  const headers = {
794
1074
  Accept: "application/json",
795
1075
  "Content-Type": "application/json",
@@ -812,7 +1092,10 @@ function createProviderAdapter(config) {
812
1092
  } else {
813
1093
  headers.Authorization = `Bearer ${bearer}`;
814
1094
  if (workspace) {
815
- if (!principal.grantedWorkspaces?.some((item) => item.id === workspace.id) && principal.workspaceId !== workspace.id) throw new HostedAuthError(403, "principal_mismatch");
1095
+ if (!principal.grantedWorkspaces?.some(
1096
+ (item) => item.id === workspace.id
1097
+ ) && principal.workspaceId !== workspace.id)
1098
+ throw new HostedAuthError(403, "principal_mismatch");
816
1099
  headers[WORKSPACE_HEADER] = workspace.id;
817
1100
  }
818
1101
  }
@@ -823,27 +1106,41 @@ function createProviderAdapter(config) {
823
1106
  body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
824
1107
  signal: AbortSignal.timeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS)
825
1108
  });
826
- const text = await response.text();
1109
+ const text = options.responseFormat === "bryn-sse" && response.ok ? await readBrynStream(response) : await response.text();
827
1110
  if (!response.ok) {
828
1111
  const forbidden = response.status === 403 ? errorDetail(403, text, bearer) : void 0;
829
1112
  if (forbidden?.code && GRANT_DENIALS.has(forbidden.code)) {
830
1113
  throw new HostedAuthError(
831
1114
  403,
832
1115
  "delegated_request_denied",
833
- forbidden.code === "insufficient_scope" && options.path === "/api/v2/guest-token/create" ? { ...forbidden, message: "Guest/tenant operations require mcp:embeds:write. Ask an admin for a role that allows it, then connect again." } : forbidden
1116
+ forbidden.code === "insufficient_scope" && options.path === "/api/v2/guest-token/create" ? {
1117
+ ...forbidden,
1118
+ message: "Guest/tenant operations require mcp:embeds:write. Ask an admin for a role that allows it, then connect again."
1119
+ } : forbidden
834
1120
  );
835
1121
  }
836
- const status = response.status === 403 && options.responseFormat === "studio-sse" ? 403 : response.status === 401 ? 401 : response.status >= 500 ? 503 : response.status === 403 || response.status === 404 ? 404 : 400;
1122
+ const status = response.status === 403 && (options.responseFormat === "studio-sse" || options.responseFormat === "bryn-sse") ? 403 : response.status === 401 ? 401 : response.status >= 500 ? 503 : response.status === 403 || response.status === 404 ? 404 : 400;
837
1123
  const detail = status === 401 || status === 503 || response.status === 403 ? void 0 : errorDetail(response.status, text, bearer);
838
- throw new HostedAuthError(status, "delegated_request_denied", detail);
1124
+ throw new HostedAuthError(
1125
+ status,
1126
+ "delegated_request_denied",
1127
+ detail,
1128
+ response.status === 429 ? parseRetryAfter(response.headers.get("retry-after")) : void 0
1129
+ );
839
1130
  }
840
1131
  try {
841
- return options.responseFormat === "studio-sse" ? parseStudioStream(text) : text ? JSON.parse(text) : null;
1132
+ return options.responseFormat === "studio-sse" ? parseStudioStream(text) : options.responseFormat === "bryn-sse" ? parseBrynStream(text) : text ? JSON.parse(text) : null;
842
1133
  } catch {
843
1134
  throw new HostedAuthError(503, "delegated_response_invalid");
844
1135
  }
845
1136
  }
846
1137
  };
1138
+ adapter.getBrynCatalogue = (principal, delegation) => adapter.executeRequest(
1139
+ { method: "GET", path: "/api/mcp/bryn/catalog", auth: "service" },
1140
+ principal,
1141
+ delegation
1142
+ );
1143
+ return adapter;
847
1144
  }
848
1145
 
849
1146
  // src/hosted/main.ts
@@ -852,33 +1149,51 @@ async function startHosted() {
852
1149
  let adapter;
853
1150
  if (config.testOnly) {
854
1151
  const file = process.env.DATABRAIN_HOSTED_TEST_FIXTURES;
855
- if (!file || !isAbsolute(file)) throw new Error("Hosted test mode requires an absolute fixture store path.");
856
- const { createTestOnlyAdapter } = await import("./test-only-LJGOY2QA.js");
1152
+ if (!file || !isAbsolute(file))
1153
+ throw new Error(
1154
+ "Hosted test mode requires an absolute fixture store path."
1155
+ );
1156
+ const { createTestOnlyAdapter } = await import("./test-only-VH3FF5ML.js");
857
1157
  adapter = createTestOnlyAdapter(file);
858
1158
  } else {
859
1159
  const file = process.env.DATABRAIN_HOSTED_ADAPTER_MODULE;
860
1160
  if (!file) {
861
1161
  adapter = createProviderAdapter(config);
862
- process.stderr.write("[databrain] OAuth configured: data tools run as the signed-in user through the issuer; PAT membership requires a trusted adapter.\n");
1162
+ process.stderr.write(
1163
+ "[databrain] OAuth configured: data tools run as the signed-in user through the issuer; PAT membership requires a trusted adapter.\n"
1164
+ );
863
1165
  } else {
864
- if (!isAbsolute(file)) throw new Error("DATABRAIN_HOSTED_ADAPTER_MODULE must be an absolute path.");
1166
+ if (!isAbsolute(file))
1167
+ throw new Error(
1168
+ "DATABRAIN_HOSTED_ADAPTER_MODULE must be an absolute path."
1169
+ );
865
1170
  try {
866
1171
  adapter = (await import(pathToFileURL(file).href)).adapter;
867
1172
  } catch {
868
1173
  throw new Error("Could not load the trusted hosted adapter.");
869
1174
  }
870
1175
  }
871
- if (!adapter || ["verifyOAuth", "resolvePatGrant", "executeRequest"].some((key) => typeof adapter[key] !== "function")) throw new Error("Invalid trusted hosted adapter.");
1176
+ if (!adapter || ["verifyOAuth", "resolvePatGrant", "executeRequest"].some(
1177
+ (key) => typeof adapter[key] !== "function"
1178
+ ))
1179
+ throw new Error("Invalid trusted hosted adapter.");
872
1180
  }
873
1181
  const patPath = process.env.DATABRAIN_HOSTED_PAT_STORE;
874
- if (patPath && !isAbsolute(patPath)) throw new Error("Hosted PAT store must be an absolute path.");
875
- const server = createHostedHttpServer(config, adapter, patPath ? new FilePatStore(patPath) : void 0);
1182
+ if (patPath && !isAbsolute(patPath))
1183
+ throw new Error("Hosted PAT store must be an absolute path.");
1184
+ const server = createHostedHttpServer(
1185
+ config,
1186
+ adapter,
1187
+ patPath ? new FilePatStore(patPath) : void 0
1188
+ );
876
1189
  await new Promise((resolve, reject) => {
877
1190
  server.once("error", reject);
878
1191
  server.listen(config.port, config.bind, resolve);
879
1192
  });
880
- 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" : ""}
881
- `);
1193
+ process.stderr.write(
1194
+ `[databrain] hosted HTTP ready on ${config.bind}:${config.port}/mcp${config.testOnly ? " \u2014 TEST ONLY: synthetic identity and data; no OAuth consent" : ""}
1195
+ `
1196
+ );
882
1197
  const shutdown = () => {
883
1198
  server.close(() => process.exit(0));
884
1199
  setTimeout(() => {
@@ -892,4 +1207,4 @@ async function startHosted() {
892
1207
  export {
893
1208
  startHosted
894
1209
  };
895
- //# sourceMappingURL=main-VEKNKWL5.js.map
1210
+ //# sourceMappingURL=main-2FKQQLWX.js.map