@databrainhq/mcp-server 0.3.0-rc.14 → 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.
package/dist/index.js CHANGED
@@ -3,25 +3,32 @@ import {
3
3
  DEFAULT_POSTHOG_API_KEY,
4
4
  DEFAULT_POSTHOG_HOST,
5
5
  loadConfig
6
- } from "./chunk-S65KA3Z7.js";
6
+ } from "./chunk-NXE2A4WV.js";
7
+ import {
8
+ loadBrynCatalogue,
9
+ registerBrynTools,
10
+ registerBrynTurns,
11
+ registerCatalogueTurns
12
+ } from "./chunk-V5JINLOA.js";
7
13
  import {
8
14
  registerAllPrompts,
9
15
  registerAllResources,
10
16
  registerAllTools
11
- } from "./chunk-UYFFFAYV.js";
17
+ } from "./chunk-6MD7KOAU.js";
12
18
  import {
13
19
  DatabrainClient,
14
20
  MCP_SERVER_VERSION,
15
21
  logger
16
- } from "./chunk-AWICHHDU.js";
22
+ } from "./chunk-4E57ZRZO.js";
17
23
  import "./chunk-2F7V6RZR.js";
24
+ import "./chunk-3AH56XX7.js";
18
25
 
19
26
  // src/index.ts
20
27
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21
28
 
22
29
  // src/server.ts
23
30
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
- function createServer(config) {
31
+ function createServer(config, brynCatalogue) {
25
32
  const client = new DatabrainClient(config);
26
33
  const auth = client.getActiveAuth();
27
34
  let operatorBlurb;
@@ -82,6 +89,11 @@ Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, fi
82
89
  }
83
90
  );
84
91
  registerAllTools(server, client);
92
+ if (brynCatalogue?.tools.length || brynCatalogue?.turns.length) {
93
+ registerBrynTools(server, client, brynCatalogue);
94
+ registerBrynTurns(server, client);
95
+ registerCatalogueTurns(server, client, brynCatalogue);
96
+ }
85
97
  registerAllResources(server);
86
98
  registerAllPrompts(server);
87
99
  return server;
@@ -91,6 +103,29 @@ Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, fi
91
103
  import { createHash } from "crypto";
92
104
  import { instrument } from "@posthog/mcp";
93
105
  import { PostHog } from "posthog-node";
106
+ var CONTENT_FREE_PROPERTIES = /* @__PURE__ */ new Set([
107
+ "$mcp_source",
108
+ "$mcp_duration_ms",
109
+ "$mcp_is_error",
110
+ "$mcp_tool_name",
111
+ "$mcp_tool_category",
112
+ "$mcp_server_name",
113
+ "$mcp_server_version",
114
+ "$mcp_protocol_version",
115
+ "$process_person_profile"
116
+ ]);
117
+ var contentFreeMcpCapture = (event) => {
118
+ if (event.event === "$exception" || event.event === "$identify") return null;
119
+ const properties = Object.fromEntries(
120
+ Object.entries(event.properties).filter(([key, value]) => {
121
+ if (!CONTENT_FREE_PROPERTIES.has(key)) return false;
122
+ if (key === "$mcp_tool_name" || key === "$mcp_tool_category")
123
+ return typeof value === "string" && /^[a-z][a-z0-9_:-]{0,63}$/.test(value);
124
+ return true;
125
+ })
126
+ );
127
+ return { ...event, properties };
128
+ };
94
129
  function distinctIdFromToken(token) {
95
130
  if (!token) return null;
96
131
  return createHash("sha256").update(token).digest("hex").slice(0, 32);
@@ -107,7 +142,8 @@ function setupAnalytics(server, config) {
107
142
  });
108
143
  instrument(server, posthog, {
109
144
  logger: (msg) => logger.debug(`[posthog] ${msg}`),
110
- identify: { distinctId }
145
+ identify: { distinctId },
146
+ beforeSend: contentFreeMcpCapture
111
147
  });
112
148
  return posthog;
113
149
  }
