@databrainhq/mcp-server 0.3.0-rc.3 → 0.3.0-rc.5

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 (40) 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-4LIULTLT.js → chunk-4DYPD273.js} +204 -67
  9. package/dist/chunk-4DYPD273.js.map +1 -0
  10. package/dist/{chunk-DZOYK5BB.js → chunk-5IQ7MVUH.js} +1044 -1029
  11. package/dist/chunk-5IQ7MVUH.js.map +1 -0
  12. package/dist/chunk-PRE4CGOV.js +1903 -0
  13. package/dist/chunk-PRE4CGOV.js.map +1 -0
  14. package/dist/{chunk-OHTV24NG.js → chunk-V64ZQINV.js} +2 -2
  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-VPCN2764.js → main-Q6XT4O46.js} +34 -8
  32. package/dist/main-Q6XT4O46.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-4LIULTLT.js.map +0 -1
  36. package/dist/chunk-CX3CS62P.js.map +0 -1
  37. package/dist/chunk-DZOYK5BB.js.map +0 -1
  38. package/dist/main-VPCN2764.js.map +0 -1
  39. /package/dist/{chunk-OHTV24NG.js.map → chunk-V64ZQINV.js.map} +0 -0
  40. /package/dist/{test-only-B4EVUNFU.js.map → test-only-TJ3DQUYW.js.map} +0 -0
@@ -1,23 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ APPLY_TO_PRODUCTION_CONFIRMATION,
3
4
  APP_MIME,
4
5
  CHART_URI,
5
- DatabrainClient,
6
6
  KPI_URI,
7
- MCP_SERVER_VERSION,
8
7
  READ_ONLY,
8
+ RENDER_TYPES,
9
+ WRITE_CREATE,
9
10
  appMeta,
10
11
  chartResult,
11
12
  createServiceBundle,
13
+ draftPage,
14
+ draftSchema,
12
15
  enableChartApps,
13
16
  pageSchema,
14
17
  referenceSchema,
15
18
  registerAllPrompts,
16
19
  registerAllResources,
17
20
  registerAllTools,
18
- toolContext,
19
- viewSchema
20
- } from "./chunk-DZOYK5BB.js";
21
+ rowsSchema,
22
+ saveDraftAsDashboard,
23
+ viewSchema,
24
+ workspacesForDatamart
25
+ } from "./chunk-5IQ7MVUH.js";
26
+ import {
27
+ DatabrainApiError,
28
+ DatabrainClient,
29
+ MCP_SERVER_VERSION,
30
+ toolContext
31
+ } from "./chunk-PRE4CGOV.js";
21
32
 
22
33
  // src/apps/server.ts
