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

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.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createChartAppServer
4
- } from "../chunk-NDVW3ZSJ.js";
5
- import "../chunk-7DS7OLWP.js";
4
+ } from "../chunk-4LIULTLT.js";
5
+ import "../chunk-DZOYK5BB.js";
6
6
  export {
7
7
  createChartAppServer
8
8
  };
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadConfig
4
- } from "../chunk-RF3HTNO7.js";
4
+ } from "../chunk-OHTV24NG.js";
5
5
  import {
6
6
  createChartAppServer
7
- } from "../chunk-NDVW3ZSJ.js";
8
- import "../chunk-7DS7OLWP.js";
7
+ } from "../chunk-4LIULTLT.js";
8
+ import "../chunk-DZOYK5BB.js";
9
9
 
10
10
  // src/apps/stdio.ts
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -6,6 +6,7 @@ import {
6
6
  KPI_URI,
7
7
  MCP_SERVER_VERSION,
8
8
  READ_ONLY,
9
+ appMeta,
9
10
  chartResult,
10
11
  createServiceBundle,
11
12
  enableChartApps,
@@ -16,7 +17,7 @@ import {
16
17
  registerAllTools,
17
18
  toolContext,
18
19
  viewSchema
19
- } from "./chunk-7DS7OLWP.js";
20
+ } from "./chunk-DZOYK5BB.js";
20
21
 
21
22
  // src/apps/server.ts
