@databrainhq/mcp-server 0.2.17 → 0.3.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +21 -0
- package/.claude-plugin/plugin.json +13 -0
- package/.mcp.json +11 -0
- package/README.md +171 -7
- package/dist/apps/server.d.ts +323 -0
- package/dist/apps/server.js +11 -0
- package/dist/apps/server.js.map +1 -0
- package/dist/apps/stdio.d.ts +2 -0
- package/dist/apps/stdio.js +16 -0
- package/dist/apps/stdio.js.map +1 -0
- package/dist/chunk-5IGP6A6T.js +496 -0
- package/dist/chunk-5IGP6A6T.js.map +1 -0
- package/dist/chunk-CUPL7CAN.js +75 -0
- package/dist/chunk-CUPL7CAN.js.map +1 -0
- package/dist/chunk-ERTAKB2H.js +11093 -0
- package/dist/chunk-ERTAKB2H.js.map +1 -0
- package/dist/chunk-N7X762VU.js +34 -0
- package/dist/chunk-N7X762VU.js.map +1 -0
- package/dist/chunk-OCBTNAAO.js +1946 -0
- package/dist/chunk-OCBTNAAO.js.map +1 -0
- package/dist/chunk-Z3ZHN4UW.js +130 -0
- package/dist/chunk-Z3ZHN4UW.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +495 -0
- package/dist/cli.js.map +1 -0
- package/dist/custom-chart/runtime-shim.js +146 -0
- package/dist/custom-chart/template/AGENTS.md +374 -0
- package/dist/custom-chart/template/README.md +374 -0
- package/dist/custom-chart/template/chart.css +28 -0
- package/dist/custom-chart/template/chart.ts +192 -0
- package/dist/custom-chart/template/custom-chart.d.ts +78 -0
- package/dist/custom-chart/template/icon.svg +1 -0
- package/dist/custom-chart/template/manifest.json +23 -0
- package/dist/custom-chart/template/package.json +14 -0
- package/dist/custom-chart/template/sample-data.json +18 -0
- package/dist/custom-chart/template/tsconfig.json +12 -0
- package/dist/index.js +65 -9787
- package/dist/index.js.map +1 -1
- package/dist/main-KZRHWE3K.js +857 -0
- package/dist/main-KZRHWE3K.js.map +1 -0
- package/dist/test-only-K7QTSVO4.js +38 -0
- package/dist/test-only-K7QTSVO4.js.map +1 -0
- package/package.json +17 -5
- package/skills/author-dashboard/SKILL.md +30 -0
- package/skills/embed-dashboard/SKILL.md +23 -0
- package/skills/style-dashboard/SKILL.md +58 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
APPLY_TO_PRODUCTION_CONFIRMATION,
|
|
4
|
+
APP_MIME,
|
|
5
|
+
CHART_URI,
|
|
6
|
+
KPI_URI,
|
|
7
|
+
READ_ONLY,
|
|
8
|
+
RENDER_TYPES,
|
|
9
|
+
WRITE_CREATE,
|
|
10
|
+
appMeta,
|
|
11
|
+
chartResult,
|
|
12
|
+
createServiceBundle,
|
|
13
|
+
draftPage,
|
|
14
|
+
draftSchema,
|
|
15
|
+
pageSchema,
|
|
16
|
+
referenceSchema,
|
|
17
|
+
rowsSchema,
|
|
18
|
+
saveDraftAsDashboard,
|
|
19
|
+
viewSchema,
|
|
20
|
+
workspacesForDatamart
|
|
21
|
+
} from "./chunk-ERTAKB2H.js";
|
|
22
|
+
import {
|
|
23
|
+
DatabrainApiError,
|
|
24
|
+
DatabrainClient,
|
|
25
|
+
toolContext
|
|
26
|
+
} from "./chunk-OCBTNAAO.js";
|
|
27
|
+
|
|
28
|
+
// src/apps/registry.ts
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
|
|
31
|
+
// src/apps/backend.ts
|
|
32
|
+
var RENDER_PATH = "/api/v2/dashboard/mcp-chart";
|
|
33
|
+
var UncachedChartClient = class extends DatabrainClient {
|
|
34
|
+
constructor(delegate) {
|
|
35
|
+
super({ apiUrl: delegate.getApiUrl(), demoDomain: delegate.getDemoDomain() });
|
|
36
|
+
this.delegate = delegate;
|
|
37
|
+
}
|
|
38
|
+
request(options) {
|
|
39
|
+
return this.delegate.request({ ...options, cacheKey: void 0, cacheTtlMs: void 0 });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function createChartBackend(client) {
|
|
43
|
+
const services = createServiceBundle(new UncachedChartClient(client));
|
|
44
|
+
return {
|
|
45
|
+
async execute(action, rawReference, rawView) {
|
|
46
|
+
const reference = referenceSchema.parse(rawReference);
|
|
47
|
+
const view = viewSchema.parse(rawView);
|
|
48
|
+
const app = await services.widgetService.resolveDataAppName(void 0, reference.embedId);
|
|
49
|
+
if (!app.ok) throw new Error("Chart scope unavailable");
|
|
50
|
+
const scope = await services.widgetService.ensureReadableDashboardScope({
|
|
51
|
+
embedId: reference.embedId,
|
|
52
|
+
clientId: reference.clientId,
|
|
53
|
+
dataAppName: app.value
|
|
54
|
+
});
|
|
55
|
+
if (!scope.ok) throw new Error("Chart scope denied");
|
|
56
|
+
const { token } = await services.guestTokenService.generateToken({
|
|
57
|
+
clientId: reference.clientId,
|
|
58
|
+
dataAppName: app.value,
|
|
59
|
+
allowedEmbeds: [reference.embedId],
|
|
60
|
+
permissions: {
|
|
61
|
+
isDisableMetricCreation: true,
|
|
62
|
+
isEnableManageMetrics: false,
|
|
63
|
+
isEnableMetricUpdation: false,
|
|
64
|
+
isEnableArchiveMetrics: false
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (!token) throw new Error("Guest scope unavailable");
|
|
68
|
+
const page = await client.request({
|
|
69
|
+
method: "POST",
|
|
70
|
+
path: RENDER_PATH,
|
|
71
|
+
auth: "none",
|
|
72
|
+
token,
|
|
73
|
+
body: {
|
|
74
|
+
action,
|
|
75
|
+
embedId: reference.embedId,
|
|
76
|
+
widgetId: reference.widgetId,
|
|
77
|
+
...view,
|
|
78
|
+
pageSize: 50,
|
|
79
|
+
width: 600,
|
|
80
|
+
height: 320,
|
|
81
|
+
animation: false
|
|
82
|
+
},
|
|
83
|
+
skipDataUnwrap: true
|
|
84
|
+
});
|
|
85
|
+
return pageSchema.parse(page);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/apps/tile.ts
|
|
91
|
+
var TILE_SCRIPT = String.raw`(()=>{
|
|
92
|
+
const $=id=>document.getElementById(id),root=document.documentElement,pending=new Map();
|
|
93
|
+
let seq=0,origin='*',ready=false,closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null,dialect=null,shown=false,draft=null,rows=[],saved=null,attempt='',workspace=null;
|
|
94
|
+
/* One lifecycle, two sets of names. Hosts ship either or both, so notifications go out
|
|
95
|
+
under both and arrive under either. Same order in both rows: result, input, context. */
|
|
96
|
+
const DASH=['ui/notifications/tool-result','ui/notifications/tool-input','ui/notifications/host-context-changed'],
|
|
97
|
+
SNAKE=['notifications/tools/call/result','notifications/tools/call/input','notifications/host_context/changed'],
|
|
98
|
+
INIT=['ui/notifications/initialized','notifications/initialized'],
|
|
99
|
+
SIZE=['ui/notifications/size-changed','notifications/size_changed'],
|
|
100
|
+
DISPLAY=['ui/request-display-mode','requestDisplayMode'];
|
|
101
|
+
function send(message){if(!closed)parent.postMessage({jsonrpc:'2.0',...message},origin)}
|
|
102
|
+
function notify(names,params){for(const method of names)send({method,params})}
|
|
103
|
+
function rpc(method,params){return new Promise((resolve,reject)=>{const id=++seq;
|
|
104
|
+
const timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'))},65000);
|
|
105
|
+
pending.set(id,{resolve,reject,timer});send({id,method,params})})}
|
|
106
|
+
/* Try each alias while the host says it never heard of it; any other error stands. */
|
|
107
|
+
async function ask(names,params){let last;
|
|
108
|
+
for(const method of names){try{return await rpc(method,params)}catch(error){last=error;if(error.code!==-32601)throw error}}
|
|
109
|
+
throw last}
|
|
110
|
+
/* Fixed codes only; never log customer rows. */
|
|
111
|
+
function trouble(code){send({method:'notifications/error',params:{code}})}
|
|
112
|
+
/* One formatter for rail, table, KPI and answer line. Compact past 10K, grouped past 100,
|
|
113
|
+
two decimals below; null is an absence, so it prints as a dash and never as 0. */
|
|
114
|
+
function fmt(v){if(typeof v!=='number')return v==null||v===''?'\u2014':String(v);const a=Math.abs(v);
|
|
115
|
+
return new Intl.NumberFormat(navigator.language||'en',a>=1e4?{notation:'compact',maximumFractionDigits:1}:a>=100?{maximumFractionDigits:0}:{maximumFractionDigits:2}).format(v)}
|
|
116
|
+
function exact(el,v){el.title=typeof v==='number'?String(v):''}
|
|
117
|
+
/* The answer line: what this shows, and the headline value. Gated on carrying a number or
|
|
118
|
+
a count, so it never renders as a second title or an empty reserved slot. */
|
|
119
|
+
function answer(p){if(p.description)return p.description;const bits=[];
|
|
120
|
+
if(p.kind==='dashboard'){const n=p.widgets?.length||0;return n?n+(n===1?' chart':' charts')+' on this dashboard':''}
|
|
121
|
+
const [dim,measure]=p.columns||[];const pts=(p.points||[]).filter(q=>typeof q.value==='number');
|
|
122
|
+
if(pts.length<2)return '';if(dim&&measure&&!p.draft)bits.push(measure+' by '+dim);
|
|
123
|
+
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 ')}
|
|
124
|
+
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'}
|
|
125
|
+
function paint(){const img=$('chart-image');if(!img||!rawSvg)return;
|
|
126
|
+
const c=getComputedStyle(document.body).color,mix=pc=>'color-mix(in srgb,'+c+' '+pc+'%,transparent)';
|
|
127
|
+
img.src='data:image/svg+xml,'+encodeURIComponent(rawSvg.replace(/<\/svg>\s*$/i,'<style>text{fill:'+mix(72)+'!important}'
|
|
128
|
+
+'[stroke="#dbdee4"],[stroke="#f1f1f2" i]{stroke:'+mix(13)+'!important}[stroke="#54555a"],[stroke="#d4d4d8" i]{stroke:'+mix(30)+'!important}'
|
|
129
|
+
+'</style></svg>'))}
|
|
130
|
+
function controls(){document.querySelectorAll('button,input,select').forEach(e=>{e.disabled=busy});
|
|
131
|
+
$('refresh').hidden=!ref;$('next').hidden=!next;$('range').hidden=!ref;$('plot').toggleAttribute('data-busy',busy);
|
|
132
|
+
$('save').hidden=!draft||!!saved||!$('form').hidden;
|
|
133
|
+
$('expand').hidden=!shown||root.dataset.kind==='dashboard'||!context.availableDisplayModes?.includes('fullscreen');
|
|
134
|
+
root.toggleAttribute('data-deep',context.displayMode==='fullscreen'||$('expand').hidden)}
|
|
135
|
+
function host(change){context={...context,...change};if(change.theme==='dark'||change.theme==='light')root.style.colorScheme=change.theme;
|
|
136
|
+
const vars=change.styles?.variables||{};for(const [key,value] of Object.entries(vars)){
|
|
137
|
+
if(/^--(?:color-|font-|border-|shadow-)/.test(key)&&typeof value==='string'&&!/url\s*\(|@import/i.test(value))root.style.setProperty(key,value)}
|
|
138
|
+
const face=change.styles?.css?.fonts;if(typeof face==='string'&&face.length<4000&&!/@import|javascript:/i.test(face))$('hostfont').textContent=face;
|
|
139
|
+
const insets=change.safeAreaInsets;if(insets)for(const edge of ['top','right','bottom','left']){
|
|
140
|
+
const n=insets[edge];if(Number.isFinite(n))root.style.setProperty('--safe-'+edge,Math.max(0,Math.min(n,200))+'px')}
|
|
141
|
+
root.dataset.mode=context.displayMode==='fullscreen'?'full':'inline';
|
|
142
|
+
$('expand').textContent=context.displayMode==='fullscreen'?'Back to conversation':'Expand';paint();controls()}
|
|
143
|
+
function scope(p,n){const bits=[];
|
|
144
|
+
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');
|
|
145
|
+
if(view.dateRange)bits.push(view.dateRange.from+' to '+view.dateRange.to);
|
|
146
|
+
if(next)bits.push('more pages available');
|
|
147
|
+
$('scope').textContent=bits.join(' · ')}
|
|
148
|
+
function resize(){if(ready)notify(SIZE,{width:document.body.scrollWidth,height:document.body.scrollHeight})}
|
|
149
|
+
function table(rows,columns){$('rows').replaceChildren();$('data').hidden=!rows?.length;if(!rows?.length)return;
|
|
150
|
+
$('count').textContent=rows.length===1?'1 row behind this':rows.length+' rows behind this';
|
|
151
|
+
const t=document.createElement('table');
|
|
152
|
+
const num=name=>rows.some(r=>typeof r[name]==='number');
|
|
153
|
+
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)}
|
|
154
|
+
const head=document.createElement('thead');head.append(header);t.append(head);const body=document.createElement('tbody');
|
|
155
|
+
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)}
|
|
156
|
+
async function display(mode){try{const answer=await ask(DISPLAY,{mode});
|
|
157
|
+
host({displayMode:answer.mode||context.displayMode});if(root.dataset.mode==='full'&&!$('data').hidden)$('data').open=true;resize()}
|
|
158
|
+
catch{status('Expand is unavailable in this host.',1)}}
|
|
159
|
+
function wait(){busy=true;if(!$('plot').firstChild)$('plot').dataset.state='wait';status('Loading chart…');controls()}
|
|
160
|
+
async function call(name,extra={}){if(!ready||!ref||busy)return;run(name,{reference:ref,view:{...view,...extra}})}
|
|
161
|
+
/* The draft tools take the draft and the rows on screen instead of a reference. */
|
|
162
|
+
async function run(name,args){if(!ready||busy)return;wait();const rev=++revision;
|
|
163
|
+
const selection=workspace?(name==='save_chart_as_dashboard'?{workspaceName:args.workspaceName||workspace.name}:{workspace:workspace.id}):{};
|
|
164
|
+
try{const result=await rpc('tools/call',{name,arguments:{...args,...selection,context:'tile'}});
|
|
165
|
+
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()}}
|
|
166
|
+
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)}
|
|
167
|
+
function render(result){busy=false;controls();
|
|
168
|
+
if(result?._meta?.['databrain/workspace'])workspace=result._meta['databrain/workspace'];
|
|
169
|
+
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)}
|
|
170
|
+
else if(sp.choices?.length){choose(sp.choices);status('Choose a workspace, then save again.',1)}
|
|
171
|
+
else status((result.content?.[0]?.text)||'Could not save. Try again.',1);controls();resize();return}
|
|
172
|
+
if(result?.isError&&draft&&$('form').hidden===false){$('plot').dataset.state='ready';status((result.content?.[0]?.text)||'Could not save. Try again.',1);controls();return}
|
|
173
|
+
if(result?.isError){trouble('tool-returned-error');
|
|
174
|
+
if(shown&&$('plot').firstChild){stale();controls();return}shown=false;controls();
|
|
175
|
+
status(ref?'Chart unavailable. Check your access to this dashboard, then Refresh.'
|
|
176
|
+
:'Chart unavailable. Ask again to retry.',1);return}
|
|
177
|
+
const p=result?.structuredContent;if(!p||typeof p!=='object'){shown=false;controls();status('No chart data came back for this result.',1);return}
|
|
178
|
+
revision++;shown=true;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();delete $('rail').dataset.value;
|
|
179
|
+
ref=p.reference||null;view=p.view||{};next=p.nextCursor||null;draft=p.draft||null;rows=p.rows||[];saved=null;$('form').hidden=true;
|
|
180
|
+
$('title').textContent=p.title||'Analytics';$('description').textContent=answer(p);root.dataset.kind=p.kind;$('choices').toggleAttribute('data-grid',p.kind==='dashboard');
|
|
181
|
+
if(p.kind==='dashboard'){$('plot').dataset.state='off';
|
|
182
|
+
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)}
|
|
183
|
+
status(p.widgets?.length?'':'No charts on this dashboard yet. Add one in Databrain.');
|
|
184
|
+
}else if(p.kind==='kpi'){
|
|
185
|
+
const number=document.createElement('p');number.className='kpi';number.textContent=fmt(p.value);exact(number,p.value);$('plot').append(number);status('');
|
|
186
|
+
}else if(p.svg){
|
|
187
|
+
const box=document.createElement('div');box.className='chart';box.style.aspectRatio=p.width+'/'+p.height;
|
|
188
|
+
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);
|
|
189
|
+
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');
|
|
190
|
+
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+'%';
|
|
191
|
+
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;
|
|
192
|
+
b.onclick=()=>{box.querySelectorAll('.point').forEach(e=>e.setAttribute('aria-pressed','false'));b.setAttribute('aria-pressed','true');hover();
|
|
193
|
+
$('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);
|
|
194
|
+
status('');
|
|
195
|
+
}else if(p.kind==='table'){
|
|
196
|
+
// ask_question returns a table by design; calling that a missing chart called success a failure.
|
|
197
|
+
$('plot').dataset.state='off';status('');
|
|
198
|
+
}else{$('plot').dataset.state='off';
|
|
199
|
+
status('Nothing to show. Try a wider date range, then Refresh.',1)}
|
|
200
|
+
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));
|
|
201
|
+
b.onclick=()=>run('preview_chart',{draft,rows,chartType:t});$('choices').append(b)}$('choices').dataset.chips='1'}else delete $('choices').dataset.chips;
|
|
202
|
+
if(p.previewError)status('Cannot draw this as '+(p.draft?.chartType||'a chart')+' ('+p.previewError+'). Showing the rows.',1);
|
|
203
|
+
paint();table(p.rows||[],p.columns||Object.keys(p.rows?.[0]||{}));scope(p,p.rows?.length||0);
|
|
204
|
+
const only=!p.svg&&p.value===undefined&&p.kind!=='dashboard';$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}
|
|
205
|
+
$('refresh').onclick=()=>call('refresh_widget',{cursor:undefined});$('next').onclick=()=>call('render_chart',{cursor:next});
|
|
206
|
+
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()}
|
|
207
|
+
$('save').onclick=()=>{attempt=crypto.randomUUID();$('dtitle').value=draft?.title||'';choose(draft?.workspaces||[])};
|
|
208
|
+
$('cancel').onclick=()=>{$('form').hidden=true;status('');controls();resize()};
|
|
209
|
+
$('commit').onclick=()=>{const t=$('dtitle').value.trim();if(!t){status('Name the dashboard.',1);return}
|
|
210
|
+
run('save_chart_as_dashboard',{draft,chartType:draft.chartType,dashboardTitle:t,workspaceName:$('wsrow').hidden?undefined:$('ws').value,attemptId:attempt,confirm:'APPLY_TO_PRODUCTION'})};
|
|
211
|
+
$('expand').onclick=()=>display(context.displayMode==='fullscreen'?'inline':'fullscreen');
|
|
212
|
+
$('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})};
|
|
213
|
+
function receive(event){if(event.source!==parent||closed)return;const m=event.data;
|
|
214
|
+
if(!m||Array.isArray(m)||m.jsonrpc!=='2.0'||(origin!=='*'&&event.origin!==origin))return;
|
|
215
|
+
if(!m.method&&pending.has(m.id)&&('result'in m||'error'in m)){if(origin==='*'&&event.origin!=='null')origin=event.origin;
|
|
216
|
+
const p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);
|
|
217
|
+
if(m.error){const error=Error('host error');error.code=m.error.code;p.reject(error)}else p.resolve(m.result);return}
|
|
218
|
+
if(m.method==='ping'&&m.id!==undefined){send({id:m.id,result:{}});return}
|
|
219
|
+
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}
|
|
220
|
+
if(!ready||m.id!==undefined)return;
|
|
221
|
+
let slot=DASH.indexOf(m.method),kind='dashed';
|
|
222
|
+
if(slot<0){slot=SNAKE.indexOf(m.method);kind='snake'}
|
|
223
|
+
if(slot>=0){
|
|
224
|
+
/* A dual host posts each notification twice. Lock to the first name and drop its twin:
|
|
225
|
+
same payload, and a second render is a visible flicker. */
|
|
226
|
+
if(!dialect)dialect=kind;if(dialect!==kind)return;
|
|
227
|
+
// The snake result nests one level deeper.
|
|
228
|
+
if(slot===0)render(kind==='snake'?m.params&&m.params.result:m.params);
|
|
229
|
+
else if(slot===1)wait();
|
|
230
|
+
else{host(m.params||{});resize()}
|
|
231
|
+
return}
|
|
232
|
+
if(m.method==='ui/notifications/tool-cancelled'){revision++;busy=false;$('plot').dataset.state='off';status('Request cancelled.');controls()}}
|
|
233
|
+
addEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();
|
|
234
|
+
rpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})
|
|
235
|
+
.then(r=>{if(r.protocolVersion!=='2026-01-26')throw Error('version');host(r.hostContext||{});ready=true;notify(INIT,{});resize()})
|
|
236
|
+
.catch(()=>{trouble('initialize-failed');status('This chart needs an MCP Apps host.',1)});
|
|
237
|
+
})();`;
|
|
238
|
+
var SERVED_SCRIPT = TILE_SCRIPT.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "").replace(/\n{2,}/g, "\n");
|
|
239
|
+
function tileHtml(kind = "chart") {
|
|
240
|
+
return strip(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
241
|
+
<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'">
|
|
242
|
+
<title>Databrain ${kind} tile</title><style id="hostfont"></style><style>
|
|
243
|
+
/* Host tokens are the Claude MCP Apps style-variable names. Every one carries a fallback:
|
|
244
|
+
hosts may ship any subset, so nothing here may depend on a token existing. */
|
|
245
|
+
:root{color-scheme:light dark;
|
|
246
|
+
--dbn-bg:var(--color-background-primary,Canvas);--dbn-text:var(--color-text-primary,CanvasText);
|
|
247
|
+
--dbn-mute:var(--color-text-secondary,GrayText);--dbn-faint:var(--color-text-tertiary,GrayText);
|
|
248
|
+
--dbn-edge:var(--color-border-secondary,color-mix(in srgb,currentColor 20%,transparent));
|
|
249
|
+
--dbn-line:var(--color-border-tertiary,color-mix(in srgb,currentColor 9%,transparent));
|
|
250
|
+
--dbn-accent:var(--color-text-info,LinkText);--dbn-ring:var(--color-ring-primary,var(--dbn-accent));
|
|
251
|
+
--dbn-bad:var(--color-text-danger,var(--dbn-text));
|
|
252
|
+
--dbn-quiet:color-mix(in srgb,currentColor 6%,transparent);
|
|
253
|
+
--bw:var(--border-width-regular,1px);--r-s:var(--border-radius-sm,6px);--r-m:var(--border-radius-md,8px);
|
|
254
|
+
--med:var(--font-weight-medium,500);--semi:var(--font-weight-semibold,600);--xs:var(--font-text-xs-size,12px);
|
|
255
|
+
/* One curve. Quick / standard / display-mode. Corporate: no overshoot. */
|
|
256
|
+
--e:cubic-bezier(.2,0,0,1);--q:120ms;--s:300ms;--l:400ms}
|
|
257
|
+
*{box-sizing:border-box}
|
|
258
|
+
body{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);
|
|
259
|
+
font:var(--font-text-sm-size,14px)/var(--font-text-sm-line-height,1.4) var(--font-sans,system-ui)}
|
|
260
|
+
main{padding:20px;max-width:1200px;margin:auto;transition:padding var(--l) var(--e)}
|
|
261
|
+
header{display:flex;align-items:start;justify-content:space-between;gap:12px}header>div{min-width:0}
|
|
262
|
+
.acts{display:flex;gap:6px;flex:none;align-items:center}
|
|
263
|
+
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)}
|
|
264
|
+
p{margin:6px 0;color:var(--dbn-mute);overflow-wrap:anywhere}
|
|
265
|
+
#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}
|
|
266
|
+
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}
|
|
267
|
+
button{cursor:pointer;transition:background-color var(--q) var(--e),border-color var(--q) var(--e)}
|
|
268
|
+
input{max-width:160px;min-width:0}button:hover:not(:disabled){background:var(--dbn-quiet)}button:disabled{color:var(--dbn-faint);cursor:default}
|
|
269
|
+
button:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--dbn-ring);outline-offset:2px}
|
|
270
|
+
/* The picker is a scannable grid of quiet chips: 26 charts fit in nine rows, not a wall. */
|
|
271
|
+
#choices[data-grid]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:6px;margin-top:12px}
|
|
272
|
+
#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}
|
|
273
|
+
#choices[data-grid] button:hover:not(:disabled){background:var(--dbn-quiet);border-color:var(--dbn-edge)}
|
|
274
|
+
/* One primary per state: the drill pill, and Next page when there is a next page. */
|
|
275
|
+
#choices button.primary,#next{flex:none;text-align:center;border:var(--bw) solid var(--dbn-accent);
|
|
276
|
+
border-radius:var(--r-s);padding:7px 10px;font-weight:var(--med);color:var(--dbn-accent)}
|
|
277
|
+
#choices button.primary{align-self:flex-start;margin-top:10px}
|
|
278
|
+
/* Refresh is a re-fetch, not a choice. Quiet it so it stops reading as a fifth chart. */
|
|
279
|
+
#refresh{border-color:transparent;color:var(--dbn-mute);padding:7px 8px}
|
|
280
|
+
#refresh:hover:not(:disabled){color:var(--dbn-text);background:none;text-decoration:underline}
|
|
281
|
+
#status:empty{margin:0}
|
|
282
|
+
#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)}
|
|
283
|
+
#status[data-ok]{color:var(--color-text-success,var(--dbn-text));border-color:var(--color-border-success,color-mix(in srgb,currentColor 40%,transparent))}
|
|
284
|
+
/* "Show as": one row of quiet chips, the current type pressed. Never a second title. */
|
|
285
|
+
#choices[data-chips]{flex-direction:row;flex-wrap:wrap;gap:6px;margin-top:10px}
|
|
286
|
+
.chip{border-color:var(--dbn-line);border-radius:999px;padding:5px 11px;font-size:var(--xs);color:var(--dbn-mute)}
|
|
287
|
+
.chip:hover:not(:disabled){color:var(--dbn-text);border-color:var(--dbn-edge)}
|
|
288
|
+
.chip[aria-pressed=true]{color:var(--dbn-text);border-color:var(--dbn-text);background:var(--dbn-quiet)}
|
|
289
|
+
/* Save is the one commit on a draft, so it is the one filled button in the tile. */
|
|
290
|
+
#save,#commit{border-color:var(--dbn-accent);background:var(--dbn-accent);color:var(--dbn-bg);font-weight:var(--med)}
|
|
291
|
+
#save:hover:not(:disabled),#commit:hover:not(:disabled){background:color-mix(in srgb,var(--dbn-accent) 85%,var(--dbn-text))}
|
|
292
|
+
#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}
|
|
293
|
+
[hidden]{display:none!important}#form label{flex-direction:column;align-items:stretch;gap:4px;font-size:var(--xs)}
|
|
294
|
+
#form input,#form select{max-width:none;width:100%;background:color-mix(in srgb,currentColor 4%,var(--dbn-bg))}
|
|
295
|
+
#wsone{color:var(--dbn-mute);font-size:var(--xs)}#wsone:empty{display:none}
|
|
296
|
+
#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))}
|
|
297
|
+
/* Reserved plot. The skeleton is a static bar silhouette matching the 15:8 backend render:
|
|
298
|
+
Claude's guidance asks for a skeleton that matches the final layout, and bans nothing here,
|
|
299
|
+
but shimmer is banned by our own motion budget - so this paints once and never repaints. */
|
|
300
|
+
#plot{position:relative;transition:opacity var(--q) var(--e)}#plot[data-busy],#plot[data-state=stale]{opacity:.55}
|
|
301
|
+
#plot[data-state=wait]{aspect-ratio:15/8}
|
|
302
|
+
#plot[data-state=wait]::before,#plot[data-state=ready]::before{content:'';position:absolute;pointer-events:none;inset:16% 6% 13%;
|
|
303
|
+
border-bottom:var(--bw) solid var(--dbn-line);transition:opacity var(--s) var(--e);
|
|
304
|
+
--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}
|
|
305
|
+
#plot[data-state=ready]::before{opacity:0}
|
|
306
|
+
.chart{position:relative;width:100%;margin:12px 0}.chart img{width:100%;height:100%;display:block}
|
|
307
|
+
/* A point's geometry comes from the renderer and can be 8px; the hit area must not.
|
|
308
|
+
::after pads it to at least 24px; ::before is the marker that says "you can pick this". */
|
|
309
|
+
.point{position:absolute;padding:0;border:0;background:transparent;border-radius:50%}
|
|
310
|
+
.point::after{content:'';position:absolute;inset:-8px}
|
|
311
|
+
.point::before{content:'';position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px;border-radius:50%;
|
|
312
|
+
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)}
|
|
313
|
+
.point:hover::before,.point:focus-visible::before{opacity:.7;transform:none}
|
|
314
|
+
.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)}
|
|
315
|
+
.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)}
|
|
316
|
+
/* The readout rail. At rest it states the scope of what is on screen; on selection it
|
|
317
|
+
becomes the value. One slot, two jobs, so selecting a bar never reflows the tile. */
|
|
318
|
+
#rail{display:flex;align-items:center;min-height:2.5em;margin:2px 0 0}
|
|
319
|
+
#scope,#value{margin:0;display:none;overflow-wrap:anywhere}
|
|
320
|
+
#rail:not([data-value]) #scope{display:block;color:var(--dbn-mute);font-size:var(--xs)}
|
|
321
|
+
#rail:not([data-value]):has(#scope:empty){display:none}
|
|
322
|
+
#rail[data-value] #value{display:flex;flex-direction:column;gap:1px;line-height:1.15}
|
|
323
|
+
#vlabel{color:var(--dbn-mute);font-size:var(--xs)}
|
|
324
|
+
#vnum{font-size:var(--font-heading-md-size,18px);font-weight:var(--semi);letter-spacing:-.015em;font-variant-numeric:tabular-nums}
|
|
325
|
+
[data-mode=full] #vnum{font-size:var(--font-heading-xl-size,24px)}
|
|
326
|
+
footer,.dates{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
|
327
|
+
#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}
|
|
328
|
+
label{display:flex;align-items:center;gap:6px;color:var(--dbn-mute)}
|
|
329
|
+
#range{margin-top:10px}#data{margin-top:2px}
|
|
330
|
+
html:not([data-deep]) #range,html:not([data-deep]) #data:not([data-only]){display:none}
|
|
331
|
+
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)}
|
|
332
|
+
#rows{overflow:auto;max-height:260px;border-radius:var(--r-s)}
|
|
333
|
+
table{border-collapse:collapse;width:100%;font-size:var(--xs)}
|
|
334
|
+
th,td{text-align:left;padding:8px;border-bottom:var(--bw) solid var(--dbn-line);overflow-wrap:anywhere}
|
|
335
|
+
td.n,th.n{text-align:right;font-variant-numeric:tabular-nums}
|
|
336
|
+
th{font-weight:var(--semi);position:sticky;top:0;background:var(--dbn-bg);color:var(--dbn-mute);
|
|
337
|
+
font-size:var(--xs);letter-spacing:.02em;text-transform:uppercase}
|
|
338
|
+
/* Inline to fullscreen is a layout morph, not a new screen: main widens over 400ms on the
|
|
339
|
+
one curve and every dependent box follows it. Fullscreen is the detailed view, so the
|
|
340
|
+
table loses its inline height cap. */
|
|
341
|
+
/* The SVG is rendered at 600x320 and scaled by width, so an unbounded fullscreen column
|
|
342
|
+
makes the chart's own axis type larger than the tile's heading. Hold a measure. */
|
|
343
|
+
[data-mode=full] main{padding:28px 32px;max-width:900px}
|
|
344
|
+
[data-mode=full] h1{font-size:var(--font-heading-xl-size,24px);line-height:var(--font-heading-xl-line-height,1.25)}
|
|
345
|
+
[data-mode=full] .kpi{font-size:clamp(44px,7vw,72px)}
|
|
346
|
+
[data-mode=full] #rows{max-height:none}
|
|
347
|
+
/* Touch: the host gives vertical pan to the conversation, so a nested scroller is a trap. */
|
|
348
|
+
@media(pointer:coarse){button,input{min-height:44px}summary{padding:12px 0}#rows{max-height:none;overflow:visible}}
|
|
349
|
+
@media(max-width:480px){main{padding:14px}}
|
|
350
|
+
/* Entrance. Content first, chrome 60ms later: 300+60 = 360ms, inside the 400ms budget.
|
|
351
|
+
No stagger past two groups - a chat tile that cascades reads as lag, not luxury. */
|
|
352
|
+
@keyframes rise{from{opacity:0;transform:translateY(6px)}}
|
|
353
|
+
@keyframes land{from{opacity:0;transform:translateY(4px) scale(.995)}}
|
|
354
|
+
header,#status,#plot{animation:rise var(--s) var(--e) both}
|
|
355
|
+
#rail,#choices,footer,#range,#data{animation:rise var(--s) var(--e) 60ms both}
|
|
356
|
+
.chart,.kpi{animation:land var(--s) var(--e) both}
|
|
357
|
+
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}
|
|
358
|
+
</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>
|
|
359
|
+
<p id="status" role="status">Loading chart\u2026</p><div id="plot" data-state="wait"></div>
|
|
360
|
+
<div id="rail"><p id="scope"></p><p id="value"><span id="vlabel"></span><b id="vnum"></b></p></div><div id="choices"></div>
|
|
361
|
+
<footer><button id="save" hidden>Save as dashboard</button><button id="next" hidden>Next page</button></footer>
|
|
362
|
+
<form id="form" hidden onsubmit="return false"><label>Dashboard name<input id="dtitle" type="text" maxlength="200" autocomplete="off"></label>
|
|
363
|
+
<label id="wsrow" hidden>Workspace<select id="ws"></select></label><p id="wsone"></p>
|
|
364
|
+
<div class="dates"><button id="commit" type="button">Save to Databrain</button><button id="cancel" type="button">Cancel</button></div></form>
|
|
365
|
+
<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>
|
|
366
|
+
<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>`);
|
|
367
|
+
}
|
|
368
|
+
function strip(html) {
|
|
369
|
+
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>`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/apps/registry.ts
|
|
373
|
+
var actions = {
|
|
374
|
+
render_chart: "Render one authorized chart page as SVG with point metadata. Cursor pagination stays within the same guest scope.",
|
|
375
|
+
refresh_widget: "Refresh a chart in the current guest scope. Revalidates access before returning data.",
|
|
376
|
+
drill_down: "Explore a server-issued drill identifier within the current chart and guest scope.",
|
|
377
|
+
change_date_range: "Render a chart for an ordered date range within the current guest scope."
|
|
378
|
+
};
|
|
379
|
+
function registerChartApps(server, client, backend = createChartBackend(client)) {
|
|
380
|
+
for (const [name, description] of Object.entries(actions)) {
|
|
381
|
+
const action = name;
|
|
382
|
+
const tool = server.tool(name, description, {
|
|
383
|
+
reference: referenceSchema,
|
|
384
|
+
view: viewSchema.default({}),
|
|
385
|
+
context: z.string().max(200).optional().describe("Chart interaction context")
|
|
386
|
+
}, READ_ONLY, async ({ reference, view }) => {
|
|
387
|
+
try {
|
|
388
|
+
if (action === "drill_down" && !view.drillId || action === "change_date_range" && !view.dateRange) throw new Error("Missing view selector");
|
|
389
|
+
const page = pageSchema.parse(await toolContext.run(
|
|
390
|
+
{ tool: name, category: "embedding", workflowIntent: "chart_app", inputMode: "app" },
|
|
391
|
+
() => backend.execute(action, reference, view)
|
|
392
|
+
));
|
|
393
|
+
if (page.svg && (!/^\s*<svg\b/i.test(page.svg) || /<\s*(?:script|foreignObject|iframe|image|animate\w*|set)\b|<!DOCTYPE|<!ENTITY|\bon\w+\s*=|@(?:import|keyframes)|\banimation\s*:|(?:href\s*=\s*["'](?!#)|url\s*\(\s*["']?(?!#))/i.test(page.svg))) {
|
|
394
|
+
throw new Error("Unsafe SVG");
|
|
395
|
+
}
|
|
396
|
+
return chartResult(`${page.kind === "kpi" ? "KPI" : "Chart"} ready. ${page.rows.length} rows in this page.`, {
|
|
397
|
+
...page,
|
|
398
|
+
reference,
|
|
399
|
+
view
|
|
400
|
+
});
|
|
401
|
+
} catch {
|
|
402
|
+
return { isError: true, content: [{
|
|
403
|
+
type: "text",
|
|
404
|
+
text: "Chart unavailable. Check dashboard access and the chart renderer integration, then retry."
|
|
405
|
+
}] };
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
tool.update({ _meta: appMeta(["app"]) });
|
|
409
|
+
}
|
|
410
|
+
registerDraftTools(server, client);
|
|
411
|
+
for (const [uri, kind] of [[CHART_URI, "chart"], [KPI_URI, "kpi"]]) {
|
|
412
|
+
server.resource(`Databrain ${kind} tile`, uri, { mimeType: APP_MIME, description: `Interactive ${kind} using host theme and scoped server tools` }, async () => ({
|
|
413
|
+
contents: [{ uri, mimeType: APP_MIME, text: tileHtml(kind), _meta: { ui: { prefersBorder: true } } }]
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function registerDraftTools(server, client) {
|
|
418
|
+
const preview = server.tool(
|
|
419
|
+
"preview_chart",
|
|
420
|
+
"Redraw an unsaved ask_question answer as another chart type, from the rows already on screen. No query runs.",
|
|
421
|
+
{
|
|
422
|
+
draft: draftSchema,
|
|
423
|
+
rows: rowsSchema,
|
|
424
|
+
chartType: z.enum(RENDER_TYPES),
|
|
425
|
+
context: z.string().max(200).optional().describe("Chart interaction context")
|
|
426
|
+
},
|
|
427
|
+
READ_ONLY,
|
|
428
|
+
async ({ draft, rows, chartType }) => {
|
|
429
|
+
try {
|
|
430
|
+
const page = await draftPage(client, draft, rows, chartType);
|
|
431
|
+
return chartResult(`${page.kind === "table" ? "Table" : "Chart"} ready as ${chartType}.`, page);
|
|
432
|
+
} catch {
|
|
433
|
+
return { isError: true, content: [{ type: "text", text: "Chart preview unavailable. Ask the question again to retry." }] };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
preview.update({ _meta: appMeta(["app"]) });
|
|
438
|
+
const save = server.tool(
|
|
439
|
+
"save_chart_as_dashboard",
|
|
440
|
+
`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.`,
|
|
441
|
+
{
|
|
442
|
+
draft: draftSchema,
|
|
443
|
+
chartType: z.enum(RENDER_TYPES),
|
|
444
|
+
dashboardTitle: z.string().trim().min(1).max(200),
|
|
445
|
+
workspaceName: z.string().trim().min(1).max(256).optional(),
|
|
446
|
+
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."),
|
|
447
|
+
confirm: z.string().optional(),
|
|
448
|
+
context: z.string().max(200).optional().describe("Chart interaction context")
|
|
449
|
+
},
|
|
450
|
+
WRITE_CREATE,
|
|
451
|
+
async ({ draft, chartType, dashboardTitle, workspaceName, attemptId, confirm }) => {
|
|
452
|
+
if (confirm !== APPLY_TO_PRODUCTION_CONFIRMATION) {
|
|
453
|
+
return { isError: true, content: [{
|
|
454
|
+
type: "text",
|
|
455
|
+
text: `Saving writes to production. Retry with confirm: "${APPLY_TO_PRODUCTION_CONFIRMATION}".`
|
|
456
|
+
}] };
|
|
457
|
+
}
|
|
458
|
+
const choices = await workspacesForDatamart(client, draft.datamartName, "authorize");
|
|
459
|
+
const refuse = (text) => ({
|
|
460
|
+
isError: true,
|
|
461
|
+
content: [{ type: "text", text }],
|
|
462
|
+
structuredContent: { kind: "save", ok: false, choices }
|
|
463
|
+
});
|
|
464
|
+
const target = workspaceName ? choices.find((w) => w.trim().toLowerCase() === workspaceName.toLowerCase()) : choices.length === 1 ? choices[0] : void 0;
|
|
465
|
+
if (workspaceName && !target) {
|
|
466
|
+
return refuse(`Workspace "${workspaceName}" cannot run this datamart's SQL. Choose one of: ${choices.join(", ")}.`);
|
|
467
|
+
}
|
|
468
|
+
if (!target) {
|
|
469
|
+
return refuse(choices.length ? `Choose a workspace: ${choices.join(", ")}.` : `No workspace is built on datamart "${draft.datamartName}". Create one in Databrain, then save again.`);
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const saved = await saveDraftAsDashboard(client, draft, { chartType, dashboardTitle, workspaceName: target, attemptId });
|
|
473
|
+
return chartResult(
|
|
474
|
+
`Saved "${saved.title}" to workspace ${saved.workspaceName} (dashboard ${saved.dashboardId}, metric ${saved.metricId}).`,
|
|
475
|
+
{ kind: "save", ok: true, ...saved, draft: { ...draft, chartType } }
|
|
476
|
+
);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
const code = error instanceof DatabrainApiError ? error.code : "SAVE_FAILED";
|
|
479
|
+
return {
|
|
480
|
+
isError: true,
|
|
481
|
+
content: [{
|
|
482
|
+
type: "text",
|
|
483
|
+
text: `Could not save (${code}). Check the workspace name and your access, then try again.`
|
|
484
|
+
}],
|
|
485
|
+
structuredContent: { kind: "save", ok: false, code }
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
save.update({ _meta: appMeta(["model", "app"]) });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export {
|
|
494
|
+
registerChartApps
|
|
495
|
+
};
|
|
496
|
+
//# sourceMappingURL=chunk-5IGP6A6T.js.map
|