23
34
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -87,7 +98,7 @@ function createChartBackend(client) {
87
98
  // src/apps/tile.ts
88
99
  var TILE_SCRIPT = String.raw`(()=>{
89
100
  const $=id=>document.getElementById(id),root=document.documentElement,pending=new Map();
90
- let seq=0,origin='*',ready=false,closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null,dialect=null,shown=false;
101
+ 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='';
91
102
  /* One lifecycle, two sets of names. Hosts ship either or both, so notifications go out
92
103
  under both and arrive under either. Same order in both rows: result, input, context. */
93
104
  const DASH=['ui/notifications/tool-result','ui/notifications/tool-input','ui/notifications/host-context-changed'],
@@ -98,7 +109,7 @@ DISPLAY=['ui/request-display-mode','requestDisplayMode'];
98
109
  function send(message){if(!closed)parent.postMessage({jsonrpc:'2.0',...message},origin)}
99
110
  function notify(names,params){for(const method of names)send({method,params})}
100
111
  function rpc(method,params){return new Promise((resolve,reject)=>{const id=++seq;
101
- const timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'))},30000);
112
+ const timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'))},65000);
102
113
  pending.set(id,{resolve,reject,timer});send({id,method,params})})}
103
114
  /* Try each alias while the host says it never heard of it; any other error stands. */
104
115
  async function ask(names,params){let last;
@@ -106,15 +117,28 @@ for(const method of names){try{return await rpc(method,params)}catch(error){last
106
117
  throw last}
107
118
  /* Fixed code only. The tile sits inside customer rows; a log is no place to leak them. */
108
119
  function trouble(code){send({method:'notifications/error',params:{code}})}
109
- function status(text,bad){const s=$('status');s.textContent=text;s.toggleAttribute('data-error',!!bad);if(bad)$('plot').dataset.state='off'}
120
+ /* One formatter for rail, table, KPI and answer line. Compact past 10K, grouped past 100,
121
+ two decimals below; null is an absence, so it prints as a dash and never as 0. */
122
+ function fmt(v){if(typeof v!=='number')return v==null||v===''?'\u2014':String(v);const a=Math.abs(v);
123
+ return new Intl.NumberFormat(navigator.language||'en',a>=1e4?{notation:'compact',maximumFractionDigits:1}:a>=100?{maximumFractionDigits:0}:{maximumFractionDigits:2}).format(v)}
124
+ function exact(el,v){el.title=typeof v==='number'?String(v):''}
125
+ /* The answer line: what this shows, and the headline value. Gated on carrying a number or
126
+ a count, so it never renders as a second title or an empty reserved slot. */
127
+ function answer(p){if(p.description)return p.description;const bits=[];
128
+ if(p.kind==='dashboard'){const n=p.widgets?.length||0;return n?n+(n===1?' chart':' charts')+' on this dashboard':''}
129
+ const [dim,measure]=p.columns||[];const pts=(p.points||[]).filter(q=>typeof q.value==='number');
130
+ if(pts.length<2)return '';if(dim&&measure&&!p.draft)bits.push(measure+' by '+dim);
131
+ const 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 ')}
132
+ function 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'}
110
133
  function paint(){const img=$('chart-image');if(!img||!rawSvg)return;
111
134
  const c=getComputedStyle(document.body).color,mix=pc=>'color-mix(in srgb,'+c+' '+pc+'%,transparent)';
112
- img.src='data:image/svg+xml,'+encodeURIComponent(rawSvg.replace(/<\/svg>\s*$/i,'<style>text{fill:'+c+'!important}'
113
- +'[stroke="#dbdee4"]{stroke:'+mix(13)+'!important}[stroke="#54555a"]{stroke:'+mix(30)+'!important}'
135
+ img.src='data:image/svg+xml,'+encodeURIComponent(rawSvg.replace(/<\/svg>\s*$/i,'<style>text{fill:'+mix(72)+'!important}'
136
+ +'[stroke="#dbdee4"],[stroke="#f1f1f2" i]{stroke:'+mix(13)+'!important}[stroke="#54555a"],[stroke="#d4d4d8" i]{stroke:'+mix(30)+'!important}'
114
137
  +'</style></svg>'))}
115
138
  function controls(){document.querySelectorAll('button,input,select').forEach(e=>{e.disabled=busy});
116
139
  $('refresh').hidden=!ref;$('next').hidden=!next;$('range').hidden=!ref;$('plot').toggleAttribute('data-busy',busy);
117
- $('expand').hidden=!shown||!context.availableDisplayModes?.includes('fullscreen');
140
+ $('save').hidden=!draft||!!saved||!$('form').hidden;
141
+ $('expand').hidden=!shown||root.dataset.kind==='dashboard'||!context.availableDisplayModes?.includes('fullscreen');
118
142
  root.toggleAttribute('data-deep',context.displayMode==='fullscreen'||$('expand').hidden)}
119
143
  function host(change){context={...context,...change};if(change.theme==='dark'||change.theme==='light')root.style.colorScheme=change.theme;
120
144
  const vars=change.styles?.variables||{};for(const [key,value] of Object.entries(vars)){
@@ -125,7 +149,7 @@ const n=insets[edge];if(Number.isFinite(n))root.style.setProperty('--safe-'+edge
125
149
  root.dataset.mode=context.displayMode==='fullscreen'?'full':'inline';
126
150
  $('expand').textContent=context.displayMode==='fullscreen'?'Back to conversation':'Expand';paint();controls()}
127
151
  function scope(p,n){const bits=[];
128
- if(p.kind==='chart'||p.kind==='table')bits.push(n?(n===1?'1 row':n+' rows'):'No rows in this view');
152
+ if(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');
129
153
  if(view.dateRange)bits.push(view.dateRange.from+' to '+view.dateRange.to);
130
154
  if(next)bits.push('more pages available');
131
155
  $('scope').textContent=bits.join(' · ')}
@@ -136,33 +160,41 @@ const t=document.createElement('table');
136
160
  const num=name=>rows.some(r=>typeof r[name]==='number');
137
161
  const 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)}
138
162
  const head=document.createElement('thead');head.append(header);t.append(head);const body=document.createElement('tbody');
139
- for(const row of rows.slice(0,50)){const tr=document.createElement('tr');for(const key of columns){const td=document.createElement('td');td.textContent=String(row[key]??'');if(num(key))td.className='n';tr.append(td)}body.append(tr)}t.append(body);$('rows').append(t)}
163
+ for(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)}
140
164
  async function display(mode){try{const answer=await ask(DISPLAY,{mode});
141
165
  host({displayMode:answer.mode||context.displayMode});if(root.dataset.mode==='full'&&!$('data').hidden)$('data').open=true;resize()}
142
- catch{status('Expanded view is unavailable in this host.',1)}}
166
+ catch{status('Expand is unavailable in this host.',1)}}
143
167
  function wait(){busy=true;if(!$('plot').firstChild)$('plot').dataset.state='wait';status('Loading chart…');controls()}
144
- async function call(name,extra={}){if(!ready||!ref||busy)return;wait();const rev=++revision;
145
- try{const result=await rpc('tools/call',{name,arguments:{reference:ref,view:{...view,...extra},context:'User is interacting with the chart tile.'}});
146
- if(rev===revision)render(result)}catch{if(rev===revision){trouble('tool-call-failed');status('Could not load the chart. Select Refresh to try again.',1)}}finally{busy=false;controls()}}
168
+ async function call(name,extra={}){if(!ready||!ref||busy)return;run(name,{reference:ref,view:{...view,...extra}})}
169
+ /* The draft tools take the draft and the rows on screen instead of a reference. */
170
+ async function run(name,args){if(!ready||busy)return;wait();const rev=++revision;
171
+ try{const result=await rpc('tools/call',{name,arguments:{...args,context:'tile'}});
172
+ if(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()}}
173
+ function 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)}
147
174
  function render(result){busy=false;controls();
148
- if(result?.isError){trouble('tool-returned-error');shown=false;controls();
149
- status(ref?'Chart unavailable. Check your access to this dashboard, then select Refresh.'
150
- :'Chart unavailable. Ask for this chart again to retry.',1);return}
175
+ const 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)}
176
+ else if(sp.choices?.length){choose(sp.choices);status('Choose a workspace, then save again.',1)}
177
+ else status((result.content?.[0]?.text)||'Could not save. Try again.',1);controls();resize();return}
178
+ if(result?.isError&&draft&&$('form').hidden===false){$('plot').dataset.state='ready';status((result.content?.[0]?.text)||'Could not save. Try again.',1);controls();return}
179
+ if(result?.isError){trouble('tool-returned-error');
180
+ if(shown&&$('plot').firstChild){stale();controls();return}shown=false;controls();
181
+ status(ref?'Chart unavailable. Check your access to this dashboard, then Refresh.'
182
+ :'Chart unavailable. Ask again to retry.',1);return}
151
183
  const p=result?.structuredContent;if(!p||typeof p!=='object'){shown=false;controls();status('No chart data came back for this result.',1);return}
152
184
  revision++;shown=true;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();delete $('rail').dataset.value;
153
- ref=p.reference||null;view=p.view||{};next=p.nextCursor||null;
154
- $('title').textContent=p.title||'Analytics';$('description').textContent=p.description||'';
185
+ ref=p.reference||null;view=p.view||{};next=p.nextCursor||null;draft=p.draft||null;rows=p.rows||[];saved=null;$('form').hidden=true;
186
+ $('title').textContent=p.title||'Analytics';$('description').textContent=answer(p);root.dataset.kind=p.kind;$('choices').toggleAttribute('data-grid',p.kind==='dashboard');
155
187
  if(p.kind==='dashboard'){$('plot').dataset.state='off';
156
188
  for(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)}
157
- status(p.widgets?.length?'':'This dashboard has no charts yet. Add one in Databrain, then ask again.');
189
+ status(p.widgets?.length?'':'No charts on this dashboard yet. Add one in Databrain.');
158
190
  }else if(p.kind==='kpi'){
159
- const number=document.createElement('p');number.className='kpi';number.textContent=String(p.value??'—');$('plot').append(number);status('');
191
+ const number=document.createElement('p');number.className='kpi';number.textContent=fmt(p.value);exact(number,p.value);$('plot').append(number);status('');
160
192
  }else if(p.svg){
161
193
  const box=document.createElement('div');box.className='chart';box.style.aspectRatio=p.width+'/'+p.height;
162
- const img=document.createElement('img');img.id='chart-image';rawSvg=p.svg;img.alt=p.title||'Chart';img.onerror=()=>{status('The chart image could not be drawn. The rows behind it are below.',1);$('data').open=$('data').toggleAttribute('data-only',true)};box.append(img);
163
- for(const point of p.points||[]){const b=document.createElement('button');b.className='point';b.setAttribute('aria-label',point.label+': '+point.value);b.setAttribute('aria-pressed','false');
194
+ const 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);
195
+ for(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');
164
196
  b.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+'%';
165
- const hover=()=>{$('vlabel').textContent=point.label;$('vnum').textContent=point.value;$('rail').dataset.value='1'};b.onmouseenter=hover;b.onfocus=hover;
197
+ const hover=()=>{$('vlabel').textContent=point.label;$('vnum').textContent=fmt(point.value);exact($('vnum'),point.value);$('rail').dataset.value='1'};b.onmouseenter=hover;b.onfocus=hover;
166
198
  b.onclick=()=>{box.querySelectorAll('.point').forEach(e=>e.setAttribute('aria-pressed','false'));b.setAttribute('aria-pressed','true');hover();
167
199
  $('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);
168
200
  status('');
@@ -170,12 +202,20 @@ status('');
170
202
  // ask_question returns a table by design; calling that a missing chart called success a failure.
171
203
  $('plot').dataset.state='off';status('');
172
204
  }else{$('plot').dataset.state='off';
173
- status('Nothing to show for this view. Try a wider date range, then select Refresh.',1)}
205
+ status('Nothing to show. Try a wider date range, then Refresh.',1)}
206
+ if(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));
207
+ b.onclick=()=>run('preview_chart',{draft,rows,chartType:t});$('choices').append(b)}$('choices').dataset.chips='1'}else delete $('choices').dataset.chips;
208
+ if(p.previewError)status('Cannot draw this as '+(p.draft?.chartType||'a chart')+' ('+p.previewError+'). Showing the rows.',1);
174
209
  paint();table(p.rows||[],p.columns||Object.keys(p.rows?.[0]||{}));scope(p,p.rows?.length||0);
175
210
  const only=!p.svg&&p.value===undefined&&p.kind!=='dashboard';$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}
176
211
  $('refresh').onclick=()=>call('refresh_widget',{cursor:undefined});$('next').onclick=()=>call('render_chart',{cursor:next});
212
+ function 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()}
213
+ $('save').onclick=()=>{attempt=crypto.randomUUID();$('dtitle').value=draft?.title||'';choose(draft?.workspaces||[])};
214
+ $('cancel').onclick=()=>{$('form').hidden=true;status('');controls();resize()};
215
+ $('commit').onclick=()=>{const t=$('dtitle').value.trim();if(!t){status('Name the dashboard.',1);return}
216
+ run('save_chart_as_dashboard',{draft,chartType:draft.chartType,dashboardTitle:t,workspaceName:$('wsrow').hidden?undefined:$('ws').value,attemptId:attempt,confirm:'APPLY_TO_PRODUCTION'})};
177
217
  $('expand').onclick=()=>display(context.displayMode==='fullscreen'?'inline':'fullscreen');
178
- $('dates').onclick=()=>{const from=$('from').value,to=$('to').value;if(!from||!to||from>to){status('Choose a start date on or before the end date.',1);return}call('change_date_range',{dateRange:{from,to},cursor:undefined,drillId:undefined})};
218
+ $('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})};
179
219
  function receive(event){if(event.source!==parent||closed)return;const m=event.data;
180
220
  if(!m||Array.isArray(m)||m.jsonrpc!=='2.0'||(origin!=='*'&&event.origin!==origin))return;
181
221
  if(!m.method&&pending.has(m.id)&&('result'in m||'error'in m)){if(origin==='*'&&event.origin!=='null')origin=event.origin;
@@ -195,14 +235,15 @@ if(slot===0)render(kind==='snake'?m.params&&m.params.result:m.params);
195
235
  else if(slot===1)wait();
196
236
  else{host(m.params||{});resize()}
197
237
  return}
198
- if(m.method==='ui/notifications/tool-cancelled'){revision++;busy=false;$('plot').dataset.state='off';status('Chart request cancelled.');controls()}}
238
+ if(m.method==='ui/notifications/tool-cancelled'){revision++;busy=false;$('plot').dataset.state='off';status('Request cancelled.');controls()}}
199
239
  addEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();
200
- rpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain chart tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})
240
+ rpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})
201
241
  .then(r=>{if(r.protocolVersion!=='2026-01-26')throw Error('version');host(r.hostContext||{});ready=true;notify(INIT,{});resize()})
202
- .catch(()=>{trouble('initialize-failed');status('This chart needs a host that supports MCP Apps.',1)});
242
+ .catch(()=>{trouble('initialize-failed');status('This chart needs an MCP Apps host.',1)});
203
243
  })();`;
244
+ var SERVED_SCRIPT = TILE_SCRIPT.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "").replace(/\n{2,}/g, "\n");
204
245
  function tileHtml(kind = "chart") {
205
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
246
+ return strip(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
206
247
  <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'">
207
248
  <title>Databrain ${kind} tile</title><style id="hostfont"></style><style>
208
249
  /* Host tokens are the Claude MCP Apps style-variable names. Every one carries a fallback:
@@ -213,10 +254,10 @@ function tileHtml(kind = "chart") {
213
254
  --dbn-edge:var(--color-border-secondary,color-mix(in srgb,currentColor 20%,transparent));
214
255
  --dbn-line:var(--color-border-tertiary,color-mix(in srgb,currentColor 9%,transparent));
215
256
  --dbn-accent:var(--color-text-info,LinkText);--dbn-ring:var(--color-ring-primary,var(--dbn-accent));
216
- --dbn-bad:var(--color-text-danger,var(--dbn-text));--dbn-bad-edge:var(--color-border-danger,currentColor);
257
+ --dbn-bad:var(--color-text-danger,var(--dbn-text));
217
258
  --dbn-quiet:color-mix(in srgb,currentColor 6%,transparent);
218
- --bw:var(--border-width-regular,1px);--r-s:var(--border-radius-sm,6px);--r-m:var(--border-radius-md,8px);--r-l:var(--border-radius-lg,10px);
219
- --med:var(--font-weight-medium,500);--semi:var(--font-weight-semibold,600);
259
+ --bw:var(--border-width-regular,1px);--r-s:var(--border-radius-sm,6px);--r-m:var(--border-radius-md,8px);
260
+ --med:var(--font-weight-medium,500);--semi:var(--font-weight-semibold,600);--xs:var(--font-text-xs-size,12px);
220
261
  /* One curve. Quick / standard / display-mode. Corporate: no overshoot. */
221
262
  --e:cubic-bezier(.2,0,0,1);--q:120ms;--s:300ms;--l:400ms}
222
263
  *{box-sizing:border-box}
@@ -224,68 +265,82 @@ body{margin:0;padding:var(--safe-top,0px) var(--safe-right,0px) var(--safe-botto
224
265
  font:var(--font-text-sm-size,14px)/var(--font-text-sm-line-height,1.4) var(--font-sans,system-ui)}
225
266
  main{padding:20px;max-width:1200px;margin:auto;transition:padding var(--l) var(--e)}
226
267
  header{display:flex;align-items:start;justify-content:space-between;gap:12px}header>div{min-width:0}
268
+ .acts{display:flex;gap:6px;flex:none;align-items:center}
227
269
  h1{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)}
228
- p{margin:6px 0;color:var(--dbn-mute);overflow-wrap:anywhere}#expand{flex:none}
229
- button,input{font:inherit;color:inherit;background:var(--dbn-bg);border:var(--bw) solid var(--dbn-edge);border-radius:var(--r-s);padding:7px 10px}
270
+ p{margin:6px 0;color:var(--dbn-mute);overflow-wrap:anywhere}
271
+ #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}
272
+ button,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}
230
273
  button{cursor:pointer;transition:background-color var(--q) var(--e),border-color var(--q) var(--e)}