22
23
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -43,7 +44,11 @@ function createChartBackend(client) {
43
44
  const view = viewSchema.parse(rawView);
44
45
  const app = await services.widgetService.resolveDataAppName(void 0, reference.embedId);
45
46
  if (!app.ok) throw new Error("Chart scope unavailable");
46
- const scope = await services.widgetService.ensureTenantDashboardScope({ ...reference, dataAppName: app.value });
47
+ const scope = await services.widgetService.ensureReadableDashboardScope({
48
+ embedId: reference.embedId,
49
+ clientId: reference.clientId,
50
+ dataAppName: app.value
51
+ });
47
52
  if (!scope.ok) throw new Error("Chart scope denied");
48
53
  const { token } = await services.guestTokenService.generateToken({
49
54
  clientId: reference.clientId,
@@ -82,16 +87,34 @@ function createChartBackend(client) {
82
87
  // src/apps/tile.ts
83
88
  var TILE_SCRIPT = String.raw`(()=>{
84
89
  const $=id=>document.getElementById(id),root=document.documentElement,pending=new Map();
85
- let seq=0,origin='*',ready=false,closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null;
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;
91
+ /* One lifecycle, two sets of names. Hosts ship either or both, so notifications go out
92
+ under both and arrive under either. Same order in both rows: result, input, context. */
93
+ const DASH=['ui/notifications/tool-result','ui/notifications/tool-input','ui/notifications/host-context-changed'],
94
+ SNAKE=['notifications/tools/call/result','notifications/tools/call/input','notifications/host_context/changed'],
95
+ INIT=['ui/notifications/initialized','notifications/initialized'],
96
+ SIZE=['ui/notifications/size-changed','notifications/size_changed'],
97
+ DISPLAY=['ui/request-display-mode','requestDisplayMode'];
86
98
  function send(message){if(!closed)parent.postMessage({jsonrpc:'2.0',...message},origin)}
99
+ function notify(names,params){for(const method of names)send({method,params})}
87
100
  function rpc(method,params){return new Promise((resolve,reject)=>{const id=++seq;
88
101
  const timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'))},30000);
89
102
  pending.set(id,{resolve,reject,timer});send({id,method,params})})}
103
+ /* Try each alias while the host says it never heard of it; any other error stands. */
104
+ async function ask(names,params){let last;
105
+ for(const method of names){try{return await rpc(method,params)}catch(error){last=error;if(error.code!==-32601)throw error}}
106
+ throw last}
107
+ /* Fixed code only. The tile sits inside customer rows; a log is no place to leak them. */
108
+ function trouble(code){send({method:'notifications/error',params:{code}})}
90
109
  function status(text,bad){const s=$('status');s.textContent=text;s.toggleAttribute('data-error',!!bad);if(bad)$('plot').dataset.state='off'}
91
- function paint(){const img=$('chart-image');if(img&&rawSvg)img.src='data:image/svg+xml,'+encodeURIComponent(rawSvg.replace(/<\/svg>\s*$/i,'<style>text{fill:'+getComputedStyle(document.body).color+'!important}</style></svg>'))}
110
+ function paint(){const img=$('chart-image');if(!img||!rawSvg)return;
111
+ 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}'
114
+ +'</style></svg>'))}
92
115
  function controls(){document.querySelectorAll('button,input,select').forEach(e=>{e.disabled=busy});
93
116
  $('refresh').hidden=!ref;$('next').hidden=!next;$('range').hidden=!ref;$('plot').toggleAttribute('data-busy',busy);
94
- $('expand').hidden=!context.availableDisplayModes?.includes('fullscreen');
117
+ $('expand').hidden=!shown||!context.availableDisplayModes?.includes('fullscreen');
95
118
  root.toggleAttribute('data-deep',context.displayMode==='fullscreen'||$('expand').hidden)}
96
119
  function host(change){context={...context,...change};if(change.theme==='dark'||change.theme==='light')root.style.colorScheme=change.theme;
97
120
  const vars=change.styles?.variables||{};for(const [key,value] of Object.entries(vars)){
@@ -101,61 +124,82 @@ const insets=change.safeAreaInsets;if(insets)for(const edge of ['top','right','b
101
124
  const n=insets[edge];if(Number.isFinite(n))root.style.setProperty('--safe-'+edge,Math.max(0,Math.min(n,200))+'px')}
102
125
  root.dataset.mode=context.displayMode==='fullscreen'?'full':'inline';
103
126
  $('expand').textContent=context.displayMode==='fullscreen'?'Back to conversation':'Expand';paint();controls()}
104
- function resize(){if(ready)send({method:'ui/notifications/size-changed',params:{width:document.body.scrollWidth,height:document.body.scrollHeight}})}
127
+ 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');
129
+ if(view.dateRange)bits.push(view.dateRange.from+' to '+view.dateRange.to);
130
+ if(next)bits.push('more pages available');
131
+ $('scope').textContent=bits.join(' · ')}
132
+ function resize(){if(ready)notify(SIZE,{width:document.body.scrollWidth,height:document.body.scrollHeight})}
105
133
  function table(rows,columns){$('rows').replaceChildren();$('data').hidden=!rows?.length;if(!rows?.length)return;
106
- $('count').textContent=rows.length===1?'Show the 1 row behind this':'Show the '+rows.length+' rows behind this';
134
+ $('count').textContent=rows.length===1?'1 row behind this':rows.length+' rows behind this';
107
135
  const t=document.createElement('table');
108
- const header=document.createElement('tr');for(const name of columns){const th=document.createElement('th');th.scope='col';th.textContent=name;header.append(th)}
136
+ const num=name=>rows.some(r=>typeof r[name]==='number');
137
+ 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)}
109
138
  const head=document.createElement('thead');head.append(header);t.append(head);const body=document.createElement('tbody');
110
- 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]??'');tr.append(td)}body.append(tr)}t.append(body);$('rows').append(t)}
111
- async function display(mode){try{const answer=await rpc('ui/request-display-mode',{mode});
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)}
140
+ async function display(mode){try{const answer=await ask(DISPLAY,{mode});
112
141
  host({displayMode:answer.mode||context.displayMode});if(root.dataset.mode==='full'&&!$('data').hidden)$('data').open=true;resize()}
113
142
  catch{status('Expanded view is unavailable in this host.',1)}}
114
143
  function wait(){busy=true;if(!$('plot').firstChild)$('plot').dataset.state='wait';status('Loading chart…');controls()}
115
144
  async function call(name,extra={}){if(!ready||!ref||busy)return;wait();const rev=++revision;
116
145
  try{const result=await rpc('tools/call',{name,arguments:{reference:ref,view:{...view,...extra},context:'User is interacting with the chart tile.'}});
117
- if(rev===revision)render(result)}catch{if(rev===revision)status('Could not load the chart. Select Refresh to try again.',1)}finally{busy=false;controls()}}
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()}}
118
147
  function render(result){busy=false;controls();
119
- if(result?.isError){status('Chart unavailable. Check your access to this dashboard, then select Refresh.',1);return}
120
- const p=result?.structuredContent;if(!p||typeof p!=='object'){status('No chart data came back for this result.',1);return}
121
- revision++;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();$('tip').textContent='';delete $('tip').dataset.value;
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}
151
+ const p=result?.structuredContent;if(!p||typeof p!=='object'){shown=false;controls();status('No chart data came back for this result.',1);return}
152
+ revision++;shown=true;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();delete $('rail').dataset.value;
122
153
  ref=p.reference||null;view=p.view||{};next=p.nextCursor||null;
123
154
  $('title').textContent=p.title||'Analytics';$('description').textContent=p.description||'';
124
155
  if(p.kind==='dashboard'){$('plot').dataset.state='off';
125
156
  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)}
126
- status(p.widgets?.length?'Choose a chart to explore.':'This dashboard has no charts yet.');
157
+ status(p.widgets?.length?'':'This dashboard has no charts yet. Add one in Databrain, then ask again.');
127
158
  }else if(p.kind==='kpi'){
128
159
  const number=document.createElement('p');number.className='kpi';number.textContent=String(p.value??'—');$('plot').append(number);status('');
129
160
  }else if(p.svg){
130
161
  const box=document.createElement('div');box.className='chart';box.style.aspectRatio=p.width+'/'+p.height;
131
- const img=document.createElement('img');img.id='chart-image';rawSvg=p.svg;img.alt=p.title||'Chart';img.onerror=()=>{status('Chart image unavailable. The data is in the table below.',1);$('data').open=$('data').toggleAttribute('data-only',true)};box.append(img);
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);
132
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');
133
164
  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+'%';
134
- const hover=()=>{$('tip').textContent=point.label+': '+point.value;$('tip').dataset.value='1'};b.onmouseenter=hover;b.onfocus=hover;
165
+ const hover=()=>{$('vlabel').textContent=point.label;$('vnum').textContent=point.value;$('rail').dataset.value='1'};b.onmouseenter=hover;b.onfocus=hover;
135
166
  b.onclick=()=>{box.querySelectorAll('.point').forEach(e=>e.setAttribute('aria-pressed','false'));b.setAttribute('aria-pressed','true');hover();
136
167
  $('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);
137
- status(p.rows?.length?'':'This chart has no data for the current view.');if(p.points?.length)$('tip').textContent='Select part of the chart to read its value.';
138
- }else{$('plot').dataset.state='off';status(p.rows?.length?'Chart preview is unavailable. The data is in the table below.':'No data for this view.',!p.rows?.length)}
139
- paint();table(p.rows||[],p.columns||Object.keys(p.rows?.[0]||{}));
140
- const only=!p.svg&&p.value===undefined;$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}
168
+ status('');
169
+ }else if(p.kind==='table'){
170
+ // ask_question returns a table by design; calling that a missing chart called success a failure.
171
+ $('plot').dataset.state='off';status('');
172
+ }else{$('plot').dataset.state='off';
173
+ status('Nothing to show for this view. Try a wider date range, then select Refresh.',1)}
174
+ paint();table(p.rows||[],p.columns||Object.keys(p.rows?.[0]||{}));scope(p,p.rows?.length||0);
175
+ const only=!p.svg&&p.value===undefined&&p.kind!=='dashboard';$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}
141
176
  $('refresh').onclick=()=>call('refresh_widget',{cursor:undefined});$('next').onclick=()=>call('render_chart',{cursor:next});
142
177
  $('expand').onclick=()=>display(context.displayMode==='fullscreen'?'inline':'fullscreen');
143
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})};
144
179
  function receive(event){if(event.source!==parent||closed)return;const m=event.data;
145
180
  if(!m||Array.isArray(m)||m.jsonrpc!=='2.0'||(origin!=='*'&&event.origin!==origin))return;
146
181
  if(!m.method&&pending.has(m.id)&&('result'in m||'error'in m)){if(origin==='*'&&event.origin!=='null')origin=event.origin;
147
- const p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);m.error?p.reject(Error('host error')):p.resolve(m.result);return}
182
+ const p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);
183
+ if(m.error){const error=Error('host error');error.code=m.error.code;p.reject(error)}else p.resolve(m.result);return}
148
184
  if(m.method==='ping'&&m.id!==undefined){send({id:m.id,result:{}});return}
149
185
  if(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}
150
186
  if(!ready||m.id!==undefined)return;
151
- if(m.method==='ui/notifications/tool-result'){render(m.params)}
152
- else if(m.method==='ui/notifications/tool-input'){wait()}
153
- else if(m.method==='ui/notifications/host-context-changed'){host(m.params||{});resize()}
154
- else if(m.method==='ui/notifications/tool-cancelled'){revision++;busy=false;$('plot').dataset.state='off';status('Chart request cancelled.');controls()}}
187
+ let slot=DASH.indexOf(m.method),kind='dashed';
188
+ if(slot<0){slot=SNAKE.indexOf(m.method);kind='snake'}
189
+ if(slot>=0){
190
+ /* A dual host posts each notification twice. Lock to the first name and drop its twin:
191
+ same payload, and a second render is a visible flicker. */
192
+ if(!dialect)dialect=kind;if(dialect!==kind)return;
193
+ // The snake result nests one level deeper.
194
+ if(slot===0)render(kind==='snake'?m.params&&m.params.result:m.params);
195
+ else if(slot===1)wait();
196
+ else{host(m.params||{});resize()}
197
+ return}
198
+ if(m.method==='ui/notifications/tool-cancelled'){revision++;busy=false;$('plot').dataset.state='off';status('Chart request cancelled.');controls()}}
155
199
  addEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();
156
200
  rpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain chart tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})
157
- .then(r=>{if(r.protocolVersion!=='2026-01-26')throw Error('version');host(r.hostContext||{});ready=true;send({method:'ui/notifications/initialized'});resize()})
158
- .catch(()=>status('This chart needs a host that supports MCP Apps.',1));
201
+ .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)});
159
203
  })();`;
160
204
  function tileHtml(kind = "chart") {
161
205
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@@ -166,7 +210,8 @@ function tileHtml(kind = "chart") {
166
210
  :root{color-scheme:light dark;
167
211
  --dbn-bg:var(--color-background-primary,Canvas);--dbn-text:var(--color-text-primary,CanvasText);
168
212
  --dbn-mute:var(--color-text-secondary,GrayText);--dbn-faint:var(--color-text-tertiary,GrayText);
169
- --dbn-edge:var(--color-border-secondary,ButtonBorder);--dbn-line:var(--color-border-tertiary,var(--dbn-edge));
213
+ --dbn-edge:var(--color-border-secondary,color-mix(in srgb,currentColor 20%,transparent));
214
+ --dbn-line:var(--color-border-tertiary,color-mix(in srgb,currentColor 9%,transparent));
170
215
  --dbn-accent:var(--color-text-info,LinkText);--dbn-ring:var(--color-ring-primary,var(--dbn-accent));
171
216
  --dbn-bad:var(--color-text-danger,var(--dbn-text));--dbn-bad-edge:var(--color-border-danger,currentColor);
172
217
  --dbn-quiet:color-mix(in srgb,currentColor 6%,transparent);
@@ -185,9 +230,21 @@ button,input{font:inherit;color:inherit;background:var(--dbn-bg);border:var(--bw
185
230
  button{cursor:pointer;transition:background-color var(--q) var(--e),border-color var(--q) var(--e)}
186
231
  input{max-width:160px;min-width:0}button:hover:not(:disabled){background:var(--dbn-quiet)}button:disabled{color:var(--dbn-faint);cursor:default}
187
232
  button:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--dbn-ring);outline-offset:2px}
188
- #choices button.primary{border-color:var(--dbn-accent);font-weight:var(--med)}
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)}
236
+ /* One primary per state: the drill pill, and Next page when there is a next page. */
237
+ #choices button.primary,#next{flex:none;text-align:center;border:var(--bw) solid var(--dbn-accent);
238
+ border-radius:var(--r-s);padding:7px 10px;font-weight:var(--med);color:var(--dbn-accent)}
239
+ #choices button.primary{align-self:flex-start;margin-top:10px}
240
+ /* 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}
242
+ #refresh:hover:not(:disabled){color:var(--dbn-text);background:none;text-decoration:underline}
189
243
  #status:empty{margin:0}
190
- #status[data-error]{color:var(--dbn-bad);font-weight:var(--med);border-left:2px solid var(--dbn-bad-edge);background:var(--color-background-danger,transparent);border-radius:0 var(--r-s) var(--r-s) 0;padding:8px 10px;margin:8px 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}
191
248
  /* Reserved plot. The skeleton is a static bar silhouette matching the 15:8 backend render:
192
249
  Claude's guidance asks for a skeleton that matches the final layout, and bans nothing here,
193
250
  but shimmer is banned by our own motion budget - so this paints once and never repaints. */
@@ -207,9 +264,18 @@ outline:2px solid transparent;outline-offset:1px;transition:background-color var
207
264
  .point:hover{background:color-mix(in srgb,currentColor 12%,transparent)}
208
265
  .point[aria-pressed=true]{outline-color:var(--dbn-accent);background:color-mix(in srgb,var(--dbn-accent) 18%,transparent)}
209
266
  .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)}
210
- #tip{color:var(--dbn-mute);font-size:var(--font-text-xs-size,12px);overflow-wrap:anywhere;min-height:1.4em}
211
- #tip[data-value]{color:var(--dbn-text);font-weight:var(--med);font-variant-numeric:tabular-nums}
212
- #choices,footer,.dates{display:flex;gap:8px;flex-wrap:wrap;align-items:center}#choices{margin:10px 0 0}footer{margin-top:12px}.dates{margin:8px 0 2px}
267
+ /* The readout rail. At rest it states the scope of what is on screen; on selection it
268
+ becomes the value. One slot, two jobs, so selecting a bar never reflows the tile. */
269
+ #rail{display:flex;align-items:center;min-height:2.5em;margin:2px 0 0}
270
+ #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)}
272
+ #rail:not([data-value]):has(#scope:empty){display:none}
273
+ #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)}
275
+ #vnum{font-size:var(--font-heading-md-size,18px);font-weight:var(--semi);letter-spacing:-.015em;font-variant-numeric:tabular-nums}
276
+ [data-mode=full] #vnum{font-size:var(--font-heading-xl-size,24px)}
277
+ 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}
213
279
  label{display:flex;align-items:center;gap:6px;color:var(--dbn-mute)}
214
280
  #range{margin-top:10px}#data{margin-top:2px}#data[hidden],#range[hidden]{display:none}
215
281
  html:not([data-deep]) #range,html:not([data-deep]) #data:not([data-only]){display:none}
@@ -217,7 +283,9 @@ summary{cursor:pointer;color:var(--dbn-mute);font-size:var(--font-text-xs-size,1
217
283
  #rows{overflow:auto;max-height:260px;border-radius:var(--r-s)}
218
284
  table{border-collapse:collapse;width:100%;font-size:var(--font-text-xs-size,12px)}
219
285
  th,td{text-align:left;padding:8px;border-bottom:var(--bw) solid var(--dbn-line);overflow-wrap:anywhere}
220
- td:last-child{font-variant-numeric:tabular-nums}th{font-weight:var(--semi);position:sticky;top:0;background:var(--dbn-bg);color:var(--dbn-mute)}
286
+ td.n,th.n{text-align:right;font-variant-numeric:tabular-nums}
287
+ 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}
221
289
  /* Inline to fullscreen is a layout morph, not a new screen: main widens over 400ms on the
222
290
  one curve and every dependent box follows it. Fullscreen is the detailed view, so the
223
291
  table loses its inline height cap. */
@@ -235,11 +303,12 @@ td:last-child{font-variant-numeric:tabular-nums}th{font-weight:var(--semi);posit
235
303
  @keyframes rise{from{opacity:0;transform:translateY(6px)}}
236
304
  @keyframes land{from{opacity:0;transform:translateY(4px) scale(.995)}}
237
305
  header,#status,#plot{animation:rise var(--s) var(--e) both}
238
- #tip,#choices,footer,#range,#data{animation:rise var(--s) var(--e) 60ms both}
306
+ #rail,#choices,footer,#range,#data{animation:rise var(--s) var(--e) 60ms both}
239
307
  .chart,.kpi{animation:land var(--s) var(--e) both}
240
308
  @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}
241
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>
242
- <p id="status" role="status">Loading chart\u2026</p><div id="plot" data-state="wait"></div><div id="tip"></div><div id="choices"></div>
310
+ <p id="status" role="status">Loading chart\u2026</p><div id="plot" data-state="wait"></div>
311
+ <div id="rail"><p id="scope"></p><p id="value"><span id="vlabel"></span><b id="vnum"></b></p></div><div id="choices"></div>
243
312
  <footer><button id="refresh" hidden>Refresh</button><button id="next" hidden>Next page</button></footer>
244
313
  <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>
245
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>`;
@@ -281,7 +350,7 @@ function registerChartApps(server, client, backend = createChartBackend(client))
281
350
  }] };
282
351
  }
283
352
  });
284
- tool.update({ _meta: { ui: { resourceUri: CHART_URI, visibility: ["app"] } } });
353
+ tool.update({ _meta: appMeta(["app"]) });
285
354
  }
286
355
  for (const [uri, kind] of [[CHART_URI, "chart"], [KPI_URI, "kpi"]]) {
287
356
  server.resource(`Databrain ${kind} tile`, uri, { mimeType: APP_MIME, description: `Interactive ${kind} using host theme and scoped server tools` }, async () => ({
@@ -307,4 +376,4 @@ function createChartAppServer(config, backend) {
307
376
  export {
308
377
  createChartAppServer
309
378
  };
310
- //# sourceMappingURL=chunk-NDVW3ZSJ.js.map
379
+ //# sourceMappingURL=chunk-4LIULTLT.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 { 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';\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 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","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;\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'))},30000);\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}})}\nfunction status(text,bad){const s=$('status');s.textContent=text;s.toggleAttribute('data-error',!!bad);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:'+c+'!important}'\n+'[stroke=\"#dbdee4\"]{stroke:'+mix(13)+'!important}[stroke=\"#54555a\"]{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$('expand').hidden=!shown||!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(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=String(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('Expanded view 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;wait();const rev=++revision;\ntry{const result=await rpc('tools/call',{name,arguments:{reference:ref,view:{...view,...extra},context:'User is interacting with the chart tile.'}});\nif(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()}}\nfunction render(result){busy=false;controls();\nif(result?.isError){trouble('tool-returned-error');shown=false;controls();\nstatus(ref?'Chart unavailable. Check your access to this dashboard, then select Refresh.'\n:'Chart unavailable. Ask for this chart 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;\n$('title').textContent=p.title||'Analytics';$('description').textContent=p.description||'';\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?'':'This dashboard has no charts yet. Add one in Databrain, then ask again.');\n}else if(p.kind==='kpi'){\nconst number=document.createElement('p');number.className='kpi';number.textContent=String(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 image could not be drawn. The rows behind it 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+': '+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=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 for this view. Try a wider date range, then select Refresh.',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});\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('Choose a start date on or before 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('Chart 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 chart 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 a host that supports MCP Apps.',1)});\n})();`;\n\nexport function tileHtml(kind: 'chart' | 'kpi' = 'chart'): string {\n return `<!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));--dbn-bad-edge:var(--color-border-danger,currentColor);\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);--r-l:var(--border-radius-lg,10px);\n--med:var(--font-weight-medium,500);--semi:var(--font-weight-semibold,600);\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}\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}#expand{flex:none}\nbutton,input{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#choices button{text-align:left;border-color:transparent;border-bottom-color:var(--dbn-line);border-radius:0;padding:10px 2px}\n#choices button:first-child{border-top:var(--bw) solid var(--dbn-line)}\n#choices button:hover:not(:disabled){background:var(--dbn-quiet);border-radius:var(--r-s)}\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-left:2px;padding-right:8px}\n#refresh:hover:not(:disabled){color:var(--dbn-text);background:none;text-decoration:underline}\n#status:empty{margin:0}\n#status[data-error]{color:var(--dbn-bad);font-weight:var(--med);\nborder-left:2px solid var(--color-border-danger,color-mix(in srgb,currentColor 40%,transparent));\nbackground:var(--color-background-danger,color-mix(in srgb,currentColor 5%,transparent));\nborder-radius:0 var(--r-m) var(--r-m) 0;padding:10px 12px;margin:10px 0}\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]{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);\nbackground:linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 0 100%/11% 44% no-repeat,\nlinear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 22% 100%/11% 71% no-repeat,\nlinear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 44% 100%/11% 57% no-repeat,\nlinear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 66% 100%/11% 86% no-repeat,\nlinear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 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.point{position:absolute;padding:0;border:0;background:transparent;border-radius:var(--border-radius-xs,4px);\noutline:2px solid transparent;outline-offset:1px;transition:background-color var(--q) var(--e),outline-color var(--q) var(--e)}\n.point:hover{background:color-mix(in srgb,currentColor 12%,transparent)}\n.point[aria-pressed=true]{outline-color:var(--dbn-accent);background:color-mix(in srgb,var(--dbn-accent) 18%,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(--font-text-xs-size,12px)}\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(--font-text-xs-size,12px)}\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}.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}#data[hidden],#range[hidden]{display:none}\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(--font-text-xs-size,12px);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(--font-text-xs-size,12px)}\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(--font-text-xs-size,12px);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><button id=\"expand\" hidden>Expand</button></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=\"refresh\" hidden>Refresh</button><button id=\"next\" hidden>Next page</button></footer>\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>${TILE_SCRIPT}</script></body></html>`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,iBAAiB;;;ACA1B,SAAS,SAAS;;;ACKX,IAAM,cAAc;AAM3B,IAAM,sBAAN,cAAkC,gBAAgB;AAAA,EAChD,YAA6B,UAA2B;AACtD,UAAM,EAAE,QAAQ,SAAS,UAAU,GAAG,YAAY,SAAS,cAAc,EAAE,CAAC;AADjD;AAAA,EAE7B;AAAA,EACS,QAAW,SAAqC;AACvD,WAAO,KAAK,SAAS,QAAW,EAAE,GAAG,SAAS,UAAU,QAAW,YAAY,OAAU,CAAC;AAAA,EAC5F;AACF;AAGO,SAAS,mBAAmB,QAAuC;AAExE,QAAM,WAAW,oBAAoB,IAAI,oBAAoB,MAAM,CAAC;AACpE,SAAO;AAAA,IACL,MAAM,QAAQ,QAAQ,cAAc,SAAS;AAC3C,YAAM,YAAY,gBAAgB,MAAM,YAAY;AACpD,YAAM,OAAO,WAAW,MAAM,OAAO;AAErC,YAAM,MAAM,MAAM,SAAS,cAAc,mBAAmB,QAAW,UAAU,OAAO;AACxF,UAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,yBAAyB;AACtD,YAAM,QAAQ,MAAM,SAAS,cAAc,6BAA6B;AAAA,QACtE,SAAS,UAAU;AAAA,QAAS,UAAU,UAAU;AAAA,QAAU,aAAa,IAAI;AAAA,MAAM,CAAC;AACpF,UAAI,CAAC,MAAM,GAAI,OAAM,IAAI,MAAM,oBAAoB;AACnD,YAAM,EAAE,MAAM,IAAI,MAAM,SAAS,kBAAkB,cAAc;AAAA,QAC/D,UAAU,UAAU;AAAA,QAAU,aAAa,IAAI;AAAA,QAAO,eAAe,CAAC,UAAU,OAAO;AAAA,QACvF,aAAa;AAAA,UAAE,yBAAyB;AAAA,UAAM,uBAAuB;AAAA,UACnE,wBAAwB;AAAA,UAAO,wBAAwB;AAAA,QAAM;AAAA,MACjE,CAAC;AACD,UAAI,CAAC,MAAO,OAAM,IAAI,MAAM,yBAAyB;AACrD,YAAM,OAAO,MAAM,OAAO,QAAiB;AAAA,QACzC,QAAQ;AAAA,QAAQ,MAAM;AAAA,QAAa,MAAM;AAAA,QAAQ;AAAA,QACjD,MAAM;AAAA,UAAE;AAAA,UAAQ,SAAS,UAAU;AAAA,UAAS,UAAU,UAAU;AAAA,UAAU,GAAG;AAAA,UAC3E,UAAU;AAAA,UAAI,OAAO;AAAA,UAAK,QAAQ;AAAA,UAAK,WAAW;AAAA,QAAM;AAAA,QAC1D,gBAAgB;AAAA,MAClB,CAAC;AACD,aAAO,WAAW,MAAM,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;;;AChDO,IAAM,cAAc,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqH3B,SAAS,SAAS,OAAwB,SAAiB;AAChE,SAAO;AAAA;AAAA,mBAEU,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oLA2G6J,WAAW;AAC/L;;;AF3NA,IAAM,UAAuC;AAAA,EAC3C,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,YAAY;AAAA,EACZ,mBAAmB;AACrB;AAGO,SAAS,kBAAkB,QAAmB,QAAyB,UAAwB,mBAAmB,MAAM,GAAS;AACtI,aAAW,CAAC,MAAM,WAAW,KAAK,OAAO,QAAQ,OAAO,GAAG;AACzD,UAAM,SAAS;AACf,UAAM,OAAO,OAAO,KAAK,MAAM,aAAa;AAAA,MAC1C,WAAW;AAAA,MAAiB,MAAM,WAAW,QAAQ,CAAC,CAAC;AAAA,MACvD,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,2BAA2B;AAAA,IAC9E,GAAG,WAAW,OAAO,EAAE,WAAW,KAAK,MAAM;AAC3C,UAAI;AACF,YAAK,WAAW,gBAAgB,CAAC,KAAK,WAAa,WAAW,uBAAuB,CAAC,KAAK,UAAY,OAAM,IAAI,MAAM,uBAAuB;AAC9I,cAAM,OAAO,WAAW,MAAM,MAAM,YAAY;AAAA,UAAI,EAAE,MAAM,MAAM,UAAU,aAAa,gBAAgB,aAAa,WAAW,MAAM;AAAA,UACrI,MAAM,QAAQ,QAAQ,QAAQ,WAAW,IAAI;AAAA,QAAC,CAAC;AAGjD,YAAI,KAAK,QAAQ,CAAC,cAAc,KAAK,KAAK,GAAG,KAC3C,kLAAkL,KAAK,KAAK,GAAG,IAAI;AACnM,gBAAM,IAAI,MAAM,YAAY;AAAA,QAC9B;AACA,eAAO,YAAY,GAAG,KAAK,SAAS,QAAQ,QAAQ,OAAO,WAAW,KAAK,KAAK,MAAM,uBAAuB;AAAA,UAC3G,GAAG;AAAA,UAAM;AAAA,UAAW;AAAA,QACtB,CAAC;AAAA,MACH,QAAQ;AACN,eAAO,EAAE,SAAS,MAAM,SAAS,CAAC;AAAA,UAAE,MAAM;AAAA,UACxC,MAAM;AAAA,QAA4F,CAAC,EAAE;AAAA,MACzG;AAAA,IACF,CAAC;AACD,SAAK,OAAO,EAAE,OAAO,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AAAA,EACzC;AACA,aAAW,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC,WAAW,OAAO,GAAG,CAAC,SAAS,KAAK,CAAC,GAAY;AAC3E,WAAO,SAAS,aAAa,IAAI,SAAS,KAAK,EAAE,UAAU,UAAU,aAAa,eAAe,IAAI,4CAA4C,GAAG,aAAa;AAAA,MAC/J,UAAU,CAAC,EAAE,KAAK,UAAU,UAAU,MAAM,SAAS,IAAI,GAAG,OAAO,EAAE,IAAI,EAAE,eAAe,KAAK,EAAE,EAAE,CAAC;AAAA,IACtG,EAAE;AAAA,EACJ;AACF;;;ADtCO,SAAS,qBAAqB,QAAgB,SAAmC;AACtF,QAAM,SAAS,IAAI,gBAAgB,MAAM;AACzC,QAAM,SAAS,IAAI,UAAU,EAAE,MAAM,wBAAwB,SAAS,mBAAmB,GAAG;AAAA,IAC1F,cAAc;AAAA,EAEhB,CAAC;AACD,kBAAgB,MAAM;AACtB,mBAAiB,QAAQ,MAAM;AAC/B,uBAAqB,MAAM;AAC3B,qBAAmB,MAAM;AACzB,oBAAkB,QAAQ,QAAQ,OAAO;AACzC,SAAO;AACT;","names":[]}
@@ -99,4 +99,4 @@ export {
99
99
  FilePatStore,
100
100
  createBearerVerifier
101
101
  };
102
- //# sourceMappingURL=chunk-JCXUZ3QZ.js.map
102
+ //# sourceMappingURL=chunk-CX3CS62P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hosted/types.ts","../src/hosted/auth.ts"],"sourcesContent":["import type { RequestOptions } from '../client/types.js';\n\nexport const HOSTED_SCOPES = [\n 'mcp:dashboards:read', 'mcp:dashboards:write',\n 'mcp:datamarts:read', 'mcp:datamarts:write', 'mcp:embeds:write', 'mcp:query',\n] as const;\nexport type HostedScope = typeof HOSTED_SCOPES[number];\n\n/** Only the trusted auth adapter may produce this context. Never populate it from MCP input. */\nexport interface HostedPrincipal {\n actorKind: 'user';\n credentialType: 'oauth' | 'pat';\n userId: string;\n companyId: string;\n workspaceId: string;\n workspaceName?: string;\n /** OAuth application ID, NOT the tenant identifier called clientId by legacy tools. */\n clientId: string;\n resource: string;\n scopes: HostedScope[];\n /** Required for stored PATs. OAuth may delegate expiry to fresh provider validation. */\n expiresAt?: number;\n /** Explicit tenant allow-list resolved by the backend; absent means no tenant access. */\n tenantIds?: string[];\n}\n\nexport interface PatRecord {\n hash: string;\n revokedAt: number | null;\n principal: HostedPrincipal;\n}\n\nexport interface PatStore {\n findByHash(hash: string): Promise<PatRecord | undefined>;\n}\n\n/** Production integration must recheck membership, policy and revocation on every call. */\nexport interface HostedAdapter {\n verifyOAuth(token: string, resource: string): Promise<HostedPrincipal>;\n resolvePatGrant(principal: HostedPrincipal): Promise<HostedPrincipal>;\n /**\n * Execute as this user/workspace with backend resource and tenant checks and auditing.\n * Receives NO incoming bearer or server service credential. Credential exchange, if\n * needed, belongs in the trusted adapter. Do not impersonate a static service token.\n * Return the raw API result: the shared client performs its usual data unwrapping.\n */\n executeRequest(options: RequestOptions, principal: HostedPrincipal): Promise<unknown>;\n}\n\nexport interface HostedConfig {\n publicUrl: string;\n issuer: string;\n port: number;\n bind: string;\n testOnly: boolean;\n}\n\nexport class HostedAuthError extends Error {\n constructor(public readonly status: 401 | 403 | 503, public readonly code: string) {\n super(code);\n }\n}\n","import { createHash, timingSafeEqual } from 'node:crypto';\nimport { open } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { z } from 'zod';\nimport { HOSTED_SCOPES, HostedAuthError } from './types.js';\nimport type { HostedAdapter, HostedPrincipal, PatRecord, PatStore } from './types.js';\n\nconst identity = z.string().min(1).max(512).regex(/^[^\\x00-\\x1f\\x7f]+$/);\nexport const principalSchema = z.object({\n actorKind: z.literal('user'), credentialType: z.enum(['oauth', 'pat']),\n userId: identity, companyId: identity, workspaceId: identity, workspaceName: identity.optional(),\n clientId: identity, resource: z.string().url(), scopes: z.array(z.enum(HOSTED_SCOPES)),\n expiresAt: z.number().finite().positive().optional(), tenantIds: z.array(identity).optional(),\n}).strict();\nconst recordSchema = z.object({\n hash: z.string().regex(/^[a-f0-9]{64}$/), revokedAt: z.number().finite().nullable(),\n principal: principalSchema,\n}).strict();\n\nexport function hashCredential(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/** Reread on every request: an atomic file replacement revokes immediately, without restart. */\nexport class FilePatStore implements PatStore {\n constructor(private readonly path: string) {}\n async findByHash(hash: string): Promise<PatRecord | undefined> {\n const handle = await open(this.path, constants.O_RDONLY | constants.O_NOFOLLOW);\n try {\n const stat = await handle.stat();\n if (!stat.isFile() || (stat.mode & 0o077) !== 0 || stat.size > 1_048_576 ||\n (process.getuid && stat.uid !== process.getuid())) throw new Error('Invalid credential store');\n const records = z.array(recordSchema).max(1000).parse(JSON.parse(await handle.readFile('utf8')));\n const matches = records.filter(record => timingSafeEqual(Buffer.from(record.hash, 'hex'), Buffer.from(hash, 'hex')));\n // Duplicate records are ambiguous, including an active/revoked pair.\n if (matches.length > 1) throw new Error('Ambiguous credential store');\n return matches[0];\n } finally { await handle.close(); }\n }\n}\n\nfunction validatePrincipal(value: unknown, resource: string, kind: 'oauth' | 'pat'): HostedPrincipal {\n const parsed = principalSchema.safeParse(value);\n if (!parsed.success || parsed.data.resource !== resource || parsed.data.credentialType !== kind ||\n (parsed.data.expiresAt !== undefined && parsed.data.expiresAt <= Date.now() / 1000) ||\n (kind === 'pat' && parsed.success && parsed.data.expiresAt === undefined)) throw new HostedAuthError(401, 'invalid_token');\n return parsed.data;\n}\n\nexport function createBearerVerifier(adapter: HostedAdapter, resource: string, pats?: PatStore) {\n return async (token: string): Promise<HostedPrincipal> => {\n try {\n if (!/^[A-Za-z0-9._~+\\/-]+=*$/.test(token) || token.length > 8192) throw new HostedAuthError(401, 'invalid_token');\n if (!token.startsWith('dbp_')) {\n return validatePrincipal(await adapter.verifyOAuth(token, resource), resource, 'oauth');\n }\n if (!/^dbp_[A-Za-z0-9_-]{32,}$/.test(token)) throw new HostedAuthError(401, 'invalid_token');\n const record = await pats?.findByHash(hashCredential(token));\n if (!record || record.revokedAt !== null || record.hash !== hashCredential(token)) throw new HostedAuthError(401, 'invalid_token');\n const stored = validatePrincipal(record.principal, resource, 'pat');\n const resolved = validatePrincipal(await adapter.resolvePatGrant(structuredClone(stored)), resource, 'pat');\n // Membership resolution may narrow a stored grant, never move or expand it.\n for (const key of ['userId', 'companyId', 'workspaceId', 'clientId', 'resource'] as const) {\n if (resolved[key] !== stored[key]) throw new HostedAuthError(401, 'invalid_token');\n }\n if (resolved.expiresAt! > stored.expiresAt! || resolved.scopes.some(scope => !stored.scopes.includes(scope)) ||\n resolved.tenantIds?.some(id => !stored.tenantIds?.includes(id))) throw new HostedAuthError(401, 'invalid_token');\n return resolved;\n } catch (error) {\n if (error instanceof HostedAuthError) throw error;\n // No raw verifier, filesystem or remote errors can disclose credential values.\n throw new HostedAuthError(401, 'invalid_token');\n }\n };\n}\n"],"mappings":"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","names":[]}
@@ -24,7 +24,7 @@ var logger = {
24
24
  // package.json
25
25
  var package_default = {
26
26
  name: "@databrainhq/mcp-server",
27
- version: "0.3.0-rc.1",
27
+ version: "0.3.0-rc.3",
28
28
  description: "MCP server for Databrain embedded analytics - connect databases, create datamarts, configure embeds, and generate frontend code",
29
29
  type: "module",
30
30
  bin: {
@@ -577,6 +577,7 @@ import { z as z2 } from "zod";
577
577
  var CHART_URI = "ui://databrain/chart-tile";
578
578
  var KPI_URI = "ui://databrain/kpi-tile";
579
579
  var APP_MIME = "text/html;profile=mcp-app";
580
+ var RESOURCE_URI_META_KEY = "ui/resourceUri";
580
581
  var PAGE_SIZE = 50;
581
582
  var MAX_RESULT_CHARS = 125e3;
582
583
  var id = z2.string().trim().min(1).max(256);
@@ -639,6 +640,9 @@ function firstRows(value) {
639
640
  }
640
641
 
641
642
  // src/apps/feature.ts
643
+ function appMeta(visibility) {
644
+ return { ui: { resourceUri: CHART_URI, visibility }, [RESOURCE_URI_META_KEY]: CHART_URI };
645
+ }
642
646
  var enabled = /* @__PURE__ */ new WeakSet();
643
647
  function enableChartApps(server) {
644
648
  enabled.add(server);
@@ -647,10 +651,14 @@ function hasChartApps(server) {
647
651
  return enabled.has(server);
648
652
  }
649
653
  function tagChartTool(server, tool) {
650
- if (hasChartApps(server)) tool.update({ _meta: { ui: { resourceUri: CHART_URI, visibility: ["model", "app"] } } });
654
+ if (hasChartApps(server)) tool.update({ _meta: appMeta(["model", "app"]) });
651
655
  }
652
656
  function chartResult(summary, payload) {
653
- return bounded({ content: [{ type: "text", text: summary }], structuredContent: payload });
657
+ return bounded({
658
+ content: [{ type: "text", text: summary }],
659
+ structuredContent: payload,
660
+ _meta: appMeta(["model", "app"])
661
+ });
654
662
  }
655
663
 
656
664
  // src/tools/tool-metadata.ts
@@ -1672,6 +1680,54 @@ var WidgetService = class {
1672
1680
  }
1673
1681
  };
1674
1682
  }
1683
+ /**
1684
+ * Read-only sibling of `ensureTenantDashboardScope`.
1685
+ *
1686
+ * The tenant gate above exists to stop a WRITE landing on the data-app
1687
+ * dashboard when the caller meant a tenant's copy, so it insists the
1688
+ * tenant-scoped embed lookup returns the target. A data-app embed has no
1689
+ * tenant row, `listEmbeds(dataAppName, clientId)` answers with an empty list,
1690
+ * and the render path was denied before it ever reached the backend. That is
1691
+ * the wrong trade for a read: the chart route re-derives everything from the
1692
+ * guest token it is handed (embed inside the token's data app, `allowedEmbeds`,
1693
+ * the tenancy subset for the client, and per-metric client visibility), so the
1694
+ * only thing this check can usefully add is refusing another tenant's embed.
1695
+ */
1696
+ async ensureReadableDashboardScope(params) {
1697
+ const clientId = params.clientId.trim();
1698
+ if (!clientId) {
1699
+ return {
1700
+ ok: false,
1701
+ error: "`clientId` cannot be empty. Pick a tenant/client first."
1702
+ };
1703
+ }
1704
+ let ctx;
1705
+ try {
1706
+ ctx = await this.resolveEmbedContext(params.embedId);
1707
+ } catch (err) {
1708
+ return { ok: false, error: err.message };
1709
+ }
1710
+ if (ctx.embedClientId && ctx.embedClientId !== clientId) {
1711
+ return {
1712
+ ok: false,
1713
+ error: `Embed "${params.embedId}" belongs to another client. Pass the \`clientId\` the embed was created for.`
1714
+ };
1715
+ }
1716
+ if (!ctx.embedClientId && ctx.externalDashboardCreatedBy && ctx.externalDashboardCreatedBy !== clientId) {
1717
+ return {
1718
+ ok: false,
1719
+ error: `Dashboard behind embed "${params.embedId}" belongs to another client. Pass the \`clientId\` the dashboard was created for.`
1720
+ };
1721
+ }
1722
+ return {
1723
+ ok: true,
1724
+ value: {
1725
+ embedId: params.embedId,
1726
+ clientId,
1727
+ dataAppName: ctx.dataAppName || params.dataAppName || null
1728
+ }
1729
+ };
1730
+ }
1675
1731
  /**
1676
1732
  * Resolve a datasource for a widget. Priority:
1677
1733
  * 1. Explicit integrationId + integrationName (fast path).
@@ -1857,6 +1913,7 @@ Do not guess between the two \u2014 wait for the user to choose.`
1857
1913
  dataAppName,
1858
1914
  externalDashboardId,
1859
1915
  dashboardId,
1916
+ embedClientId: isNonEmptyString3(exact.clientId) && exact.clientId || (metadata && isNonEmptyString3(metadata.clientId) ? metadata.clientId : null),
1860
1917
  externalDashboardCreatedBy: (externalDashboard && isNonEmptyString3(externalDashboard.createdBy) ? externalDashboard.createdBy : null) || (isNonEmptyString3(exact.createdBy) ? exact.createdBy : null) || (metadata && isNonEmptyString3(metadata.createdBy) ? metadata.createdBy : null) || (isNonEmptyString3(exact.clientId) ? exact.clientId : null)
1861
1918
  };
1862
1919
  }
@@ -6015,6 +6072,12 @@ function registerGetSemanticLayerGenerationStatus(server, client) {
6015
6072
 
6016
6073
  // src/tools/widgets/list-widgets.ts
6017
6074
  import { z as z29 } from "zod";
6075
+ function widgetReferenceId(widget) {
6076
+ for (const candidate of [widget.externalMetricId, widget.id, widget._id, widget.metricId]) {
6077
+ if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim();
6078
+ }
6079
+ return void 0;
6080
+ }
6018
6081
  function registerListWidgets(server, client) {
6019
6082
  const services = createServiceBundle(client);
6020
6083
  const tool = server.tool(
@@ -6043,17 +6106,18 @@ function registerListWidgets(server, client) {
6043
6106
  );
6044
6107
  if (hasChartApps(server)) {
6045
6108
  const visible = (Array.isArray(widgets) ? widgets : []).slice(0, 50);
6046
- return chartResult(`${visible.length} widgets available in the first dashboard page.`, {
6109
+ const references = visible.flatMap((w) => {
6110
+ const widgetId = widgetReferenceId(w);
6111
+ return widgetId ? [{
6112
+ name: typeof w.name === "string" ? w.name.slice(0, 500) : "Untitled widget",
6113
+ reference: { widgetId, embedId, clientId: params.clientId }
6114
+ }] : [];
6115
+ });
6116
+ return chartResult(`${references.length} widgets available in the first dashboard page.`, {
6047
6117
  kind: "dashboard",
6048
6118
  title: "Dashboard",
6049
6119
  rows: [],
6050
- widgets: visible.flatMap((w) => {
6051
- const widgetId = w.externalMetricId || w.id || w._id;
6052
- return typeof widgetId === "string" ? [{
6053
- name: typeof w.name === "string" ? w.name.slice(0, 500) : "Untitled widget",
6054
- reference: { widgetId, embedId, clientId: params.clientId }
6055
- }] : [];
6056
- }),
6120
+ widgets: references,
6057
6121
  description: Array.isArray(widgets) && widgets.length > 50 ? "Showing the first 50 widgets." : ""
6058
6122
  });
6059
6123
  }
@@ -10389,10 +10453,11 @@ export {
10389
10453
  referenceSchema,
10390
10454
  viewSchema,
10391
10455
  pageSchema,
10456
+ appMeta,
10392
10457
  enableChartApps,
10393
10458
  chartResult,
10394
10459
  registerAllTools,
10395
10460
  registerAllResources,
10396
10461
  registerAllPrompts
10397
10462
  };
10398
- //# sourceMappingURL=chunk-7DS7OLWP.js.map
10463
+ //# sourceMappingURL=chunk-DZOYK5BB.js.map