@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/apps/server.js +4 -4
- package/dist/apps/stdio.js +5 -5
- package/dist/chunk-3AH56XX7.js +28 -0
- package/dist/chunk-3AH56XX7.js.map +1 -0
- package/dist/{chunk-AWICHHDU.js → chunk-4E57ZRZO.js} +152 -5
- package/dist/chunk-4E57ZRZO.js.map +1 -0
- package/dist/{chunk-UYFFFAYV.js → chunk-6MD7KOAU.js} +5 -4
- package/dist/{chunk-VQ5G5AZV.js → chunk-FJLRZJ4A.js} +4 -4
- package/dist/{chunk-CEGHG5U3.js → chunk-MUZ3KEJX.js} +5 -23
- package/dist/chunk-MUZ3KEJX.js.map +1 -0
- package/dist/{chunk-S65KA3Z7.js → chunk-NXE2A4WV.js} +2 -2
- package/dist/{chunk-NIC5MAGC.js → chunk-QQ6KNULX.js} +3 -3
- package/dist/chunk-V5JINLOA.js +506 -0
- package/dist/chunk-V5JINLOA.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +51 -7
- package/dist/index.js.map +1 -1
- package/dist/{main-QCBVJSSR.js → main-2FKQQLWX.js} +154 -32
- package/dist/main-2FKQQLWX.js.map +1 -0
- package/dist/{test-only-6474NMTR.js → test-only-VH3FF5ML.js} +5 -3
- package/dist/{test-only-6474NMTR.js.map → test-only-VH3FF5ML.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-AWICHHDU.js.map +0 -1
- package/dist/chunk-CEGHG5U3.js.map +0 -1
- package/dist/main-QCBVJSSR.js.map +0 -1
- /package/dist/{chunk-UYFFFAYV.js.map → chunk-6MD7KOAU.js.map} +0 -0
- /package/dist/{chunk-VQ5G5AZV.js.map → chunk-FJLRZJ4A.js.map} +0 -0
- /package/dist/{chunk-S65KA3Z7.js.map → chunk-NXE2A4WV.js.map} +0 -0
- /package/dist/{chunk-NIC5MAGC.js.map → chunk-QQ6KNULX.js.map} +0 -0
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-
|
|
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-
|
|
17
|
+
} from "./chunk-6MD7KOAU.js";
|
|
12
18
|
import {
|
|
13
19
|
DatabrainClient,
|
|
14
20
|
MCP_SERVER_VERSION,
|
|
15
21
|
logger
|
|
16
|
-
} from "./chunk-
|
|
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-
|
|
159
|
+
const { startHosted } = await import("./main-2FKQQLWX.js");
|
|
124
160
|
await startHosted();
|
|
125
161
|
return;
|
|
126
162
|
}
|
|
127
163
|
const config = loadConfig();
|
|
128
|
-
|
|
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":";;;;;;;;;;;;;;;;;;;AAAA,SAAS,4BAA4B;;;ACArC,SAAS,iBAAiB;AAQnB,SAAS,aAAa,QAA2B;AACtD,QAAM,SAAS,IAAI,gBAAgB,MAAM;AACzC,QAAM,OAAO,OAAO,cAAc;AAClC,MAAI;AACJ,MAAI,KAAK,WAAW,4BAA4B;AAC9C,oBACE;AAAA,EACJ,WAAW,KAAK,WAAW,iBAAiB;AAC1C,oBACE;AAAA,EACJ,WAAW,KAAK,WAAW,aAAa;AACtC,oBACE;AAAA,EACJ,OAAO;AACL,oBACE;AAAA,EACJ;AAEA,QAAM,qBAAqB,OAAO,QAC9B,wJACA,OAAO,eACL,uFACA;AACN,QAAM,iBAAiB,OAAO,YAAY,IACtC,6DACA,OAAO,QACL,yIACA;AAEN,QAAM,SAAS,IAAI;AAAA,IACjB;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,MACT,aACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,cACE,gBACA;AAAA,ykBACqkB,kBAAkB;AAAA,kSACzT,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAwBhT;AAAA,EACF;AAEA,mBAAiB,QAAQ,MAAM;AAC/B,uBAAqB,MAAM;AAC3B,qBAAmB,MAAM;AAEzB,SAAO;AACT;;;ACjFA,SAAS,kBAAkB;AAE3B,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AAOxB,SAAS,oBAAoB,OAA0C;AACrE,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AACrE;AAQO,SAAS,eACd,QACA,QACgB;AAChB,QAAM,aAAa;AAAA,IACjB,OAAO,gBAAgB,OAAO;AAAA,EAChC;AACA,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,IAAI,QAAQ,OAAO,iBAAiB,yBAAyB;AAAA,IAC3E,MAAM,OAAO,eAAe;AAAA,EAC9B,CAAC;AAED,aAAW,QAAQ,SAAS;AAAA,IAC1B,QAAQ,CAAC,QAAQ,OAAO,MAAM,aAAa,GAAG,EAAE;AAAA,IAChD,UAAU,EAAE,WAAW;AAAA,EACzB,CAAC;AAED,SAAO;AACT;;;AFnCA,SAAS,OAAO,KAAmB;AACjC,UAAQ,OAAO,MAAM,eAAe,GAAG;AAAA,CAAI;AAC7C;AAEA,eAAe,OAAsB;AACnC,MAAI;AACF,QAAI,QAAQ,KAAK,SAAS,QAAQ,GAAG;AACnC,YAAM,EAAE,YAAY,IAAI,MAAM,OAAO,oBAAkB;AACvD,YAAM,YAAY;AAClB;AAAA,IACF;AACA,UAAM,SAAS,WAAW;AAC1B,UAAM,SAAS,aAAa,MAAM;AAClC,UAAM,UAAU,eAAe,QAAQ,MAAM;AAC7C,UAAM,YAAY,IAAI,qBAAqB;AAE3C,QAAI,eAAe;AACnB,UAAM,iBAAiB,YAA2B;AAChD,UAAI,gBAAgB,CAAC,QAAS;AAC9B,qBAAe;AACf,YAAM,QAAQ,SAAS;AAAA,IACzB;AAMA,YAAQ,GAAG,cAAc,MAAM,KAAK,eAAe,CAAC;AACpD,YAAQ;AAAA,MACN;AAAA,MACA,MAAM,KAAK,eAAe,EAAE,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,IACxD;AACA,YAAQ;AAAA,MACN;AAAA,MACA,MAAM,KAAK,eAAe,EAAE,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,IACxD;AAEA,UAAM,OAAO,QAAQ,SAAS;AAC9B,WAAO,IAAI,kBAAkB,QAAQ;AAAA,EACvC,SAAS,OAAO;AAId,WAAO;AAAA,MACL,2BAA2B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,IACnF;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;","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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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"] :
|
|
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(
|
|
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 >
|
|
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(
|
|
799
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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-
|
|
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-
|
|
1210
|
+
//# sourceMappingURL=main-2FKQQLWX.js.map
|