231
274
  input{max-width:160px;min-width:0}button:hover:not(:disabled){background:var(--dbn-quiet)}button:disabled{color:var(--dbn-faint);cursor:default}
232
275
  button:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--dbn-ring);outline-offset:2px}
233
- #choices button{text-align:left;border-color:transparent;border-bottom-color:var(--dbn-line);border-radius:0;padding:10px 2px}
234
- #choices button:first-child{border-top:var(--bw) solid var(--dbn-line)}
235
- #choices button:hover:not(:disabled){background:var(--dbn-quiet);border-radius:var(--r-s)}
276
+ /* The picker is a scannable grid of quiet chips: 26 charts fit in nine rows, not a wall. */
277
+ #choices[data-grid]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:6px;margin-top:12px}
278
+ #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}
279
+ #choices[data-grid] button:hover:not(:disabled){background:var(--dbn-quiet);border-color:var(--dbn-edge)}
236
280
  /* One primary per state: the drill pill, and Next page when there is a next page. */
237
281
  #choices button.primary,#next{flex:none;text-align:center;border:var(--bw) solid var(--dbn-accent);
238
282
  border-radius:var(--r-s);padding:7px 10px;font-weight:var(--med);color:var(--dbn-accent)}
239
283
  #choices button.primary{align-self:flex-start;margin-top:10px}
