@databrainhq/mcp-server 0.3.0-rc.4 → 0.3.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +56 -1
  2. package/dist/apps/server.d.ts +6 -6
  3. package/dist/apps/server.js +3 -2
  4. package/dist/apps/stdio.js +4 -3
  5. package/dist/apps/stdio.js.map +1 -1
  6. package/dist/{chunk-CX3CS62P.js → chunk-3GF7WSKK.js} +4 -2
  7. package/dist/chunk-3GF7WSKK.js.map +1 -0
  8. package/dist/{chunk-NZGQJBJV.js → chunk-7G5QBEPC.js} +8 -6
  9. package/dist/{chunk-NZGQJBJV.js.map → chunk-7G5QBEPC.js.map} +1 -1
  10. package/dist/{chunk-3JFMWXRO.js → chunk-SDFC7PKT.js} +2 -2
  11. package/dist/chunk-SKU37UVJ.js +1934 -0
  12. package/dist/chunk-SKU37UVJ.js.map +1 -0
  13. package/dist/{chunk-I35U2LBX.js → chunk-Z2SF3K6P.js} +955 -1118
  14. package/dist/chunk-Z2SF3K6P.js.map +1 -0
  15. package/dist/cli.d.ts +15 -0
  16. package/dist/cli.js +495 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/custom-chart/runtime-shim.js +146 -0
  19. package/dist/custom-chart/template/AGENTS.md +374 -0
  20. package/dist/custom-chart/template/README.md +374 -0
  21. package/dist/custom-chart/template/chart.css +28 -0
  22. package/dist/custom-chart/template/chart.ts +192 -0
  23. package/dist/custom-chart/template/custom-chart.d.ts +78 -0
  24. package/dist/custom-chart/template/icon.svg +1 -0
  25. package/dist/custom-chart/template/manifest.json +23 -0
  26. package/dist/custom-chart/template/package.json +14 -0
  27. package/dist/custom-chart/template/sample-data.json +18 -0
  28. package/dist/custom-chart/template/tsconfig.json +12 -0
  29. package/dist/index.js +11 -7
  30. package/dist/index.js.map +1 -1
  31. package/dist/{main-QHUNQWID.js → main-NGBJJTV6.js} +34 -8
  32. package/dist/main-NGBJJTV6.js.map +1 -0
  33. package/dist/{test-only-B4EVUNFU.js → test-only-TJ3DQUYW.js} +2 -2
  34. package/package.json +6 -3
  35. package/dist/chunk-CX3CS62P.js.map +0 -1
  36. package/dist/chunk-I35U2LBX.js.map +0 -1
  37. package/dist/main-QHUNQWID.js.map +0 -1
  38. /package/dist/{chunk-3JFMWXRO.js.map → chunk-SDFC7PKT.js.map} +0 -0
  39. /package/dist/{test-only-B4EVUNFU.js.map → test-only-TJ3DQUYW.js.map} +0 -0
package/README.md CHANGED
@@ -280,6 +280,27 @@ At least one of `DATABRAIN_SERVICE_TOKEN` or `DATABRAIN_API_TOKEN` must be set.
280
280
 
281
281
  ---
282
282
 
283
+ ## Hosted (HTTPS) deployment
284
+
285
+ `node dist/index.js --http` starts the same server as a hosted Streamable HTTP endpoint at `/mcp`. Callers authenticate with an OAuth bearer token on every request; the server never reads `DATABRAIN_SERVICE_TOKEN` or `DATABRAIN_API_TOKEN` in this mode. Full design notes are in [`specs/hosted-http.md`](specs/hosted-http.md).
286
+
287
+ | Variable | Required | Default | Description |
288
+ |----------|----------|---------|-------------|
289
+ | `DATABRAIN_HOSTED_PUBLIC_URL` | Yes | — | Exact public HTTPS URL of the endpoint, ending in `/mcp` (e.g. `https://mcp.usedatabrain.com/mcp`). Used for discovery metadata and audience checks. |
290
+ | `DATABRAIN_HOSTED_ISSUER` | Yes | — | HTTPS origin of the trusted authorization server. Bearer tokens are verified against `<issuer>/oauth/principal`. |
291
+ | `DATABRAIN_HOSTED_BIND` | No | `127.0.0.1` | Listener address. Set `0.0.0.0` in a container so the load balancer can reach it. |
292
+ | `DATABRAIN_HOSTED_PORT` | No | `3144` | Listener port. |
293
+
294
+ `GET /health` (or `HEAD`) returns `200 {"status":"ok"}` with no authentication. It is the only route that skips the Host check, so an ALB health check with the target IP:port as Host succeeds. Point the target group health check at `/health`.
295
+
296
+ The `Dockerfile` at the repo root builds the hosted server from source: `docker build -t databrain-mcp-hosted .` then run it with the two required variables and `-p 3144:3144`.
297
+
298
+ The load balancer **must forward the original `Host` header unchanged**. Requests to `/mcp` and the `/.well-known/oauth-protected-resource` metadata are answered only when Host matches the host of `DATABRAIN_HOSTED_PUBLIC_URL`; anything else gets `403 invalid_host`.
299
+
300
+ **Current limits.** Without `DATABRAIN_HOSTED_ADAPTER_MODULE` (an absolute path to a trusted delegated adapter, see the spec), the default adapter verifies identity only. `whoami`, tool/resource/prompt discovery and OAuth verification work today; every data tool answers `503 delegated_backend_adapter_required` until that adapter is configured.
301
+
302
+ ---
303
+
283
304
  ## Available Prompts
284
305
 
285
306
  Prompts are guided workflows your AI assistant can follow. Ask for them by name or describe what you want.
@@ -298,7 +319,7 @@ Prompts are guided workflows your AI assistant can follow. Ask for them by name
298
319
 
299
320
  ## Tools Reference
300
321
 
301
- 35 tools organized by category. Your AI assistant picks the right tools automatically — you don't need to call them directly.
322
+ 44 tools organized by category (plus 6 chart App tools on the opt-in `databrain-mcp-chart-apps` entry). Your AI assistant picks the right tools automatically. You do not need to call them directly.
302
323
 
303
324
  > **Note:** Datasource and workspace creation happen outside MCP. Datamarts can be planned with `create_datamart` in `dry_run` mode and created only with `confirm: "APPLY_TO_PRODUCTION"`. The semantic layer is managed via MCP tools (`get_semantic_layer`, `update_semantic_layer`, `start_semantic_layer_generation`, `get_semantic_layer_generation_status`).
304
325
 
@@ -360,6 +381,8 @@ Prompts are guided workflows your AI assistant can follow. Ask for them by name
360
381
  | `update_semantic_layer` | Improve semantic layer quality (add descriptions, synonyms, example questions) |
361
382
  | `start_semantic_layer_generation` | Start semantic-layer generation for a datamart |
362
383
  | `get_semantic_layer_generation_status` | Check semantic-layer generation progress |
384
+
385
+ Semantic-layer generation uses the datamart ID returned by `list_datamarts` and calls the service-token API. Pass `datamartId` directly when the ID is already known.
363
386
  </details>
364
387
 
365
388
  <details>
