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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,7 +29,7 @@ Your AI app opens a Databrain sign-in page, you pick one or more workspaces (or
29
29
 
30
30
  When you approve more than one workspace, the AI names the workspace on each call (`workspace` or `workspaceName`); `whoami` lists the ones you approved. With "All workspaces I can access", workspaces you get later are included, and `create_workspace` can make a new one if your role allows it.
31
31
 
32
- The hosted connector includes chart and KPI MCP Apps. `ask_question` returns an inline tile, and `list_widgets` opens a chart chooser in compatible hosts. Both current `_meta.ui.resourceUri` and legacy `_meta['ui/resourceUri']` pointers are included; other hosts retain the text summary and structured data. Tile controls retain the answer's granted workspace. Saving requires `mcp:dashboards:write`, `mcp:query`, the live workspace role, and `APPLY_TO_PRODUCTION` confirmation. No new environment variable is needed, including on self-hosted installs.
32
+ The hosted connector includes chart and KPI MCP Apps. `render_metric_tile` renders an existing saved metric directly without an embed, data app or datamart; `ask_question` returns a new answer as an inline tile, and `list_widgets` opens a chart chooser in compatible hosts. Both current `_meta.ui.resourceUri` and legacy `_meta['ui/resourceUri']` pointers are included; other hosts retain the text summary and structured data. Tile controls retain the answer's granted workspace. Saving requires `mcp:dashboards:write`, `mcp:query`, the live workspace role, and `APPLY_TO_PRODUCTION` confirmation. No new environment variable is needed, including on self-hosted installs. Saved category and measure settings drive bar, line, pie, doughnut, stacked and combo tiles. Gauges render inline, pivot/table tiles show a scrollable table, and text KPIs stay text. Large dashboard documents open a compact chart picker.
33
33
 
34
34
  <details>
35
35
  <summary><b>Claude (claude.ai and Claude Desktop)</b></summary>
@@ -448,13 +448,13 @@ Prompts are guided workflows your AI assistant can follow. Ask for them by name
448
448
  | `ask_question` | Ask a natural language question — gets converted to SQL, returns data + chart suggestion |
449
449
  | `get_semantic_layer` | Inspect semantic layer metadata (table/column descriptions, synonyms, completion score) |
450
450
  | `update_semantic_layer` | Improve semantic layer quality (add descriptions, synonyms, example questions) |
451
- | `start_semantic_layer_generation` | Start semantic-layer generation for a datamart |
452
- | `get_semantic_layer_generation_status` | Check semantic-layer generation progress |
451
+ | `start_semantic_layer_generation` | Start semantic-layer generation for a datamart (hosted OAuth) |
452
+ | `get_semantic_layer_generation_status` | Check semantic-layer generation progress (hosted OAuth) |
453
453
  | `search_semantic_model` | Find relevant v2 entities and their related entities |
454
454
  | `get_semantic_entity` | Read one v2 entity by kind and name with the current content hash |
455
455
  | `propose_semantic_change` | Validate candidate YAML or a patch and return its plan without writing |
456
456
  | `apply_semantic_change` | Preview by default, or apply live with a reviewed hash and production confirmation |
457
- | `eval_semantic_model` | Run saved queries and optional questions and return pass rate and verdicts |
457
+ | `eval_semantic_model` | Check closed-world model assertions (id/question/exercises); does not run SQL |
458
458
 
459
459
  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.
460
460
 
@@ -463,7 +463,7 @@ For a v2 model, the five model tools use `datamartId` and the configured service
463
463
  1. Use `search_semantic_model` with `question` and optional `k` (default 10, maximum 100). If the server has no retrieval route, the tool pulls the model and ranks name, alias, synonyms and description, then includes adjacent entities. Hidden tables and columns are excluded from local search.
464
464
  2. Use `get_semantic_entity` with `kind` and an exact `name`. Column names use `table.column`. Kinds are `table`, `column`, `measure`, `metric`, `join`, `entity`, `glossary`, `saved_query`, `instruction` and `access_rule`.
465
465
  3. Use `propose_semantic_change` with exactly one of `source` (complete YAML), `yaml_patch`, or `change`. It returns the candidate source, validation errors, the server plan and `expected_content_hash`. Invalid source returns errors and no plan.
466
- 4. Use `eval_semantic_model` with the proposed `source` and optional `questions`. Omitting source evaluates the stored model. The server returns pass rate and individual verdicts.
466
+ 4. Use `eval_semantic_model` with the proposed `source` and optional `questions`. Omitting source evaluates the stored model. Each question needs `id`, `question`, and `exercises` (exact model entity names). An omitted list becomes empty. This checks the model; it does not execute SQL.
467
467
  5. Review the plan. Call `apply_semantic_change` with the exact returned `source` and `expected_content_hash`. Its `dry_run` defaults to true. A live apply requires `dry_run: false` and user supplied `confirm: "APPLY_TO_PRODUCTION"`. The assistant must never supply confirmation on its own. Explicit `expected_content_hash: null` selects the first apply only when no layer is stored. Apply changes the live model for all consumers.
468
468
 
469
469
  A YAML patch merges mappings and named lists by name, including tables and columns. Other lists replace the original list. An empty list clears a list. Null removes a field. For example, `yaml_patch: "tables:\n - name: orders\n description: Customer purchases\n"` changes that description and preserves other tables and fields. Patch output normalizes YAML formatting and does not preserve comments. Review the returned source.
@@ -618,3 +618,6 @@ All tokens are managed automatically. You provide your service token once in the
618
618
  ## License
619
619
 
620
620
  MIT
621
+
622
+
623
+ Saved hosted charts: call `list_dashboards`, then `get_dashboard` or `list_widgets` with its UUID/slug and workspace. Omit clientId for the saved dashboard view. Call `render_metric_tile({workspaceName, metricId, clientId?, display:"chart"})` to open a metricRef directly; the saved client must be supplied for a client-scoped metric. `display:"kpi"` returns the saved scalar, or labels the sum of a chart's sole numeric series across the full authorized result; multiple series refuse instead of guessing. Refresh and date-range controls recheck the same workspace and saved client. Use workspaceId for the wid in app links. Tenant/API-key reads still use embedId/clientId, with optional dataAppName for OAuth ambiguity. These operations create no dashboards, metrics or embeds.
@@ -123,19 +123,37 @@ declare const configSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
123
123
  }>;