240
284
  /* Refresh is a re-fetch, not a choice. Quiet it so it stops reading as a fifth chart. */
241
- #refresh{border-color:transparent;color:var(--dbn-mute);padding-left:2px;padding-right:8px}
285
+ #refresh{border-color:transparent;color:var(--dbn-mute);padding:7px 8px}
242
286
  #refresh:hover:not(:disabled){color:var(--dbn-text);background:none;text-decoration:underline}
243
287
  #status:empty{margin:0}
244
- #status[data-error]{color:var(--dbn-bad);font-weight:var(--med);
245
- border-left:2px solid var(--color-border-danger,color-mix(in srgb,currentColor 40%,transparent));
246
- background:var(--color-background-danger,color-mix(in srgb,currentColor 5%,transparent));
247
- border-radius:0 var(--r-m) var(--r-m) 0;padding:10px 12px;margin:10px 0}
288
+ #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)}
289
+ #status[data-ok]{color:var(--color-text-success,var(--dbn-text));border-color:var(--color-border-success,color-mix(in srgb,currentColor 40%,transparent))}
290
+ /* "Show as": one row of quiet chips, the current type pressed. Never a second title. */
291
+ #choices[data-chips]{flex-direction:row;flex-wrap:wrap;gap:6px;margin-top:10px}
292
+ .chip{border-color:var(--dbn-line);border-radius:999px;padding:5px 11px;font-size:var(--xs);color:var(--dbn-mute)}
293
+ .chip:hover:not(:disabled){color:var(--dbn-text);border-color:var(--dbn-edge)}
294
+ .chip[aria-pressed=true]{color:var(--dbn-text);border-color:var(--dbn-text);background:var(--dbn-quiet)}
295
+ /* Save is the one commit on a draft, so it is the one filled button in the tile. */
296
+ #save,#commit{border-color:var(--dbn-accent);background:var(--dbn-accent);color:var(--dbn-bg);font-weight:var(--med)}
297
+ #save:hover:not(:disabled),#commit:hover:not(:disabled){background:color-mix(in srgb,var(--dbn-accent) 85%,var(--dbn-text))}
298
+ #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}
299
+ [hidden]{display:none!important}#form label{flex-direction:column;align-items:stretch;gap:4px;font-size:var(--xs)}
300
+ #form input,#form select{max-width:none;width:100%;background:color-mix(in srgb,currentColor 4%,var(--dbn-bg))}
301
+ #wsone{color:var(--dbn-mute);font-size:var(--xs)}#wsone:empty{display:none}
302
+ #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))}
248
303
  /* Reserved plot. The skeleton is a static bar silhouette matching the 15:8 backend render:
249
304
  Claude's guidance asks for a skeleton that matches the final layout, and bans nothing here,
250
305
  but shimmer is banned by our own motion budget - so this paints once and never repaints. */