@@ -388,6 +411,38 @@ Production writes through `create_or_update_metric_from_query` default to `dry_r
388
411
  | `list_scheduled_reports` | List configured scheduled reports for a dashboard/client scope |
389
412
  </details>
390
413
 
414
+ <details>
415
+ <summary><b>Custom chart types</b> (5)</summary>
416
+
417
+ | Tool | Description |
418
+ |------|-------------|
419
+ | `custom_chart_scaffold` | Return the files of a new chart project (manifest, chart.ts, css, icon, sample data, author guide). Nothing is written or persisted |
420
+ | `custom_chart_validate` | Validate a manifest and bundle with the local rules and, when a service token is configured, the server rules. Never persists |
421
+ | `custom_chart_upload` | Dry-run or upload a built bundle as a private chart (version 1) or a preview version; `publish: true` publishes in the same apply |
422
+ | `list_custom_charts` | List the company's custom chart types with status and version ids |
423
+ | `publish_custom_chart` | Dry-run or publish a version so every builder can use chart type `custom:<key>` |
424
+
425
+ `custom_chart_upload` and `publish_custom_chart` default to `dry_run` and apply only with `confirm: "APPLY_TO_PRODUCTION"`. Hosted scopes: `mcp:custom-charts:read` and `mcp:custom-charts:write`. Build the bundle with the CLI below.
426
+ </details>
427
+
428
+ ---
429
+
430
+ ## Custom chart CLI
431
+
432
+ The package also installs a `databrain` binary for chart authors:
433
+
434
+ ```bash
435
+ npx -p @databrainhq/mcp-server databrain custom-chart init my-chart
436
+ cd my-chart # read AGENTS.md, edit chart.ts
437
+ databrain custom-chart validate # manifest, icon, css
438
+ databrain custom-chart build # writes dist/index.js, manifest.json, icon.svg, index.css, bundle.zip
439
+ databrain custom-chart dev # local sandbox page on http://127.0.0.1:4174
440
+ DATABRAIN_SERVICE_TOKEN=... databrain custom-chart upload # build, server validate, plan, confirm, upload
441
+ DATABRAIN_SERVICE_TOKEN=... databrain custom-chart upload --publish --yes
442
+ ```
443
+
444
+ Problems print as `path code message` and the exit code is 1 when there is any. `upload` needs `DATABRAIN_SERVICE_TOKEN` and, on self-hosted instances, `DATABRAIN_API_URL`. The template's `AGENTS.md` is the full author guide: manifest reference, data shape, theme, interactions, troubleshooting.
445
+
391
446
  ---
392
447
 
393
448
  ## Built-in Knowledge
@@ -197,8 +197,8 @@ declare const pageSchema: z.ZodEffects<z.ZodObject<{
197
197
  drillId: z.ZodOptional<z.ZodString>;
198
198
  }, "strict", z.ZodTypeAny, {
199
199
  value: string | number | boolean | null;
200
- label: string;
201
200
  key: string;
201
+ label: string;
202
202
  x: number;
203
203
  y: number;
204
204
  width: number;
@@ -206,8 +206,8 @@ declare const pageSchema: z.ZodEffects<z.ZodObject<{
206
206
  drillId?: string | undefined;
207
207
  }, {
208
208
  value: string | number | boolean | null;
209
- label: string;
210
209
  key: string;
210
+ label: string;
211
211
  x: number;
212
212
  y: number;
213
213
  width: number;
@@ -228,8 +228,8 @@ declare const pageSchema: z.ZodEffects<z.ZodObject<{
228
228
  rows: Record<string, string | number | boolean | null>[];
229
229
  points: {
230
230
  value: string | number | boolean | null;
231
- label: string;
232
231
  key: string;
232
+ label: string;
233
233
  x: number;
234
234
  y: number;
235
235
  width: number;
@@ -251,8 +251,8 @@ declare const pageSchema: z.ZodEffects<z.ZodObject<{
251
251
  rows: Record<string, string | number | boolean | null>[];
252
252
  points: {
253
253
  value: string | number | boolean | null;
254
- label: string;
255
254
  key: string;
255
+ label: string;
256
256
  x: number;
257
257
  y: number;
258
258
  width: number;
@@ -274,8 +274,8 @@ declare const pageSchema: z.ZodEffects<z.ZodObject<{
274
274
  rows: Record<string, string | number | boolean | null>[];
275
275
  points: {
276
276
  value: string | number | boolean | null;
277
- label: string;
278
277
  key: string;
278
+ label: string;
279
279
  x: number;
280
280
  y: number;
281
281
  width: number;
@@ -297,8 +297,8 @@ declare const pageSchema: z.ZodEffects<z.ZodObject<{
297
297
  rows: Record<string, string | number | boolean | null>[];
298
298
  points: {
299
299
  value: string | number | boolean | null;
300
- label: string;
301
300
  key: string;
301
+ label: string;
302
302
  x: number;
303
303
  y: number;
304
304
  width: number;
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createChartAppServer
4
- } from "../chunk-NZGQJBJV.js";
5
- import "../chunk-I35U2LBX.js";
4
+ } from "../chunk-7G5QBEPC.js";
5
+ import "../chunk-Z2SF3K6P.js";
6
+ import "../chunk-SKU37UVJ.js";
6
7
  export {
7
8
  createChartAppServer
8
9
  };
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadConfig
4
- } from "../chunk-3JFMWXRO.js";
4
+ } from "../chunk-SDFC7PKT.js";
5
5
  import {
6
6
  createChartAppServer
7
- } from "../chunk-NZGQJBJV.js";
8
- import "../chunk-I35U2LBX.js";
7
+ } from "../chunk-7G5QBEPC.js";
8
+ import "../chunk-Z2SF3K6P.js";
9
+ import "../chunk-SKU37UVJ.js";
9
10
 
10
11
  // src/apps/stdio.ts
11
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/apps/stdio.ts"],"sourcesContent":["import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { loadConfig } from '../utils/config.js';\nimport { createChartAppServer } from './server.js';\n\n// Separate opt-in command until the owners integrate both shared entrypoints.\nconst server = createChartAppServer(loadConfig());\nawait server.connect(new StdioServerTransport());\n"],"mappings":";;;;;;;;;;AAAA,SAAS,4BAA4B;AAKrC,IAAM,SAAS,qBAAqB,WAAW,CAAC;AAChD,MAAM,OAAO,QAAQ,IAAI,qBAAqB,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/apps/stdio.ts"],"sourcesContent":["import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { loadConfig } from '../utils/config.js';\nimport { createChartAppServer } from './server.js';\n\n// Separate opt-in command until the owners integrate both shared entrypoints.\nconst server = createChartAppServer(loadConfig());\nawait server.connect(new StdioServerTransport());\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,4BAA4B;AAKrC,IAAM,SAAS,qBAAqB,WAAW,CAAC;AAChD,MAAM,OAAO,QAAQ,IAAI,qBAAqB,CAAC;","names":[]}
@@ -7,7 +7,9 @@ var HOSTED_SCOPES = [
7
7
  "mcp:datamarts:read",
8
8
  "mcp:datamarts:write",
9
9
  "mcp:embeds:write",
10
- "mcp:query"
10
+ "mcp:query",
11
+ "mcp:custom-charts:read",
12
+ "mcp:custom-charts:write"
11
13
  ];
12
14
  var HostedAuthError = class extends Error {
13
15
  constructor(status, code) {
@@ -99,4 +101,4 @@ export {
99
101
  FilePatStore,
100
102
  createBearerVerifier
101
103
  };
102
- //# sourceMappingURL=chunk-CX3CS62P.js.map
104
+ //# sourceMappingURL=chunk-3GF7WSKK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hosted/types.ts","../src/hosted/auth.ts"],"sourcesContent":["import type { RequestOptions } from '../client/types.js';\n\nexport const HOSTED_SCOPES = [\n 'mcp:dashboards:read', 'mcp:dashboards:write',\n 'mcp:datamarts:read', 'mcp:datamarts:write', 'mcp:embeds:write', 'mcp:query',\n 'mcp:custom-charts:read', 'mcp:custom-charts:write',\n] as const;\nexport type HostedScope = typeof HOSTED_SCOPES[number];\n\n/** Only the trusted auth adapter may produce this context. Never populate it from MCP input. */\nexport interface HostedPrincipal {\n actorKind: 'user';\n credentialType: 'oauth' | 'pat';\n userId: string;\n companyId: string;\n workspaceId: string;\n workspaceName?: string;\n /** OAuth application ID, NOT the tenant identifier called clientId by legacy tools. */\n clientId: string;\n resource: string;\n scopes: HostedScope[];\n /** Required for stored PATs. OAuth may delegate expiry to fresh provider validation. */\n expiresAt?: number;\n /** Explicit tenant allow-list resolved by the backend; absent means no tenant access. */\n tenantIds?: string[];\n}\n\nexport interface PatRecord {\n hash: string;\n revokedAt: number | null;\n principal: HostedPrincipal;\n}\n\nexport interface PatStore {\n findByHash(hash: string): Promise<PatRecord | undefined>;\n}\n\n/** Production integration must recheck membership, policy and revocation on every call. */\nexport interface HostedAdapter {\n verifyOAuth(token: string, resource: string): Promise<HostedPrincipal>;\n resolvePatGrant(principal: HostedPrincipal): Promise<HostedPrincipal>;\n /**\n * Execute as this user/workspace with backend resource and tenant checks and auditing.\n * Receives NO incoming bearer or server service credential. Credential exchange, if\n * needed, belongs in the trusted adapter. Do not impersonate a static service token.\n * Return the raw API result: the shared client performs its usual data unwrapping.\n */\n executeRequest(options: RequestOptions, principal: HostedPrincipal): Promise<unknown>;\n}\n\nexport interface HostedConfig {\n publicUrl: string;\n issuer: string;\n port: number;\n bind: string;\n testOnly: boolean;\n}\n\nexport class HostedAuthError extends Error {\n constructor(public readonly status: 401 | 403 | 503, public readonly code: string) {\n super(code);\n }\n}\n","import { createHash, timingSafeEqual } from 'node:crypto';\nimport { open } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { z } from 'zod';\nimport { HOSTED_SCOPES, HostedAuthError } from './types.js';\nimport type { HostedAdapter, HostedPrincipal, PatRecord, PatStore } from './types.js';\n\nconst identity = z.string().min(1).max(512).regex(/^[^\\x00-\\x1f\\x7f]+$/);\nexport const principalSchema = z.object({\n actorKind: z.literal('user'), credentialType: z.enum(['oauth', 'pat']),\n userId: identity, companyId: identity, workspaceId: identity, workspaceName: identity.optional(),\n clientId: identity, resource: z.string().url(), scopes: z.array(z.enum(HOSTED_SCOPES)),\n expiresAt: z.number().finite().positive().optional(), tenantIds: z.array(identity).optional(),\n}).strict();\nconst recordSchema = z.object({\n hash: z.string().regex(/^[a-f0-9]{64}$/), revokedAt: z.number().finite().nullable(),\n principal: principalSchema,\n}).strict();\n\nexport function hashCredential(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/** Reread on every request: an atomic file replacement revokes immediately, without restart. */\nexport class FilePatStore implements PatStore {\n constructor(private readonly path: string) {}\n async findByHash(hash: string): Promise<PatRecord | undefined> {\n const handle = await open(this.path, constants.O_RDONLY | constants.O_NOFOLLOW);\n try {\n const stat = await handle.stat();\n if (!stat.isFile() || (stat.mode & 0o077) !== 0 || stat.size > 1_048_576 ||\n (process.getuid && stat.uid !== process.getuid())) throw new Error('Invalid credential store');\n const records = z.array(recordSchema).max(1000).parse(JSON.parse(await handle.readFile('utf8')));\n const matches = records.filter(record => timingSafeEqual(Buffer.from(record.hash, 'hex'), Buffer.from(hash, 'hex')));\n // Duplicate records are ambiguous, including an active/revoked pair.\n if (matches.length > 1) throw new Error('Ambiguous credential store');\n return matches[0];\n } finally { await handle.close(); }\n }\n}\n\nfunction validatePrincipal(value: unknown, resource: string, kind: 'oauth' | 'pat'): HostedPrincipal {\n const parsed = principalSchema.safeParse(value);\n if (!parsed.success || parsed.data.resource !== resource || parsed.data.credentialType !== kind ||\n (parsed.data.expiresAt !== undefined && parsed.data.expiresAt <= Date.now() / 1000) ||\n (kind === 'pat' && parsed.success && parsed.data.expiresAt === undefined)) throw new HostedAuthError(401, 'invalid_token');\n return parsed.data;\n}\n\nexport function createBearerVerifier(adapter: HostedAdapter, resource: string, pats?: PatStore) {\n return async (token: string): Promise<HostedPrincipal> => {\n try {\n if (!/^[A-Za-z0-9._~+\\/-]+=*$/.test(token) || token.length > 8192) throw new HostedAuthError(401, 'invalid_token');\n if (!token.startsWith('dbp_')) {\n return validatePrincipal(await adapter.verifyOAuth(token, resource), resource, 'oauth');\n }\n if (!/^dbp_[A-Za-z0-9_-]{32,}$/.test(token)) throw new HostedAuthError(401, 'invalid_token');\n const record = await pats?.findByHash(hashCredential(token));\n if (!record || record.revokedAt !== null || record.hash !== hashCredential(token)) throw new HostedAuthError(401, 'invalid_token');\n const stored = validatePrincipal(record.principal, resource, 'pat');\n const resolved = validatePrincipal(await adapter.resolvePatGrant(structuredClone(stored)), resource, 'pat');\n // Membership resolution may narrow a stored grant, never move or expand it.\n for (const key of ['userId', 'companyId', 'workspaceId', 'clientId', 'resource'] as const) {\n if (resolved[key] !== stored[key]) throw new HostedAuthError(401, 'invalid_token');\n }\n if (resolved.expiresAt! > stored.expiresAt! || resolved.scopes.some(scope => !stored.scopes.includes(scope)) ||\n resolved.tenantIds?.some(id => !stored.tenantIds?.includes(id))) throw new HostedAuthError(401, 'invalid_token');\n return resolved;\n } catch (error) {\n if (error instanceof HostedAuthError) throw error;\n // No raw verifier, filesystem or remote errors can disclose credential values.\n throw new HostedAuthError(401, 'invalid_token');\n }\n };\n}\n"],"mappings":"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","names":[]}
@@ -3,10 +3,7 @@ import {
3
3
  APPLY_TO_PRODUCTION_CONFIRMATION,
4
4
  APP_MIME,
5
5
  CHART_URI,
6
- DatabrainApiError,
7
- DatabrainClient,
8
6
  KPI_URI,
9
- MCP_SERVER_VERSION,
10
7
  READ_ONLY,
11
8
  RENDER_TYPES,
12
9
  WRITE_CREATE,
@@ -23,10 +20,15 @@ import {
23
20
  registerAllTools,
24
21
  rowsSchema,
25
22
  saveDraftAsDashboard,
26
- toolContext,
27
23
  viewSchema,
28
24
  workspacesForDatamart
29
- } from "./chunk-I35U2LBX.js";
25
+ } from "./chunk-Z2SF3K6P.js";
26
+ import {
27
+ DatabrainApiError,
28
+ DatabrainClient,
29
+ MCP_SERVER_VERSION,
30
+ toolContext
31
+ } from "./chunk-SKU37UVJ.js";
30
32
 
31
33
  // src/apps/server.ts
32
34
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -511,4 +513,4 @@ function createChartAppServer(config, backend) {
511
513
  export {
512
514
  createChartAppServer
513
515
  };
514
- //# sourceMappingURL=chunk-NZGQJBJV.js.map
516
+ //# sourceMappingURL=chunk-7G5QBEPC.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/apps/server.ts","../src/apps/registry.ts","../src/apps/backend.ts","../src/apps/tile.ts"],"sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { DatabrainClient } from '../client/databrain-client.js';\nimport type { Config } from '../utils/config.js';\nimport { registerAllTools } from '../tools/registry.js';\nimport { registerAllResources } from '../resources/registry.js';\nimport { registerAllPrompts } from '../prompts/registry.js';\nimport { MCP_SERVER_VERSION } from '../version.js';\nimport { enableChartApps } from './feature.js';\nimport { registerChartApps } from './registry.js';\nimport type { ChartBackend } from './backend.js';\n\n/** Reviewable composition seam; hosted owners must retain their principal-bound client/policy. */\nexport function createChartAppServer(config: Config, backend?: ChartBackend): McpServer {\n const client = new DatabrainClient(config);\n const server = new McpServer({ name: 'databrain-chart-apps', version: MCP_SERVER_VERSION }, {\n instructions: 'Use read-only chart previews to inspect analytics. Chart interactions inherit guest scope. ' +\n 'Never request or expose credentials. Mutation tools retain their existing dry-run and confirmation requirements.',\n });\n enableChartApps(server);\n registerAllTools(server, client);\n registerAllResources(server);\n registerAllPrompts(server);\n registerChartApps(server, client, backend);\n return server;\n}\n","import { z } from 'zod';\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { DatabrainClient } from '../client/databrain-client.js';\nimport { DatabrainApiError } from '../utils/errors.js';\nimport { READ_ONLY } from '../tools/annotations.js';\nimport { toolContext } from '../utils/tool-context.js';\nimport { createChartBackend, type ChartBackend, type ChartAction } from './backend.js';\nimport { APP_MIME, CHART_URI, KPI_URI, pageSchema, referenceSchema, viewSchema } from './contract.js';\nimport { appMeta, chartResult } from './feature.js';\nimport { tileHtml } from './tile.js';\nimport { draftSchema, draftPage, RENDER_TYPES, saveDraftAsDashboard, workspacesForDatamart } from './preview.js';\nimport { rowsSchema } from './contract.js';\nimport { WRITE_CREATE } from '../tools/annotations.js';\nimport { APPLY_TO_PRODUCTION_CONFIRMATION } from '../tools/widgets/production-guards.js';\n\nconst actions: Record<ChartAction, string> = {\n render_chart: 'Render one authorized chart page as SVG with point metadata. Cursor pagination stays within the same guest scope.',\n refresh_widget: 'Refresh a chart in the current guest scope. Revalidates access before returning data.',\n drill_down: 'Explore a server-issued drill identifier within the current chart and guest scope.',\n change_date_range: 'Render a chart for an ordered date range within the current guest scope.',\n};\n\n/** Call after registerAllTools so existing registration policy/instrumentation remains active. */\nexport function registerChartApps(server: McpServer, client: DatabrainClient, backend: ChartBackend = createChartBackend(client)): void {\n for (const [name, description] of Object.entries(actions)) {\n const action = name as ChartAction;\n const tool = server.tool(name, description, {\n reference: referenceSchema, view: viewSchema.default({}),\n context: z.string().max(200).optional().describe('Chart interaction context'),\n }, READ_ONLY, async ({ reference, view }) => {\n try {\n if ((action === 'drill_down' && !view.drillId) || (action === 'change_date_range' && !view.dateRange)) throw new Error('Missing view selector');\n const page = pageSchema.parse(await toolContext.run({ tool: name, category: 'embedding', workflowIntent: 'chart_app', inputMode: 'app' },\n () => backend.execute(action, reference, view)));\n // SVG is rendered as an isolated image, never inserted into the HTML DOM.\n // Reject active/external/animated constructs as a further contract check.\n if (page.svg && (!/^\\s*<svg\\b/i.test(page.svg) ||\n /<\\s*(?:script|foreignObject|iframe|image|animate\\w*|set)\\b|<!DOCTYPE|<!ENTITY|\\bon\\w+\\s*=|@(?:import|keyframes)|\\banimation\\s*:|(?:href\\s*=\\s*[\"'](?!#)|url\\s*\\(\\s*[\"']?(?!#))/i.test(page.svg))) {\n throw new Error('Unsafe SVG');\n }\n return chartResult(`${page.kind === 'kpi' ? 'KPI' : 'Chart'} ready. ${page.rows.length} rows in this page.`, {\n ...page, reference, view,\n });\n } catch {\n return { isError: true, content: [{ type: 'text' as const,\n text: 'Chart unavailable. Check dashboard access and the chart renderer integration, then retry.' }] };\n }\n });\n tool.update({ _meta: appMeta(['app']) });\n }\n registerDraftTools(server, client);\n for (const [uri, kind] of [[CHART_URI, 'chart'], [KPI_URI, 'kpi']] as const) {\n server.resource(`Databrain ${kind} tile`, uri, { mimeType: APP_MIME, description: `Interactive ${kind} using host theme and scoped server tools` }, async () => ({\n contents: [{ uri, mimeType: APP_MIME, text: tileHtml(kind), _meta: { ui: { prefersBorder: true } } }],\n }));\n }\n}\n\n/** The tile's controls over an unsaved answer. `preview_chart` is the \"show as\" row and\n * is app-only: the model already has ask_question. `save_chart_as_dashboard` is the commit,\n * visible to both, and gated the way every production write here is gated. */\nfunction registerDraftTools(server: McpServer, client: DatabrainClient): void {\n const preview = server.tool('preview_chart',\n 'Redraw an unsaved ask_question answer as another chart type, from the rows already on screen. No query runs.',\n { draft: draftSchema, rows: rowsSchema, chartType: z.enum(RENDER_TYPES),\n context: z.string().max(200).optional().describe('Chart interaction context') },\n READ_ONLY, async ({ draft, rows, chartType }) => {\n try {\n const page = await draftPage(client, draft, rows, chartType);\n return chartResult(`${page.kind === 'table' ? 'Table' : 'Chart'} ready as ${chartType}.`, page);\n } catch {\n return { isError: true, content: [{ type: 'text' as const, text: 'Chart preview unavailable. Ask the question again to retry.' }] };\n }\n });\n preview.update({ _meta: appMeta(['app']) });\n\n const save = server.tool('save_chart_as_dashboard',\n 'Save an ask_question answer: creates a workspace metric from its SQL and a new dashboard holding it. ' +\n `Production write; requires confirm: \"${APPLY_TO_PRODUCTION_CONFIRMATION}\". ` +\n 'A workspaceName is needed when the datamart backs more than one workspace.',\n { draft: draftSchema, chartType: z.enum(RENDER_TYPES),\n dashboardTitle: z.string().trim().min(1).max(200),\n workspaceName: z.string().trim().min(1).max(256).optional(),\n attemptId: z.string().trim().min(1).max(64).optional()\n .describe('Identifies one save attempt. Reuse it to retry safely; omit or change it to save again on purpose.'),\n confirm: z.string().optional(),\n context: z.string().max(200).optional().describe('Chart interaction context') },\n WRITE_CREATE, async ({ draft, chartType, dashboardTitle, workspaceName, attemptId, confirm }) => {\n if (confirm !== APPLY_TO_PRODUCTION_CONFIRMATION) {\n return { isError: true, content: [{ type: 'text' as const,\n text: `Saving writes to production. Retry with confirm: \"${APPLY_TO_PRODUCTION_CONFIRMATION}\".` }] };\n }\n // The draft's workspace list is a hint for the tile. The caller wrote it, so the\n // set a save may target is recomputed here from the datamart, on the server's own credential.\n const choices = await workspacesForDatamart(client, draft.datamartName, 'authorize');\n const refuse = (text: string) => ({ isError: true, content: [{ type: 'text' as const, text }],\n structuredContent: { kind: 'save', ok: false, choices } });\n // A named workspace must be one the draft's datamart can serve, spelled the draft's way.\n const target = workspaceName\n ? choices.find(w => w.trim().toLowerCase() === workspaceName.toLowerCase())\n : choices.length === 1 ? choices[0] : undefined;\n if (workspaceName && !target) {\n return refuse(`Workspace \"${workspaceName}\" cannot run this datamart's SQL. Choose one of: ${choices.join(', ')}.`);\n }\n if (!target) {\n return refuse(choices.length\n ? `Choose a workspace: ${choices.join(', ')}.`\n : `No workspace is built on datamart \"${draft.datamartName}\". Create one in Databrain, then save again.`);\n }\n try {\n const saved = await saveDraftAsDashboard(client, draft, { chartType, dashboardTitle, workspaceName: target, attemptId });\n return chartResult(`Saved \"${saved.title}\" to workspace ${saved.workspaceName} (dashboard ${saved.dashboardId}, metric ${saved.metricId}).`,\n { kind: 'save', ok: true, ...saved, draft: { ...draft, chartType } });\n } catch (error) {\n const code = error instanceof DatabrainApiError ? error.code : 'SAVE_FAILED';\n return { isError: true, content: [{ type: 'text' as const,\n text: `Could not save (${code}). Check the workspace name and your access, then try again.` }],\n structuredContent: { kind: 'save', ok: false, code } };\n }\n });\n save.update({ _meta: appMeta(['model', 'app']) });\n}\n","import { DatabrainClient } from '../client/databrain-client.js';\nimport type { RequestOptions } from '../client/types.js';\nimport { createServiceBundle } from '../services/factory.js';\nimport { pageSchema, referenceSchema, viewSchema, type ChartPage, type ChartReference, type ChartView } from './contract.js';\n\nexport const RENDER_PATH = '/api/v2/dashboard/mcp-chart';\nexport type ChartAction = 'render_chart' | 'refresh_widget' | 'drill_down' | 'change_date_range';\nexport interface ChartBackend {\n execute(action: ChartAction, reference: ChartReference, view: ChartView): Promise<ChartPage>;\n}\n\nclass UncachedChartClient extends DatabrainClient {\n constructor(private readonly delegate: DatabrainClient) {\n super({ apiUrl: delegate.getApiUrl(), demoDomain: delegate.getDemoDomain() });\n }\n override request<T>(options: RequestOptions): Promise<T> {\n return this.delegate.request<T>({ ...options, cacheKey: undefined, cacheTtlMs: undefined });\n }\n}\n\n/** Proposed backend route; no local renderer, ambient service fallback, or UI credentials. */\nexport function createChartBackend(client: DatabrainClient): ChartBackend {\n // The general embed lookup caches for five minutes. Authorization must not.\n const services = createServiceBundle(new UncachedChartClient(client));\n return {\n async execute(action, rawReference, rawView) {\n const reference = referenceSchema.parse(rawReference);\n const view = viewSchema.parse(rawView);\n // Every page/refresh/drill re-reads the embed live, so a revoked tenant stops working at once.\n const app = await services.widgetService.resolveDataAppName(undefined, reference.embedId);\n if (!app.ok) throw new Error('Chart scope unavailable');\n const scope = await services.widgetService.ensureReadableDashboardScope({\n embedId: reference.embedId, clientId: reference.clientId, dataAppName: app.value });\n if (!scope.ok) throw new Error('Chart scope denied');\n const { token } = await services.guestTokenService.generateToken({\n clientId: reference.clientId, dataAppName: app.value, allowedEmbeds: [reference.embedId],\n permissions: { isDisableMetricCreation: true, isEnableManageMetrics: false,\n isEnableMetricUpdation: false, isEnableArchiveMetrics: false },\n });\n if (!token) throw new Error('Guest scope unavailable');\n const page = await client.request<unknown>({\n method: 'POST', path: RENDER_PATH, auth: 'none', token,\n body: { action, embedId: reference.embedId, widgetId: reference.widgetId, ...view,\n pageSize: 50, width: 600, height: 320, animation: false },\n skipDataUnwrap: true,\n });\n return pageSchema.parse(page);\n },\n };\n}\n","/** Deliberately no imports in the iframe. All requests go through the host bridge. */\nexport const TILE_SCRIPT = String.raw`(()=>{\nconst $=id=>document.getElementById(id),root=document.documentElement,pending=new Map();\nlet seq=0,origin='*',ready=false,closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null,dialect=null,shown=false,draft=null,rows=[],saved=null,attempt='';\n/* One lifecycle, two sets of names. Hosts ship either or both, so notifications go out\n under both and arrive under either. Same order in both rows: result, input, context. */\nconst DASH=['ui/notifications/tool-result','ui/notifications/tool-input','ui/notifications/host-context-changed'],\nSNAKE=['notifications/tools/call/result','notifications/tools/call/input','notifications/host_context/changed'],\nINIT=['ui/notifications/initialized','notifications/initialized'],\nSIZE=['ui/notifications/size-changed','notifications/size_changed'],\nDISPLAY=['ui/request-display-mode','requestDisplayMode'];\nfunction send(message){if(!closed)parent.postMessage({jsonrpc:'2.0',...message},origin)}\nfunction notify(names,params){for(const method of names)send({method,params})}\nfunction rpc(method,params){return new Promise((resolve,reject)=>{const id=++seq;\nconst timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'))},65000);\npending.set(id,{resolve,reject,timer});send({id,method,params})})}\n/* Try each alias while the host says it never heard of it; any other error stands. */\nasync function ask(names,params){let last;\nfor(const method of names){try{return await rpc(method,params)}catch(error){last=error;if(error.code!==-32601)throw error}}\nthrow last}\n/* Fixed code only. The tile sits inside customer rows; a log is no place to leak them. */\nfunction trouble(code){send({method:'notifications/error',params:{code}})}\n/* One formatter for rail, table, KPI and answer line. Compact past 10K, grouped past 100,\n two decimals below; null is an absence, so it prints as a dash and never as 0. */\nfunction fmt(v){if(typeof v!=='number')return v==null||v===''?'\\u2014':String(v);const a=Math.abs(v);\nreturn new Intl.NumberFormat(navigator.language||'en',a>=1e4?{notation:'compact',maximumFractionDigits:1}:a>=100?{maximumFractionDigits:0}:{maximumFractionDigits:2}).format(v)}\nfunction exact(el,v){el.title=typeof v==='number'?String(v):''}\n/* The answer line: what this shows, and the headline value. Gated on carrying a number or\n a count, so it never renders as a second title or an empty reserved slot. */\nfunction answer(p){if(p.description)return p.description;const bits=[];\nif(p.kind==='dashboard'){const n=p.widgets?.length||0;return n?n+(n===1?' chart':' charts')+' on this dashboard':''}\nconst [dim,measure]=p.columns||[];const pts=(p.points||[]).filter(q=>typeof q.value==='number');\nif(pts.length<2)return '';if(dim&&measure&&!p.draft)bits.push(measure+' by '+dim);\nconst top=pts.reduce((a,b)=>b.value>a.value?b:a);bits.push(top.label.split(' \\u2014 ')[0]+' highest at '+fmt(top.value));return bits.join(' \\u00b7 ')}\nfunction status(text,bad,ok){const s=$('status');s.textContent=text;s.toggleAttribute('data-error',!!bad);s.toggleAttribute('data-ok',!!ok);if(bad)$('plot').dataset.state='off'}\nfunction paint(){const img=$('chart-image');if(!img||!rawSvg)return;\nconst c=getComputedStyle(document.body).color,mix=pc=>'color-mix(in srgb,'+c+' '+pc+'%,transparent)';\nimg.src='data:image/svg+xml,'+encodeURIComponent(rawSvg.replace(/<\\/svg>\\s*$/i,'<style>text{fill:'+mix(72)+'!important}'\n+'[stroke=\"#dbdee4\"],[stroke=\"#f1f1f2\" i]{stroke:'+mix(13)+'!important}[stroke=\"#54555a\"],[stroke=\"#d4d4d8\" i]{stroke:'+mix(30)+'!important}'\n+'</style></svg>'))}\nfunction controls(){document.querySelectorAll('button,input,select').forEach(e=>{e.disabled=busy});\n$('refresh').hidden=!ref;$('next').hidden=!next;$('range').hidden=!ref;$('plot').toggleAttribute('data-busy',busy);\n$('save').hidden=!draft||!!saved||!$('form').hidden;\n$('expand').hidden=!shown||root.dataset.kind==='dashboard'||!context.availableDisplayModes?.includes('fullscreen');\nroot.toggleAttribute('data-deep',context.displayMode==='fullscreen'||$('expand').hidden)}\nfunction host(change){context={...context,...change};if(change.theme==='dark'||change.theme==='light')root.style.colorScheme=change.theme;\nconst vars=change.styles?.variables||{};for(const [key,value] of Object.entries(vars)){\nif(/^--(?:color-|font-|border-|shadow-)/.test(key)&&typeof value==='string'&&!/url\\s*\\(|@import/i.test(value))root.style.setProperty(key,value)}\nconst face=change.styles?.css?.fonts;if(typeof face==='string'&&face.length<4000&&!/@import|javascript:/i.test(face))$('hostfont').textContent=face;\nconst insets=change.safeAreaInsets;if(insets)for(const edge of ['top','right','bottom','left']){\nconst n=insets[edge];if(Number.isFinite(n))root.style.setProperty('--safe-'+edge,Math.max(0,Math.min(n,200))+'px')}\nroot.dataset.mode=context.displayMode==='fullscreen'?'full':'inline';\n$('expand').textContent=context.displayMode==='fullscreen'?'Back to conversation':'Expand';paint();controls()}\nfunction scope(p,n){const bits=[];\nif(p.kind==='chart'||p.kind==='table')bits.push(p.rowsTotal>n?'first '+n+' of '+p.rowsTotal+' rows':n?(n===1?'1 row':n+' rows'):'No rows in this view');\nif(view.dateRange)bits.push(view.dateRange.from+' to '+view.dateRange.to);\nif(next)bits.push('more pages available');\n$('scope').textContent=bits.join(' · ')}\nfunction resize(){if(ready)notify(SIZE,{width:document.body.scrollWidth,height:document.body.scrollHeight})}\nfunction table(rows,columns){$('rows').replaceChildren();$('data').hidden=!rows?.length;if(!rows?.length)return;\n$('count').textContent=rows.length===1?'1 row behind this':rows.length+' rows behind this';\nconst t=document.createElement('table');\nconst num=name=>rows.some(r=>typeof r[name]==='number');\nconst header=document.createElement('tr');for(const name of columns){const th=document.createElement('th');th.scope='col';th.textContent=name;if(num(name))th.className='n';header.append(th)}\nconst head=document.createElement('thead');head.append(header);t.append(head);const body=document.createElement('tbody');\nfor(const row of rows.slice(0,50)){const tr=document.createElement('tr');for(const key of columns){const td=document.createElement('td');td.textContent=fmt(row[key]);exact(td,row[key]);if(num(key))td.className='n';tr.append(td)}body.append(tr)}t.append(body);$('rows').append(t)}\nasync function display(mode){try{const answer=await ask(DISPLAY,{mode});\nhost({displayMode:answer.mode||context.displayMode});if(root.dataset.mode==='full'&&!$('data').hidden)$('data').open=true;resize()}\ncatch{status('Expand is unavailable in this host.',1)}}\nfunction wait(){busy=true;if(!$('plot').firstChild)$('plot').dataset.state='wait';status('Loading chart…');controls()}\nasync function call(name,extra={}){if(!ready||!ref||busy)return;run(name,{reference:ref,view:{...view,...extra}})}\n/* The draft tools take the draft and the rows on screen instead of a reference. */\nasync function run(name,args){if(!ready||busy)return;wait();const rev=++revision;\ntry{const result=await rpc('tools/call',{name,arguments:{...args,context:'tile'}});\nif(rev===revision)render(result)}catch{if(rev===revision){trouble('tool-call-failed');if(name==='save_chart_as_dashboard'){$('plot').dataset.state='ready';status('Could not save. Try again.',1)}else if(name==='preview_chart'){status('Could not draw that. Pick another type.',1);$('plot').dataset.state='ready'}else stale()}}finally{busy=false;controls()}}\nfunction stale(){if(shown&&$('plot').firstChild){status('Could not refresh. Showing the last chart that loaded. Select Refresh to try again.',1);$('plot').dataset.state='stale'}else status('Could not load the chart. Select Refresh to try again.',1)}\nfunction render(result){busy=false;controls();\nconst sp=result?.structuredContent;if(sp&&sp.kind==='save'){$('plot').dataset.state='ready';if(sp.ok){saved=sp;$('form').hidden=true;status('Saved \"'+sp.title+'\" to workspace '+sp.workspaceName+'.',0,1)}\nelse if(sp.choices?.length){choose(sp.choices);status('Choose a workspace, then save again.',1)}\nelse status((result.content?.[0]?.text)||'Could not save. Try again.',1);controls();resize();return}\nif(result?.isError&&draft&&$('form').hidden===false){$('plot').dataset.state='ready';status((result.content?.[0]?.text)||'Could not save. Try again.',1);controls();return}\nif(result?.isError){trouble('tool-returned-error');\nif(shown&&$('plot').firstChild){stale();controls();return}shown=false;controls();\nstatus(ref?'Chart unavailable. Check your access to this dashboard, then Refresh.'\n:'Chart unavailable. Ask again to retry.',1);return}\nconst p=result?.structuredContent;if(!p||typeof p!=='object'){shown=false;controls();status('No chart data came back for this result.',1);return}\nrevision++;shown=true;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();delete $('rail').dataset.value;\nref=p.reference||null;view=p.view||{};next=p.nextCursor||null;draft=p.draft||null;rows=p.rows||[];saved=null;$('form').hidden=true;\n$('title').textContent=p.title||'Analytics';$('description').textContent=answer(p);root.dataset.kind=p.kind;$('choices').toggleAttribute('data-grid',p.kind==='dashboard');\nif(p.kind==='dashboard'){$('plot').dataset.state='off';\nfor(const item of p.widgets||[]){const b=document.createElement('button');b.textContent=item.name||'Untitled widget';b.onclick=()=>{ref=item.reference;view={};call('render_chart')};$('choices').append(b)}\nstatus(p.widgets?.length?'':'No charts on this dashboard yet. Add one in Databrain.');\n}else if(p.kind==='kpi'){\nconst number=document.createElement('p');number.className='kpi';number.textContent=fmt(p.value);exact(number,p.value);$('plot').append(number);status('');\n}else if(p.svg){\nconst box=document.createElement('div');box.className='chart';box.style.aspectRatio=p.width+'/'+p.height;\nconst img=document.createElement('img');img.id='chart-image';rawSvg=p.svg;img.alt=p.title||'Chart';img.onerror=()=>{status('The chart could not be drawn. The rows are below.',1);$('data').open=$('data').toggleAttribute('data-only',true)};box.append(img);\nfor(const point of p.points||[]){const b=document.createElement('button');b.className='point';b.setAttribute('aria-label',point.label+': '+fmt(point.value));b.setAttribute('aria-pressed','false');\nb.style.left=100*point.x/p.width+'%';b.style.top=100*point.y/p.height+'%';b.style.width=100*point.width/p.width+'%';b.style.height=100*point.height/p.height+'%';\nconst hover=()=>{$('vlabel').textContent=point.label;$('vnum').textContent=fmt(point.value);exact($('vnum'),point.value);$('rail').dataset.value='1'};b.onmouseenter=hover;b.onfocus=hover;\nb.onclick=()=>{box.querySelectorAll('.point').forEach(e=>e.setAttribute('aria-pressed','false'));b.setAttribute('aria-pressed','true');hover();\n$('choices').replaceChildren();if(point.drillId){const drill=document.createElement('button');drill.className='primary';drill.textContent='Explore selected data';drill.onclick=()=>call('drill_down',{drillId:point.drillId,cursor:undefined});$('choices').append(drill)}};box.append(b)}$('plot').append(box);\nstatus('');\n}else if(p.kind==='table'){\n// ask_question returns a table by design; calling that a missing chart called success a failure.\n$('plot').dataset.state='off';status('');\n}else{$('plot').dataset.state='off';\nstatus('Nothing to show. Try a wider date range, then Refresh.',1)}\nif(draft&&p.previewTypes?.length){for(const t of p.previewTypes){const b=document.createElement('button');b.textContent=t[0].toUpperCase()+t.slice(1);b.className='chip';b.setAttribute('aria-pressed',String(t===draft.chartType));\nb.onclick=()=>run('preview_chart',{draft,rows,chartType:t});$('choices').append(b)}$('choices').dataset.chips='1'}else delete $('choices').dataset.chips;\nif(p.previewError)status('Cannot draw this as '+(p.draft?.chartType||'a chart')+' ('+p.previewError+'). Showing the rows.',1);\npaint();table(p.rows||[],p.columns||Object.keys(p.rows?.[0]||{}));scope(p,p.rows?.length||0);\nconst only=!p.svg&&p.value===undefined&&p.kind!=='dashboard';$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}\n$('refresh').onclick=()=>call('refresh_widget',{cursor:undefined});$('next').onclick=()=>call('render_chart',{cursor:next});\nfunction choose(names){const w=$('ws');w.replaceChildren();for(const n of names){const o=document.createElement('option');o.value=n;o.textContent=n;w.append(o)}$('wsrow').hidden=names.length<2;$('wsone').textContent=names.length===1?'in workspace '+names[0]:'';$('form').hidden=false;$('dtitle').focus();controls();resize()}\n$('save').onclick=()=>{attempt=crypto.randomUUID();$('dtitle').value=draft?.title||'';choose(draft?.workspaces||[])};\n$('cancel').onclick=()=>{$('form').hidden=true;status('');controls();resize()};\n$('commit').onclick=()=>{const t=$('dtitle').value.trim();if(!t){status('Name the dashboard.',1);return}\nrun('save_chart_as_dashboard',{draft,chartType:draft.chartType,dashboardTitle:t,workspaceName:$('wsrow').hidden?undefined:$('ws').value,attemptId:attempt,confirm:'APPLY_TO_PRODUCTION'})};\n$('expand').onclick=()=>display(context.displayMode==='fullscreen'?'inline':'fullscreen');\n$('dates').onclick=()=>{const from=$('from').value,to=$('to').value;if(!from||!to||from>to){status('Start date must not be after the end date.',1);return}call('change_date_range',{dateRange:{from,to},cursor:undefined,drillId:undefined})};\nfunction receive(event){if(event.source!==parent||closed)return;const m=event.data;\nif(!m||Array.isArray(m)||m.jsonrpc!=='2.0'||(origin!=='*'&&event.origin!==origin))return;\nif(!m.method&&pending.has(m.id)&&('result'in m||'error'in m)){if(origin==='*'&&event.origin!=='null')origin=event.origin;\nconst p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);\nif(m.error){const error=Error('host error');error.code=m.error.code;p.reject(error)}else p.resolve(m.result);return}\nif(m.method==='ping'&&m.id!==undefined){send({id:m.id,result:{}});return}\nif(m.method==='ui/resource-teardown'){send({id:m.id,result:{}});closed=true;revision++;for(const p of pending.values()){clearTimeout(p.timer);p.reject(Error('closed'))}pending.clear();observer.disconnect();removeEventListener('message',receive);return}\nif(!ready||m.id!==undefined)return;\nlet slot=DASH.indexOf(m.method),kind='dashed';\nif(slot<0){slot=SNAKE.indexOf(m.method);kind='snake'}\nif(slot>=0){\n/* A dual host posts each notification twice. Lock to the first name and drop its twin:\n same payload, and a second render is a visible flicker. */\nif(!dialect)dialect=kind;if(dialect!==kind)return;\n// The snake result nests one level deeper.\nif(slot===0)render(kind==='snake'?m.params&&m.params.result:m.params);\nelse if(slot===1)wait();\nelse{host(m.params||{});resize()}\nreturn}\nif(m.method==='ui/notifications/tool-cancelled'){revision++;busy=false;$('plot').dataset.state='off';status('Request cancelled.');controls()}}\naddEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();\nrpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})\n.then(r=>{if(r.protocolVersion!=='2026-01-26')throw Error('version');host(r.hostContext||{});ready=true;notify(INIT,{});resize()})\n.catch(()=>{trouble('initialize-failed');status('This chart needs an MCP Apps host.',1)});\n})();`;\n\n/** Source comments explain the tile; the browser does not need them and the host budget\n * does. Block comments are stripped here, so the served tile stays under the ceiling. */\nconst SERVED_SCRIPT = TILE_SCRIPT.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/^\\s*\\/\\/.*$/gm, '').replace(/\\n{2,}/g, '\\n');\nexport function tileHtml(kind: 'chart' | 'kpi' = 'chart'): string {\n return strip(`<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'\">\n<title>Databrain ${kind} tile</title><style id=\"hostfont\"></style><style>\n/* Host tokens are the Claude MCP Apps style-variable names. Every one carries a fallback:\n hosts may ship any subset, so nothing here may depend on a token existing. */\n:root{color-scheme:light dark;\n--dbn-bg:var(--color-background-primary,Canvas);--dbn-text:var(--color-text-primary,CanvasText);\n--dbn-mute:var(--color-text-secondary,GrayText);--dbn-faint:var(--color-text-tertiary,GrayText);\n--dbn-edge:var(--color-border-secondary,color-mix(in srgb,currentColor 20%,transparent));\n--dbn-line:var(--color-border-tertiary,color-mix(in srgb,currentColor 9%,transparent));\n--dbn-accent:var(--color-text-info,LinkText);--dbn-ring:var(--color-ring-primary,var(--dbn-accent));\n--dbn-bad:var(--color-text-danger,var(--dbn-text));\n--dbn-quiet:color-mix(in srgb,currentColor 6%,transparent);\n--bw:var(--border-width-regular,1px);--r-s:var(--border-radius-sm,6px);--r-m:var(--border-radius-md,8px);\n--med:var(--font-weight-medium,500);--semi:var(--font-weight-semibold,600);--xs:var(--font-text-xs-size,12px);\n/* One curve. Quick / standard / display-mode. Corporate: no overshoot. */\n--e:cubic-bezier(.2,0,0,1);--q:120ms;--s:300ms;--l:400ms}\n*{box-sizing:border-box}\nbody{margin:0;padding:var(--safe-top,0px) var(--safe-right,0px) var(--safe-bottom,0px) var(--safe-left,0px);background:var(--dbn-bg);color:var(--dbn-text);\nfont:var(--font-text-sm-size,14px)/var(--font-text-sm-line-height,1.4) var(--font-sans,system-ui)}\nmain{padding:20px;max-width:1200px;margin:auto;transition:padding var(--l) var(--e)}\nheader{display:flex;align-items:start;justify-content:space-between;gap:12px}header>div{min-width:0}\n.acts{display:flex;gap:6px;flex:none;align-items:center}\nh1{font-size:var(--font-heading-md-size,18px);line-height:var(--font-heading-md-line-height,1.4);margin:0 0 6px;font-weight:var(--semi);letter-spacing:-.01em;overflow-wrap:anywhere;transition:font-size var(--l) var(--e)}\np{margin:6px 0;color:var(--dbn-mute);overflow-wrap:anywhere}\n#description{font-size:var(--xs);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-top:2px}#description:empty{display:none}\nbutton,input,select{font:inherit;color:inherit;background:var(--dbn-bg);border:var(--bw) solid var(--dbn-edge);border-radius:var(--r-s);padding:7px 10px}\nbutton{cursor:pointer;transition:background-color var(--q) var(--e),border-color var(--q) var(--e)}\ninput{max-width:160px;min-width:0}button:hover:not(:disabled){background:var(--dbn-quiet)}button:disabled{color:var(--dbn-faint);cursor:default}\nbutton:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--dbn-ring);outline-offset:2px}\n/* The picker is a scannable grid of quiet chips: 26 charts fit in nine rows, not a wall. */\n#choices[data-grid]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:6px;margin-top:12px}\n#choices[data-grid] button{text-align:left;border-color:var(--dbn-line);padding:8px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}\n#choices[data-grid] button:hover:not(:disabled){background:var(--dbn-quiet);border-color:var(--dbn-edge)}\n/* One primary per state: the drill pill, and Next page when there is a next page. */\n#choices button.primary,#next{flex:none;text-align:center;border:var(--bw) solid var(--dbn-accent);\nborder-radius:var(--r-s);padding:7px 10px;font-weight:var(--med);color:var(--dbn-accent)}\n#choices button.primary{align-self:flex-start;margin-top:10px}\n/* Refresh is a re-fetch, not a choice. Quiet it so it stops reading as a fifth chart. */\n#refresh{border-color:transparent;color:var(--dbn-mute);padding:7px 8px}\n#refresh:hover:not(:disabled){color:var(--dbn-text);background:none;text-decoration:underline}\n#status:empty{margin:0}\n#status[data-ok],#status[data-error]{font-weight:var(--med);border-left:2px solid;border-radius:0 var(--r-m) var(--r-m) 0;padding:10px 12px;margin:10px 0;background:color-mix(in srgb,currentColor 5%,transparent)}\n#status[data-ok]{color:var(--color-text-success,var(--dbn-text));border-color:var(--color-border-success,color-mix(in srgb,currentColor 40%,transparent))}\n/* \"Show as\": one row of quiet chips, the current type pressed. Never a second title. */\n#choices[data-chips]{flex-direction:row;flex-wrap:wrap;gap:6px;margin-top:10px}\n.chip{border-color:var(--dbn-line);border-radius:999px;padding:5px 11px;font-size:var(--xs);color:var(--dbn-mute)}\n.chip:hover:not(:disabled){color:var(--dbn-text);border-color:var(--dbn-edge)}\n.chip[aria-pressed=true]{color:var(--dbn-text);border-color:var(--dbn-text);background:var(--dbn-quiet)}\n/* Save is the one commit on a draft, so it is the one filled button in the tile. */\n#save,#commit{border-color:var(--dbn-accent);background:var(--dbn-accent);color:var(--dbn-bg);font-weight:var(--med)}\n#save:hover:not(:disabled),#commit:hover:not(:disabled){background:color-mix(in srgb,var(--dbn-accent) 85%,var(--dbn-text))}\n#form{margin-top:12px;padding:12px;border:var(--bw) solid var(--dbn-line);border-radius:var(--r-m);display:flex;flex-direction:column;gap:8px}\n[hidden]{display:none!important}#form label{flex-direction:column;align-items:stretch;gap:4px;font-size:var(--xs)}\n#form input,#form select{max-width:none;width:100%;background:color-mix(in srgb,currentColor 4%,var(--dbn-bg))}\n#wsone{color:var(--dbn-mute);font-size:var(--xs)}#wsone:empty{display:none}\n#status[data-error]{color:var(--dbn-bad);border-color:var(--color-border-danger,color-mix(in srgb,currentColor 40%,transparent));background:var(--color-background-danger,color-mix(in srgb,currentColor 5%,transparent))}\n/* Reserved plot. The skeleton is a static bar silhouette matching the 15:8 backend render:\n Claude's guidance asks for a skeleton that matches the final layout, and bans nothing here,\n but shimmer is banned by our own motion budget - so this paints once and never repaints. */\n#plot{position:relative;transition:opacity var(--q) var(--e)}#plot[data-busy],#plot[data-state=stale]{opacity:.55}\n#plot[data-state=wait]{aspect-ratio:15/8}\n#plot[data-state=wait]::before,#plot[data-state=ready]::before{content:'';position:absolute;pointer-events:none;inset:16% 6% 13%;\nborder-bottom:var(--bw) solid var(--dbn-line);transition:opacity var(--s) var(--e);\n--g:linear-gradient(var(--dbn-quiet),var(--dbn-quiet));background:var(--g) 0 100%/11% 44% no-repeat,var(--g) 22% 100%/11% 71% no-repeat,var(--g) 44% 100%/11% 57% no-repeat,var(--g) 66% 100%/11% 86% no-repeat,var(--g) 88% 100%/11% 33% no-repeat}\n#plot[data-state=ready]::before{opacity:0}\n.chart{position:relative;width:100%;margin:12px 0}.chart img{width:100%;height:100%;display:block}\n/* A point's geometry comes from the renderer and can be 8px; the hit area must not.\n ::after pads it to at least 24px; ::before is the marker that says \"you can pick this\". */\n.point{position:absolute;padding:0;border:0;background:transparent;border-radius:50%}\n.point::after{content:'';position:absolute;inset:-8px}\n.point::before{content:'';position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px;border-radius:50%;\nbackground:var(--dbn-accent);box-shadow:0 0 0 2px var(--dbn-bg);opacity:0;transform:scale(.6);transition:opacity var(--q) var(--e),transform var(--q) var(--e)}\n.point:hover::before,.point:focus-visible::before{opacity:.7;transform:none}\n.point[aria-pressed=true]::before{opacity:1;transform:none;box-shadow:0 0 0 2px var(--dbn-bg),0 0 0 4px color-mix(in srgb,var(--dbn-accent) 35%,transparent)}\n.kpi{font-size:clamp(34px,10vw,44px);line-height:1.1;font-weight:var(--semi);letter-spacing:-.025em;font-variant-numeric:tabular-nums;color:var(--dbn-text);margin:14px 0 6px;transition:font-size var(--l) var(--e)}\n/* The readout rail. At rest it states the scope of what is on screen; on selection it\n becomes the value. One slot, two jobs, so selecting a bar never reflows the tile. */\n#rail{display:flex;align-items:center;min-height:2.5em;margin:2px 0 0}\n#scope,#value{margin:0;display:none;overflow-wrap:anywhere}\n#rail:not([data-value]) #scope{display:block;color:var(--dbn-mute);font-size:var(--xs)}\n#rail:not([data-value]):has(#scope:empty){display:none}\n#rail[data-value] #value{display:flex;flex-direction:column;gap:1px;line-height:1.15}\n#vlabel{color:var(--dbn-mute);font-size:var(--xs)}\n#vnum{font-size:var(--font-heading-md-size,18px);font-weight:var(--semi);letter-spacing:-.015em;font-variant-numeric:tabular-nums}\n[data-mode=full] #vnum{font-size:var(--font-heading-xl-size,24px)}\nfooter,.dates{display:flex;gap:8px;flex-wrap:wrap;align-items:center}\n#choices{display:flex;flex-direction:column;align-items:stretch;margin:10px 0 0}footer{margin-top:12px}footer:not(:has(button:not([hidden]))){display:none}.dates{margin:8px 0 2px}\nlabel{display:flex;align-items:center;gap:6px;color:var(--dbn-mute)}\n#range{margin-top:10px}#data{margin-top:2px}\nhtml:not([data-deep]) #range,html:not([data-deep]) #data:not([data-only]){display:none}\nsummary{cursor:pointer;color:var(--dbn-mute);font-size:var(--xs);padding:5px 0;width:fit-content;transition:color var(--q) var(--e)}summary:hover{color:var(--dbn-text)}\n#rows{overflow:auto;max-height:260px;border-radius:var(--r-s)}\ntable{border-collapse:collapse;width:100%;font-size:var(--xs)}\nth,td{text-align:left;padding:8px;border-bottom:var(--bw) solid var(--dbn-line);overflow-wrap:anywhere}\ntd.n,th.n{text-align:right;font-variant-numeric:tabular-nums}\nth{font-weight:var(--semi);position:sticky;top:0;background:var(--dbn-bg);color:var(--dbn-mute);\nfont-size:var(--xs);letter-spacing:.02em;text-transform:uppercase}\n/* Inline to fullscreen is a layout morph, not a new screen: main widens over 400ms on the\n one curve and every dependent box follows it. Fullscreen is the detailed view, so the\n table loses its inline height cap. */\n/* The SVG is rendered at 600x320 and scaled by width, so an unbounded fullscreen column\n makes the chart's own axis type larger than the tile's heading. Hold a measure. */\n[data-mode=full] main{padding:28px 32px;max-width:900px}\n[data-mode=full] h1{font-size:var(--font-heading-xl-size,24px);line-height:var(--font-heading-xl-line-height,1.25)}\n[data-mode=full] .kpi{font-size:clamp(44px,7vw,72px)}\n[data-mode=full] #rows{max-height:none}\n/* Touch: the host gives vertical pan to the conversation, so a nested scroller is a trap. */\n@media(pointer:coarse){button,input{min-height:44px}summary{padding:12px 0}#rows{max-height:none;overflow:visible}}\n@media(max-width:480px){main{padding:14px}}\n/* Entrance. Content first, chrome 60ms later: 300+60 = 360ms, inside the 400ms budget.\n No stagger past two groups - a chat tile that cascades reads as lag, not luxury. */\n@keyframes rise{from{opacity:0;transform:translateY(6px)}}\n@keyframes land{from{opacity:0;transform:translateY(4px) scale(.995)}}\nheader,#status,#plot{animation:rise var(--s) var(--e) both}\n#rail,#choices,footer,#range,#data{animation:rise var(--s) var(--e) 60ms both}\n.chart,.kpi{animation:land var(--s) var(--e) both}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n</style></head><body><main id=\"tile\"><header><div><h1 id=\"title\">Analytics</h1><p id=\"description\"></p></div><div class=\"acts\"><button id=\"refresh\" hidden>Refresh</button><button id=\"expand\" hidden>Expand</button></div></header>\n<p id=\"status\" role=\"status\">Loading chart…</p><div id=\"plot\" data-state=\"wait\"></div>\n<div id=\"rail\"><p id=\"scope\"></p><p id=\"value\"><span id=\"vlabel\"></span><b id=\"vnum\"></b></p></div><div id=\"choices\"></div>\n<footer><button id=\"save\" hidden>Save as dashboard</button><button id=\"next\" hidden>Next page</button></footer>\n<form id=\"form\" hidden onsubmit=\"return false\"><label>Dashboard name<input id=\"dtitle\" type=\"text\" maxlength=\"200\" autocomplete=\"off\"></label>\n<label id=\"wsrow\" hidden>Workspace<select id=\"ws\"></select></label><p id=\"wsone\"></p>\n<div class=\"dates\"><button id=\"commit\" type=\"button\">Save to Databrain</button><button id=\"cancel\" type=\"button\">Cancel</button></div></form>\n<details id=\"range\" hidden><summary>Change the date range</summary><div class=\"dates\"><label>From <input id=\"from\" type=\"date\"></label><label>To <input id=\"to\" type=\"date\"></label><button id=\"dates\" type=\"button\">Apply dates</button></div></details>\n<details id=\"data\" hidden><summary id=\"count\">Show the data behind this</summary><div id=\"rows\" tabindex=\"0\" role=\"region\" aria-label=\"Chart data\"></div></details></main><script>${SERVED_SCRIPT}</script></body></html>`);\n}\nfunction strip(html: string): string {\n return html.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/\\n{2,}/g, '\\n')\n .replace(/<style>([\\s\\S]*?)<\\/style><\\/head>/, (_m, css: string) => `<style>${css.replace(/;}/g, '}').replace(/\\n/g, '')}</style></head>`);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/apps/server.ts","../src/apps/registry.ts","../src/apps/backend.ts","../src/apps/tile.ts"],"sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { DatabrainClient } from '../client/databrain-client.js';\nimport type { Config } from '../utils/config.js';\nimport { registerAllTools } from '../tools/registry.js';\nimport { registerAllResources } from '../resources/registry.js';\nimport { registerAllPrompts } from '../prompts/registry.js';\nimport { MCP_SERVER_VERSION } from '../version.js';\nimport { enableChartApps } from './feature.js';\nimport { registerChartApps } from './registry.js';\nimport type { ChartBackend } from './backend.js';\n\n/** Reviewable composition seam; hosted owners must retain their principal-bound client/policy. */\nexport function createChartAppServer(config: Config, backend?: ChartBackend): McpServer {\n const client = new DatabrainClient(config);\n const server = new McpServer({ name: 'databrain-chart-apps', version: MCP_SERVER_VERSION }, {\n instructions: 'Use read-only chart previews to inspect analytics. Chart interactions inherit guest scope. ' +\n 'Never request or expose credentials. Mutation tools retain their existing dry-run and confirmation requirements.',\n });\n enableChartApps(server);\n registerAllTools(server, client);\n registerAllResources(server);\n registerAllPrompts(server);\n registerChartApps(server, client, backend);\n return server;\n}\n","import { z } from 'zod';\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { DatabrainClient } from '../client/databrain-client.js';\nimport { DatabrainApiError } from '../utils/errors.js';\nimport { READ_ONLY } from '../tools/annotations.js';\nimport { toolContext } from '../utils/tool-context.js';\nimport { createChartBackend, type ChartBackend, type ChartAction } from './backend.js';\nimport { APP_MIME, CHART_URI, KPI_URI, pageSchema, referenceSchema, viewSchema } from './contract.js';\nimport { appMeta, chartResult } from './feature.js';\nimport { tileHtml } from './tile.js';\nimport { draftSchema, draftPage, RENDER_TYPES, saveDraftAsDashboard, workspacesForDatamart } from './preview.js';\nimport { rowsSchema } from './contract.js';\nimport { WRITE_CREATE } from '../tools/annotations.js';\nimport { APPLY_TO_PRODUCTION_CONFIRMATION } from '../tools/widgets/production-guards.js';\n\nconst actions: Record<ChartAction, string> = {\n render_chart: 'Render one authorized chart page as SVG with point metadata. Cursor pagination stays within the same guest scope.',\n refresh_widget: 'Refresh a chart in the current guest scope. Revalidates access before returning data.',\n drill_down: 'Explore a server-issued drill identifier within the current chart and guest scope.',\n change_date_range: 'Render a chart for an ordered date range within the current guest scope.',\n};\n\n/** Call after registerAllTools so existing registration policy/instrumentation remains active. */\nexport function registerChartApps(server: McpServer, client: DatabrainClient, backend: ChartBackend = createChartBackend(client)): void {\n for (const [name, description] of Object.entries(actions)) {\n const action = name as ChartAction;\n const tool = server.tool(name, description, {\n reference: referenceSchema, view: viewSchema.default({}),\n context: z.string().max(200).optional().describe('Chart interaction context'),\n }, READ_ONLY, async ({ reference, view }) => {\n try {\n if ((action === 'drill_down' && !view.drillId) || (action === 'change_date_range' && !view.dateRange)) throw new Error('Missing view selector');\n const page = pageSchema.parse(await toolContext.run({ tool: name, category: 'embedding', workflowIntent: 'chart_app', inputMode: 'app' },\n () => backend.execute(action, reference, view)));\n // SVG is rendered as an isolated image, never inserted into the HTML DOM.\n // Reject active/external/animated constructs as a further contract check.\n if (page.svg && (!/^\\s*<svg\\b/i.test(page.svg) ||\n /<\\s*(?:script|foreignObject|iframe|image|animate\\w*|set)\\b|<!DOCTYPE|<!ENTITY|\\bon\\w+\\s*=|@(?:import|keyframes)|\\banimation\\s*:|(?:href\\s*=\\s*[\"'](?!#)|url\\s*\\(\\s*[\"']?(?!#))/i.test(page.svg))) {\n throw new Error('Unsafe SVG');\n }\n return chartResult(`${page.kind === 'kpi' ? 'KPI' : 'Chart'} ready. ${page.rows.length} rows in this page.`, {\n ...page, reference, view,\n });\n } catch {\n return { isError: true, content: [{ type: 'text' as const,\n text: 'Chart unavailable. Check dashboard access and the chart renderer integration, then retry.' }] };\n }\n });\n tool.update({ _meta: appMeta(['app']) });\n }\n registerDraftTools(server, client);\n for (const [uri, kind] of [[CHART_URI, 'chart'], [KPI_URI, 'kpi']] as const) {\n server.resource(`Databrain ${kind} tile`, uri, { mimeType: APP_MIME, description: `Interactive ${kind} using host theme and scoped server tools` }, async () => ({\n contents: [{ uri, mimeType: APP_MIME, text: tileHtml(kind), _meta: { ui: { prefersBorder: true } } }],\n }));\n }\n}\n\n/** The tile's controls over an unsaved answer. `preview_chart` is the \"show as\" row and\n * is app-only: the model already has ask_question. `save_chart_as_dashboard` is the commit,\n * visible to both, and gated the way every production write here is gated. */\nfunction registerDraftTools(server: McpServer, client: DatabrainClient): void {\n const preview = server.tool('preview_chart',\n 'Redraw an unsaved ask_question answer as another chart type, from the rows already on screen. No query runs.',\n { draft: draftSchema, rows: rowsSchema, chartType: z.enum(RENDER_TYPES),\n context: z.string().max(200).optional().describe('Chart interaction context') },\n READ_ONLY, async ({ draft, rows, chartType }) => {\n try {\n const page = await draftPage(client, draft, rows, chartType);\n return chartResult(`${page.kind === 'table' ? 'Table' : 'Chart'} ready as ${chartType}.`, page);\n } catch {\n return { isError: true, content: [{ type: 'text' as const, text: 'Chart preview unavailable. Ask the question again to retry.' }] };\n }\n });\n preview.update({ _meta: appMeta(['app']) });\n\n const save = server.tool('save_chart_as_dashboard',\n 'Save an ask_question answer: creates a workspace metric from its SQL and a new dashboard holding it. ' +\n `Production write; requires confirm: \"${APPLY_TO_PRODUCTION_CONFIRMATION}\". ` +\n 'A workspaceName is needed when the datamart backs more than one workspace.',\n { draft: draftSchema, chartType: z.enum(RENDER_TYPES),\n dashboardTitle: z.string().trim().min(1).max(200),\n workspaceName: z.string().trim().min(1).max(256).optional(),\n attemptId: z.string().trim().min(1).max(64).optional()\n .describe('Identifies one save attempt. Reuse it to retry safely; omit or change it to save again on purpose.'),\n confirm: z.string().optional(),\n context: z.string().max(200).optional().describe('Chart interaction context') },\n WRITE_CREATE, async ({ draft, chartType, dashboardTitle, workspaceName, attemptId, confirm }) => {\n if (confirm !== APPLY_TO_PRODUCTION_CONFIRMATION) {\n return { isError: true, content: [{ type: 'text' as const,\n text: `Saving writes to production. Retry with confirm: \"${APPLY_TO_PRODUCTION_CONFIRMATION}\".` }] };\n }\n // The draft's workspace list is a hint for the tile. The caller wrote it, so the\n // set a save may target is recomputed here from the datamart, on the server's own credential.\n const choices = await workspacesForDatamart(client, draft.datamartName, 'authorize');\n const refuse = (text: string) => ({ isError: true, content: [{ type: 'text' as const, text }],\n structuredContent: { kind: 'save', ok: false, choices } });\n // A named workspace must be one the draft's datamart can serve, spelled the draft's way.\n const target = workspaceName\n ? choices.find(w => w.trim().toLowerCase() === workspaceName.toLowerCase())\n : choices.length === 1 ? choices[0] : undefined;\n if (workspaceName && !target) {\n return refuse(`Workspace \"${workspaceName}\" cannot run this datamart's SQL. Choose one of: ${choices.join(', ')}.`);\n }\n if (!target) {\n return refuse(choices.length\n ? `Choose a workspace: ${choices.join(', ')}.`\n : `No workspace is built on datamart \"${draft.datamartName}\". Create one in Databrain, then save again.`);\n }\n try {\n const saved = await saveDraftAsDashboard(client, draft, { chartType, dashboardTitle, workspaceName: target, attemptId });\n return chartResult(`Saved \"${saved.title}\" to workspace ${saved.workspaceName} (dashboard ${saved.dashboardId}, metric ${saved.metricId}).`,\n { kind: 'save', ok: true, ...saved, draft: { ...draft, chartType } });\n } catch (error) {\n const code = error instanceof DatabrainApiError ? error.code : 'SAVE_FAILED';\n return { isError: true, content: [{ type: 'text' as const,\n text: `Could not save (${code}). Check the workspace name and your access, then try again.` }],\n structuredContent: { kind: 'save', ok: false, code } };\n }\n });\n save.update({ _meta: appMeta(['model', 'app']) });\n}\n","import { DatabrainClient } from '../client/databrain-client.js';\nimport type { RequestOptions } from '../client/types.js';\nimport { createServiceBundle } from '../services/factory.js';\nimport { pageSchema, referenceSchema, viewSchema, type ChartPage, type ChartReference, type ChartView } from './contract.js';\n\nexport const RENDER_PATH = '/api/v2/dashboard/mcp-chart';\nexport type ChartAction = 'render_chart' | 'refresh_widget' | 'drill_down' | 'change_date_range';\nexport interface ChartBackend {\n execute(action: ChartAction, reference: ChartReference, view: ChartView): Promise<ChartPage>;\n}\n\nclass UncachedChartClient extends DatabrainClient {\n constructor(private readonly delegate: DatabrainClient) {\n super({ apiUrl: delegate.getApiUrl(), demoDomain: delegate.getDemoDomain() });\n }\n override request<T>(options: RequestOptions): Promise<T> {\n return this.delegate.request<T>({ ...options, cacheKey: undefined, cacheTtlMs: undefined });\n }\n}\n\n/** Proposed backend route; no local renderer, ambient service fallback, or UI credentials. */\nexport function createChartBackend(client: DatabrainClient): ChartBackend {\n // The general embed lookup caches for five minutes. Authorization must not.\n const services = createServiceBundle(new UncachedChartClient(client));\n return {\n async execute(action, rawReference, rawView) {\n const reference = referenceSchema.parse(rawReference);\n const view = viewSchema.parse(rawView);\n // Every page/refresh/drill re-reads the embed live, so a revoked tenant stops working at once.\n const app = await services.widgetService.resolveDataAppName(undefined, reference.embedId);\n if (!app.ok) throw new Error('Chart scope unavailable');\n const scope = await services.widgetService.ensureReadableDashboardScope({\n embedId: reference.embedId, clientId: reference.clientId, dataAppName: app.value });\n if (!scope.ok) throw new Error('Chart scope denied');\n const { token } = await services.guestTokenService.generateToken({\n clientId: reference.clientId, dataAppName: app.value, allowedEmbeds: [reference.embedId],\n permissions: { isDisableMetricCreation: true, isEnableManageMetrics: false,\n isEnableMetricUpdation: false, isEnableArchiveMetrics: false },\n });\n if (!token) throw new Error('Guest scope unavailable');\n const page = await client.request<unknown>({\n method: 'POST', path: RENDER_PATH, auth: 'none', token,\n body: { action, embedId: reference.embedId, widgetId: reference.widgetId, ...view,\n pageSize: 50, width: 600, height: 320, animation: false },\n skipDataUnwrap: true,\n });\n return pageSchema.parse(page);\n },\n };\n}\n","/** Deliberately no imports in the iframe. All requests go through the host bridge. */\nexport const TILE_SCRIPT = String.raw`(()=>{\nconst $=id=>document.getElementById(id),root=document.documentElement,pending=new Map();\nlet seq=0,origin='*',ready=false,closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null,dialect=null,shown=false,draft=null,rows=[],saved=null,attempt='';\n/* One lifecycle, two sets of names. Hosts ship either or both, so notifications go out\n under both and arrive under either. Same order in both rows: result, input, context. */\nconst DASH=['ui/notifications/tool-result','ui/notifications/tool-input','ui/notifications/host-context-changed'],\nSNAKE=['notifications/tools/call/result','notifications/tools/call/input','notifications/host_context/changed'],\nINIT=['ui/notifications/initialized','notifications/initialized'],\nSIZE=['ui/notifications/size-changed','notifications/size_changed'],\nDISPLAY=['ui/request-display-mode','requestDisplayMode'];\nfunction send(message){if(!closed)parent.postMessage({jsonrpc:'2.0',...message},origin)}\nfunction notify(names,params){for(const method of names)send({method,params})}\nfunction rpc(method,params){return new Promise((resolve,reject)=>{const id=++seq;\nconst timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'))},65000);\npending.set(id,{resolve,reject,timer});send({id,method,params})})}\n/* Try each alias while the host says it never heard of it; any other error stands. */\nasync function ask(names,params){let last;\nfor(const method of names){try{return await rpc(method,params)}catch(error){last=error;if(error.code!==-32601)throw error}}\nthrow last}\n/* Fixed code only. The tile sits inside customer rows; a log is no place to leak them. */\nfunction trouble(code){send({method:'notifications/error',params:{code}})}\n/* One formatter for rail, table, KPI and answer line. Compact past 10K, grouped past 100,\n two decimals below; null is an absence, so it prints as a dash and never as 0. */\nfunction fmt(v){if(typeof v!=='number')return v==null||v===''?'\\u2014':String(v);const a=Math.abs(v);\nreturn new Intl.NumberFormat(navigator.language||'en',a>=1e4?{notation:'compact',maximumFractionDigits:1}:a>=100?{maximumFractionDigits:0}:{maximumFractionDigits:2}).format(v)}\nfunction exact(el,v){el.title=typeof v==='number'?String(v):''}\n/* The answer line: what this shows, and the headline value. Gated on carrying a number or\n a count, so it never renders as a second title or an empty reserved slot. */\nfunction answer(p){if(p.description)return p.description;const bits=[];\nif(p.kind==='dashboard'){const n=p.widgets?.length||0;return n?n+(n===1?' chart':' charts')+' on this dashboard':''}\nconst [dim,measure]=p.columns||[];const pts=(p.points||[]).filter(q=>typeof q.value==='number');\nif(pts.length<2)return '';if(dim&&measure&&!p.draft)bits.push(measure+' by '+dim);\nconst top=pts.reduce((a,b)=>b.value>a.value?b:a);bits.push(top.label.split(' \\u2014 ')[0]+' highest at '+fmt(top.value));return bits.join(' \\u00b7 ')}\nfunction status(text,bad,ok){const s=$('status');s.textContent=text;s.toggleAttribute('data-error',!!bad);s.toggleAttribute('data-ok',!!ok);if(bad)$('plot').dataset.state='off'}\nfunction paint(){const img=$('chart-image');if(!img||!rawSvg)return;\nconst c=getComputedStyle(document.body).color,mix=pc=>'color-mix(in srgb,'+c+' '+pc+'%,transparent)';\nimg.src='data:image/svg+xml,'+encodeURIComponent(rawSvg.replace(/<\\/svg>\\s*$/i,'<style>text{fill:'+mix(72)+'!important}'\n+'[stroke=\"#dbdee4\"],[stroke=\"#f1f1f2\" i]{stroke:'+mix(13)+'!important}[stroke=\"#54555a\"],[stroke=\"#d4d4d8\" i]{stroke:'+mix(30)+'!important}'\n+'</style></svg>'))}\nfunction controls(){document.querySelectorAll('button,input,select').forEach(e=>{e.disabled=busy});\n$('refresh').hidden=!ref;$('next').hidden=!next;$('range').hidden=!ref;$('plot').toggleAttribute('data-busy',busy);\n$('save').hidden=!draft||!!saved||!$('form').hidden;\n$('expand').hidden=!shown||root.dataset.kind==='dashboard'||!context.availableDisplayModes?.includes('fullscreen');\nroot.toggleAttribute('data-deep',context.displayMode==='fullscreen'||$('expand').hidden)}\nfunction host(change){context={...context,...change};if(change.theme==='dark'||change.theme==='light')root.style.colorScheme=change.theme;\nconst vars=change.styles?.variables||{};for(const [key,value] of Object.entries(vars)){\nif(/^--(?:color-|font-|border-|shadow-)/.test(key)&&typeof value==='string'&&!/url\\s*\\(|@import/i.test(value))root.style.setProperty(key,value)}\nconst face=change.styles?.css?.fonts;if(typeof face==='string'&&face.length<4000&&!/@import|javascript:/i.test(face))$('hostfont').textContent=face;\nconst insets=change.safeAreaInsets;if(insets)for(const edge of ['top','right','bottom','left']){\nconst n=insets[edge];if(Number.isFinite(n))root.style.setProperty('--safe-'+edge,Math.max(0,Math.min(n,200))+'px')}\nroot.dataset.mode=context.displayMode==='fullscreen'?'full':'inline';\n$('expand').textContent=context.displayMode==='fullscreen'?'Back to conversation':'Expand';paint();controls()}\nfunction scope(p,n){const bits=[];\nif(p.kind==='chart'||p.kind==='table')bits.push(p.rowsTotal>n?'first '+n+' of '+p.rowsTotal+' rows':n?(n===1?'1 row':n+' rows'):'No rows in this view');\nif(view.dateRange)bits.push(view.dateRange.from+' to '+view.dateRange.to);\nif(next)bits.push('more pages available');\n$('scope').textContent=bits.join(' · ')}\nfunction resize(){if(ready)notify(SIZE,{width:document.body.scrollWidth,height:document.body.scrollHeight})}\nfunction table(rows,columns){$('rows').replaceChildren();$('data').hidden=!rows?.length;if(!rows?.length)return;\n$('count').textContent=rows.length===1?'1 row behind this':rows.length+' rows behind this';\nconst t=document.createElement('table');\nconst num=name=>rows.some(r=>typeof r[name]==='number');\nconst header=document.createElement('tr');for(const name of columns){const th=document.createElement('th');th.scope='col';th.textContent=name;if(num(name))th.className='n';header.append(th)}\nconst head=document.createElement('thead');head.append(header);t.append(head);const body=document.createElement('tbody');\nfor(const row of rows.slice(0,50)){const tr=document.createElement('tr');for(const key of columns){const td=document.createElement('td');td.textContent=fmt(row[key]);exact(td,row[key]);if(num(key))td.className='n';tr.append(td)}body.append(tr)}t.append(body);$('rows').append(t)}\nasync function display(mode){try{const answer=await ask(DISPLAY,{mode});\nhost({displayMode:answer.mode||context.displayMode});if(root.dataset.mode==='full'&&!$('data').hidden)$('data').open=true;resize()}\ncatch{status('Expand is unavailable in this host.',1)}}\nfunction wait(){busy=true;if(!$('plot').firstChild)$('plot').dataset.state='wait';status('Loading chart…');controls()}\nasync function call(name,extra={}){if(!ready||!ref||busy)return;run(name,{reference:ref,view:{...view,...extra}})}\n/* The draft tools take the draft and the rows on screen instead of a reference. */\nasync function run(name,args){if(!ready||busy)return;wait();const rev=++revision;\ntry{const result=await rpc('tools/call',{name,arguments:{...args,context:'tile'}});\nif(rev===revision)render(result)}catch{if(rev===revision){trouble('tool-call-failed');if(name==='save_chart_as_dashboard'){$('plot').dataset.state='ready';status('Could not save. Try again.',1)}else if(name==='preview_chart'){status('Could not draw that. Pick another type.',1);$('plot').dataset.state='ready'}else stale()}}finally{busy=false;controls()}}\nfunction stale(){if(shown&&$('plot').firstChild){status('Could not refresh. Showing the last chart that loaded. Select Refresh to try again.',1);$('plot').dataset.state='stale'}else status('Could not load the chart. Select Refresh to try again.',1)}\nfunction render(result){busy=false;controls();\nconst sp=result?.structuredContent;if(sp&&sp.kind==='save'){$('plot').dataset.state='ready';if(sp.ok){saved=sp;$('form').hidden=true;status('Saved \"'+sp.title+'\" to workspace '+sp.workspaceName+'.',0,1)}\nelse if(sp.choices?.length){choose(sp.choices);status('Choose a workspace, then save again.',1)}\nelse status((result.content?.[0]?.text)||'Could not save. Try again.',1);controls();resize();return}\nif(result?.isError&&draft&&$('form').hidden===false){$('plot').dataset.state='ready';status((result.content?.[0]?.text)||'Could not save. Try again.',1);controls();return}\nif(result?.isError){trouble('tool-returned-error');\nif(shown&&$('plot').firstChild){stale();controls();return}shown=false;controls();\nstatus(ref?'Chart unavailable. Check your access to this dashboard, then Refresh.'\n:'Chart unavailable. Ask again to retry.',1);return}\nconst p=result?.structuredContent;if(!p||typeof p!=='object'){shown=false;controls();status('No chart data came back for this result.',1);return}\nrevision++;shown=true;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();delete $('rail').dataset.value;\nref=p.reference||null;view=p.view||{};next=p.nextCursor||null;draft=p.draft||null;rows=p.rows||[];saved=null;$('form').hidden=true;\n$('title').textContent=p.title||'Analytics';$('description').textContent=answer(p);root.dataset.kind=p.kind;$('choices').toggleAttribute('data-grid',p.kind==='dashboard');\nif(p.kind==='dashboard'){$('plot').dataset.state='off';\nfor(const item of p.widgets||[]){const b=document.createElement('button');b.textContent=item.name||'Untitled widget';b.onclick=()=>{ref=item.reference;view={};call('render_chart')};$('choices').append(b)}\nstatus(p.widgets?.length?'':'No charts on this dashboard yet. Add one in Databrain.');\n}else if(p.kind==='kpi'){\nconst number=document.createElement('p');number.className='kpi';number.textContent=fmt(p.value);exact(number,p.value);$('plot').append(number);status('');\n}else if(p.svg){\nconst box=document.createElement('div');box.className='chart';box.style.aspectRatio=p.width+'/'+p.height;\nconst img=document.createElement('img');img.id='chart-image';rawSvg=p.svg;img.alt=p.title||'Chart';img.onerror=()=>{status('The chart could not be drawn. The rows are below.',1);$('data').open=$('data').toggleAttribute('data-only',true)};box.append(img);\nfor(const point of p.points||[]){const b=document.createElement('button');b.className='point';b.setAttribute('aria-label',point.label+': '+fmt(point.value));b.setAttribute('aria-pressed','false');\nb.style.left=100*point.x/p.width+'%';b.style.top=100*point.y/p.height+'%';b.style.width=100*point.width/p.width+'%';b.style.height=100*point.height/p.height+'%';\nconst hover=()=>{$('vlabel').textContent=point.label;$('vnum').textContent=fmt(point.value);exact($('vnum'),point.value);$('rail').dataset.value='1'};b.onmouseenter=hover;b.onfocus=hover;\nb.onclick=()=>{box.querySelectorAll('.point').forEach(e=>e.setAttribute('aria-pressed','false'));b.setAttribute('aria-pressed','true');hover();\n$('choices').replaceChildren();if(point.drillId){const drill=document.createElement('button');drill.className='primary';drill.textContent='Explore selected data';drill.onclick=()=>call('drill_down',{drillId:point.drillId,cursor:undefined});$('choices').append(drill)}};box.append(b)}$('plot').append(box);\nstatus('');\n}else if(p.kind==='table'){\n// ask_question returns a table by design; calling that a missing chart called success a failure.\n$('plot').dataset.state='off';status('');\n}else{$('plot').dataset.state='off';\nstatus('Nothing to show. Try a wider date range, then Refresh.',1)}\nif(draft&&p.previewTypes?.length){for(const t of p.previewTypes){const b=document.createElement('button');b.textContent=t[0].toUpperCase()+t.slice(1);b.className='chip';b.setAttribute('aria-pressed',String(t===draft.chartType));\nb.onclick=()=>run('preview_chart',{draft,rows,chartType:t});$('choices').append(b)}$('choices').dataset.chips='1'}else delete $('choices').dataset.chips;\nif(p.previewError)status('Cannot draw this as '+(p.draft?.chartType||'a chart')+' ('+p.previewError+'). Showing the rows.',1);\npaint();table(p.rows||[],p.columns||Object.keys(p.rows?.[0]||{}));scope(p,p.rows?.length||0);\nconst only=!p.svg&&p.value===undefined&&p.kind!=='dashboard';$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}\n$('refresh').onclick=()=>call('refresh_widget',{cursor:undefined});$('next').onclick=()=>call('render_chart',{cursor:next});\nfunction choose(names){const w=$('ws');w.replaceChildren();for(const n of names){const o=document.createElement('option');o.value=n;o.textContent=n;w.append(o)}$('wsrow').hidden=names.length<2;$('wsone').textContent=names.length===1?'in workspace '+names[0]:'';$('form').hidden=false;$('dtitle').focus();controls();resize()}\n$('save').onclick=()=>{attempt=crypto.randomUUID();$('dtitle').value=draft?.title||'';choose(draft?.workspaces||[])};\n$('cancel').onclick=()=>{$('form').hidden=true;status('');controls();resize()};\n$('commit').onclick=()=>{const t=$('dtitle').value.trim();if(!t){status('Name the dashboard.',1);return}\nrun('save_chart_as_dashboard',{draft,chartType:draft.chartType,dashboardTitle:t,workspaceName:$('wsrow').hidden?undefined:$('ws').value,attemptId:attempt,confirm:'APPLY_TO_PRODUCTION'})};\n$('expand').onclick=()=>display(context.displayMode==='fullscreen'?'inline':'fullscreen');\n$('dates').onclick=()=>{const from=$('from').value,to=$('to').value;if(!from||!to||from>to){status('Start date must not be after the end date.',1);return}call('change_date_range',{dateRange:{from,to},cursor:undefined,drillId:undefined})};\nfunction receive(event){if(event.source!==parent||closed)return;const m=event.data;\nif(!m||Array.isArray(m)||m.jsonrpc!=='2.0'||(origin!=='*'&&event.origin!==origin))return;\nif(!m.method&&pending.has(m.id)&&('result'in m||'error'in m)){if(origin==='*'&&event.origin!=='null')origin=event.origin;\nconst p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);\nif(m.error){const error=Error('host error');error.code=m.error.code;p.reject(error)}else p.resolve(m.result);return}\nif(m.method==='ping'&&m.id!==undefined){send({id:m.id,result:{}});return}\nif(m.method==='ui/resource-teardown'){send({id:m.id,result:{}});closed=true;revision++;for(const p of pending.values()){clearTimeout(p.timer);p.reject(Error('closed'))}pending.clear();observer.disconnect();removeEventListener('message',receive);return}\nif(!ready||m.id!==undefined)return;\nlet slot=DASH.indexOf(m.method),kind='dashed';\nif(slot<0){slot=SNAKE.indexOf(m.method);kind='snake'}\nif(slot>=0){\n/* A dual host posts each notification twice. Lock to the first name and drop its twin:\n same payload, and a second render is a visible flicker. */\nif(!dialect)dialect=kind;if(dialect!==kind)return;\n// The snake result nests one level deeper.\nif(slot===0)render(kind==='snake'?m.params&&m.params.result:m.params);\nelse if(slot===1)wait();\nelse{host(m.params||{});resize()}\nreturn}\nif(m.method==='ui/notifications/tool-cancelled'){revision++;busy=false;$('plot').dataset.state='off';status('Request cancelled.');controls()}}\naddEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();\nrpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})\n.then(r=>{if(r.protocolVersion!=='2026-01-26')throw Error('version');host(r.hostContext||{});ready=true;notify(INIT,{});resize()})\n.catch(()=>{trouble('initialize-failed');status('This chart needs an MCP Apps host.',1)});\n})();`;\n\n/** Source comments explain the tile; the browser does not need them and the host budget\n * does. Block comments are stripped here, so the served tile stays under the ceiling. */\nconst SERVED_SCRIPT = TILE_SCRIPT.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/^\\s*\\/\\/.*$/gm, '').replace(/\\n{2,}/g, '\\n');\nexport function tileHtml(kind: 'chart' | 'kpi' = 'chart'): string {\n return strip(`<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'\">\n<title>Databrain ${kind} tile</title><style id=\"hostfont\"></style><style>\n/* Host tokens are the Claude MCP Apps style-variable names. Every one carries a fallback:\n hosts may ship any subset, so nothing here may depend on a token existing. */\n:root{color-scheme:light dark;\n--dbn-bg:var(--color-background-primary,Canvas);--dbn-text:var(--color-text-primary,CanvasText);\n--dbn-mute:var(--color-text-secondary,GrayText);--dbn-faint:var(--color-text-tertiary,GrayText);\n--dbn-edge:var(--color-border-secondary,color-mix(in srgb,currentColor 20%,transparent));\n--dbn-line:var(--color-border-tertiary,color-mix(in srgb,currentColor 9%,transparent));\n--dbn-accent:var(--color-text-info,LinkText);--dbn-ring:var(--color-ring-primary,var(--dbn-accent));\n--dbn-bad:var(--color-text-danger,var(--dbn-text));\n--dbn-quiet:color-mix(in srgb,currentColor 6%,transparent);\n--bw:var(--border-width-regular,1px);--r-s:var(--border-radius-sm,6px);--r-m:var(--border-radius-md,8px);\n--med:var(--font-weight-medium,500);--semi:var(--font-weight-semibold,600);--xs:var(--font-text-xs-size,12px);\n/* One curve. Quick / standard / display-mode. Corporate: no overshoot. */\n--e:cubic-bezier(.2,0,0,1);--q:120ms;--s:300ms;--l:400ms}\n*{box-sizing:border-box}\nbody{margin:0;padding:var(--safe-top,0px) var(--safe-right,0px) var(--safe-bottom,0px) var(--safe-left,0px);background:var(--dbn-bg);color:var(--dbn-text);\nfont:var(--font-text-sm-size,14px)/var(--font-text-sm-line-height,1.4) var(--font-sans,system-ui)}\nmain{padding:20px;max-width:1200px;margin:auto;transition:padding var(--l) var(--e)}\nheader{display:flex;align-items:start;justify-content:space-between;gap:12px}header>div{min-width:0}\n.acts{display:flex;gap:6px;flex:none;align-items:center}\nh1{font-size:var(--font-heading-md-size,18px);line-height:var(--font-heading-md-line-height,1.4);margin:0 0 6px;font-weight:var(--semi);letter-spacing:-.01em;overflow-wrap:anywhere;transition:font-size var(--l) var(--e)}\np{margin:6px 0;color:var(--dbn-mute);overflow-wrap:anywhere}\n#description{font-size:var(--xs);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-top:2px}#description:empty{display:none}\nbutton,input,select{font:inherit;color:inherit;background:var(--dbn-bg);border:var(--bw) solid var(--dbn-edge);border-radius:var(--r-s);padding:7px 10px}\nbutton{cursor:pointer;transition:background-color var(--q) var(--e),border-color var(--q) var(--e)}\ninput{max-width:160px;min-width:0}button:hover:not(:disabled){background:var(--dbn-quiet)}button:disabled{color:var(--dbn-faint);cursor:default}\nbutton:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--dbn-ring);outline-offset:2px}\n/* The picker is a scannable grid of quiet chips: 26 charts fit in nine rows, not a wall. */\n#choices[data-grid]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:6px;margin-top:12px}\n#choices[data-grid] button{text-align:left;border-color:var(--dbn-line);padding:8px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}\n#choices[data-grid] button:hover:not(:disabled){background:var(--dbn-quiet);border-color:var(--dbn-edge)}\n/* One primary per state: the drill pill, and Next page when there is a next page. */\n#choices button.primary,#next{flex:none;text-align:center;border:var(--bw) solid var(--dbn-accent);\nborder-radius:var(--r-s);padding:7px 10px;font-weight:var(--med);color:var(--dbn-accent)}\n#choices button.primary{align-self:flex-start;margin-top:10px}\n/* Refresh is a re-fetch, not a choice. Quiet it so it stops reading as a fifth chart. */\n#refresh{border-color:transparent;color:var(--dbn-mute);padding:7px 8px}\n#refresh:hover:not(:disabled){color:var(--dbn-text);background:none;text-decoration:underline}\n#status:empty{margin:0}\n#status[data-ok],#status[data-error]{font-weight:var(--med);border-left:2px solid;border-radius:0 var(--r-m) var(--r-m) 0;padding:10px 12px;margin:10px 0;background:color-mix(in srgb,currentColor 5%,transparent)}\n#status[data-ok]{color:var(--color-text-success,var(--dbn-text));border-color:var(--color-border-success,color-mix(in srgb,currentColor 40%,transparent))}\n/* \"Show as\": one row of quiet chips, the current type pressed. Never a second title. */\n#choices[data-chips]{flex-direction:row;flex-wrap:wrap;gap:6px;margin-top:10px}\n.chip{border-color:var(--dbn-line);border-radius:999px;padding:5px 11px;font-size:var(--xs);color:var(--dbn-mute)}\n.chip:hover:not(:disabled){color:var(--dbn-text);border-color:var(--dbn-edge)}\n.chip[aria-pressed=true]{color:var(--dbn-text);border-color:var(--dbn-text);background:var(--dbn-quiet)}\n/* Save is the one commit on a draft, so it is the one filled button in the tile. */\n#save,#commit{border-color:var(--dbn-accent);background:var(--dbn-accent);color:var(--dbn-bg);font-weight:var(--med)}\n#save:hover:not(:disabled),#commit:hover:not(:disabled){background:color-mix(in srgb,var(--dbn-accent) 85%,var(--dbn-text))}\n#form{margin-top:12px;padding:12px;border:var(--bw) solid var(--dbn-line);border-radius:var(--r-m);display:flex;flex-direction:column;gap:8px}\n[hidden]{display:none!important}#form label{flex-direction:column;align-items:stretch;gap:4px;font-size:var(--xs)}\n#form input,#form select{max-width:none;width:100%;background:color-mix(in srgb,currentColor 4%,var(--dbn-bg))}\n#wsone{color:var(--dbn-mute);font-size:var(--xs)}#wsone:empty{display:none}\n#status[data-error]{color:var(--dbn-bad);border-color:var(--color-border-danger,color-mix(in srgb,currentColor 40%,transparent));background:var(--color-background-danger,color-mix(in srgb,currentColor 5%,transparent))}\n/* Reserved plot. The skeleton is a static bar silhouette matching the 15:8 backend render:\n Claude's guidance asks for a skeleton that matches the final layout, and bans nothing here,\n but shimmer is banned by our own motion budget - so this paints once and never repaints. */\n#plot{position:relative;transition:opacity var(--q) var(--e)}#plot[data-busy],#plot[data-state=stale]{opacity:.55}\n#plot[data-state=wait]{aspect-ratio:15/8}\n#plot[data-state=wait]::before,#plot[data-state=ready]::before{content:'';position:absolute;pointer-events:none;inset:16% 6% 13%;\nborder-bottom:var(--bw) solid var(--dbn-line);transition:opacity var(--s) var(--e);\n--g:linear-gradient(var(--dbn-quiet),var(--dbn-quiet));background:var(--g) 0 100%/11% 44% no-repeat,var(--g) 22% 100%/11% 71% no-repeat,var(--g) 44% 100%/11% 57% no-repeat,var(--g) 66% 100%/11% 86% no-repeat,var(--g) 88% 100%/11% 33% no-repeat}\n#plot[data-state=ready]::before{opacity:0}\n.chart{position:relative;width:100%;margin:12px 0}.chart img{width:100%;height:100%;display:block}\n/* A point's geometry comes from the renderer and can be 8px; the hit area must not.\n ::after pads it to at least 24px; ::before is the marker that says \"you can pick this\". */\n.point{position:absolute;padding:0;border:0;background:transparent;border-radius:50%}\n.point::after{content:'';position:absolute;inset:-8px}\n.point::before{content:'';position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px;border-radius:50%;\nbackground:var(--dbn-accent);box-shadow:0 0 0 2px var(--dbn-bg);opacity:0;transform:scale(.6);transition:opacity var(--q) var(--e),transform var(--q) var(--e)}\n.point:hover::before,.point:focus-visible::before{opacity:.7;transform:none}\n.point[aria-pressed=true]::before{opacity:1;transform:none;box-shadow:0 0 0 2px var(--dbn-bg),0 0 0 4px color-mix(in srgb,var(--dbn-accent) 35%,transparent)}\n.kpi{font-size:clamp(34px,10vw,44px);line-height:1.1;font-weight:var(--semi);letter-spacing:-.025em;font-variant-numeric:tabular-nums;color:var(--dbn-text);margin:14px 0 6px;transition:font-size var(--l) var(--e)}\n/* The readout rail. At rest it states the scope of what is on screen; on selection it\n becomes the value. One slot, two jobs, so selecting a bar never reflows the tile. */\n#rail{display:flex;align-items:center;min-height:2.5em;margin:2px 0 0}\n#scope,#value{margin:0;display:none;overflow-wrap:anywhere}\n#rail:not([data-value]) #scope{display:block;color:var(--dbn-mute);font-size:var(--xs)}\n#rail:not([data-value]):has(#scope:empty){display:none}\n#rail[data-value] #value{display:flex;flex-direction:column;gap:1px;line-height:1.15}\n#vlabel{color:var(--dbn-mute);font-size:var(--xs)}\n#vnum{font-size:var(--font-heading-md-size,18px);font-weight:var(--semi);letter-spacing:-.015em;font-variant-numeric:tabular-nums}\n[data-mode=full] #vnum{font-size:var(--font-heading-xl-size,24px)}\nfooter,.dates{display:flex;gap:8px;flex-wrap:wrap;align-items:center}\n#choices{display:flex;flex-direction:column;align-items:stretch;margin:10px 0 0}footer{margin-top:12px}footer:not(:has(button:not([hidden]))){display:none}.dates{margin:8px 0 2px}\nlabel{display:flex;align-items:center;gap:6px;color:var(--dbn-mute)}\n#range{margin-top:10px}#data{margin-top:2px}\nhtml:not([data-deep]) #range,html:not([data-deep]) #data:not([data-only]){display:none}\nsummary{cursor:pointer;color:var(--dbn-mute);font-size:var(--xs);padding:5px 0;width:fit-content;transition:color var(--q) var(--e)}summary:hover{color:var(--dbn-text)}\n#rows{overflow:auto;max-height:260px;border-radius:var(--r-s)}\ntable{border-collapse:collapse;width:100%;font-size:var(--xs)}\nth,td{text-align:left;padding:8px;border-bottom:var(--bw) solid var(--dbn-line);overflow-wrap:anywhere}\ntd.n,th.n{text-align:right;font-variant-numeric:tabular-nums}\nth{font-weight:var(--semi);position:sticky;top:0;background:var(--dbn-bg);color:var(--dbn-mute);\nfont-size:var(--xs);letter-spacing:.02em;text-transform:uppercase}\n/* Inline to fullscreen is a layout morph, not a new screen: main widens over 400ms on the\n one curve and every dependent box follows it. Fullscreen is the detailed view, so the\n table loses its inline height cap. */\n/* The SVG is rendered at 600x320 and scaled by width, so an unbounded fullscreen column\n makes the chart's own axis type larger than the tile's heading. Hold a measure. */\n[data-mode=full] main{padding:28px 32px;max-width:900px}\n[data-mode=full] h1{font-size:var(--font-heading-xl-size,24px);line-height:var(--font-heading-xl-line-height,1.25)}\n[data-mode=full] .kpi{font-size:clamp(44px,7vw,72px)}\n[data-mode=full] #rows{max-height:none}\n/* Touch: the host gives vertical pan to the conversation, so a nested scroller is a trap. */\n@media(pointer:coarse){button,input{min-height:44px}summary{padding:12px 0}#rows{max-height:none;overflow:visible}}\n@media(max-width:480px){main{padding:14px}}\n/* Entrance. Content first, chrome 60ms later: 300+60 = 360ms, inside the 400ms budget.\n No stagger past two groups - a chat tile that cascades reads as lag, not luxury. */\n@keyframes rise{from{opacity:0;transform:translateY(6px)}}\n@keyframes land{from{opacity:0;transform:translateY(4px) scale(.995)}}\nheader,#status,#plot{animation:rise var(--s) var(--e) both}\n#rail,#choices,footer,#range,#data{animation:rise var(--s) var(--e) 60ms both}\n.chart,.kpi{animation:land var(--s) var(--e) both}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n</style></head><body><main id=\"tile\"><header><div><h1 id=\"title\">Analytics</h1><p id=\"description\"></p></div><div class=\"acts\"><button id=\"refresh\" hidden>Refresh</button><button id=\"expand\" hidden>Expand</button></div></header>\n<p id=\"status\" role=\"status\">Loading chart…</p><div id=\"plot\" data-state=\"wait\"></div>\n<div id=\"rail\"><p id=\"scope\"></p><p id=\"value\"><span id=\"vlabel\"></span><b id=\"vnum\"></b></p></div><div id=\"choices\"></div>\n<footer><button id=\"save\" hidden>Save as dashboard</button><button id=\"next\" hidden>Next page</button></footer>\n<form id=\"form\" hidden onsubmit=\"return false\"><label>Dashboard name<input id=\"dtitle\" type=\"text\" maxlength=\"200\" autocomplete=\"off\"></label>\n<label id=\"wsrow\" hidden>Workspace<select id=\"ws\"></select></label><p id=\"wsone\"></p>\n<div class=\"dates\"><button id=\"commit\" type=\"button\">Save to Databrain</button><button id=\"cancel\" type=\"button\">Cancel</button></div></form>\n<details id=\"range\" hidden><summary>Change the date range</summary><div class=\"dates\"><label>From <input id=\"from\" type=\"date\"></label><label>To <input id=\"to\" type=\"date\"></label><button id=\"dates\" type=\"button\">Apply dates</button></div></details>\n<details id=\"data\" hidden><summary id=\"count\">Show the data behind this</summary><div id=\"rows\" tabindex=\"0\" role=\"region\" aria-label=\"Chart data\"></div></details></main><script>${SERVED_SCRIPT}</script></body></html>`);\n}\nfunction strip(html: string): string {\n return html.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/\\n{2,}/g, '\\n')\n .replace(/<style>([\\s\\S]*?)<\\/style><\\/head>/, (_m, css: string) => `<style>${css.replace(/;}/g, '}').replace(/\\n/g, '')}</style></head>`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,iBAAiB;;;ACA1B,SAAS,SAAS;;;ACKX,IAAM,cAAc;AAM3B,IAAM,sBAAN,cAAkC,gBAAgB;AAAA,EAChD,YAA6B,UAA2B;AACtD,UAAM,EAAE,QAAQ,SAAS,UAAU,GAAG,YAAY,SAAS,cAAc,EAAE,CAAC;AADjD;AAAA,EAE7B;AAAA,EACS,QAAW,SAAqC;AACvD,WAAO,KAAK,SAAS,QAAW,EAAE,GAAG,SAAS,UAAU,QAAW,YAAY,OAAU,CAAC;AAAA,EAC5F;AACF;AAGO,SAAS,mBAAmB,QAAuC;AAExE,QAAM,WAAW,oBAAoB,IAAI,oBAAoB,MAAM,CAAC;AACpE,SAAO;AAAA,IACL,MAAM,QAAQ,QAAQ,cAAc,SAAS;AAC3C,YAAM,YAAY,gBAAgB,MAAM,YAAY;AACpD,YAAM,OAAO,WAAW,MAAM,OAAO;AAErC,YAAM,MAAM,MAAM,SAAS,cAAc,mBAAmB,QAAW,UAAU,OAAO;AACxF,UAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,yBAAyB;AACtD,YAAM,QAAQ,MAAM,SAAS,cAAc,6BAA6B;AAAA,QACtE,SAAS,UAAU;AAAA,QAAS,UAAU,UAAU;AAAA,QAAU,aAAa,IAAI;AAAA,MAAM,CAAC;AACpF,UAAI,CAAC,MAAM,GAAI,OAAM,IAAI,MAAM,oBAAoB;AACnD,YAAM,EAAE,MAAM,IAAI,MAAM,SAAS,kBAAkB,cAAc;AAAA,QAC/D,UAAU,UAAU;AAAA,QAAU,aAAa,IAAI;AAAA,QAAO,eAAe,CAAC,UAAU,OAAO;AAAA,QACvF,aAAa;AAAA,UAAE,yBAAyB;AAAA,UAAM,uBAAuB;AAAA,UACnE,wBAAwB;AAAA,UAAO,wBAAwB;AAAA,QAAM;AAAA,MACjE,CAAC;AACD,UAAI,CAAC,MAAO,OAAM,IAAI,MAAM,yBAAyB;AACrD,YAAM,OAAO,MAAM,OAAO,QAAiB;AAAA,QACzC,QAAQ;AAAA,QAAQ,MAAM;AAAA,QAAa,MAAM;AAAA,QAAQ;AAAA,QACjD,MAAM;AAAA,UAAE;AAAA,UAAQ,SAAS,UAAU;AAAA,UAAS,UAAU,UAAU;AAAA,UAAU,GAAG;AAAA,UAC3E,UAAU;AAAA,UAAI,OAAO;AAAA,UAAK,QAAQ;AAAA,UAAK,WAAW;AAAA,QAAM;AAAA,QAC1D,gBAAgB;AAAA,MAClB,CAAC;AACD,aAAO,WAAW,MAAM,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;;;AChDO,IAAM,cAAc,OAAO;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;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;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;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoJlC,IAAM,gBAAgB,YAAY,QAAQ,qBAAqB,EAAE,EAAE,QAAQ,iBAAiB,EAAE,EAAE,QAAQ,WAAW,IAAI;AAChH,SAAS,SAAS,OAAwB,SAAiB;AAChE,SAAO,MAAM;AAAA;AAAA,mBAEI,IAAI;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;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;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oLA4H6J,aAAa,yBAAyB;AAC1N;AACA,SAAS,MAAM,MAAsB;AACnC,SAAO,KAAK,QAAQ,qBAAqB,EAAE,EAAE,QAAQ,WAAW,IAAI,EACjE,QAAQ,sCAAsC,CAAC,IAAI,QAAgB,UAAU,IAAI,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE,CAAC,iBAAiB;AAC7I;;;AF3QA,IAAM,UAAuC;AAAA,EAC3C,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,YAAY;AAAA,EACZ,mBAAmB;AACrB;AAGO,SAAS,kBAAkB,QAAmB,QAAyB,UAAwB,mBAAmB,MAAM,GAAS;AACtI,aAAW,CAAC,MAAM,WAAW,KAAK,OAAO,QAAQ,OAAO,GAAG;AACzD,UAAM,SAAS;AACf,UAAM,OAAO,OAAO,KAAK,MAAM,aAAa;AAAA,MAC1C,WAAW;AAAA,MAAiB,MAAM,WAAW,QAAQ,CAAC,CAAC;AAAA,MACvD,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,2BAA2B;AAAA,IAC9E,GAAG,WAAW,OAAO,EAAE,WAAW,KAAK,MAAM;AAC3C,UAAI;AACF,YAAK,WAAW,gBAAgB,CAAC,KAAK,WAAa,WAAW,uBAAuB,CAAC,KAAK,UAAY,OAAM,IAAI,MAAM,uBAAuB;AAC9I,cAAM,OAAO,WAAW,MAAM,MAAM,YAAY;AAAA,UAAI,EAAE,MAAM,MAAM,UAAU,aAAa,gBAAgB,aAAa,WAAW,MAAM;AAAA,UACrI,MAAM,QAAQ,QAAQ,QAAQ,WAAW,IAAI;AAAA,QAAC,CAAC;AAGjD,YAAI,KAAK,QAAQ,CAAC,cAAc,KAAK,KAAK,GAAG,KAC3C,kLAAkL,KAAK,KAAK,GAAG,IAAI;AACnM,gBAAM,IAAI,MAAM,YAAY;AAAA,QAC9B;AACA,eAAO,YAAY,GAAG,KAAK,SAAS,QAAQ,QAAQ,OAAO,WAAW,KAAK,KAAK,MAAM,uBAAuB;AAAA,UAC3G,GAAG;AAAA,UAAM;AAAA,UAAW;AAAA,QACtB,CAAC;AAAA,MACH,QAAQ;AACN,eAAO,EAAE,SAAS,MAAM,SAAS,CAAC;AAAA,UAAE,MAAM;AAAA,UACxC,MAAM;AAAA,QAA4F,CAAC,EAAE;AAAA,MACzG;AAAA,IACF,CAAC;AACD,SAAK,OAAO,EAAE,OAAO,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AAAA,EACzC;AACA,qBAAmB,QAAQ,MAAM;AACjC,aAAW,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC,WAAW,OAAO,GAAG,CAAC,SAAS,KAAK,CAAC,GAAY;AAC3E,WAAO,SAAS,aAAa,IAAI,SAAS,KAAK,EAAE,UAAU,UAAU,aAAa,eAAe,IAAI,4CAA4C,GAAG,aAAa;AAAA,MAC/J,UAAU,CAAC,EAAE,KAAK,UAAU,UAAU,MAAM,SAAS,IAAI,GAAG,OAAO,EAAE,IAAI,EAAE,eAAe,KAAK,EAAE,EAAE,CAAC;AAAA,IACtG,EAAE;AAAA,EACJ;AACF;AAKA,SAAS,mBAAmB,QAAmB,QAA+B;AAC5E,QAAM,UAAU,OAAO;AAAA,IAAK;AAAA,IAC1B;AAAA,IACA;AAAA,MAAE,OAAO;AAAA,MAAa,MAAM;AAAA,MAAY,WAAW,EAAE,KAAK,YAAY;AAAA,MACpE,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,2BAA2B;AAAA,IAAE;AAAA,IAChF;AAAA,IAAW,OAAO,EAAE,OAAO,MAAM,UAAU,MAAM;AAC/C,UAAI;AACF,cAAM,OAAO,MAAM,UAAU,QAAQ,OAAO,MAAM,SAAS;AAC3D,eAAO,YAAY,GAAG,KAAK,SAAS,UAAU,UAAU,OAAO,aAAa,SAAS,KAAK,IAAI;AAAA,MAChG,QAAQ;AACN,eAAO,EAAE,SAAS,MAAM,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,8DAA8D,CAAC,EAAE;AAAA,MACpI;AAAA,IACF;AAAA,EAAC;AACH,UAAQ,OAAO,EAAE,OAAO,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AAE1C,QAAM,OAAO,OAAO;AAAA,IAAK;AAAA,IACvB,6IACwC,gCAAgC;AAAA,IAExE;AAAA,MAAE,OAAO;AAAA,MAAa,WAAW,EAAE,KAAK,YAAY;AAAA,MAClD,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,MAChD,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,MAC1D,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS,EAClD,SAAS,oGAAoG;AAAA,MAChH,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,MAC7B,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,2BAA2B;AAAA,IAAE;AAAA,IAChF;AAAA,IAAc,OAAO,EAAE,OAAO,WAAW,gBAAgB,eAAe,WAAW,QAAQ,MAAM;AAC/F,UAAI,YAAY,kCAAkC;AAChD,eAAO,EAAE,SAAS,MAAM,SAAS,CAAC;AAAA,UAAE,MAAM;AAAA,UACxC,MAAM,qDAAqD,gCAAgC;AAAA,QAAK,CAAC,EAAE;AAAA,MACvG;AAGA,YAAM,UAAU,MAAM,sBAAsB,QAAQ,MAAM,cAAc,WAAW;AACnF,YAAM,SAAS,CAAC,UAAkB;AAAA,QAAE,SAAS;AAAA,QAAM,SAAS,CAAC,EAAE,MAAM,QAAiB,KAAK,CAAC;AAAA,QAC1F,mBAAmB,EAAE,MAAM,QAAQ,IAAI,OAAO,QAAQ;AAAA,MAAE;AAE1D,YAAM,SAAS,gBACX,QAAQ,KAAK,OAAK,EAAE,KAAK,EAAE,YAAY,MAAM,cAAc,YAAY,CAAC,IACxE,QAAQ,WAAW,IAAI,QAAQ,CAAC,IAAI;AACxC,UAAI,iBAAiB,CAAC,QAAQ;AAC5B,eAAO,OAAO,cAAc,aAAa,oDAAoD,QAAQ,KAAK,IAAI,CAAC,GAAG;AAAA,MACpH;AACA,UAAI,CAAC,QAAQ;AACX,eAAO,OAAO,QAAQ,SAClB,uBAAuB,QAAQ,KAAK,IAAI,CAAC,MACzC,sCAAsC,MAAM,YAAY,8CAA8C;AAAA,MAC5G;AACA,UAAI;AACF,cAAM,QAAQ,MAAM,qBAAqB,QAAQ,OAAO,EAAE,WAAW,gBAAgB,eAAe,QAAQ,UAAU,CAAC;AACvH,eAAO;AAAA,UAAY,UAAU,MAAM,KAAK,kBAAkB,MAAM,aAAa,eAAe,MAAM,WAAW,YAAY,MAAM,QAAQ;AAAA,UACrI,EAAE,MAAM,QAAQ,IAAI,MAAM,GAAG,OAAO,OAAO,EAAE,GAAG,OAAO,UAAU,EAAE;AAAA,QAAC;AAAA,MACxE,SAAS,OAAO;AACd,cAAM,OAAO,iBAAiB,oBAAoB,MAAM,OAAO;AAC/D,eAAO;AAAA,UAAE,SAAS;AAAA,UAAM,SAAS,CAAC;AAAA,YAAE,MAAM;AAAA,YACxC,MAAM,mBAAmB,IAAI;AAAA,UAA+D,CAAC;AAAA,UAC7F,mBAAmB,EAAE,MAAM,QAAQ,IAAI,OAAO,KAAK;AAAA,QAAE;AAAA,MACzD;AAAA,IACF;AAAA,EAAC;AACH,OAAK,OAAO,EAAE,OAAO,QAAQ,CAAC,SAAS,KAAK,CAAC,EAAE,CAAC;AAClD;;;AD7GO,SAAS,qBAAqB,QAAgB,SAAmC;AACtF,QAAM,SAAS,IAAI,gBAAgB,MAAM;AACzC,QAAM,SAAS,IAAI,UAAU,EAAE,MAAM,wBAAwB,SAAS,mBAAmB,GAAG;AAAA,IAC1F,cAAc;AAAA,EAEhB,CAAC;AACD,kBAAgB,MAAM;AACtB,mBAAiB,QAAQ,MAAM;AAC/B,uBAAqB,MAAM;AAC3B,qBAAmB,MAAM;AACzB,oBAAkB,QAAQ,QAAQ,OAAO;AACzC,SAAO;AACT;","names":[]}
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  oauthConfigSchema,
4
4
  oauthHttpsUrl
5
- } from "./chunk-I35U2LBX.js";
5
+ } from "./chunk-SKU37UVJ.js";
6
6
 
7
7
  // src/utils/config.ts
8
8
  import { z, ZodError } from "zod";
@@ -72,4 +72,4 @@ export {
72
72
  DEFAULT_POSTHOG_HOST,
73
73
  loadConfig
74
74
  };
75
- //# sourceMappingURL=chunk-3JFMWXRO.js.map
75
+ //# sourceMappingURL=chunk-SDFC7PKT.js.map