124
124
  type Config = z.infer<typeof configSchema>;
125
125
 
126
- declare const referenceSchema: z.ZodObject<{
126
+ declare const referenceSchema: z.ZodUnion<[z.ZodObject<{
127
127
  embedId: z.ZodString;
128
128
  clientId: z.ZodString;
129
129
  widgetId: z.ZodString;
130
+ dataAppName: z.ZodOptional<z.ZodString>;
130
131
  }, "strict", z.ZodTypeAny, {
131
132
  clientId: string;
132
133
  embedId: string;
133
134
  widgetId: string;
135
+ dataAppName?: string | undefined;
134
136
  }, {
135
137
  clientId: string;
136
138
  embedId: string;
137
139
  widgetId: string;
138
- }>;
140
+ dataAppName?: string | undefined;
141
+ }>, z.ZodObject<{
142
+ dashboardId: z.ZodString;
143
+ widgetId: z.ZodString;
144
+ clientId: z.ZodOptional<z.ZodString>;
145
+ display: z.ZodOptional<z.ZodEnum<["chart", "kpi"]>>;
146
+ }, "strict", z.ZodTypeAny, {
147
+ widgetId: string;
148
+ dashboardId: string;
149
+ clientId?: string | undefined;
150
+ display?: "chart" | "kpi" | undefined;
151
+ }, {
152
+ widgetId: string;
153
+ dashboardId: string;
154
+ clientId?: string | undefined;
155
+ display?: "chart" | "kpi" | undefined;
156
+ }>]>;
139
157
  type ChartReference = z.infer<typeof referenceSchema>;
140
158
  declare const viewSchema: z.ZodObject<{
141
159
  cursor: z.ZodOptional<z.ZodString>;
@@ -172,8 +190,10 @@ declare const viewSchema: z.ZodObject<{
172
190
  } | undefined;
173
191
  }>;