251
- #plot{position:relative;transition:opacity var(--q) var(--e)}#plot[data-busy]{opacity:.55}
306
+ #plot{position:relative;transition:opacity var(--q) var(--e)}#plot[data-busy],#plot[data-state=stale]{opacity:.55}
252
307
  #plot[data-state=wait]{aspect-ratio:15/8}
253
308
  #plot[data-state=wait]::before,#plot[data-state=ready]::before{content:'';position:absolute;pointer-events:none;inset:16% 6% 13%;
254
309
  border-bottom:var(--bw) solid var(--dbn-line);transition:opacity var(--s) var(--e);
255
- background:linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 0 100%/11% 44% no-repeat,
256
- linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 22% 100%/11% 71% no-repeat,
257
- linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 44% 100%/11% 57% no-repeat,
258
- linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 66% 100%/11% 86% no-repeat,
259
- linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 88% 100%/11% 33% no-repeat}
310
+ --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}
260
311
  #plot[data-state=ready]::before{opacity:0}
261
312
  .chart{position:relative;width:100%;margin:12px 0}.chart img{width:100%;height:100%;display:block}
262
- .point{position:absolute;padding:0;border:0;background:transparent;border-radius:var(--border-radius-xs,4px);
263
- outline:2px solid transparent;outline-offset:1px;transition:background-color var(--q) var(--e),outline-color var(--q) var(--e)}
264
- .point:hover{background:color-mix(in srgb,currentColor 12%,transparent)}
265
- .point[aria-pressed=true]{outline-color:var(--dbn-accent);background:color-mix(in srgb,var(--dbn-accent) 18%,transparent)}
313
+ /* A point's geometry comes from the renderer and can be 8px; the hit area must not.
314
+ ::after pads it to at least 24px; ::before is the marker that says "you can pick this". */
315
+ .point{position:absolute;padding:0;border:0;background:transparent;border-radius:50%}
316
+ .point::after{content:'';position:absolute;inset:-8px}
317
+ .point::before{content:'';position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px;border-radius:50%;
318
+ 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)}
319
+ .point:hover::before,.point:focus-visible::before{opacity:.7;transform:none}
320
+ .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)}
266
321
  .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)}