@@ -120,12 +156,20 @@ function banner(msg) {
120
156
  async function main() {
121
157
  try {
122
158
  if (process.argv.includes("--http")) {
123
- const { startHosted } = await import("./main-QCBVJSSR.js");
159
+ const { startHosted } = await import("./main-2FKQQLWX.js");
124
160
  await startHosted();
125
161
  return;
126
162
  }
127
163
  const config = loadConfig();
128
- const server = createServer(config);
164
+ let brynCatalogue;
165
+ try {
166
+ brynCatalogue = await loadBrynCatalogue(new DatabrainClient(config));
167
+ } catch (error) {
168
+ logger.warn(
169
+ `Bryn tool discovery failed: ${error instanceof Error ? error.name : "unknown"}`
170
+ );
171
+ }
172
+ const server = createServer(config, brynCatalogue);
129
173
  const posthog = setupAnalytics(server, config);
130
174
  const transport = new StdioServerTransport();
131
175
  let shuttingDown = false;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/server.ts","../src/utils/analytics.ts"],"sourcesContent":["import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { createServer } from './server.js';\nimport { setupAnalytics } from './utils/analytics.js';\nimport { loadConfig } from './utils/config.js';\nimport { logger } from './utils/logger.js';\nimport { MCP_SERVER_VERSION } from './version.js';\n\nfunction banner(msg: string): void {\n process.stderr.write(`[databrain] ${msg}\\n`);\n}\n\nasync function main(): Promise<void> {\n try {\n if (process.argv.includes('--http')) {\n const { startHosted } = await import('./hosted/main.js');\n await startHosted();\n return;\n }\n const config = loadConfig();\n const server = createServer(config);\n const posthog = setupAnalytics(server, config);\n const transport = new StdioServerTransport();\n\n let shuttingDown = false;\n const flushAnalytics = async (): Promise<void> => {\n if (shuttingDown || !posthog) return;\n shuttingDown = true;\n await posthog.shutdown();\n };\n // StdioServerTransport never observes stdin ending, so the MCP client\n // disconnecting drains the event loop and exits the process without ever\n // calling transport/server onclose. `beforeExit` is what actually fires\n // in that path; posthog-node's flush timers are unref'd so they don't\n // keep the process alive on their own.\n process.on('beforeExit', () => void flushAnalytics());\n process.on(\n 'SIGINT',\n () => void flushAnalytics().then(() => process.exit(0))\n );\n process.on(\n 'SIGTERM',\n () => void flushAnalytics().then(() => process.exit(0))\n );\n\n await server.connect(transport);\n banner(`v${MCP_SERVER_VERSION} ready`);\n } catch (error) {\n // An Error serializes to {} in JSON, which left a self-hosted operator with no\n // reason. Config errors name the setting, never its value; the logger also\n // scrubs registered secrets.\n logger.error(\n `Failed to start server: ${error instanceof Error ? error.message : String(error)}`\n );\n process.exit(1);\n }\n}\n\nmain();\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { DatabrainClient } from './client/databrain-client.js';\nimport { registerAllPrompts } from './prompts/registry.js';\nimport { registerAllResources } from './resources/registry.js';\nimport { registerAllTools } from './tools/registry.js';\nimport type { Config } from './utils/config.js';\nimport { MCP_SERVER_VERSION } from './version.js';\n\nexport function createServer(config: Config): McpServer {\n const client = new DatabrainClient(config);\n const auth = client.getActiveAuth();\n let operatorBlurb: string;\n if (auth.source === 'oauth-client-credentials') {\n operatorBlurb =\n 'OPERATOR IDENTITY: OAuth client-credentials mode is configured for org-level calls. Tokens identify a service application, not a human operator. The backend must validate the token and map its issuer/subject to an authorized principal. Configuration alone does not prove backend acceptance. Do NOT ask the user for credentials or tokens.\\n\\n';\n } else if (auth.source === 'service-token') {\n operatorBlurb =\n 'OPERATOR IDENTITY: service-token mode. Calls are authenticated at org level — actions in the audit log are tagged actor=system, not a specific user. Per-user identity binding will be added when the OAuth-based login flow ships. Do NOT ask the user for credentials or tokens.\\n\\n';\n } else if (auth.source === 'api-token') {\n operatorBlurb =\n 'OPERATOR IDENTITY: API-token-only mode. A per-data-app API token is configured, but DATABRAIN_SERVICE_TOKEN is not. Embedding operations can use the configured API token, but org-level setup tools require DATABRAIN_SERVICE_TOKEN in the MCP config.\\n\\n';\n } else {\n operatorBlurb =\n 'OPERATOR IDENTITY: no usable token is configured. The MCP server normally refuses this config at startup. Ask the user to add DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN to the MCP config before proceeding.\\n\\n';\n }\n\n const serviceTokenStatus = config.oauth\n ? 'OAuth client credentials are configured for service-level operations. A configured DATABRAIN_SERVICE_TOKEN is used only if token acquisition fails.'\n : config.serviceToken\n ? 'This MCP server has DATABRAIN_SERVICE_TOKEN configured; never ask the user for it.'\n : 'DATABRAIN_SERVICE_TOKEN is not configured. Ask the user to add it to the MCP config only if org-level setup is needed.';\n const apiTokenStatus = client.hasApiToken()\n ? 'This MCP server has a per-data-app API token configured.'\n : config.oauth\n ? 'Created/rotated via create_api_token or rotate_api_token using configured OAuth service credentials, once the backend supports them.'\n : 'Created/rotated via create_api_token or rotate_api_token during setup when DATABRAIN_SERVICE_TOKEN is available.';\n\n const server = new McpServer(\n {\n name: 'databrain',\n version: MCP_SERVER_VERSION,\n description:\n 'Databrain MCP Server — build customer-facing embedded analytics AND run internal analytics on your own data. Supports embed setup, semantic-layer maintenance, natural-language querying, SQL→metric creation (embedded production metrics and unpublished internal workspace metrics), theming, filters, permissions, and guest token generation.',\n },\n {\n instructions:\n operatorBlurb +\n 'Databrain uses two token types — never confuse them:\\n' +\n `• SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, sync_datasource, list_datamarts, create_datamart, list_workspaces, list_data_apps, list_tenants, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status, custom_chart_validate (server check), custom_chart_upload, list_custom_charts, publish_custom_chart. ${serviceTokenStatus}\\n` +\n `• API TOKEN (per data app): scoped to one data app, used for embedding operations (list_embeds, get_embed_details, create_embed, update_embed, generate_guest_token), data-app scoped dashboard listing, widget CRUD/metric creation through guest-token APIs, and list_scheduled_reports. ${apiTokenStatus}\\n\\n` +\n 'FIRST classify the request before calling tools — this avoids wasted turns:\\n' +\n '• (A) EMBEDDING: putting Databrain dashboards/charts in front of YOUR customers (data apps, embeds, guest tokens, per-customer dashboard widgets). Needs an API token and usually a clientId.\\n' +\n '• (B) INTERNAL ANALYTICS: exploring your own data or migrating SQL into your own Databrain metrics for internal use. Workspace-scoped, uses the service token, NO clientId. Creating an unpublished workspace metric (create_workspace_metric) lives here — it is NOT embedding.\\n\\n' +\n \"5 core use cases — match the user's scenario to the right prompt:\\n\" +\n '1. Embed existing dashboard → prompt: embed-existing-dashboard (list_data_apps → create_api_token → list_dashboards → create_embed → generate_guest_token → generate_embed_code)\\n' +\n '2. Blank dashboard per client → prompt: embed-blank-dashboard (list_data_apps → list_datamarts → create_embed with clientId per client → generate_guest_token → generate_embed_code)\\n' +\n '3. Explore & query data → prompt: query-data (list_datamarts → get_semantic_layer → if empty, auto-generate descriptions with update_semantic_layer or use populate-semantic-layer prompt → ask_question)\\n' +\n '4. SQL → embedded production metric on a customer dashboard (EMBEDDING, requires clientId) → prompt: sql-to-metric-migration (validate_metric_spec → create_or_update_metric_from_query dry_run → apply only with explicit production confirmation)\\n' +\n '5. SQL → unpublished workspace metric (INTERNAL ANALYTICS, no clientId, not attached to any dashboard) → prompt: sql-to-workspace-metric (discover_datasource_schema → create_workspace_metric dry_run → apply with explicit confirmation)\\n\\n' +\n 'Production safety: OAuth may not be available yet. In service-token mode, writes are actor=system. For create_datamart and create_or_update_metric_from_query, dry_run is the default. Apply requires confirm=\"APPLY_TO_PRODUCTION\" after the user reviews the plan. Widget validation and mutation tools must verify the resolved embed is tenant-scoped for the requested clientId; if not, they must refuse to touch it as a likely production/data-app dashboard. Do not apply from a vague request.\\n\\n' +\n 'Semantic models: search_semantic_model, get_semantic_entity, propose_semantic_change, apply_semantic_change, and eval_semantic_model use the configured service credential and datamartId from list_datamarts. For a v2 model, use these tools instead of update_semantic_layer. Search and inspect first. Propose a change, review its errors and plan, then evaluate its returned source. Apply that exact source with its expected_content_hash only after the user supplies confirm=\"APPLY_TO_PRODUCTION\" and dry_run=false. Never supply confirmation yourself. Apply changes the live model for every consumer. A conflict requires a new proposal and review. dry_run defaults to true and never writes.\\n\\n' +\n 'Custom chart types (chart type custom:<key>): custom_chart_scaffold returns a draft and project files. In hosted agents, custom_chart_edit sends the description and draft to Chart studio Bryn when installed; if unavailable, edit draft.code yourself. Pass chartTs, manifest, bundleCss and iconSvg for server-side compilation without files or imports. custom_chart_preview shows validated draft code on bounded real sample rows without saving. custom_chart_validate checks a manifest and bundle without persisting, custom_chart_upload creates the chart or a preview version only with mode=\"apply\" and confirm=\"APPLY_TO_PRODUCTION\" (dry_run shows the plan first), and publish_custom_chart makes a version live for every builder only with the same confirmation because every dashboard using that key re-renders. For local projects only, build with `databrain custom-chart build`. A custom metric uses chartType=\"custom:<key>\" and customChart={key,bindings,options}; its MCP App tile loads only the company published bundle in the app/embed sandbox. Hosts without MCP Apps show rows and a clear fallback.\\n\\n' +\n 'Infrastructure creation is intentionally narrow in MCP: datasource and workspace creation still happen outside this workflow; sync_datasource and dry-run/apply create_datamart are available. The semantic layer is populated and maintained via MCP tools (get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status). When the semantic layer is empty, use these tools — do NOT send the user to the UI for semantic metadata.\\n\\n' +\n 'After embed setup, customize with: brand-my-embed prompt for theming. Use update_embed (body passthrough) for filters, localization, and permissions.\\n' +\n 'Use generate_guest_token for frontend auth and generate_embed_code for framework snippets.\\n' +\n 'For advanced embed configuration (UI toggles, filters, i18n), use update_embed with a body passthrough.\\n' +\n 'To diagnose embed issues, use get_embed_details and reason about the response.\\n' +\n 'Do NOT ask the user for any tokens — manage them via MCP tools.\\n\\n' +\n 'IMPORTANT — accessSettings defaults:\\n' +\n 'When calling create_embed, only pass accessSettings: { datamartName: \"...\" } plus any flags the user explicitly wants enabled. For multi-tenant dashboard embeds, also pass clientId. The tool auto-fills all required isAllow* fields with secure defaults (all false). Do NOT guess or enumerate accessSettings fields.\\n\\n' +\n 'IMPORTANT — create_data_app does NOT accept workspaceName. Only pass { name: \"...\" }.\\n\\n' +\n 'IMPORTANT — Be interactive. Present results from each tool call and ask the user before proceeding to the next step. Never batch multiple mutating operations without user confirmation.\\n' +\n 'Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, filters, permissions, datasource, datamart, workspace, metric creation.',\n }\n );\n\n registerAllTools(server, client);\n registerAllResources(server);\n registerAllPrompts(server);\n\n return server;\n}\n","import { createHash } from 'node:crypto';\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { instrument } from '@posthog/mcp';\nimport { PostHog } from 'posthog-node';\nimport type { Config } from './config.js';\nimport { DEFAULT_POSTHOG_API_KEY, DEFAULT_POSTHOG_HOST } from './config.js';\nimport { logger } from './logger.js';\n\n// Derives a stable per-install distinct_id from the active auth token without\n// ever sending the token itself to PostHog.\nfunction distinctIdFromToken(token: string | undefined): string | null {\n if (!token) return null;\n return createHash('sha256').update(token).digest('hex').slice(0, 32);\n}\n\n/**\n * Wires PostHog MCP analytics (tool calls, initialize, exceptions) into the\n * server. Always on — not user-configurable. Returns the PostHog client so\n * the caller can flush/shutdown it on exit, or null when no auth token is\n * configured yet (nothing to derive a distinct_id from).\n */\nexport function setupAnalytics(\n server: McpServer,\n config: Config\n): PostHog | null {\n const distinctId = distinctIdFromToken(\n config.serviceToken || config.apiToken\n );\n if (!distinctId) {\n return null;\n }\n\n const posthog = new PostHog(config.posthogApiKey || DEFAULT_POSTHOG_API_KEY, {\n host: config.posthogHost || DEFAULT_POSTHOG_HOST,\n });\n\n instrument(server, posthog, {\n logger: (msg) => logger.debug(`[posthog] ${msg}`),\n identify: { distinctId },\n });\n\n return posthog;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/server.ts","../src/utils/analytics.ts"],"sourcesContent":["import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { type BrynCatalogue, loadBrynCatalogue } from './bryn/catalogue.js';\nimport { DatabrainClient } from './client/databrain-client.js';\nimport { createServer } from './server.js';\nimport { setupAnalytics } from './utils/analytics.js';\nimport { loadConfig } from './utils/config.js';\nimport { logger } from './utils/logger.js';\nimport { MCP_SERVER_VERSION } from './version.js';\n\nfunction banner(msg: string): void {\n process.stderr.write(`[databrain] ${msg}\\n`);\n}\n\nasync function main(): Promise<void> {\n try {\n if (process.argv.includes('--http')) {\n const { startHosted } = await import('./hosted/main.js');\n await startHosted();\n return;\n }\n const config = loadConfig();\n let brynCatalogue: BrynCatalogue | undefined;\n try {\n brynCatalogue = await loadBrynCatalogue(new DatabrainClient(config));\n } catch (error) {\n logger.warn(\n `Bryn tool discovery failed: ${error instanceof Error ? error.name : 'unknown'}`\n );\n }\n const server = createServer(config, brynCatalogue);\n const posthog = setupAnalytics(server, config);\n const transport = new StdioServerTransport();\n\n let shuttingDown = false;\n const flushAnalytics = async (): Promise<void> => {\n if (shuttingDown || !posthog) return;\n shuttingDown = true;\n await posthog.shutdown();\n };\n // StdioServerTransport never observes stdin ending, so the MCP client\n // disconnecting drains the event loop and exits the process without ever\n // calling transport/server onclose. `beforeExit` is what actually fires\n // in that path; posthog-node's flush timers are unref'd so they don't\n // keep the process alive on their own.\n process.on('beforeExit', () => void flushAnalytics());\n process.on(\n 'SIGINT',\n () => void flushAnalytics().then(() => process.exit(0))\n );\n process.on(\n 'SIGTERM',\n () => void flushAnalytics().then(() => process.exit(0))\n );\n\n await server.connect(transport);\n banner(`v${MCP_SERVER_VERSION} ready`);\n } catch (error) {\n // An Error serializes to {} in JSON, which left a self-hosted operator with no\n // reason. Config errors name the setting, never its value; the logger also\n // scrubs registered secrets.\n logger.error(\n `Failed to start server: ${error instanceof Error ? error.message : String(error)}`\n );\n process.exit(1);\n }\n}\n\nmain();\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { BrynCatalogue } from './bryn/catalogue.js';\nimport { registerCatalogueTurns } from './bryn/catalogueTurns.js';\nimport { registerBrynTools } from './bryn/register.js';\nimport { registerBrynTurns } from './bryn/turns.js';\nimport { DatabrainClient } from './client/databrain-client.js';\nimport { registerAllPrompts } from './prompts/registry.js';\nimport { registerAllResources } from './resources/registry.js';\nimport { registerAllTools } from './tools/registry.js';\nimport type { Config } from './utils/config.js';\nimport { MCP_SERVER_VERSION } from './version.js';\n\nexport function createServer(\n config: Config,\n brynCatalogue?: BrynCatalogue\n): McpServer {\n const client = new DatabrainClient(config);\n const auth = client.getActiveAuth();\n let operatorBlurb: string;\n if (auth.source === 'oauth-client-credentials') {\n operatorBlurb =\n 'OPERATOR IDENTITY: OAuth client-credentials mode is configured for org-level calls. Tokens identify a service application, not a human operator. The backend must validate the token and map its issuer/subject to an authorized principal. Configuration alone does not prove backend acceptance. Do NOT ask the user for credentials or tokens.\\n\\n';\n } else if (auth.source === 'service-token') {\n operatorBlurb =\n 'OPERATOR IDENTITY: service-token mode. Calls are authenticated at org level — actions in the audit log are tagged actor=system, not a specific user. Per-user identity binding will be added when the OAuth-based login flow ships. Do NOT ask the user for credentials or tokens.\\n\\n';\n } else if (auth.source === 'api-token') {\n operatorBlurb =\n 'OPERATOR IDENTITY: API-token-only mode. A per-data-app API token is configured, but DATABRAIN_SERVICE_TOKEN is not. Embedding operations can use the configured API token, but org-level setup tools require DATABRAIN_SERVICE_TOKEN in the MCP config.\\n\\n';\n } else {\n operatorBlurb =\n 'OPERATOR IDENTITY: no usable token is configured. The MCP server normally refuses this config at startup. Ask the user to add DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN to the MCP config before proceeding.\\n\\n';\n }\n\n const serviceTokenStatus = config.oauth\n ? 'OAuth client credentials are configured for service-level operations. A configured DATABRAIN_SERVICE_TOKEN is used only if token acquisition fails.'\n : config.serviceToken\n ? 'This MCP server has DATABRAIN_SERVICE_TOKEN configured; never ask the user for it.'\n : 'DATABRAIN_SERVICE_TOKEN is not configured. Ask the user to add it to the MCP config only if org-level setup is needed.';\n const apiTokenStatus = client.hasApiToken()\n ? 'This MCP server has a per-data-app API token configured.'\n : config.oauth\n ? 'Created/rotated via create_api_token or rotate_api_token using configured OAuth service credentials, once the backend supports them.'\n : 'Created/rotated via create_api_token or rotate_api_token during setup when DATABRAIN_SERVICE_TOKEN is available.';\n\n const server = new McpServer(\n {\n name: 'databrain',\n version: MCP_SERVER_VERSION,\n description:\n 'Databrain MCP Server — build customer-facing embedded analytics AND run internal analytics on your own data. Supports embed setup, semantic-layer maintenance, natural-language querying, SQL→metric creation (embedded production metrics and unpublished internal workspace metrics), theming, filters, permissions, and guest token generation.',\n },\n {\n instructions:\n operatorBlurb +\n 'Databrain uses two token types — never confuse them:\\n' +\n `• SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, sync_datasource, list_datamarts, create_datamart, list_workspaces, list_data_apps, list_tenants, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status, custom_chart_validate (server check), custom_chart_upload, list_custom_charts, publish_custom_chart. ${serviceTokenStatus}\\n` +\n `• API TOKEN (per data app): scoped to one data app, used for embedding operations (list_embeds, get_embed_details, create_embed, update_embed, generate_guest_token), data-app scoped dashboard listing, widget CRUD/metric creation through guest-token APIs, and list_scheduled_reports. ${apiTokenStatus}\\n\\n` +\n 'FIRST classify the request before calling tools — this avoids wasted turns:\\n' +\n '• (A) EMBEDDING: putting Databrain dashboards/charts in front of YOUR customers (data apps, embeds, guest tokens, per-customer dashboard widgets). Needs an API token and usually a clientId.\\n' +\n '• (B) INTERNAL ANALYTICS: exploring your own data or migrating SQL into your own Databrain metrics for internal use. Workspace-scoped, uses the service token, NO clientId. Creating an unpublished workspace metric (create_workspace_metric) lives here — it is NOT embedding.\\n\\n' +\n \"5 core use cases — match the user's scenario to the right prompt:\\n\" +\n '1. Embed existing dashboard → prompt: embed-existing-dashboard (list_data_apps → create_api_token → list_dashboards → create_embed → generate_guest_token → generate_embed_code)\\n' +\n '2. Blank dashboard per client → prompt: embed-blank-dashboard (list_data_apps → list_datamarts → create_embed with clientId per client → generate_guest_token → generate_embed_code)\\n' +\n '3. Explore & query data → prompt: query-data (list_datamarts → get_semantic_layer → if empty, auto-generate descriptions with update_semantic_layer or use populate-semantic-layer prompt → ask_question)\\n' +\n '4. SQL → embedded production metric on a customer dashboard (EMBEDDING, requires clientId) → prompt: sql-to-metric-migration (validate_metric_spec → create_or_update_metric_from_query dry_run → apply only with explicit production confirmation)\\n' +\n '5. SQL → unpublished workspace metric (INTERNAL ANALYTICS, no clientId, not attached to any dashboard) → prompt: sql-to-workspace-metric (discover_datasource_schema → create_workspace_metric dry_run → apply with explicit confirmation)\\n\\n' +\n 'Production safety: OAuth may not be available yet. In service-token mode, writes are actor=system. For create_datamart and create_or_update_metric_from_query, dry_run is the default. Apply requires confirm=\"APPLY_TO_PRODUCTION\" after the user reviews the plan. Widget validation and mutation tools must verify the resolved embed is tenant-scoped for the requested clientId; if not, they must refuse to touch it as a likely production/data-app dashboard. Do not apply from a vague request.\\n\\n' +\n 'Semantic models: search_semantic_model, get_semantic_entity, propose_semantic_change, apply_semantic_change, and eval_semantic_model use the configured service credential and datamartId from list_datamarts. For a v2 model, use these tools instead of update_semantic_layer. Search and inspect first. Propose a change, review its errors and plan, then evaluate its returned source. Apply that exact source with its expected_content_hash only after the user supplies confirm=\"APPLY_TO_PRODUCTION\" and dry_run=false. Never supply confirmation yourself. Apply changes the live model for every consumer. A conflict requires a new proposal and review. dry_run defaults to true and never writes.\\n\\n' +\n 'Custom chart types (chart type custom:<key>): custom_chart_scaffold returns a draft and project files. In hosted agents, custom_chart_edit sends the description and draft to Chart studio Bryn when installed; if unavailable, edit draft.code yourself. Pass chartTs, manifest, bundleCss and iconSvg for server-side compilation without files or imports. custom_chart_preview shows validated draft code on bounded real sample rows without saving. custom_chart_validate checks a manifest and bundle without persisting, custom_chart_upload creates the chart or a preview version only with mode=\"apply\" and confirm=\"APPLY_TO_PRODUCTION\" (dry_run shows the plan first), and publish_custom_chart makes a version live for every builder only with the same confirmation because every dashboard using that key re-renders. For local projects only, build with `databrain custom-chart build`. A custom metric uses chartType=\"custom:<key>\" and customChart={key,bindings,options}; its MCP App tile loads only the company published bundle in the app/embed sandbox. Hosts without MCP Apps show rows and a clear fallback.\\n\\n' +\n 'Infrastructure creation is intentionally narrow in MCP: datasource and workspace creation still happen outside this workflow; sync_datasource and dry-run/apply create_datamart are available. The semantic layer is populated and maintained via MCP tools (get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status). When the semantic layer is empty, use these tools — do NOT send the user to the UI for semantic metadata.\\n\\n' +\n 'After embed setup, customize with: brand-my-embed prompt for theming. Use update_embed (body passthrough) for filters, localization, and permissions.\\n' +\n 'Use generate_guest_token for frontend auth and generate_embed_code for framework snippets.\\n' +\n 'For advanced embed configuration (UI toggles, filters, i18n), use update_embed with a body passthrough.\\n' +\n 'To diagnose embed issues, use get_embed_details and reason about the response.\\n' +\n 'Do NOT ask the user for any tokens — manage them via MCP tools.\\n\\n' +\n 'IMPORTANT — accessSettings defaults:\\n' +\n 'When calling create_embed, only pass accessSettings: { datamartName: \"...\" } plus any flags the user explicitly wants enabled. For multi-tenant dashboard embeds, also pass clientId. The tool auto-fills all required isAllow* fields with secure defaults (all false). Do NOT guess or enumerate accessSettings fields.\\n\\n' +\n 'IMPORTANT — create_data_app does NOT accept workspaceName. Only pass { name: \"...\" }.\\n\\n' +\n 'IMPORTANT — Be interactive. Present results from each tool call and ask the user before proceeding to the next step. Never batch multiple mutating operations without user confirmation.\\n' +\n 'Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, filters, permissions, datasource, datamart, workspace, metric creation.',\n }\n );\n\n registerAllTools(server, client);\n if (brynCatalogue?.tools.length || brynCatalogue?.turns.length) {\n registerBrynTools(server, client, brynCatalogue);\n registerBrynTurns(server, client);\n registerCatalogueTurns(server, client, brynCatalogue);\n }\n registerAllResources(server);\n registerAllPrompts(server);\n\n return server;\n}\n","import { createHash } from 'node:crypto';\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { type BeforeSendFn, instrument } from '@posthog/mcp';\nimport { PostHog } from 'posthog-node';\nimport type { Config } from './config.js';\nimport { DEFAULT_POSTHOG_API_KEY, DEFAULT_POSTHOG_HOST } from './config.js';\nimport { logger } from './logger.js';\n\nconst CONTENT_FREE_PROPERTIES = new Set([\n '$mcp_source',\n '$mcp_duration_ms',\n '$mcp_is_error',\n '$mcp_tool_name',\n '$mcp_tool_category',\n '$mcp_server_name',\n '$mcp_server_version',\n '$mcp_protocol_version',\n '$process_person_profile',\n]);\n\n/** No prompt, input, response, row, exception stack, or inferred intent leaves stdio. */\nexport const contentFreeMcpCapture: BeforeSendFn = (event) => {\n if (event.event === '$exception' || event.event === '$identify') return null;\n const properties = Object.fromEntries(\n Object.entries(event.properties).filter(([key, value]) => {\n if (!CONTENT_FREE_PROPERTIES.has(key)) return false;\n if (key === '$mcp_tool_name' || key === '$mcp_tool_category')\n return (\n typeof value === 'string' && /^[a-z][a-z0-9_:-]{0,63}$/.test(value)\n );\n return true;\n })\n );\n return { ...event, properties };\n};\n\n// Derives a stable per-install distinct_id from the active auth token without\n// ever sending the token itself to PostHog.\nfunction distinctIdFromToken(token: string | undefined): string | null {\n if (!token) return null;\n return createHash('sha256').update(token).digest('hex').slice(0, 32);\n}\n\n/**\n * Wires PostHog MCP analytics (tool calls, initialize, exceptions) into the\n * server. Always on — not user-configurable. Returns the PostHog client so\n * the caller can flush/shutdown it on exit, or null when no auth token is\n * configured yet (nothing to derive a distinct_id from).\n */\nexport function setupAnalytics(\n server: McpServer,\n config: Config\n): PostHog | null {\n const distinctId = distinctIdFromToken(\n config.serviceToken || config.apiToken\n );\n if (!distinctId) {\n return null;\n }\n\n const posthog = new PostHog(config.posthogApiKey || DEFAULT_POSTHOG_API_KEY, {\n host: config.posthogHost || DEFAULT_POSTHOG_HOST,\n });\n\n instrument(server, posthog, {\n logger: (msg) => logger.debug(`[posthog] ${msg}`),\n identify: { distinctId },\n beforeSend: contentFreeMcpCapture,\n });\n\n return posthog;\n}\n"],"mappings":"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","names":[]}
@@ -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-NIC5MAGC.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-UYFFFAYV.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-AWICHHDU.js";
36
+ } from "./chunk-4E57ZRZO.js";
24
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-CEGHG5U3.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";
@@ -101,6 +116,11 @@ import {
101
116
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
102
117
 
103
118
  // src/hosted/authorization.ts
119
+ function brynEntry(name, catalogue) {
120
+ return catalogue?.tools.find(
121
+ (entry) => entry.name === name && isExecutableBrynTool(entry)
122
+ );
123
+ }
104
124
  var TOOL_SCOPES = {
105
125
  whoami: [],
106
126
  list_datasources: ["mcp:datamarts:read"],
@@ -180,28 +200,51 @@ var TOOL_SCOPES = {
180
200
  drill_down: ["mcp:dashboards:read", "mcp:query"],
181
201
  change_date_range: ["mcp:dashboards:read", "mcp:query"]
182
202
  };
183
- function requiredScopes(name) {
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
+ }
184
221
  if (!Object.hasOwn(TOOL_SCOPES, name))
185
222
  throw new HostedAuthError(403, "tool_not_authorized");
186
223
  return TOOL_SCOPES[name];
187
224
  }
188
- function authorizeTool(name, principal) {
225
+ function authorizeTool(name, principal, catalogue) {
189
226
  if (principal.expiresAt !== void 0 && principal.expiresAt <= Date.now() / 1e3)
190
227
  throw new HostedAuthError(401, "invalid_token");
191
- if (requiredScopes(name).some((scope) => !principal.scopes.includes(scope)))
228
+ if (requiredScopes(name, catalogue).some(
229
+ (scope) => !principal.scopes.includes(scope)
230
+ ))
192
231
  throw new HostedAuthError(403, "insufficient_scope");
193
232
  }
194
233
  var TOOL_ARGUMENT_VALUES = {
195
234
  list_dashboards: { scope: ["auto", "data_app", "global"] }
196
235
  };
197
236
  var TOOL_OPAQUE_ARGUMENTS = {
237
+ ask_bryn_theme: ["theme", "dark", "schema", "contrast"],
238
+ ask_bryn_custom_chart: ["draft"],
198
239
  preview_chart: ["rows"],
199
240
  custom_chart_preview: ["rows", "manifest"],
200
241
  custom_chart_edit: ["draft"]
201
242
  };
202
- function assertToolArguments(name, value, principal, delegated = false) {
243
+ function assertToolArguments(name, value, principal, delegated = false, catalogue) {
203
244
  const allowed = Object.hasOwn(TOOL_ARGUMENT_VALUES, name) ? TOOL_ARGUMENT_VALUES[name] : {};
204
- 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;
205
248
  const target = selectWorkspace(value, principal);
206
249
  if (!value || typeof value !== "object" || Array.isArray(value)) {
207
250
  assertBoundArguments(value, principal, { target, delegated });
@@ -426,7 +469,7 @@ function onlyWorkspace(raw, target) {
426
469
  return raw;
427
470
  }
428
471
  var HostedClient = class extends DatabrainClient {
429
- constructor(adapter, principal, delegation) {
472
+ constructor(adapter, principal, delegation, brynCatalogue) {
430
473
  super({
431
474
  apiUrl: "https://api.usedatabrain.com",
432
475
  demoDomain: "https://demo.usedatabrain.com"
@@ -434,6 +477,7 @@ var HostedClient = class extends DatabrainClient {
434
477
  this.adapter = adapter;
435
478
  this.principal = principal;
436
479
  this.delegation = delegation;
480
+ this.brynCatalogue = brynCatalogue;
437
481
  }
438
482
  // A generated API credential must never replace the authorizing hosted principal.
439
483
  setRuntimeApiToken(_token) {
@@ -445,7 +489,7 @@ var HostedClient = class extends DatabrainClient {
445
489
  try {
446
490
  const name = toolContext.getStore()?.tool;
447
491
  if (!name) throw new HostedAuthError(403, "tool_not_authorized");
448
- authorizeTool(name, this.principal);
492
+ authorizeTool(name, this.principal, this.brynCatalogue);
449
493
  const named = callWorkspace.getStore()?.target;
450
494
  const target = named ?? selectWorkspace(options.body, this.principal, [
451
495
  "workspaceName",
@@ -457,7 +501,17 @@ var HostedClient = class extends DatabrainClient {
457
501
  const delegated = this.delegation !== void 0;
458
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(
459
503
  options.path
460
- ) ? ["draft"] : void 0;
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;
461
515
  assertBoundArguments(options.body, this.principal, {
462
516
  target,
463
517
  delegated,
@@ -551,7 +605,7 @@ function whoami(principal) {
551
605
  )
552
606
  );
553
607
  }
554
- function createHostedToolServer(principal, adapter, delegation) {
608
+ function createHostedToolServer(principal, adapter, delegation, brynCatalogue) {
555
609
  const bound = structuredClone(principal);
556
610
  const delegated = delegation !== void 0;
557
611
  const server = new McpServer(
@@ -583,8 +637,14 @@ function createHostedToolServer(principal, adapter, delegation) {
583
637
  }
584
638
  args[index] = async (...values) => {
585
639
  try {
586
- authorizeTool(name, bound);
587
- 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
+ );
588
648
  if (name === "whoami") return whoami(bound);
589
649
  const input = values[0] && typeof values[0] === "object" ? values[0] : void 0;
590
650
  if (input?.projectDir !== void 0)
@@ -628,9 +688,14 @@ function createHostedToolServer(principal, adapter, delegation) {
628
688
  };
629
689
  return register(...args);
630
690
  });
631
- const client = new HostedClient(adapter, bound, delegation);
691
+ const client = new HostedClient(adapter, bound, delegation, brynCatalogue);
632
692
  enableChartApps(server);
633
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
+ }
634
699
  registerCreateWorkspace(server, client);
635
700
  registerAllResources(server);
636
701
  registerAllPrompts(server);
@@ -641,6 +706,7 @@ function createHostedToolServer(principal, adapter, delegation) {
641
706
 
642
707
  // src/hosted/http.ts
643
708
  var HEALTH_BODY = JSON.stringify({ status: "ok" });
709
+ var MAX_MCP_BODY_BYTES = 4 * 1024 * 1024 + 64 * 1024;
644
710
  function healthPath(url) {
645
711
  const path = (url || "/").split("?")[0];
646
712
  return path === "/health";
@@ -657,7 +723,7 @@ async function readBody(req) {
657
723
  const chunks = [];
658
724
  for await (const chunk of req) {
659
725
  size += chunk.length;
660
- if (size > 1048576) throw new Error("body_too_large");
726
+ if (size > MAX_MCP_BODY_BYTES) throw new Error("body_too_large");
661
727
  chunks.push(Buffer.from(chunk));
662
728
  }
663
729
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
@@ -668,6 +734,7 @@ function createHostedHttpServer(config, adapter, pats) {
668
734
  const metadataUrl = `${publicUrl.origin}${metadataPath}`;
669
735
  const verify = createBearerVerifier(adapter, config.publicUrl, pats);
670
736
  let active = 0;
737
+ const catalogueCache = new BrynCatalogueCache();
671
738
  const server = createServer(async (req, res) => {
672
739
  if (healthPath(req.url)) {
673
740
  if (req.method !== "GET" && req.method !== "HEAD") {
@@ -776,14 +843,42 @@ function createHostedHttpServer(config, adapter, pats) {
776
843
  return;
777
844
  }
778
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
+ }
779
873
  if (body?.method === "tools/call" && typeof body.params?.name === "string") {
780
874
  try {
781
- authorizeTool(body.params.name, principal);
875
+ authorizeTool(body.params.name, principal, brynCatalogue);
782
876
  assertToolArguments(
783
877
  body.params.name,
784
878
  body.params.arguments,
785
879
  principal,
786
- delegation !== void 0
880
+ delegation !== void 0,
881
+ brynCatalogue
787
882
  );
788
883
  } catch (error) {
789
884
  if (error instanceof WorkspaceSelectionError) {
@@ -795,9 +890,10 @@ function createHostedHttpServer(config, adapter, pats) {
795
890
  return;
796
891
  }
797
892
  if (error instanceof HostedAuthError && error.code === "insufficient_scope") {
798
- const missing = requiredScopes(body.params.name).filter(
799
- (scope) => !principal.scopes.includes(scope)
800
- );
893
+ const missing = requiredScopes(
894
+ body.params.name,
895
+ brynCatalogue
896
+ ).filter((scope) => !principal.scopes.includes(scope));
801
897
  reply(res, 200, {
802
898
  jsonrpc: "2.0",
803
899
  id: body.id ?? null,
@@ -805,10 +901,25 @@ function createHostedHttpServer(config, adapter, pats) {
805
901
  });
806
902
  return;
807
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
+ });
912
+ return;
913
+ }
808
914
  throw error;
809
915
  }
810
916
  }
811
- const mcp = createHostedToolServer(principal, adapter, delegation);
917
+ const mcp = createHostedToolServer(
918
+ principal,
919
+ adapter,
920
+ delegation,
921
+ brynCatalogue
922
+ );
812
923
  const transport = new StreamableHTTPServerTransport({
813
924
  sessionIdGenerator: void 0,
814
925
  enableJsonResponse: true
@@ -909,7 +1020,7 @@ function createProviderAdapter(config) {
909
1020
  const publicOrigin = new URL(config.publicUrl).origin;
910
1021
  const bearers = /* @__PURE__ */ new WeakMap();
911
1022
  const responseSchema = principalSchema.extend({ issuer: z3.literal(config.issuer) }).strip();
912
- return {
1023
+ const adapter = {
913
1024
  async verifyOAuth(token, resource) {
914
1025
  try {
915
1026
  const response = await fetch(endpoint, {
@@ -995,7 +1106,7 @@ function createProviderAdapter(config) {
995
1106
  body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
996
1107
  signal: AbortSignal.timeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS)
997
1108
  });
998
- const text = await response.text();
1109
+ const text = options.responseFormat === "bryn-sse" && response.ok ? await readBrynStream(response) : await response.text();
999
1110
  if (!response.ok) {
1000
1111
  const forbidden = response.status === 403 ? errorDetail(403, text, bearer) : void 0;
1001
1112
  if (forbidden?.code && GRANT_DENIALS.has(forbidden.code)) {
@@ -1008,17 +1119,28 @@ function createProviderAdapter(config) {
1008
1119
  } : forbidden
1009
1120
  );
1010
1121
  }
1011
- 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;
1012
1123
  const detail = status === 401 || status === 503 || response.status === 403 ? void 0 : errorDetail(response.status, text, bearer);
1013
- 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
+ );
1014
1130
  }
1015
1131
  try {
1016
- 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;
1017
1133
  } catch {
1018
1134
  throw new HostedAuthError(503, "delegated_response_invalid");
1019
1135
  }
1020
1136
  }
1021
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;
1022
1144
  }
1023
1145
 
1024
1146
  // src/hosted/main.ts
@@ -1031,7 +1153,7 @@ async function startHosted() {
1031
1153
  throw new Error(
1032
1154
  "Hosted test mode requires an absolute fixture store path."
1033
1155
  );
1034
- const { createTestOnlyAdapter } = await import("./test-only-6474NMTR.js");
1156
+ const { createTestOnlyAdapter } = await import("./test-only-VH3FF5ML.js");
1035
1157
  adapter = createTestOnlyAdapter(file);
1036
1158
  } else {
1037
1159
  const file = process.env.DATABRAIN_HOSTED_ADAPTER_MODULE;
@@ -1085,4 +1207,4 @@ async function startHosted() {
1085
1207
  export {
1086
1208
  startHosted
1087
1209
  };
1088
- //# sourceMappingURL=main-QCBVJSSR.js.map
1210
+ //# sourceMappingURL=main-2FKQQLWX.js.map