174
192
  declare const pageSchema: z.ZodEffects<z.ZodObject<{
175
- kind: z.ZodEnum<["chart", "kpi"]>;
193
+ kind: z.ZodEnum<["chart", "kpi", "table"]>;
176
194
  title: z.ZodString;
195
+ description: z.ZodOptional<z.ZodString>;
196
+ rowsTotal: z.ZodOptional<z.ZodNumber>;
177
197
  chartSpec: z.ZodObject<{
178
198
  type: z.ZodString;
179
199
  }, "strict", z.ZodTypeAny, {
@@ -246,12 +266,14 @@ declare const pageSchema: z.ZodEffects<z.ZodObject<{
246
266
  key: string;
247
267
  drillId?: string | undefined;
248
268
  }[];
249
- kind: "chart" | "kpi";
269
+ kind: "chart" | "kpi" | "table";
250
270
  chartSpec: {
251
271
  type: string;
252
272
  };
253
273
  value?: string | number | boolean | null | undefined;
274
+ description?: string | undefined;
254
275
  svg?: string | undefined;
276
+ rowsTotal?: number | undefined;
255
277
  customChart?: {
256
278
  options: Record<string, string | number | boolean>;
257
279
  key: string;
@@ -275,12 +297,14 @@ declare const pageSchema: z.ZodEffects<z.ZodObject<{
275
297
  key: string;
276
298
  drillId?: string | undefined;
277
299
  }[];
278
- kind: "chart" | "kpi";
300
+ kind: "chart" | "kpi" | "table";
279
301
  chartSpec: {
280
302
  type: string;
281
303
  };
282
304
  value?: string | number | boolean | null | undefined;
305
+ description?: string | undefined;
283
306
  svg?: string | undefined;
307
+ rowsTotal?: number | undefined;
284
308
  customChart?: {
285
309
  options: Record<string, string | number | boolean>;
286
310
  key: string;
@@ -304,12 +328,14 @@ declare const pageSchema: z.ZodEffects<z.ZodObject<{
304
328
  key: string;
305
329
  drillId?: string | undefined;
306
330
  }[];
307
- kind: "chart" | "kpi";
331
+ kind: "chart" | "kpi" | "table";
308
332
  chartSpec: {
309
333
  type: string;
310
334
  };
311
335
  value?: string | number | boolean | null | undefined;
336
+ description?: string | undefined;
312
337
  svg?: string | undefined;
338
+ rowsTotal?: number | undefined;
313
339
  customChart?: {
314
340
  options: Record<string, string | number | boolean>;
315
341
  key: string;
@@ -333,12 +359,14 @@ declare const pageSchema: z.ZodEffects<z.ZodObject<{
333
359
  key: string;
334
360
  drillId?: string | undefined;
335
361
  }[];
336
- kind: "chart" | "kpi";
362
+ kind: "chart" | "kpi" | "table";
337
363
  chartSpec: {
338
364
  type: string;
339
365
  };
340
366
  value?: string | number | boolean | null | undefined;
367
+ description?: string | undefined;
341
368
  svg?: string | undefined;
369
+ rowsTotal?: number | undefined;
342
370
  customChart?: {
343
371
  options: Record<string, string | number | boolean>;
344
372
  key: string;
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createChartAppServer
4
- } from "../chunk-IBYBK5ZE.js";
5
- import "../chunk-NBFUKVWP.js";
6
- import "../chunk-ES2LUP4U.js";
7
- import "../chunk-RHK62ZJ6.js";
4
+ } from "../chunk-HCYZN7I3.js";
5
+ import "../chunk-233VGFHY.js";
6
+ import "../chunk-PSJVYD4W.js";
7
+ import "../chunk-AEQTEE5C.js";
8
8
  import "../chunk-Q5EYDKSA.js";
9
9
  export {
10
10
  createChartAppServer
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadConfig
4
- } from "../chunk-DM7S4R57.js";
4
+ } from "../chunk-DTFYHLYN.js";
5
5
  import {
6
6
  createChartAppServer
7
- } from "../chunk-IBYBK5ZE.js";
8
- import "../chunk-NBFUKVWP.js";
9
- import "../chunk-ES2LUP4U.js";
10
- import "../chunk-RHK62ZJ6.js";
7
+ } from "../chunk-HCYZN7I3.js";
8
+ import "../chunk-233VGFHY.js";
9
+ import "../chunk-PSJVYD4W.js";
10
+ import "../chunk-AEQTEE5C.js";
11
11
  import "../chunk-Q5EYDKSA.js";
12
12
 
13
13
  // src/apps/stdio.ts
@@ -13,6 +13,7 @@ import {
13
13
  bounded,
14
14
  chartResult,
15
15
  createServiceBundle,
16
+ dateRangeSchema,
16
17
  draftPage,
17
18
  draftSchema,
18
19
  pageSchema,
@@ -21,13 +22,13 @@ import {
21
22
  saveDraftAsDashboard,
22
23
  viewSchema,
23
24
  workspacesForDatamart
24
- } from "./chunk-ES2LUP4U.js";
25
+ } from "./chunk-PSJVYD4W.js";
25
26
  import {
26
27
  CustomChartsApi,
27
28
  DatabrainApiError,
28
29
  DatabrainClient,
29
30
  toolContext
30
- } from "./chunk-RHK62ZJ6.js";
31
+ } from "./chunk-AEQTEE5C.js";
31
32
  import {
32
33
  contentHash,
33
34
  readCustomChartBrowserRuntime,
@@ -120,7 +121,28 @@ function createChartBackend(client) {
120
121
  async execute(action, rawReference, rawView) {
121
122
  const reference = referenceSchema.parse(rawReference);
122
123
  const view = viewSchema.parse(rawView);
123
- const app = await services.widgetService.resolveDataAppName(void 0, reference.embedId);
124
+ if ("dashboardId" in reference) {
125
+ if (!client.isHosted()) throw new Error("Hosted sign-in required");
126
+ return pageSchema.parse(await client.request({
127
+ method: "POST",
128
+ path: "/api/v2/workspace/dashboard/chart",
129
+ auth: "service",
130
+ body: {
131
+ action,
132
+ dashboardId: reference.dashboardId,
133
+ widgetId: reference.widgetId,
134
+ ...reference.clientId ? { clientId: reference.clientId } : {},
135
+ ...reference.display ? { display: reference.display } : {},
136
+ ...view,
137
+ pageSize: 50,
138
+ width: 600,
139
+ height: 320,
140
+ animation: false
141
+ },
142
+ skipDataUnwrap: true
143
+ }));
144
+ }
145
+ const app = await services.widgetService.resolveDataAppName(reference.dataAppName, reference.embedId);
124
146
  if (!app.ok) throw new Error("Chart scope unavailable");
125
147
  const scope = await services.widgetService.ensureReadableDashboardScope({
126
148
  embedId: reference.embedId,
@@ -166,7 +188,7 @@ function createChartBackend(client) {
166
188
  import { transformSync } from "esbuild";
167
189
  var TILE_SCRIPT = String.raw`(()=>{
168
190
  const $=id=>document.getElementById(id),root=document.documentElement,pending=new Map();
169
- let 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='',workspace=null,custom=null,customMode=false;
191
+ let seq=0,origin='*',ready=false,early=[],closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null,dialect=null,shown=false,draft=null,rows=[],saved=null,attempt='',workspace=null,custom=null,customMode=false;
170
192
  /* One lifecycle, two sets of names. Hosts ship either or both, so notifications go out
171
193
  under both and arrive under either. Same order in both rows: result, input, context. */
172
194
  const DASH=['ui/notifications/tool-result','ui/notifications/tool-input','ui/notifications/host-context-changed'],
@@ -323,7 +345,11 @@ const p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);
323
345
  if(m.error){const error=Error('host error');error.code=m.error.code;p.reject(error)}else p.resolve(m.result);return}
324
346
  if(m.method==='ping'&&m.id!==undefined){send({id:m.id,result:{}});return}
325
347
  if(m.method==='ui/resource-teardown'){send({id:m.id,result:{}});closed=true;revision++;custom?.dispose();custom=null;for(const p of pending.values()){clearTimeout(p.timer);p.reject(Error('closed'))}pending.clear();observer.disconnect();removeEventListener('message',receive);return}
326
- if(!ready||m.id!==undefined)return;
348
+ if(m.id!==undefined)return;
349
+ /* A host can post the result before it answers initialize. Keep it for the replay. */
350
+ if(!ready){if(early.length<8)early.push(m);return}
351
+ handle(m)}
352
+ function handle(m){
327
353
  let slot=DASH.indexOf(m.method),kind='dashed';
328
354
  if(slot<0){slot=SNAKE.indexOf(m.method);kind='snake'}
329
355
  if(slot>=0){
@@ -337,9 +363,19 @@ else{host(m.params||{});resize()}
337
363
  return}
338
364
  if(m.method==='ui/notifications/tool-cancelled'){revision++;custom?.dispose();custom=null;busy=false;$('plot').dataset.state='off';status('Request cancelled.');controls()}}
339
365
  addEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();
366
+ /* Many tiles mount at once while a reply streams. A host that attaches its listener
367
+ after this frame's first initialize never sees it, and the tile then waits with no
368
+ size and no body until a reload. Ask again with backoff; the first answer wins. */
369
+ let started=false,tries=0;
370
+ function fail(){if(started)return;started=true;trouble('initialize-failed');status('This chart needs an MCP Apps host.',1)}
371
+ function start(){if(started||closed)return;tries++;
340
372
  rpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})
341
- .then(r=>{if(r.protocolVersion!=='2026-01-26')throw Error('version');host(r.hostContext||{});ready=true;notify(INIT,{});resize()})
342
- .catch(()=>{trouble('initialize-failed');status('This chart needs an MCP Apps host.',1)});
373
+ .then(r=>{if(started)return;if(r.protocolVersion!=='2026-01-26'){fail();return}started=true;
374
+ for(const p of pending.values())clearTimeout(p.timer);pending.clear();
375
+ host(r.hostContext||{});ready=true;notify(INIT,{});for(const m of early.splice(0))handle(m);resize()})
376
+ .catch(e=>{if(e.code!==undefined||tries>=10)fail()});
377
+ if(tries<10)setTimeout(start,Math.min(8000,400*2**tries))}
378
+ start();
343
379
  })();`;
344
380
  var SERVED_SCRIPT = transformSync(TILE_SCRIPT, { minify: true, target: "es2020" }).code;
345
381
  function tileHtml(kind = "chart") {
@@ -418,7 +454,7 @@ border-bottom:var(--bw) solid var(--dbn-line);transition:opacity var(--s) var(--
418
454
  background: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)}
419
455
  .point:hover::before,.point:focus-visible::before{opacity:.7;transform:none}
420
456
  .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)}
421
- .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)}
457
+ .kpi{overflow-wrap:anywhere;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)}
422
458
  /* The readout rail. At rest it states the scope of what is on screen; on selection it
423
459
  becomes the value. One slot, two jobs, so selecting a bar never reflows the tile. */
424
460
  #rail{display:flex;align-items:center;min-height:2.5em;margin:2px 0 0}
@@ -448,10 +484,11 @@ font-size:var(--xs);letter-spacing:.02em;text-transform:uppercase}
448
484
  makes the chart's own axis type larger than the tile's heading. Hold a measure. */
449
485
  [data-mode=full] main{padding:28px 32px;max-width:900px}
450
486
  [data-mode=full] h1{font-size:var(--font-heading-xl-size,24px);line-height:var(--font-heading-xl-line-height,1.25)}
451
- [data-mode=full] .kpi{font-size:clamp(44px,7vw,72px)}
487
+ [data-mode=full] .kpi{overflow-wrap:anywhere;font-size:clamp(44px,7vw,72px)}
452
488
  [data-mode=full] #rows{max-height:none}
453
489
  /* Touch: the host gives vertical pan to the conversation, so a nested scroller is a trap. */
454
490
  @media(pointer:coarse){button,input{min-height:44px}summary{padding:12px 0}#rows{max-height:none;overflow:visible}}
491
+ html[data-kind=table] #rows{max-height:360px;overflow:auto}html[data-kind=table] table{min-width:480px}
455
492
  @media(max-width:480px){main{padding:14px}}
456
493
  /* Entrance. Content first, chrome 60ms later: 300+60 = 360ms, inside the 400ms budget.
457
494
  No stagger past two groups - a chat tile that cascades reads as lag, not luxury. */
@@ -477,10 +514,10 @@ function strip(html) {
477
514
 
478
515
  // src/apps/registry.ts
479
516
  var actions = {
480
- render_chart: "Render one authorized chart page as SVG with point metadata. Cursor pagination stays within the same guest scope.",
481
- refresh_widget: "Refresh a chart in the current guest scope. Revalidates access before returning data.",
517
+ render_chart: "Render one authorized chart page as SVG with point metadata. Hosted reads use the signed-in workspace; embed references keep guest scope.",
518
+ refresh_widget: "Refresh a chart in the current workspace or guest scope. Revalidates access before returning data.",
482
519
  drill_down: "Explore a server-issued drill identifier within the current chart and guest scope.",
483
- change_date_range: "Render a chart for an ordered date range within the current guest scope."
520
+ change_date_range: "Render a chart for an ordered date range within the current workspace or guest scope."
484
521
  };
485
522
  function registerChartApps(server, client, backend = createChartBackend(client)) {
486
523
  for (const [name, description] of Object.entries(actions)) {
@@ -499,12 +536,13 @@ function registerChartApps(server, client, backend = createChartBackend(client))
499
536
  if (page.svg && (!/^\s*<svg\b/i.test(page.svg) || /<\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))) {
500
537
  throw new Error("Unsafe SVG");
501
538
  }
502
- return chartResult(`${page.kind === "kpi" ? "KPI" : "Chart"} ready. ${page.rows.length} rows in this page.${page.fallback ? " " + page.fallback : ""}`, {
539
+ return chartResult(`${page.kind === "kpi" ? "KPI" : "Chart"} ready. ${page.rows.length} rows in this page.${page.fallback ? " " + page.fallback : ""}${page.description ? " " + page.description : ""}`, {
503
540
  ...page,
504
541
  reference,
505
542
  view
506
543
  });
507
- } catch {
544
+ } catch (error) {
545
+ if (error instanceof DatabrainApiError) return error.toToolResponse();
508
546
  return { isError: true, content: [{
509
547
  type: "text",
510
548
  text: "Chart unavailable. Check dashboard access and the chart renderer integration, then retry."
@@ -513,6 +551,7 @@ function registerChartApps(server, client, backend = createChartBackend(client))
513
551
  });
514
552
  tool.update({ _meta: appMeta(["app"]) });
515
553
  }
554
+ registerMetricTile(server, client, backend);
516
555
  registerDraftTools(server, client);
517
556
  registerCustomChartAssets(server, client);
518
557
  for (const [uri, kind] of [[CHART_URI, "chart"], [KPI_URI, "kpi"]]) {
@@ -596,8 +635,53 @@ function registerDraftTools(server, client) {
596
635
  );
597
636
  save.update({ _meta: appMeta(["model", "app"]) });
598
637
  }
638
+ function registerMetricTile(server, client, backend) {
639
+ const tool = server.tool(
640
+ "render_metric_tile",
641
+ "Render an existing workspace metric without an embed, data app or datamart. Use metricRef from get_dashboard. Client-scoped metrics require their saved clientId. KPI shows the saved scalar, or the sum of one numeric chart series.",
642
+ {
643
+ workspaceName: z2.string().min(1).optional(),
644
+ workspaceId: z2.string().min(1).optional(),
645
+ metricId: z2.string().trim().min(1).max(256),
646
+ dashboardId: z2.string().trim().min(1).max(256).optional(),
647
+ display: z2.enum(["chart", "kpi"]).default("chart"),
648
+ clientId: z2.string().trim().min(1).max(256).optional(),
649
+ dateRange: dateRangeSchema.optional(),
650
+ context: z2.string().max(200).optional()
651
+ },
652
+ READ_ONLY,
653
+ async ({ workspaceName, workspaceId, metricId, dashboardId, display, clientId, dateRange }) => {
654
+ try {
655
+ if (!client.isHosted()) throw new Error("Use the hosted sign-in for workspace metrics. API-key connections retain list_widgets with an embed and clientId.");
656
+ const resolved = await client.request({
657
+ method: "POST",
658
+ path: "/api/v2/workspace/dashboard/widgets",
659
+ auth: "service",
660
+ body: { workspaceName, workspaceId, metricId, ...dashboardId ? { dashboardId } : {} }
661
+ });
662
+ if (resolved.clientId && clientId !== resolved.clientId) {
663
+ throw new Error(`CLIENT_REQUIRED: Pass clientId ${JSON.stringify(resolved.clientId)}, saved on this dashboard/workspace. No unscoped fallback is allowed.`);
664
+ }
665
+ const reference = {
666
+ dashboardId: resolved.dashboardId,
667
+ widgetId: metricId,
668
+ display,
669
+ ...clientId ? { clientId } : {}
670
+ };
671
+ const view = dateRange ? { dateRange } : {};
672
+ const page = pageSchema.parse(await backend.execute("render_chart", reference, view));
673
+ if (page.svg && (!/^\s*<svg\b/i.test(page.svg) || /<\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))) throw new Error("Unsafe SVG");
674
+ return chartResult(`${page.kind === "kpi" ? "KPI" : "Chart"} ready. ${page.rows.length} rows in this page.${page.description ? " " + page.description : ""}`, { ...page, reference, view });
675
+ } catch (error) {
676
+ if (error instanceof DatabrainApiError) return error.toToolResponse();
677
+ return { isError: true, content: [{ type: "text", text: error.message.startsWith("CLIENT_REQUIRED:") || error.message.startsWith("Use the hosted sign-in") ? error.message : "Chart unavailable. Check metric access and the saved tenant settings, then retry." }] };
678
+ }
679
+ }
680
+ );
681
+ tool.update({ _meta: appMeta(["model", "app"]) });
682
+ }
599
683
 
600
684
  export {
601
685
  registerChartApps
602
686
  };
603
- //# sourceMappingURL=chunk-NBFUKVWP.js.map
687
+ //# sourceMappingURL=chunk-233VGFHY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/apps/custom-chart-assets.ts","../src/apps/registry.ts","../src/apps/backend.ts","../src/apps/tile.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { DatabrainClient } from '../client/databrain-client.js';\nimport { CustomChartsApi } from '../api/custom-charts-api.js';\nimport { contentHash, validateBundle } from '../custom-chart/bundle.js';\nimport { READ_ONLY } from '../tools/annotations.js';\nimport { appMeta } from './feature.js';\nimport { bounded, MAX_CUSTOM_BUNDLE_CHARS, CUSTOM_BUNDLE_CHUNK_CHARS } from './contract.js';\nimport { readCustomChartBrowserRuntime } from '../custom-chart/assets.js';\n\n// Content-addressed immutable artifacts only. Authorization and the published pointer/hash\n// are fetched live before every cache lookup; no principal or API response is cached.\nconst assets = new Map<string, { text: string; expires: number }>();\n\n/** Chunk large code assets; every chunk rechecks the live published pointer and caller. */\nexport function registerCustomChartAssets(server: McpServer, client: DatabrainClient): void {\n const runtime = server.tool('load_custom_chart_runtime',\n 'Load the trusted app/embed sandbox adapter for a custom chart tile. App-only; no company data or chart code.',\n {}, READ_ONLY, async () => bounded({ content: [{ type: 'text' as const, text: 'Custom chart sandbox runtime.' }],\n _meta: { 'databrain/custom-chart-runtime': readCustomChartBrowserRuntime() } }));\n runtime.update({ _meta: appMeta(['app']) });\n const api = new CustomChartsApi(client);\n const tool = server.tool('load_custom_chart_bundle',\n 'Load one bounded chunk of the company published custom chart bundle for the tile. No draft version, credentials, or query rows.',\n { key: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/), offset: z.number().int().min(0).max(MAX_CUSTOM_BUNDLE_CHARS).default(0),\n contentHash: z.string().regex(/^[a-f0-9]{64}$/).optional() }, READ_ONLY,\n async ({ key, offset, contentHash: expected }) => {\n try {\n const detail = await api.get(key);\n if (detail.status !== 'published' || !detail.publishedVersionId) throw new Error('not published');\n const publishedHash = detail.versions?.find(version => version.id === detail.publishedVersionId)?.contentHash;\n const cached = publishedHash && /^[a-f0-9]{64}$/.test(publishedHash) ? assets.get(publishedHash) : undefined;\n let hash = publishedHash, asset = cached && cached.expires > Date.now() ? cached.text : undefined;\n if (asset === undefined) {\n const raw = await api.bundle(key, detail.publishedVersionId);\n const bundle = { ...raw, bundleCss: raw.bundleCss ?? undefined };\n if (validateBundle(bundle).length || bundle.manifest.id !== key) throw new Error('invalid bundle');\n hash = contentHash(bundle);\n if (publishedHash && publishedHash !== hash) throw new Error('published hash mismatch');\n // Only runtime artifacts cross the bridge; never any API envelope or extra fields.\n asset = JSON.stringify({ manifest: bundle.manifest, bundleJs: bundle.bundleJs, bundleCss: bundle.bundleCss ?? '', iconSvg: bundle.iconSvg });\n if (assets.size >= 4) assets.delete(assets.keys().next().value!);\n assets.set(hash, { text: asset, expires: Date.now() + 60_000 });\n }\n if (expected && hash !== expected) throw new Error('published version changed');\n if (offset > asset.length) throw new Error('offset');\n const chunk = asset.slice(offset, offset + CUSTOM_BUNDLE_CHUNK_CHARS);\n return bounded({ content: [{ type: 'text' as const, text: 'Published custom chart asset chunk.' }],\n structuredContent: { key, contentHash: hash, offset, chunk,\n nextOffset: offset + chunk.length < asset.length ? offset + chunk.length : null } });\n } catch {\n return { isError: true, content: [{ type: 'text' as const,\n text: `Could not load published custom:${key}. It may be unpublished, changed, or unavailable to this sign-in.` }] };\n }\n });\n tool.update({ _meta: appMeta(['app']) });\n}\n","import { registerCustomChartAssets } from './custom-chart-assets.js';\nimport { 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, dateRangeSchema, 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. Hosted reads use the signed-in workspace; embed references keep guest scope.',\n refresh_widget: 'Refresh a chart in the current workspace or 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 workspace or 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.${page.fallback ? ' ' + page.fallback : ''}${page.description ? ' ' + page.description : ''}`, {\n ...page, reference, view,\n });\n } catch (error) {\n if (error instanceof DatabrainApiError) return error.toToolResponse();\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 registerMetricTile(server, client, backend);\n registerDraftTools(server, client);\n registerCustomChartAssets(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, csp: { resourceDomains: [\"data:\", \"blob:\"], frameDomains: [\"blob:\"], connectDomains: [] } } } }],\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\n/** Model entry point for stored metrics; controls reuse the exact same reference and grant. */\nfunction registerMetricTile(server: McpServer, client: DatabrainClient, backend: ChartBackend): void {\n const tool = server.tool('render_metric_tile',\n 'Render an existing workspace metric without an embed, data app or datamart. Use metricRef from get_dashboard. ' +\n 'Client-scoped metrics require their saved clientId. KPI shows the saved scalar, or the sum of one numeric chart series.',\n { workspaceName: z.string().min(1).optional(), workspaceId: z.string().min(1).optional(),\n metricId: z.string().trim().min(1).max(256), dashboardId: z.string().trim().min(1).max(256).optional(),\n display: z.enum(['chart', 'kpi']).default('chart'), clientId: z.string().trim().min(1).max(256).optional(),\n dateRange: dateRangeSchema.optional(), context: z.string().max(200).optional() },\n READ_ONLY, async ({ workspaceName, workspaceId, metricId, dashboardId, display, clientId, dateRange }) => {\n try {\n if (!client.isHosted()) throw new Error('Use the hosted sign-in for workspace metrics. API-key connections retain list_widgets with an embed and clientId.');\n const resolved = await client.request<{ dashboardId: string; clientId: string | null }>({\n method: 'POST', path: '/api/v2/workspace/dashboard/widgets', auth: 'service',\n body: { workspaceName, workspaceId, metricId, ...(dashboardId ? { dashboardId } : {}) },\n });\n if (resolved.clientId && clientId !== resolved.clientId) {\n throw new Error(`CLIENT_REQUIRED: Pass clientId ${JSON.stringify(resolved.clientId)}, saved on this dashboard/workspace. No unscoped fallback is allowed.`);\n }\n const reference = { dashboardId: resolved.dashboardId, widgetId: metricId, display,\n ...(clientId ? { clientId } : {}) };\n const view = dateRange ? { dateRange } : {};\n const page = pageSchema.parse(await backend.execute('render_chart', reference, view));\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))) throw new Error('Unsafe SVG');\n return chartResult(`${page.kind === 'kpi' ? 'KPI' : 'Chart'} ready. ${page.rows.length} rows in this page.${page.description ? ' ' + page.description : ''}`, { ...page, reference, view });\n } catch (error) {\n if (error instanceof DatabrainApiError) return error.toToolResponse();\n return { isError: true, content: [{ type: 'text' as const, text: (error as Error).message.startsWith('CLIENT_REQUIRED:') || (error as Error).message.startsWith('Use the hosted sign-in')\n ? (error as Error).message : 'Chart unavailable. Check metric access and the saved tenant settings, then retry.' }] };\n }\n });\n tool.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 if ('dashboardId' in reference) {\n if (!client.isHosted()) throw new Error('Hosted sign-in required');\n return pageSchema.parse(await client.request<unknown>({\n method: 'POST', path: '/api/v2/workspace/dashboard/chart', auth: 'service',\n body: { action, dashboardId: reference.dashboardId, widgetId: reference.widgetId,\n ...(reference.clientId ? { clientId: reference.clientId } : {}),\n ...(reference.display ? { display: reference.display } : {}), ...view,\n pageSize: 50, width: 600, height: 320, animation: false },\n skipDataUnwrap: true,\n }));\n }\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(reference.dataAppName, 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","import { transformSync } from 'esbuild';\nimport { MAX_CUSTOM_BUNDLE_CHARS, CUSTOM_BUNDLE_CHUNK_CHARS } from './contract.js';\n\n/** No iframe imports. All requests use 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,early=[],closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null,dialect=null,shown=false,draft=null,rows=[],saved=null,attempt='',workspace=null,custom=null,customMode=false;\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 codes only; never log customer rows. */\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||customMode;$('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();custom?.update();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');\nif(p.customChart&&n)bits.push('calculations use this page');\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;\nconst selection=workspace?(name==='save_chart_as_dashboard'?{workspaceName:args.workspaceName||workspace.name}:{workspace:workspace.id}):{};\ntry{const result=await rpc('tools/call',{name,arguments:{...args,...selection,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();\nif(result?._meta?.['databrain/workspace'])workspace=result._meta['databrain/workspace'];\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++;custom?.dispose();custom=null;customMode=!!p.customChart;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.customChart){\nif(!rows.length){$('plot').dataset.state='off';status('No rows in this view.');}\nelse{status('Loading custom chart…');const rev=revision;\nconst box=document.createElement('div');box.style.width='100%';$('plot').append(box);\nloadCustom(result,p,box,rev);}\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.customChart&&p.value===undefined&&p.kind!=='dashboard';$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}\nasync function loadCustom(result,p,box,rev){try{\nif(!globalThis.DbnCustomChartRuntime){\nconst runtime=await rpc('tools/call',{name:'load_custom_chart_runtime',arguments:{context:'tile'}});\nif(rev!==revision)return;\nconst code=runtime._meta?.['databrain/custom-chart-runtime'];\nif(runtime.isError||typeof code!=='string'||code.length>64000)throw Error('runtime unavailable');\nconst script=document.createElement('script');script.textContent=code;document.head.append(script);script.remove();\nif(!globalThis.DbnCustomChartRuntime)throw Error('runtime unavailable');\n}\nlet bundle=result._meta?.['databrain/custom-chart-bundle'];\nif(!bundle){let text='',offset=0,hash;\nfor(let chunks=0;chunks<${Math.ceil(MAX_CUSTOM_BUNDLE_CHARS / CUSTOM_BUNDLE_CHUNK_CHARS)};chunks++){\nconst r=await rpc('tools/call',{name:'load_custom_chart_bundle',arguments:{context:'tile',key:p.customChart.key,offset,...(hash?{contentHash:hash}:{})}});\nif(rev!==revision)return;\nif(r.isError)throw Error('unavailable');const part=r.structuredContent;\nif(!part||part.key!==p.customChart.key||part.offset!==offset||typeof part.chunk!=='string'||part.chunk.length>${CUSTOM_BUNDLE_CHUNK_CHARS}||!part.chunk.length)throw Error('invalid chunk');\nif(hash&&hash!==part.contentHash)throw Error('changed');hash=part.contentHash;text+=part.chunk;\nif(text.length>${MAX_CUSTOM_BUNDLE_CHARS})throw Error('too large');if(part.nextOffset===null){bundle=JSON.parse(text);break}\nif(part.nextOffset!==offset+part.chunk.length)throw Error('invalid offset');offset=part.nextOffset;\n}\n}\nif(rev!==revision)return;if(!bundle||bundle.manifest.id!==p.customChart.key)throw Error('invalid bundle');\ncustom=DbnCustomChartRuntime.mount(box,bundle,p.customChart,p.rows||[],message=>{if(rev!==revision)return;status(message,!!message);if(message){$('data').open=true;$('data').setAttribute('data-only','')}resize()});\n}catch{if(rev===revision){status(result._meta?.['databrain/custom-chart-bundle']?'Draft preview could not open. Check the bindings and sandbox support. The rows are below.':'Published custom chart unavailable, or this host cannot open its sandbox. The rows are below.',1);$('data').open=true;$('data').setAttribute('data-only','');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++;custom?.dispose();custom=null;for(const p of pending.values()){clearTimeout(p.timer);p.reject(Error('closed'))}pending.clear();observer.disconnect();removeEventListener('message',receive);return}\nif(m.id!==undefined)return;\n/* A host can post the result before it answers initialize. Keep it for the replay. */\nif(!ready){if(early.length<8)early.push(m);return}\nhandle(m)}\nfunction handle(m){\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++;custom?.dispose();custom=null;busy=false;$('plot').dataset.state='off';status('Request cancelled.');controls()}}\naddEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();\n/* Many tiles mount at once while a reply streams. A host that attaches its listener\n after this frame's first initialize never sees it, and the tile then waits with no\n size and no body until a reload. Ask again with backoff; the first answer wins. */\nlet started=false,tries=0;\nfunction fail(){if(started)return;started=true;trouble('initialize-failed');status('This chart needs an MCP Apps host.',1)}\nfunction start(){if(started||closed)return;tries++;\nrpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})\n.then(r=>{if(started)return;if(r.protocolVersion!=='2026-01-26'){fail();return}started=true;\nfor(const p of pending.values())clearTimeout(p.timer);pending.clear();\nhost(r.hostContext||{});ready=true;notify(INIT,{});for(const m of early.splice(0))handle(m);resize()})\n.catch(e=>{if(e.code!==undefined||tries>=10)fail()});\nif(tries<10)setTimeout(start,Math.min(8000,400*2**tries))}\nstart();\n})();`;\n\n/** Compact the trusted bridge once, preserving the existing built-in result budget. */\nconst SERVED_SCRIPT = transformSync(TILE_SCRIPT, { minify: true, target: 'es2020' }).code;\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'; frame-src blob:; 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{overflow-wrap:anywhere;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{overflow-wrap:anywhere;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}}\nhtml[data-kind=table] #rows{max-height:360px;overflow:auto}html[data-kind=table] table{min-width:480px}\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":["z","z"]}
@@ -79,7 +79,7 @@ var DatabrainApiError = class extends Error {
79
79
  return "Hint: The requested resource was not found. Verify the ID or name is correct using the corresponding list tool.";
80
80
  }
81
81
  if (this.status === 409) {
82
- return "Hint: A resource with this name already exists. Use a different name or list existing resources first.";
82
+ return "Hint: The request conflicts with the current resource state. Read the error above, inspect the resource, and retry after resolving it.";
83
83
  }
84
84
  if (this.status === 422) {
85
85
  return "Hint: The request parameters are invalid. Check the required fields and their formats.";
@@ -143,7 +143,7 @@ var logger = {
143
143
  var package_default = {
144
144
  name: "@databrainhq/mcp-server",
145
145
  mcpName: "com.usedatabrain/mcp-server",
146
- version: "0.3.0-rc.11",
146
+ version: "0.3.0-rc.13",
147
147
  description: "MCP server for Databrain embedded analytics - connect databases, create datamarts, configure embeds, and generate frontend code",
148
148
  type: "module",
149
149
  bin: {
@@ -881,4 +881,4 @@ export {
881
881
  planUpload,
882
882
  scaffoldProject
883
883
  };
884
- //# sourceMappingURL=chunk-RHK62ZJ6.js.map
884
+ //# sourceMappingURL=chunk-AEQTEE5C.js.map