267
322
  /* The readout rail. At rest it states the scope of what is on screen; on selection it
268
323
  becomes the value. One slot, two jobs, so selecting a bar never reflows the tile. */
269
324
  #rail{display:flex;align-items:center;min-height:2.5em;margin:2px 0 0}
270
325
  #scope,#value{margin:0;display:none;overflow-wrap:anywhere}
271
- #rail:not([data-value]) #scope{display:block;color:var(--dbn-mute);font-size:var(--font-text-xs-size,12px)}
326
+ #rail:not([data-value]) #scope{display:block;color:var(--dbn-mute);font-size:var(--xs)}
272
327
  #rail:not([data-value]):has(#scope:empty){display:none}
273
328
  #rail[data-value] #value{display:flex;flex-direction:column;gap:1px;line-height:1.15}
274
- #vlabel{color:var(--dbn-mute);font-size:var(--font-text-xs-size,12px)}
329
+ #vlabel{color:var(--dbn-mute);font-size:var(--xs)}
275
330
  #vnum{font-size:var(--font-heading-md-size,18px);font-weight:var(--semi);letter-spacing:-.015em;font-variant-numeric:tabular-nums}
276
331
  [data-mode=full] #vnum{font-size:var(--font-heading-xl-size,24px)}
277
332
  footer,.dates{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
278
- #choices{display:flex;flex-direction:column;align-items:stretch;margin:10px 0 0}footer{margin-top:12px}.dates{margin:8px 0 2px}
333
+ #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}
279
334
  label{display:flex;align-items:center;gap:6px;color:var(--dbn-mute)}
280
- #range{margin-top:10px}#data{margin-top:2px}#data[hidden],#range[hidden]{display:none}
335
+ #range{margin-top:10px}#data{margin-top:2px}
281
336
  html:not([data-deep]) #range,html:not([data-deep]) #data:not([data-only]){display:none}
282
- summary{cursor:pointer;color:var(--dbn-mute);font-size:var(--font-text-xs-size,12px);padding:5px 0;width:fit-content;transition:color var(--q) var(--e)}summary:hover{color:var(--dbn-text)}
337
+ summary{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)}
283
338
  #rows{overflow:auto;max-height:260px;border-radius:var(--r-s)}
284
- table{border-collapse:collapse;width:100%;font-size:var(--font-text-xs-size,12px)}
339
+ table{border-collapse:collapse;width:100%;font-size:var(--xs)}
285
340
  th,td{text-align:left;padding:8px;border-bottom:var(--bw) solid var(--dbn-line);overflow-wrap:anywhere}
286
341
  td.n,th.n{text-align:right;font-variant-numeric:tabular-nums}
287
342
  th{font-weight:var(--semi);position:sticky;top:0;background:var(--dbn-bg);color:var(--dbn-mute);
288
- font-size:var(--font-text-xs-size,12px);letter-spacing:.02em;text-transform:uppercase}
343
+ font-size:var(--xs);letter-spacing:.02em;text-transform:uppercase}
289
344
  /* Inline to fullscreen is a layout morph, not a new screen: main widens over 400ms on the
290
345
  one curve and every dependent box follows it. Fullscreen is the detailed view, so the
291
346
  table loses its inline height cap. */
@@ -306,12 +361,18 @@ header,#status,#plot{animation:rise var(--s) var(--e) both}
306
361
  #rail,#choices,footer,#range,#data{animation:rise var(--s) var(--e) 60ms both}
307
362
  .chart,.kpi{animation:land var(--s) var(--e) both}
308
363
  @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}
309
- </style></head><body><main id="tile"><header><div><h1 id="title">Analytics</h1><p id="description"></p></div><button id="expand" hidden>Expand</button></header>
364
+ </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>
310
365
  <p id="status" role="status">Loading chart\u2026</p><div id="plot" data-state="wait"></div>
311
366
  <div id="rail"><p id="scope"></p><p id="value"><span id="vlabel"></span><b id="vnum"></b></p></div><div id="choices"></div>
312
- <footer><button id="refresh" hidden>Refresh</button><button id="next" hidden>Next page</button></footer>
367
+ <footer><button id="save" hidden>Save as dashboard</button><button id="next" hidden>Next page</button></footer>
368
+ <form id="form" hidden onsubmit="return false"><label>Dashboard name<input id="dtitle" type="text" maxlength="200" autocomplete="off"></label>
369
+ <label id="wsrow" hidden>Workspace<select id="ws"></select></label><p id="wsone"></p>
370
+ <div class="dates"><button id="commit" type="button">Save to Databrain</button><button id="cancel" type="button">Cancel</button></div></form>
313
371
  <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>
314
- <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>${TILE_SCRIPT}</script></body></html>`;
372
+ <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>`);
373
+ }
374
+ function strip(html) {
375
+ return html.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\n{2,}/g, "\n").replace(/<style>([\s\S]*?)<\/style><\/head>/, (_m, css) => `<style>${css.replace(/;}/g, "}").replace(/\n/g, "")}</style></head>`);
315
376
  }
316
377
 
317
378
  // src/apps/registry.ts
@@ -352,12 +413,88 @@ function registerChartApps(server, client, backend = createChartBackend(client))
352
413
  });
353
414
  tool.update({ _meta: appMeta(["app"]) });
354
415
  }
416
+ registerDraftTools(server, client);
355
417
  for (const [uri, kind] of [[CHART_URI, "chart"], [KPI_URI, "kpi"]]) {
356
418
  server.resource(`Databrain ${kind} tile`, uri, { mimeType: APP_MIME, description: `Interactive ${kind} using host theme and scoped server tools` }, async () => ({
357
419
  contents: [{ uri, mimeType: APP_MIME, text: tileHtml(kind), _meta: { ui: { prefersBorder: true } } }]
358
420
  }));
359
421
  }
360
422
  }
423
+ function registerDraftTools(server, client) {
424
+ const preview = server.tool(
425
+ "preview_chart",
426
+ "Redraw an unsaved ask_question answer as another chart type, from the rows already on screen. No query runs.",
427
+ {
428
+ draft: draftSchema,
429
+ rows: rowsSchema,
430
+ chartType: z.enum(RENDER_TYPES),
431
+ context: z.string().max(200).optional().describe("Chart interaction context")
432
+ },
433
+ READ_ONLY,
434
+ async ({ draft, rows, chartType }) => {
435
+ try {
436
+ const page = await draftPage(client, draft, rows, chartType);
437
+ return chartResult(`${page.kind === "table" ? "Table" : "Chart"} ready as ${chartType}.`, page);
438
+ } catch {
439
+ return { isError: true, content: [{ type: "text", text: "Chart preview unavailable. Ask the question again to retry." }] };
440
+ }
441
+ }
442
+ );
443
+ preview.update({ _meta: appMeta(["app"]) });
444
+ const save = server.tool(
445
+ "save_chart_as_dashboard",
446
+ `Save an ask_question answer: creates a workspace metric from its SQL and a new dashboard holding it. Production write; requires confirm: "${APPLY_TO_PRODUCTION_CONFIRMATION}". A workspaceName is needed when the datamart backs more than one workspace.`,
447
+ {
448
+ draft: draftSchema,
449
+ chartType: z.enum(RENDER_TYPES),
450
+ dashboardTitle: z.string().trim().min(1).max(200),
451
+ workspaceName: z.string().trim().min(1).max(256).optional(),
452
+ attemptId: z.string().trim().min(1).max(64).optional().describe("Identifies one save attempt. Reuse it to retry safely; omit or change it to save again on purpose."),
453
+ confirm: z.string().optional(),
454
+ context: z.string().max(200).optional().describe("Chart interaction context")
455
+ },
456
+ WRITE_CREATE,
457
+ async ({ draft, chartType, dashboardTitle, workspaceName, attemptId, confirm }) => {
458
+ if (confirm !== APPLY_TO_PRODUCTION_CONFIRMATION) {
459
+ return { isError: true, content: [{
460
+ type: "text",
461
+ text: `Saving writes to production. Retry with confirm: "${APPLY_TO_PRODUCTION_CONFIRMATION}".`
462
+ }] };
463
+ }
464
+ const choices = await workspacesForDatamart(client, draft.datamartName, "authorize");
465
+ const refuse = (text) => ({
466
+ isError: true,
467
+ content: [{ type: "text", text }],
468
+ structuredContent: { kind: "save", ok: false, choices }
469
+ });
470
+ const target = workspaceName ? choices.find((w) => w.trim().toLowerCase() === workspaceName.toLowerCase()) : choices.length === 1 ? choices[0] : void 0;
471
+ if (workspaceName && !target) {
472
+ return refuse(`Workspace "${workspaceName}" cannot run this datamart's SQL. Choose one of: ${choices.join(", ")}.`);
473
+ }
474
+ if (!target) {
475
+ return refuse(choices.length ? `Choose a workspace: ${choices.join(", ")}.` : `No workspace is built on datamart "${draft.datamartName}". Create one in Databrain, then save again.`);
476
+ }
477
+ try {
478
+ const saved = await saveDraftAsDashboard(client, draft, { chartType, dashboardTitle, workspaceName: target, attemptId });
479
+ return chartResult(
480
+ `Saved "${saved.title}" to workspace ${saved.workspaceName} (dashboard ${saved.dashboardId}, metric ${saved.metricId}).`,
481
+ { kind: "save", ok: true, ...saved, draft: { ...draft, chartType } }
482
+ );
483
+ } catch (error) {
484
+ const code = error instanceof DatabrainApiError ? error.code : "SAVE_FAILED";
485
+ return {
486
+ isError: true,
487
+ content: [{
488
+ type: "text",
489
+ text: `Could not save (${code}). Check the workspace name and your access, then try again.`
490
+ }],
491
+ structuredContent: { kind: "save", ok: false, code }
492
+ };
493
+ }
494
+ }
495
+ );
496
+ save.update({ _meta: appMeta(["model", "app"]) });
497
+ }
361
498
 
362
499
  // src/apps/server.ts
363
500
  function createChartAppServer(config, backend) {
@@ -376,4 +513,4 @@ function createChartAppServer(config, backend) {
376
513
  export {
377
514
  createChartAppServer
378
515
  };
379
- //# sourceMappingURL=chunk-4LIULTLT.js.map
516
+ //# sourceMappingURL=chunk-4DYPD273.js.map
@@ -0,0 +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":[]}