@bicharts/chart-mcp 0.6.72 → 0.6.73
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/dist/index.mjs +4 -4
- package/package.json +1 -1
- package/skills/bic-charts/SKILL.md +6 -0
package/dist/index.mjs
CHANGED
|
@@ -244,8 +244,8 @@ data:
|
|
|
244
244
|
`),o+=`data: ${JSON.stringify(n)}
|
|
245
245
|
|
|
246
246
|
`,a.enqueue(t.encode(o)),!0}catch(o){return this.onerror?.(o),!1}}handleUnsupportedRequest(){return this.onerror?.(new Error("Method not allowed.")),new Response(JSON.stringify({jsonrpc:"2.0",error:{code:-32e3,message:"Method not allowed."},id:null}),{status:405,headers:{Allow:"GET, POST, DELETE","Content-Type":"application/json"}})}async handlePostRequest(a,t){try{let n=a.headers.get("accept");if(!n?.includes("application/json")||!n.includes("text/event-stream"))return this.onerror?.(new Error("Not Acceptable: Client must accept both application/json and text/event-stream")),this.createJsonErrorResponse(406,-32e3,"Not Acceptable: Client must accept both application/json and text/event-stream");let r=a.headers.get("content-type");if(!r||!r.includes("application/json"))return this.onerror?.(new Error("Unsupported Media Type: Content-Type must be application/json")),this.createJsonErrorResponse(415,-32e3,"Unsupported Media Type: Content-Type must be application/json");let o={headers:Object.fromEntries(a.headers.entries()),url:new URL(a.url)},i;if(t?.parsedBody!==void 0)i=t.parsedBody;else try{i=await a.json()}catch{return this.onerror?.(new Error("Parse error: Invalid JSON")),this.createJsonErrorResponse(400,-32700,"Parse error: Invalid JSON")}let s;try{Array.isArray(i)?s=i.map(y=>x8.parse(y)):s=[x8.parse(i)]}catch{return this.onerror?.(new Error("Parse error: Invalid JSON-RPC message")),this.createJsonErrorResponse(400,-32700,"Parse error: Invalid JSON-RPC message")}let l=s.some($o);if(l){if(this._initialized&&this.sessionId!==void 0)return this.onerror?.(new Error("Invalid Request: Server already initialized")),this.createJsonErrorResponse(400,-32600,"Invalid Request: Server already initialized");if(s.length>1)return this.onerror?.(new Error("Invalid Request: Only one initialization request is allowed")),this.createJsonErrorResponse(400,-32600,"Invalid Request: Only one initialization request is allowed");this.sessionId=this.sessionIdGenerator?.(),this._initialized=!0,this.sessionId&&this._onsessioninitialized&&await Promise.resolve(this._onsessioninitialized(this.sessionId))}if(!l){let y=this.validateSession(a);if(y)return y;let W=this.validateProtocolVersion(a);if(W)return W}if(!s.some(_4)){for(let y of s)this.onmessage?.(y,{authInfo:t?.authInfo,requestInfo:o});return new Response(null,{status:202})}let c=crypto.randomUUID(),d=s.find(y=>$o(y)),p=d?d.params.protocolVersion:a.headers.get("mcp-protocol-version")??fd;if(this._enableJsonResponse)return new Promise(y=>{this._streamMapping.set(c,{resolveJson:y,cleanup:()=>{this._streamMapping.delete(c)}});for(let W of s)_4(W)&&this._requestToStreamMapping.set(W.id,c);for(let W of s)this.onmessage?.(W,{authInfo:t?.authInfo,requestInfo:o})});let m=new TextEncoder,h,g=new ReadableStream({start:y=>{h=y},cancel:()=>{this._streamMapping.delete(c)}}),f={"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"};this.sessionId!==void 0&&(f["mcp-session-id"]=this.sessionId);for(let y of s)_4(y)&&(this._streamMapping.set(c,{controller:h,encoder:m,cleanup:()=>{this._streamMapping.delete(c);try{h.close()}catch{}}}),this._requestToStreamMapping.set(y.id,c));await this.writePrimingEvent(h,m,c,p);for(let y of s){let W,k;_4(y)&&this._eventStore&&p>="2025-11-25"&&(W=()=>{this.closeSSEStream(y.id)},k=()=>{this.closeStandaloneSSEStream()}),this.onmessage?.(y,{authInfo:t?.authInfo,requestInfo:o,closeSSEStream:W,closeStandaloneSSEStream:k})}return new Response(g,{status:200,headers:f})}catch(n){return this.onerror?.(n),this.createJsonErrorResponse(400,-32700,"Parse error",{data:String(n)})}}async handleDeleteRequest(a){let t=this.validateSession(a);if(t)return t;let n=this.validateProtocolVersion(a);return n||(await Promise.resolve(this._onsessionclosed?.(this.sessionId)),await this.close(),new Response(null,{status:200}))}validateSession(a){if(this.sessionIdGenerator===void 0)return;if(!this._initialized)return this.onerror?.(new Error("Bad Request: Server not initialized")),this.createJsonErrorResponse(400,-32e3,"Bad Request: Server not initialized");let t=a.headers.get("mcp-session-id");if(!t)return this.onerror?.(new Error("Bad Request: Mcp-Session-Id header is required")),this.createJsonErrorResponse(400,-32e3,"Bad Request: Mcp-Session-Id header is required");if(t!==this.sessionId)return this.onerror?.(new Error("Session not found")),this.createJsonErrorResponse(404,-32001,"Session not found")}validateProtocolVersion(a){let t=a.headers.get("mcp-protocol-version");if(t!==null&&!ht.includes(t))return this.onerror?.(new Error(`Bad Request: Unsupported protocol version: ${t} (supported versions: ${ht.join(", ")})`)),this.createJsonErrorResponse(400,-32e3,`Bad Request: Unsupported protocol version: ${t} (supported versions: ${ht.join(", ")})`)}async close(){this._streamMapping.forEach(({cleanup:a})=>{a()}),this._streamMapping.clear(),this._requestResponseMap.clear(),this.onclose?.()}closeSSEStream(a){let t=this._requestToStreamMapping.get(a);if(!t)return;let n=this._streamMapping.get(t);n&&n.cleanup()}closeStandaloneSSEStream(){let a=this._streamMapping.get(this._standaloneSseStreamId);a&&a.cleanup()}async send(a,t){let n=t?.relatedRequestId;if((o4(a)||pt(a))&&(n=a.id),n===void 0){if(o4(a)||pt(a))throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");let i;this._eventStore&&(i=await this._eventStore.storeEvent(this._standaloneSseStreamId,a));let s=this._streamMapping.get(this._standaloneSseStreamId);if(s===void 0)return;s.controller&&s.encoder&&this.writeSSEEvent(s.controller,s.encoder,a,i);return}let r=this._requestToStreamMapping.get(n);if(!r)throw new Error(`No connection established for request ID: ${String(n)}`);let o=this._streamMapping.get(r);if(!this._enableJsonResponse&&o?.controller&&o?.encoder){let i;this._eventStore&&(i=await this._eventStore.storeEvent(r,a)),this.writeSSEEvent(o.controller,o.encoder,a,i)}if(o4(a)||pt(a)){this._requestResponseMap.set(n,a);let i=Array.from(this._requestToStreamMapping.entries()).filter(([l,u])=>u===r).map(([l])=>l);if(i.every(l=>this._requestResponseMap.has(l))){if(!o)throw new Error(`No connection established for request ID: ${String(n)}`);if(this._enableJsonResponse&&o.resolveJson){let l={"Content-Type":"application/json"};this.sessionId!==void 0&&(l["mcp-session-id"]=this.sessionId);let u=i.map(c=>this._requestResponseMap.get(c));u.length===1?o.resolveJson(new Response(JSON.stringify(u[0]),{status:200,headers:l})):o.resolveJson(new Response(JSON.stringify(u),{status:200,headers:l}))}else o.cleanup();for(let l of i)this._requestResponseMap.delete(l),this._requestToStreamMapping.delete(l)}}}};var yr=class{constructor(a={}){this._requestContext=new WeakMap,this._webStandardTransport=new fr(a),this._requestListener=hu(async t=>{let n=this._requestContext.get(t);return this._webStandardTransport.handleRequest(t,{authInfo:n?.authInfo,parsedBody:n?.parsedBody})},{overrideGlobalObjects:!1})}get sessionId(){return this._webStandardTransport.sessionId}set onclose(a){this._webStandardTransport.onclose=a}get onclose(){return this._webStandardTransport.onclose}set onerror(a){this._webStandardTransport.onerror=a}get onerror(){return this._webStandardTransport.onerror}set onmessage(a){this._webStandardTransport.onmessage=a}get onmessage(){return this._webStandardTransport.onmessage}async start(){return this._webStandardTransport.start()}async close(){return this._webStandardTransport.close()}async send(a,t){return this._webStandardTransport.send(a,t)}async handleRequest(a,t,n){let r=a.auth;await hu(async i=>this._webStandardTransport.handleRequest(i,{authInfo:r,parsedBody:n}),{overrideGlobalObjects:!1})(a,t)}closeSSEStream(a){this._webStandardTransport.closeSSEStream(a)}closeStandaloneSSEStream(){this._webStandardTransport.closeStandaloneSSEStream()}};var t$=16*1024*1024;function VW(e,a){let t=i=>{let s=e.indexOf(i);return s>=0&&s+1<e.length?e[s+1]:void 0},n=e.includes("--http")||/^(1|true|yes|on)$/i.test((a.BIC_MCP_HOSTED??"").trim()),r=t("--port")??a.BIC_MCP_HTTP_PORT??"8799",o=Number(r);if(!Number.isInteger(o)||o<0||o>65535)throw new Error(`--port must be a whole number from 0 to 65535 (got "${r}").`);return{http:n,host:t("--host")??"127.0.0.1",port:o}}function Nr(e,a,t,n={}){if(e.headersSent){e.end();return}e.writeHead(a,{"Content-Type":"application/json","Cache-Control":"no-store",...n}),e.end(JSON.stringify(t))}function s7(e,a,t,n,r={}){Nr(e,a,{jsonrpc:"2.0",error:{code:t,message:n},id:null},r)}function n$(e,a,t){return new Promise((n,r)=>{let o=[],i=0,s=!1;e.on("data",l=>{if(!s){if(i+=l.length,i>t){s=!0,o.length=0,s7(a,413,-32600,`The request is larger than this server accepts (${Math.floor(t/1048576)} MB). Send the table at the chart's grain - aggregated - or with fewer columns.`,{Connection:"close"}),n(null);return}o.push(l)}}),e.on("end",()=>{s||n(Buffer.concat(o).toString("utf-8"))}),e.on("error",r)})}async function r$(e,a,t){let n=Tb(e.headers.authorization);if(!n){s7(a,401,-32001,"Sign in to use this chart server: connect to it from your app, which sends your account's access token with each request.",{"WWW-Authenticate":"Bearer"});return}let r=await n$(e,a,t.maxBodyBytes??t$);if(r===null)return;let o;try{o=JSON.parse(r)}catch{s7(a,400,-32700,"Parse error: the request body isn't JSON.");return}let i=t.newServer(),s=new yr({sessionIdGenerator:void 0});a.on("close",()=>{s.close().catch(()=>{}),i.close().catch(()=>{})}),await i.connect(s);let l=Rb(e.headers[Nl.toLowerCase()]);await _b({accessToken:n,...l?{clientIp:l}:{}},()=>s.handleRequest(e,a,o))}function ZW(e){let a=e.log??(n=>{process.stderr.write(n+`
|
|
247
|
-
`)}),t=a$((n,r)=>{let o=new URL(n.url??"/","http://localhost").pathname;if(o==="/healthz"){if(n.method!=="GET"&&n.method!=="HEAD"){Nr(r,405,{error:"Use GET."},{Allow:"GET"});return}Nr(r,200,{ok:!0,server:"bic-chart",version:e.version});return}if(o!=="/mcp"){Nr(r,404,{error:"Not found. The MCP endpoint is /mcp."});return}if(n.method!=="POST"){s7(r,405,-32e3,"Method not allowed: this server is stateless, so it takes POST only.",{Allow:"POST"});return}r$(n,r,e).catch(i=>{a(`[bic-chart http] request failed (${i?.name??"Error"})`),s7(r,500,-32603,"Internal error: the chart server couldn't handle this request. Try again.")})});return new Promise((n,r)=>{t.once("error",r),t.listen(e.port,e.host,()=>{t.off("error",r),n(t)})})}var QW=["assess_data_shape","list_eligible_charts","generate_chart","get_guide","preview_request"],l7="This hosted server receives your table, profiles it in memory and sends only the profile to generation; rows aren't stored or logged.",ek={csv_path:"this hosted server can't read files on your machine, so pass the table as data or csv_text",project_dir:"this hosted server can't see your project folder, so pass language or renderer instead",out_dir:"this hosted server returns the chart inline; it can't write to your machine",preview_html:"this hosted server returns the chart inline; it can't write to your machine"},Sr=e=>`Not accepted by this hosted server: ${ek[e]}.`,br={csv_path:T.string().optional().describe(Sr("csv_path")),project_dir:T.string().optional().describe(Sr("project_dir"))},qW={out_dir:T.string().optional().describe(Sr("out_dir")),preview_html:T.boolean().optional().describe(Sr("preview_html"))},JW=T.enum(["javascript","typescript","python",""]).optional(),YW=["csv_path","project_dir","out_dir","preview_html"],ak={assess_data_shape:{description:`Profile a dataset: per-column types, cardinality, ranges, measure/dimension roles, temporal/ordinal detection, and cross-column signals (nesting, shared values, association) - the same measurement engine the BIC Power BI visual uses before generating a chart. Pass the table as data ({columns, rows}) or csv_text. ${l7} This tool sends nothing on.`,inputs:{csv_path:br.csv_path},refuse:["csv_path"]},list_eligible_charts:{description:"The AUTHORITATIVE list of chart types that can render this dataset - the server's own eligibility engine, NOT a guess. Pass the table as data ({columns, rows}) or csv_text. "+l7+" Ranked for JavaScript (D3) unless you pass language ('python' ranks Plotly and matplotlib) or renderer ('D3'/'PLOTLY'/... for one renderer, '' for every renderer). No model runs, but the call is METERED: free within a generous hourly allowance, then usage credits per call
|
|
248
|
-
`);function f8(e,a){return ir(e,a).dir??qa(e??process.cwd())}var mu="0.6.72",ok=[],kr=null,k3={registerTool(e,a,t){ok.push({name:e,config:a,cb:t})},get server(){if(!kr)throw new Error("The local server isn't running in this process.");return kr.server}};function rk(e){let a=e?new v2({name:"bic-chart",version:mu},{instructions:nk}):new v2({name:"bic-chart",version:mu},{instructions:zW});e||(a.server.registerCapabilities({logging:{}}),yb(t=>{a.server.sendLoggingMessage({level:"notice",logger:"sign-in",data:t}).catch(()=>{})}));for(let t of ok){if(e&&!QW.includes(t.name))continue;let n=e?ak[t.name]:void 0,r={...t.config,...n?{description:n.description,inputSchema:{...t.config.inputSchema,...n.inputs??{}}}:{},annotations:{title:t.config.title,...t.config.annotations}},o=n?.refuse?(i,s)=>{let l=tk(i,n.refuse);return l?{isError:!0,content:[{type:"text",text:l}]}:t.cb(i,s)}:t.cb;a.registerTool(t.name,r,o)}return a}var u7="2026-10-06T21:48:45Z src:81e731fb800d (33 files)",gu="@bicharts/chart-host@0.6.70, @bicharts/shape-core@0.6.70",o$=T.object({columns:T.array(T.object({name:T.string(),dataType:T.enum(["String","Integer","Decimal","DateTime"]).optional().describe("Known type (DAX/semantic-model callers have it); omit to infer from values."),isMeasure:T.boolean().optional().describe("Known measure/dimension role; omit to apply the numeric-non-identifier heuristic. A semantic-model caller should set this."),format:T.string().optional().describe('Format string from the model (e.g. "$#,##0") \u2192 modelFormat.'),description:T.string().optional().describe("Column description from the model \u2192 modelDesc.")})).min(1),rows:T.array(T.union([T.array(T.any()),T.record(T.string(),T.any())])).min(1).describe("Rows as positional arrays aligned to columns, OR objects keyed by column name.")}).describe("A pre-typed {columns, rows} table (DAX query results, dataframe, arrow). Use INSTEAD of csv_path/csv_text."),fu={csv_path:T.string().optional().describe("Path to a CSV file (header row required)."),csv_text:T.string().optional().describe("Inline CSV text (alternative to csv_path)."),data:o$.optional().describe("A pre-typed {columns, rows} table \u2014 the shape a Fabric DAX query / dataframe already has. Alternative to csv_path/csv_text; when columns carry isMeasure/format/description the shape matches PBI fidelity with no guessing."),measures:T.array(T.string()).optional().describe("Columns to treat as MEASURES (the PBI 'Values' well \u2014 fields the chart aggregates). Numeric non-identifier columns are measures by default."),dimensions:T.array(T.string()).optional().describe("Columns to treat as DIMENSIONS (a PBI category/axis well \u2014 fields to group/break by). Use for numeric codes/years that should NOT be aggregated."),formats:T.record(T.string(),T.string()).optional().describe(`Per-column number/date FORMAT STRING (e.g. {"Revenue":"$#,##0","Margin":"0.0%"}). PBI supplies these from its model; a CSV can't, so provide them to steer number formatting.`),descriptions:T.record(T.string(),T.string()).optional().describe("Per-column human DESCRIPTION \u2014 gives the model semantic context the bare column name lacks."),group_by:T.array(T.string()).optional().describe("Columns that are the PRIMARY series/breakdown (the PBI 'Group by' well); the backend leads the visualization with these."),non_additive:T.array(T.string()).optional().describe("MEASURE columns that must NOT be summed (a ratio/average/rate \u2014 use mean, never stack). Usually auto-detected; this is an authoritative override."),privacy_level:T.enum(["0","10","20","30"]).optional().describe("How much detail to compute/share: 0 names only, 10 +counts, 20 +detailed stats (default), 30 +obfuscated sample rows.")};function g8(e){return{measures:e.measures,dimensions:e.dimensions,formats:e.formats,descriptions:e.descriptions,groupBy:e.group_by,nonAdditive:e.non_additive,privacyLevel:e.privacy_level??"20"}}function Cr(e){if(e.data)return jf(e.data.columns,e.data.rows,g8(e));if(e.csv_text)return B4(e.csv_text,g8(e));if(e.csv_path)return B4(c7(qa(e.csv_path),"utf-8"),g8(e));throw new Error("Provide one of: data ({columns, rows}), csv_text, or csv_path.")}k3.registerTool("assess_data_shape",{title:"Assess data shape",description:"Profile a dataset locally (no data leaves the machine): per-column types, cardinality, ranges, measure/dimension roles, temporal/ordinal detection, and cross-column signals (nesting, shared values, association). This is the same measurement engine the BIC Power BI visual uses before generating a chart.",inputSchema:fu,annotations:{readOnlyHint:!0,openWorldHint:!1}},async e=>{let a=Cr(e),t={totalRows:a.totalRows,leafCardinality:a.index.getLeafCardinality(),columns:a.shape},n=g4()?null:Kb(process.env);return{content:[{type:"text",text:JSON.stringify(t,null,2)},...n?[{type:"text",text:n}]:[]]}});k3.registerTool("get_guide",{title:"Read the build guide",description:"How to build an app with these charts, from the guide this package ships. Local and free: no network, no credentials. topic 'fabric-app' (the default) is what a Microsoft Fabric App on the Rayfin data app template needs: what to pull, generating, wiring in React, the Fabric App section and the time-savers. 'all' is the whole guide, including Blazor. 'chart-shapes' lists every chart type with the columns, limits and renderers it needs. Read it before the first generate_chart call.",inputSchema:{topic:T.enum(xW).optional().describe("'fabric-app' (default), 'all', or 'chart-shapes'.")},annotations:{readOnlyHint:!0,openWorldHint:!1}},async e=>{try{return{content:[{type:"text",text:jW(e.topic??"fabric-app")}]}}catch(a){return{isError:!0,content:[{type:"text",text:`The guide could not be read from this package (${a?.message??a}). It is also published as skills/bic-charts/SKILL.md inside @bicharts/chart-mcp on npm.`}]}}});k3.registerTool("list_eligible_charts",{title:"List eligible chart types",description:"The AUTHORITATIVE list of chart types that can render this dataset \u2014 the server's own eligibility engine, NOT a local guess. Profiles the data locally, then sends that profile to the backend. NO LLM in the loop, but this is a NETWORK call and it is METERED \u2014 see the licensing note below. By default the tool DETECTS the project's language (JS/TS vs Python from the working dir) and returns the charts for THAT language, SCOPED and ordered by how well each type fits this data (best first; `order: 'variety'` favors newer and less-common types instead), each tagged with its source language and renderer. Pass `language` ('javascript'|'python') to force it, or `renderer` ('D3'/'PLOTLY'/\u2026) to rank one specific renderer, or renderer '' for the full renderer-agnostic eligibility across ALL renderers (also ranked, by chart type alone, with no renderer/language tag). If the language can't be auto-detected and none is given, the tool asks you to specify one. Needs an account: eligibility IS the engine, so programmatic access to it is licensed. With nothing configured, the first call opens a browser sign-in, waits for it and then answers (a license key in the environment is the alternative for a machine with no browser). Free within a generous hourly allowance; beyond it a call may draw usage credits, so avoid calling it in a tight loop.",inputSchema:{...fu,language:T.enum(["javascript","typescript","python",""]).optional().describe("Project source language \u2192 the server ranks across that language's renderers (javascript/typescript = D3/Vega, python = Plotly/matplotlib). Omit to AUTO-DETECT from the working dir; `renderer` (if given) overrides this."),project_dir:T.string().optional().describe("Directory to auto-detect the project language from (default: the folder setup_fabric_app used, else the working directory or the one project folder directly inside it)."),renderer:T.enum(["D3","PLOTLY","PYMATPLOT","VEGA",""]).optional().describe("Force a specific renderer (overrides `language`): a concrete renderer ranks that renderer's charts; '' = the full renderer-agnostic eligibility (all renderers, ranked by chart type alone). Omit to use `language`/auto-detection."),level_of_detail:T.enum(["High","Medium","Low",""]).optional().describe("How much of the data the user wants to SEE \u2014 ASK THEM, do not infer. 'High' = every observation as its own mark, 'Medium' = every row shown as a distribution, 'Low' = one mark per category. Ranks this list the same way a generation would, so what you see here is what you will get. Omit if they have expressed no preference."),name_filter:T.string().optional().describe("Show only chart types matching this text (the same matcher the Power BI and Excel chart-type lists use). Three tiers, first non-empty one wins outright: the term starting a WORD in the name ('gan' -> Gantt chart, and NOT Organization chart), then mid-word in the name ('eth' -> Choropleth), then the description. A name match therefore SUPPRESSES description matches. Case- and accent-insensitive and, by default, space- and punctuation-insensitive too ('barchart' and 'bar-chart' find Bar chart; see name_filter_match). Minimum two characters, and it never reorders - the picker's ranking is preserved and the ranks stay as issued. Applied after ranking and before `top`."),name_filter_match:T.enum(["loose","exact"]).optional().describe("How name_filter matches a name. 'loose' (the default) ignores accents, case, spaces and punctuation, so 'barchart' finds Bar chart. 'exact' folds accents and case and collapses repeated spaces, and nothing else: a name typed without its space or hyphen matches nothing. The same setting the Power BI and Excel chart-type lists offer."),order:T.enum(["fit","variety"]).optional().describe("How the list is ordered. 'fit' (the default) = by how well each chart type fits THIS data, nothing else - use it to choose a chart for a question. 'variety' = the picker's selection order, which favors newer and less-common types on purpose - use it when the user wants to explore or be surprised. Both lists hold the same chart types; only the order differs."),top:T.number().optional().describe("Show only the top-N ranked results. Omit for all."),width:T.number().optional().describe("Viewport width px (default 800) \u2014 affects small-viewport / facet-budget gates."),height:T.number().optional().describe("Viewport height px (default 600).")},annotations:{readOnlyHint:!0,openWorldHint:!0}},async e=>{let a=Cr(e),t="",n="",r="",o="";if(e.renderer!==void 0)t=e.renderer;else if(e.language)n=e.language;else if(g4())n="javascript",r=" (language: javascript, this server's default - pass language or renderer for another)";else{let f=rr(f8(e.project_dir,s8));f.language?(n=f.language,r=` (auto-detected language: ${f.language} \u2014 ${f.reason})`):o=`Couldn't tell the project's language (${f.reason}), so this is the list across all renderers; pass language: "javascript" or "python", or project_dir, to narrow it.
|
|
247
|
+
`)}),t=a$((n,r)=>{let o=new URL(n.url??"/","http://localhost").pathname;if(o==="/healthz"){if(n.method!=="GET"&&n.method!=="HEAD"){Nr(r,405,{error:"Use GET."},{Allow:"GET"});return}Nr(r,200,{ok:!0,server:"bic-chart",version:e.version});return}if(o!=="/mcp"){Nr(r,404,{error:"Not found. The MCP endpoint is /mcp."});return}if(n.method!=="POST"){s7(r,405,-32e3,"Method not allowed: this server is stateless, so it takes POST only.",{Allow:"POST"});return}r$(n,r,e).catch(i=>{a(`[bic-chart http] request failed (${i?.name??"Error"})`),s7(r,500,-32603,"Internal error: the chart server couldn't handle this request. Try again.")})});return new Promise((n,r)=>{t.once("error",r),t.listen(e.port,e.host,()=>{t.off("error",r),n(t)})})}var QW=["assess_data_shape","list_eligible_charts","generate_chart","get_guide","preview_request"],l7="This hosted server receives your table, profiles it in memory and sends only the profile to generation; rows aren't stored or logged.",ek={csv_path:"this hosted server can't read files on your machine, so pass the table as data or csv_text",project_dir:"this hosted server can't see your project folder, so pass language or renderer instead",out_dir:"this hosted server returns the chart inline; it can't write to your machine",preview_html:"this hosted server returns the chart inline; it can't write to your machine"},Sr=e=>`Not accepted by this hosted server: ${ek[e]}.`,br={csv_path:T.string().optional().describe(Sr("csv_path")),project_dir:T.string().optional().describe(Sr("project_dir"))},qW={out_dir:T.string().optional().describe(Sr("out_dir")),preview_html:T.boolean().optional().describe(Sr("preview_html"))},JW=T.enum(["javascript","typescript","python",""]).optional(),YW=["csv_path","project_dir","out_dir","preview_html"],ak={assess_data_shape:{description:`Profile a dataset: per-column types, cardinality, ranges, measure/dimension roles, temporal/ordinal detection, and cross-column signals (nesting, shared values, association) - the same measurement engine the BIC Power BI visual uses before generating a chart. Pass the table as data ({columns, rows}) or csv_text. ${l7} This tool sends nothing on.`,inputs:{csv_path:br.csv_path},refuse:["csv_path"]},list_eligible_charts:{description:"The AUTHORITATIVE list of chart types that can render this dataset - the server's own eligibility engine, NOT a guess. Pass the table as data ({columns, rows}) or csv_text. "+l7+" Ranked for JavaScript (D3) unless you pass language ('python' ranks Plotly and matplotlib) or renderer ('D3'/'PLOTLY'/... for one renderer, '' for every renderer). No model runs, but the call is METERED: free within a generous hourly allowance, then usage credits per call.",inputs:{...br,language:JW.describe("Project source language: javascript/typescript ranks D3 and Vega, python ranks Plotly and matplotlib. Omit for JavaScript; `renderer`, when given, overrides it.")},refuse:["csv_path","project_dir"]},generate_chart:{description:`Generate ready-to-render chart code for a dataset: the backend picks a suitable chart type (or honors chart_type) and returns code that has passed its eligibility gates and quality checks. Pass the table as data ({columns, rows}) or csv_text. ${l7} The code and the render-ready data come back inline in the reply; this server writes no files. JavaScript (D3) unless you pass language or renderer. The D3 code is a portable render(container, data, options) function meant to be MERGED into your own app; an integration contract describes exactly how to call it. A generation uses your account's credits.`,inputs:{...br,...qW,language:JW.describe("Project source language: javascript/typescript generates D3 (the renderer @bicharts/chart-host draws), python lets the backend pick within Plotly/matplotlib. Omit for JavaScript (D3); `renderer`, when given, overrides it.")},refuse:YW},preview_request:{description:`Shows exactly what a chart generation would send on for these arguments - the measured shape of your data (column names, types, counts, ranges), the settings, and whether any sample rows travel - with the credentials removed. It sends nothing on and costs nothing. ${l7} Takes the same arguments as a generation.`,inputs:{...br,...qW},refuse:YW},get_guide:{description:"How to build an app with these charts, from the guide this server ships. Free, and it sends nothing on. topic 'fabric-app' (the default) is what a Microsoft Fabric App on the Rayfin data app template needs; 'all' is the whole guide, including Blazor; 'chart-shapes' lists every chart type with the columns, limits and renderers it needs. Some steps the guide describes, the ones that write into a project, need the local server (npx -y @bicharts/chart-mcp) rather than this one."}};function tk(e,a){let t=(a??[]).filter(n=>{let r=e?.[n];return r!=null&&r!==!1&&r!==""});return t.length===0?null:t.map(n=>`${n} isn't accepted here: ${ek[n]}.`).join(" ")}var nk=["This is RegiaBI's hosted chart server. When a request asks for RegiaBI charts, it means these tools.",l7+" It can't read or write files on your machine: pass each table as data ({columns, rows}) or csv_text, and the chart comes back inline.","Asked which charts suit the data? Answer from list_eligible_charts, not from general knowledge.","Five rules for charts that fit on the first try:","1. Pass the chart's WHOLE result at its grain, not a sample: what fits depends on its counts.","2. Give columns the plain names your app renders with (rename Table[Column] first), unchanged at render time.","3. Call list_eligible_charts on that data before choosing, and choose what answers the question as asked. Where a chart has controls drawn inside the chart, build no input beside it.","4. Host the code with @bicharts/chart-host. generate_chart returns the code, the render-ready rows and the binding: pass them as they are, and never hand-build __rowIdx__ or __geo* columns.","5. Use `prompt` for what the chart shows; selection, cross-filtering, sizing and theming are the host's: filter with useBicFilter + selects, read a chart's sliders with useBicControls + controls (@bicharts/chart-host/react). Never write a select handler, toggle or clear.","Building an app? get_guide has the setup steps. The local server (npx -y @bicharts/chart-mcp) adds project tools that write the chart files into your app for you."].join(`
|
|
248
|
+
`);function f8(e,a){return ir(e,a).dir??qa(e??process.cwd())}var mu="0.6.73",ok=[],kr=null,k3={registerTool(e,a,t){ok.push({name:e,config:a,cb:t})},get server(){if(!kr)throw new Error("The local server isn't running in this process.");return kr.server}};function rk(e){let a=e?new v2({name:"bic-chart",version:mu},{instructions:nk}):new v2({name:"bic-chart",version:mu},{instructions:zW});e||(a.server.registerCapabilities({logging:{}}),yb(t=>{a.server.sendLoggingMessage({level:"notice",logger:"sign-in",data:t}).catch(()=>{})}));for(let t of ok){if(e&&!QW.includes(t.name))continue;let n=e?ak[t.name]:void 0,r={...t.config,...n?{description:n.description,inputSchema:{...t.config.inputSchema,...n.inputs??{}}}:{},annotations:{title:t.config.title,...t.config.annotations}},o=n?.refuse?(i,s)=>{let l=tk(i,n.refuse);return l?{isError:!0,content:[{type:"text",text:l}]}:t.cb(i,s)}:t.cb;a.registerTool(t.name,r,o)}return a}var u7="2026-10-07T05:14:28Z src:5acd31ecc411 (33 files)",gu="@bicharts/chart-host@0.6.70, @bicharts/shape-core@0.6.70",o$=T.object({columns:T.array(T.object({name:T.string(),dataType:T.enum(["String","Integer","Decimal","DateTime"]).optional().describe("Known type (DAX/semantic-model callers have it); omit to infer from values."),isMeasure:T.boolean().optional().describe("Known measure/dimension role; omit to apply the numeric-non-identifier heuristic. A semantic-model caller should set this."),format:T.string().optional().describe('Format string from the model (e.g. "$#,##0") \u2192 modelFormat.'),description:T.string().optional().describe("Column description from the model \u2192 modelDesc.")})).min(1),rows:T.array(T.union([T.array(T.any()),T.record(T.string(),T.any())])).min(1).describe("Rows as positional arrays aligned to columns, OR objects keyed by column name.")}).describe("A pre-typed {columns, rows} table (DAX query results, dataframe, arrow). Use INSTEAD of csv_path/csv_text."),fu={csv_path:T.string().optional().describe("Path to a CSV file (header row required)."),csv_text:T.string().optional().describe("Inline CSV text (alternative to csv_path)."),data:o$.optional().describe("A pre-typed {columns, rows} table \u2014 the shape a Fabric DAX query / dataframe already has. Alternative to csv_path/csv_text; when columns carry isMeasure/format/description the shape matches PBI fidelity with no guessing."),measures:T.array(T.string()).optional().describe("Columns to treat as MEASURES (the PBI 'Values' well \u2014 fields the chart aggregates). Numeric non-identifier columns are measures by default."),dimensions:T.array(T.string()).optional().describe("Columns to treat as DIMENSIONS (a PBI category/axis well \u2014 fields to group/break by). Use for numeric codes/years that should NOT be aggregated."),formats:T.record(T.string(),T.string()).optional().describe(`Per-column number/date FORMAT STRING (e.g. {"Revenue":"$#,##0","Margin":"0.0%"}). PBI supplies these from its model; a CSV can't, so provide them to steer number formatting.`),descriptions:T.record(T.string(),T.string()).optional().describe("Per-column human DESCRIPTION \u2014 gives the model semantic context the bare column name lacks."),group_by:T.array(T.string()).optional().describe("Columns that are the PRIMARY series/breakdown (the PBI 'Group by' well); the backend leads the visualization with these."),non_additive:T.array(T.string()).optional().describe("MEASURE columns that must NOT be summed (a ratio/average/rate \u2014 use mean, never stack). Usually auto-detected; this is an authoritative override."),privacy_level:T.enum(["0","10","20","30"]).optional().describe("How much detail to compute/share: 0 names only, 10 +counts, 20 +detailed stats (default), 30 +obfuscated sample rows.")};function g8(e){return{measures:e.measures,dimensions:e.dimensions,formats:e.formats,descriptions:e.descriptions,groupBy:e.group_by,nonAdditive:e.non_additive,privacyLevel:e.privacy_level??"20"}}function Cr(e){if(e.data)return jf(e.data.columns,e.data.rows,g8(e));if(e.csv_text)return B4(e.csv_text,g8(e));if(e.csv_path)return B4(c7(qa(e.csv_path),"utf-8"),g8(e));throw new Error("Provide one of: data ({columns, rows}), csv_text, or csv_path.")}k3.registerTool("assess_data_shape",{title:"Assess data shape",description:"Profile a dataset locally (no data leaves the machine): per-column types, cardinality, ranges, measure/dimension roles, temporal/ordinal detection, and cross-column signals (nesting, shared values, association). This is the same measurement engine the BIC Power BI visual uses before generating a chart.",inputSchema:fu,annotations:{readOnlyHint:!0,openWorldHint:!1}},async e=>{let a=Cr(e),t={totalRows:a.totalRows,leafCardinality:a.index.getLeafCardinality(),columns:a.shape},n=g4()?null:Kb(process.env);return{content:[{type:"text",text:JSON.stringify(t,null,2)},...n?[{type:"text",text:n}]:[]]}});k3.registerTool("get_guide",{title:"Read the build guide",description:"How to build an app with these charts, from the guide this package ships. Local and free: no network, no credentials. topic 'fabric-app' (the default) is what a Microsoft Fabric App on the Rayfin data app template needs: what to pull, generating, wiring in React, the Fabric App section and the time-savers. 'all' is the whole guide, including Blazor. 'chart-shapes' lists every chart type with the columns, limits and renderers it needs.",inputSchema:{topic:T.enum(xW).optional().describe("'fabric-app' (default), 'all', or 'chart-shapes'.")},annotations:{readOnlyHint:!0,openWorldHint:!1}},async e=>{try{return{content:[{type:"text",text:jW(e.topic??"fabric-app")}]}}catch(a){return{isError:!0,content:[{type:"text",text:`The guide could not be read from this package (${a?.message??a}). It is also published as skills/bic-charts/SKILL.md inside @bicharts/chart-mcp on npm.`}]}}});k3.registerTool("list_eligible_charts",{title:"List eligible chart types",description:"The AUTHORITATIVE list of chart types that can render this dataset \u2014 the server's own eligibility engine, NOT a local guess. Profiles the data locally, then sends that profile to the backend. NO LLM in the loop, but this is a NETWORK call and it is METERED \u2014 see the licensing note below. By default the tool DETECTS the project's language (JS/TS vs Python from the working dir) and returns the charts for THAT language, SCOPED and ordered by how well each type fits this data (best first; `order: 'variety'` favors newer and less-common types instead), each tagged with its source language and renderer. Pass `language` ('javascript'|'python') to force it, or `renderer` ('D3'/'PLOTLY'/\u2026) to rank one specific renderer, or renderer '' for the full renderer-agnostic eligibility across ALL renderers (also ranked, by chart type alone, with no renderer/language tag). If the language can't be auto-detected and none is given, the tool asks you to specify one. Needs an account: eligibility IS the engine, so programmatic access to it is licensed. With nothing configured, the first call opens a browser sign-in, waits for it and then answers (a license key in the environment is the alternative for a machine with no browser). Free within a generous hourly allowance; beyond it each call draws usage credits.",inputSchema:{...fu,language:T.enum(["javascript","typescript","python",""]).optional().describe("Project source language \u2192 the server ranks across that language's renderers (javascript/typescript = D3/Vega, python = Plotly/matplotlib). Omit to AUTO-DETECT from the working dir; `renderer` (if given) overrides this."),project_dir:T.string().optional().describe("Directory to auto-detect the project language from (default: the folder setup_fabric_app used, else the working directory or the one project folder directly inside it)."),renderer:T.enum(["D3","PLOTLY","PYMATPLOT","VEGA",""]).optional().describe("Force a specific renderer (overrides `language`): a concrete renderer ranks that renderer's charts; '' = the full renderer-agnostic eligibility (all renderers, ranked by chart type alone). Omit to use `language`/auto-detection."),level_of_detail:T.enum(["High","Medium","Low",""]).optional().describe("The user's own preference for how much of the data to see, as they've stated it. 'High' = every observation as its own mark, 'Medium' = every row shown as a distribution, 'Low' = one mark per category. Ranks this list the same way a generation would. Unset when they haven't stated one; unset changes nothing."),name_filter:T.string().optional().describe("Show only chart types matching this text (the same matcher the Power BI and Excel chart-type lists use). Three tiers, first non-empty one wins outright: the term starting a WORD in the name ('gan' -> Gantt chart, and NOT Organization chart), then mid-word in the name ('eth' -> Choropleth), then the description. A name match therefore SUPPRESSES description matches. Case- and accent-insensitive and, by default, space- and punctuation-insensitive too ('barchart' and 'bar-chart' find Bar chart; see name_filter_match). Minimum two characters, and it never reorders - the picker's ranking is preserved and the ranks stay as issued. Applied after ranking and before `top`."),name_filter_match:T.enum(["loose","exact"]).optional().describe("How name_filter matches a name. 'loose' (the default) ignores accents, case, spaces and punctuation, so 'barchart' finds Bar chart. 'exact' folds accents and case and collapses repeated spaces, and nothing else: a name typed without its space or hyphen matches nothing. The same setting the Power BI and Excel chart-type lists offer."),order:T.enum(["fit","variety"]).optional().describe("How the list is ordered. 'fit' (the default) = by how well each chart type fits THIS data, nothing else - use it to choose a chart for a question. 'variety' = the picker's selection order, which favors newer and less-common types on purpose - use it when the user wants to explore or be surprised. Both lists hold the same chart types; only the order differs."),top:T.number().optional().describe("Show only the top-N ranked results. Omit for all."),width:T.number().optional().describe("Viewport width px (default 800) \u2014 affects small-viewport / facet-budget gates."),height:T.number().optional().describe("Viewport height px (default 600).")},annotations:{readOnlyHint:!0,openWorldHint:!0}},async e=>{let a=Cr(e),t="",n="",r="",o="";if(e.renderer!==void 0)t=e.renderer;else if(e.language)n=e.language;else if(g4())n="javascript",r=" (language: javascript, this server's default - pass language or renderer for another)";else{let f=rr(f8(e.project_dir,s8));f.language?(n=f.language,r=` (auto-detected language: ${f.language} \u2014 ${f.reason})`):o=`Couldn't tell the project's language (${f.reason}), so this is the list across all renderers; pass language: "javascript" or "python", or project_dir, to narrow it.
|
|
249
249
|
`}Dl("list_eligible_charts",process.env);let i=await vl({shape:a.shape,totalRows:a.totalRows,leafCardinality:a.index.getLeafCardinality(),geoExtent:a.index.getGeoExtent(),width:e.width,height:e.height,renderer:t,language:n,levelOfDetail:e.level_of_detail||void 0,order:e.order??"fit"});if(!i.ok)return{isError:!0,content:[{type:"text",text:Hl(i)?i.errorMessage:`Could not list eligible charts: ${i.errorMessage}`}]};let s=i.charts.length>0&&i.charts[0].rank!==void 0,l=n?`${n}`:t||"all renderers",u=i.charts,c=u.length,d=PN(e.name_filter_match),p=V2(u,e.name_filter,In,d);u=p.rows;let m;if(s){e.top&&e.top>0&&(u=u.slice(0,e.top));let f=(e.order??"fit")==="variety"?'ordered for variety (newer and less-common types first; order: "fit" ranks by fit to this data)':'ordered by fit to this data (order: "variety" favors newer and less-common types)';m=`Top ${u.length} chart type(s) for this data in ${l}, ${f} (authoritative \u2014 the server's eligibility engine; ${$n})${r}:`}else m=`${u.length} chart type(s) can render this data \u2014 renderer-agnostic eligibility (authoritative \u2014 the server's eligibility engine; ${$n}):`;if(m=o+m,p.tier!=="all"){let f=p.tier==="desc"?" (no NAME matched; these matched on description)":p.tier==="namePart"?" (matched mid-word in the name)":"";m=`Filtered to "${e.name_filter}"${f}: ${u.length} of ${c} eligible chart type(s). The other ${c-u.length} are eligible and simply do not match the filter.
|
|
250
250
|
`+m}let h=RW(u);if(u.length===0&&p.tier!=="all"&&c>0){let f=V2(i.refused,e.name_filter,UN,d),y=f.tier==="name"||f.tier==="namePart"?f.rows.filter(k=>k.reason).slice(0,3):[],W=y.length>0?`
|
|
251
251
|
|
|
@@ -253,12 +253,12 @@ It IS in the catalogue and was checked, and here is why it was refused for this
|
|
|
253
253
|
${y.map(k=>`\u2022 ${k.name} \u2014 ${k.reason}`).join(`
|
|
254
254
|
`)}`:"";return{content:[{type:"text",text:`No eligible chart type matches "${e.name_filter}". ${c} chart type(s) ARE eligible for this data \u2014 re-run without name_filter to see them.${W}`}]}}if(u.length===0){let f=i.refused.filter(P=>P.reason),y=[...f.filter(P=>P.isVeto),...f.filter(P=>!P.isVeto)].slice(0,5),W=i.noFitSummary?["",i.noFitSummary]:[],k=i.noFitSummary?`${i.refused.length} chart type(s) were checked, and every one was refused for this canvas. Changing the fields will not help until it is bigger.`:`${i.refused.length} chart type(s) were checked. Add or bind the field(s) named above and ask again.`,A=y.length>0?[...W,"","Why nothing fit (the server's own words, for a few of the types it checked):",...y.map(P=>`\u2022 ${P.reason}`),"",k]:W.length>0?[...W,"",k]:["","The server did not say which requirements failed - it names one only when it can prove it from the declared structure."];return{content:[{type:"text",text:[m,...A].join(`
|
|
255
255
|
`)}]}}let g=[TW(i.charts,p.rows,p.tier,e.name_filter,d,f=>f.name),UW(a.totalRows,i.refused)].filter(f=>f.length>0);return{content:[{type:"text",text:[m,"",...h,...g.flatMap(f=>["",f])].join(`
|
|
256
|
-
`)}]}});function ik(e,a){g4()&&e.renderer===void 0&&!e.language&&(e={...e,language:"javascript"});let{renderer:t,language:n,note:r}=XS(e.renderer,e.language,()=>rr(f8(e.project_dir,s8)).language);return{req:{shape:a.shape,totalRows:a.totalRows,leafCardinality:a.index.getLeafCardinality(),geoExtent:a.index.getGeoExtent(),sampleCsv:a.sampleCsv,prompt:e.prompt,chartHint:e.chart_type,rowsPolicy:e.rows_policy==="all"?"all":void 0,requiredColumns:e.required_columns,forbidDerivedColumns:e.forbid_derived_columns,levelOfDetail:e.level_of_detail||void 0,fontFamily:e.font_family||void 0,renderer:t,language:n,model:e.model,reasoningMode:e.reasoning_mode,width:e.width,height:e.height,cultureCode:e.culture||void 0},renderer:t,language:n,genNote:r}}var sk={...fu,prompt:T.string().optional().describe("Freeform guidance for the generation \u2014 the same 'user commentary' lane the Power BI visual uses. Steer encoding/axes/emphasis in plain language, e.g. 'put Year on the x-axis, color by Division', 'render citations as a chord diagram', 'log scale on revenue', 'annotate the peak'."),chart_type:T.string().optional().describe("Preferred chart type by name (honored unless the data shape disqualifies it)."),rows_policy:T.enum(["auto","all"]).optional().describe("'auto' (default) = the engine decides how to summarize. 'all' = render EXACTLY one mark/row per source row, with no Top-N, no 'Other' rollup and no sampling. Use 'all' when your app depends on every row being present (a row-per-entity table, a point per location)."),required_columns:T.array(T.string()).optional().describe("Fields that MUST be visible in the output, each as its own column/channel. AUTHORITATIVE and VERIFIED: if the generated code never references one, the server regenerates automatically rather than returning a chart that silently dropped it."),forbid_derived_columns:T.boolean().optional().describe("True = show only fields present in the data (plus required_columns) \u2014 no per-capita, share-of-total, index, rank or z-score columns the engine would otherwise add. Use when your app owns the analytics and the chart is only the display."),level_of_detail:T.enum(["High","Medium","Low",""]).optional().describe("
|
|
256
|
+
`)}]}});function ik(e,a){g4()&&e.renderer===void 0&&!e.language&&(e={...e,language:"javascript"});let{renderer:t,language:n,note:r}=XS(e.renderer,e.language,()=>rr(f8(e.project_dir,s8)).language);return{req:{shape:a.shape,totalRows:a.totalRows,leafCardinality:a.index.getLeafCardinality(),geoExtent:a.index.getGeoExtent(),sampleCsv:a.sampleCsv,prompt:e.prompt,chartHint:e.chart_type,rowsPolicy:e.rows_policy==="all"?"all":void 0,requiredColumns:e.required_columns,forbidDerivedColumns:e.forbid_derived_columns,levelOfDetail:e.level_of_detail||void 0,fontFamily:e.font_family||void 0,renderer:t,language:n,model:e.model,reasoningMode:e.reasoning_mode,width:e.width,height:e.height,cultureCode:e.culture||void 0},renderer:t,language:n,genNote:r}}var sk={...fu,prompt:T.string().optional().describe("Freeform guidance for the generation \u2014 the same 'user commentary' lane the Power BI visual uses. Steer encoding/axes/emphasis in plain language, e.g. 'put Year on the x-axis, color by Division', 'render citations as a chord diagram', 'log scale on revenue', 'annotate the peak'."),chart_type:T.string().optional().describe("Preferred chart type by name (honored unless the data shape disqualifies it)."),rows_policy:T.enum(["auto","all"]).optional().describe("'auto' (default) = the engine decides how to summarize. 'all' = render EXACTLY one mark/row per source row, with no Top-N, no 'Other' rollup and no sampling. Use 'all' when your app depends on every row being present (a row-per-entity table, a point per location)."),required_columns:T.array(T.string()).optional().describe("Fields that MUST be visible in the output, each as its own column/channel. AUTHORITATIVE and VERIFIED: if the generated code never references one, the server regenerates automatically rather than returning a chart that silently dropped it."),forbid_derived_columns:T.boolean().optional().describe("True = show only fields present in the data (plus required_columns) \u2014 no per-capita, share-of-total, index, rank or z-score columns the engine would otherwise add. Use when your app owns the analytics and the chart is only the display."),level_of_detail:T.enum(["High","Medium","Low",""]).optional().describe("The user's own preference for how much of the data to see, as they've stated it; it isn't a property of the data. 'High' = every observation drawn as its own mark (scatter, beeswarm, strip). 'Medium' = every row used, but shown as a distribution (box, violin, histogram). 'Low' = one mark per category (bars, cards, pies). Unset when the user hasn't stated one; unset is a proven no-op and the engine decides. This is a strong nudge, not a filter: it reorders charts that already fit, and an explicit chart_type still wins."),font_family:T.string().optional().describe("CSS font family for the chart's own text \u2014 titles, axis labels, tick labels, legends (e.g. 'Segoe UI' or a stack like 'Segoe UI, Arial, sans-serif'). Set ONLY when the user has named a font or stated a house style; a font isn't a property of the data or the project. Unset when none was given; unset is a proven no-op and the engine picks. Applies to whichever renderer is chosen (D3, Plotly, matplotlib and Vega each get an equivalent instruction), and an unavailable family falls back to a web-safe stack rather than failing."),renderer:T.enum(["D3","PLOTLY","PYMATPLOT","VISUAL",""]).optional().describe("Force a specific renderer (overrides `language`). Omit to use `language`/auto-detection; 'VISUAL'/'' = let the backend choose."),language:T.enum(["javascript","typescript","python",""]).optional().describe("Project source language: with no concrete `renderer`, javascript/typescript generates D3 (the renderer @bicharts/chart-host draws) and python lets the backend auto-pick within Plotly/matplotlib. Omit to AUTO-DETECT from the working dir."),project_dir:T.string().optional().describe("Directory to auto-detect the project language from (default: the folder setup_fabric_app used, else the working directory or the one project folder directly inside it)."),model:T.string().optional().describe("Model code; empty = backend default."),reasoning_mode:T.enum(["","1P","2P","3P","CP"]).optional().describe("LLM pass structure; empty = backend heuristic."),width:T.number().optional(),height:T.number().optional(),culture:T.string().regex(/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$/).max(35).optional().describe("The language the chart's own text should be written in - titles, axis labels, legends, notes - as a BCP 47 tag such as 'de-DE', 'fr' or 'ja-JP'. Default 'en-US'. Set it from the end user's locale when you know it; it does not translate the data."),wrap:T.enum(["none","esm"]).optional().describe("Code form: 'none' (default) = the raw render() function to merge; 'esm' = same code plus `export { render };` so the file can be `import`ed. Both forms are accepted everywhere - @bicharts/chart-host strips the export clause before compiling - so this is a convenience, never a compatibility choice."),include_code:T.boolean().optional().describe("Include the full code in the response \u2014 governs BOTH the text block and structuredContent.code. Default: true, or FALSE when out_dir is given (the code is then already on disk, and a large chart's code can overflow a client's tool-output limit). Set it explicitly to override either default."),preview_html:T.boolean().optional().describe("Also emit a standalone D3 preview (needs out_dir). Default false \u2014 the code is meant to be merged, not run as an index.html."),out_dir:T.string().optional().describe("Optional directory to write chart.js/py, a data.sample.json, and (if preview_html) preview.html.")};k3.registerTool("generate_chart",{title:"Generate chart code",description:"Generate ready-to-render chart code for a dataset via the BIC backend: the data shape is profiled locally, the backend picks a suitable chart type (or honors chart_type), and returns code that has passed the backend's eligibility gates and quality checks. Needs a trial or paid account: with nothing configured, the first call opens a browser sign-in, waits for it and then finishes the chart (a license key in the environment or ~/.bic/credentials.json is the alternative for a machine with no browser). The returned D3 code is a portable render(container, data, options) function meant to be MERGED into your own app; an integration contract describes exactly how to call it. A standalone HTML preview and file output are optional.",inputSchema:sk,outputSchema:Xl,annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0}},async(e,a)=>{let t=e.privacy_level??"20",n=await K0(Cr(e),t),{req:r,renderer:o,language:i,genNote:s}=ik(e,n),l=a?._meta?.progressToken,u=0,c="",d=_=>{l!==void 0&&a.sendNotification({method:"notifications/progress",params:{progressToken:l,progress:++u,message:_}}).catch(()=>{})},p=l===void 0?null:setInterval(()=>{d(c?`${c}\u2026`:"generating chart (multi-pass \u2014 can take a few minutes)\u2026")},1e4);Dl("generate_chart",process.env);let m=Date.now(),h;try{h=await Vb({...r,signal:a?.signal,onProgress:_=>{c=_,d(`${_}\u2026`)}})}finally{p&&clearInterval(p)}if(OS(h,{durationMs:Date.now()-m,culture:e.culture,totalRows:n.totalRows,build:u7},process.env),!h.ok)return{isError:!0,content:[{type:"text",text:qS(h)}]};let g=h.language.toUpperCase()==="D3",f=h.geoColumn&&h.geoKind?{column:h.geoColumn,kind:h.geoKind}:void 0,y=h.geoKind?r7(h.geoKind):void 0,W=Ol({city:h.pointCityColumn,state:h.pointStateColumn,zip:h.pointZipColumn,lat:h.pointLatColumn,lon:h.pointLonColumn,country:h.pointCountryColumn},h.geoKind),k=Ol({city:h.destPointCityColumn,state:h.destPointStateColumn,zip:h.destPointZipColumn,lat:h.destPointLatColumn,lon:h.destPointLonColumn,country:h.destPointCountryColumn},h.geoKind),A=$S(n.index,n.shape,f,W,h.code,k),P=e.wrap==="esm"&&g?ZS(h.code):h.code,C=[`Chart: ${h.finalChartName||"(backend pick)"} | language: ${h.language} | version: ${h.version}`+(h.creditCost!=null?` | credits: ${h.creditCost}`:"")+s];if(h.codeSummary&&C.push(`Summary: ${h.codeSummary}`),h.recoveredAfterMs!=null&&C.push(`Recovered: the response stream was cut after the server accepted the request, and this chart was fetched back by its correlation after ${Math.round(h.recoveredAfterMs/1e3)}s. The generation was charged once, as usual; the fetch that delivered it added the cached-serve fee.`),h.warningMessage&&C.push(`Warning: ${h.warningMessage}`),f){let _=A.geoUnmatched;C.push(`Geo: joined '${f.column}' as ${f.kind}`+(y?"":" (WARNING: no bundled geometry for this kind)")+(_&&_.count>0?` \u2014 ${_.count} region(s) unmatched: ${_.examples.join(", ")}`:""))}else h.geoKind&&C.push(`Geo: ${h.geoKind} basemap attached as options.geo`+(y?"":" (WARNING: no bundled geometry for this kind \u2014 the map will draw no land)"));W&&C.push(Kl("Points",W,A.geoPoint,A.rows.length)),k&&C.push(Kl("Destinations",k,A.geoPointDest,A.rows.length));let I=HS({language:h.language,data:A,finalChartName:h.finalChartName,code:h.code,geoKind:h.geoKind});C.push("",I);let $=await VS({code:h.code,isD3:g,outDir:e.out_dir?qa(e.out_dir):void 0,prompt:e.prompt,elicit:!g4()&&k3.server.getClientCapabilities()?.elicitation?async _=>{let S=await k3.server.elicitInput({message:_,requestedSchema:{type:"object",properties:{hoverDropLines:{type:"boolean",title:"Hover drop lines",default:!1}},required:["hoverDropLines"]}},{timeout:3e5});return S.action==="accept"?S.content?.hoverDropLines===!0?"on":"off":S.action==="decline"?"off":void 0}:void 0});$.line&&C.push("",$.line);let v={};if(e.out_dir){let _=qa(e.out_dir);vr(_,{recursive:!0});let S=h.language.toUpperCase()==="PYTHON"?"py":"js",b=Ge(_,`chart.${S}`),w=S==="py"?"#":"//",z=`${w} BIC generated chart \u2014 ${h.finalChartName||"chart"} [${h.language}]
|
|
257
257
|
${w} host contract v${Ha}. A host implementing a DIFFERENT major
|
|
258
258
|
${w} version may not interoperate with this file's mark/slot grammar.
|
|
259
259
|
${w} Regenerate with the MCP generate_chart tool \u2014 hand edits are lost.
|
|
260
260
|
`;F3(b,z+P,"utf-8");let E=Ge(_,"data.sample.json");if(F3(E,JSON.stringify({...A,meta:{hostContract:Ha,chart:h.finalChartName,...$.answer?{hoverDropLines:$.answer}:{},...e.width?{width:e.width}:{},...e.height?{height:e.height}:{}}},null,2),"utf-8"),v.code=b,v.data=E,C.push("",`Code written: ${b}`,`Sample data: ${E}`),y){let R=Ge(_,"data.geo.json");F3(R,JSON.stringify(y),"utf-8"),v.geo=R,C.push(`Geometry: ${R} (pass as options.geo \u2014 REQUIRED for this ${W?"point map: it is the BASEMAP, not a join target":"choropleth"})`)}if(g){let R=Ge(_,"chart.component.tsx");F3(R,Jb({chartName:h.finalChartName||"chart",columns:A.columns,geoKind:h.geoKind||"",binding:{geo:f??null,point:W??null,destination:k??null},plugins:Gl(P,g),defaultOptions:$.defaultOptions}),"utf-8"),v.component=R,C.push(`React host: ${R} - import { ${Hn(h.finalChartName||"chart")}, rowsFromTable } from it and render <${Hn(h.finalChartName||"chart")} rows={rowsFromTable(queryResult)} width height onSelectRows />; it wires the group, the binding, the plugins and the click contract (needs a bundler that imports "./chart.js?raw", as Vite does).`)}if(e.preview_html&&g){let R=BS({code:h.code,data:A,width:e.width,height:e.height,title:h.finalChartName||"BIC chart preview",geo:y,geoUnmatched:A.geoUnmatched,geoPoint:A.geoPoint,geoPointDest:A.geoPointDest}),B=Ge(_,"preview.html");F3(B,R,"utf-8"),v.preview=B,C.push(`Preview written: ${B} (open in a browser; loads d3 from CDN)`)}}let N=JS(e);N&&C.push("","--- code ---",P);let M=Gl(P,g);return M.length>0&&C.push("",`D3 plugins required: ${M.join(", ")} \u2014 install them and pass the host ONE d3 that carries them: assembleD3(d3, { ...plugins }) from @bicharts/chart-host (an ES-module d3 namespace cannot be assigned onto).`),C.push("",`(bic-chart MCP build ${u7})`),{content:[{type:"text",text:C.join(`
|
|
261
|
-
`)}],structuredContent:YS({res:h,renderData:A,code:P,withCode:N,plugins:M,hasGeometry:!!y,files:e.out_dir?v:void 0,integrationContract:I,optionsRead:GS(P,g),...$.state!=="none"?{dropLines:{state:$.state,...$.askUser?{askUser:$.askUser}:{}}}:{},hostContract:Ha,build:u7,binding:{geo:f??null,point:W??null,destination:k??null}})}});k3.registerTool("preview_request",{title:"Preview what a request sends",description:"Shows exactly what
|
|
261
|
+
`)}],structuredContent:YS({res:h,renderData:A,code:P,withCode:N,plugins:M,hasGeometry:!!y,files:e.out_dir?v:void 0,integrationContract:I,optionsRead:GS(P,g),...$.state!=="none"?{dropLines:{state:$.state,...$.askUser?{askUser:$.askUser}:{}}}:{},hostContract:Ha,build:u7,binding:{geo:f??null,point:W??null,destination:k??null}})}});k3.registerTool("preview_request",{title:"Preview what a request sends",description:"Shows exactly what a chart generation would send to the BIC backend for these arguments - the measured shape of your data (column names, types, counts, ranges), the settings, and whether any sample rows travel - with the credentials removed. SENDS NOTHING and needs no license: it is local and free. Takes the same arguments as a generation. It answers 'what data leaves my machine?'; the eligibility list sends a subset of the same shape.",inputSchema:sk,annotations:{readOnlyHint:!0,openWorldHint:!1}},async e=>{let a=e.privacy_level??"20",t=await K0(Cr(e),a),{req:n,genNote:r}=ik(e,t),{head:o,preview:i}=qb(n,a,r);return{content:[{type:"text",text:`${o}
|
|
262
262
|
|
|
263
263
|
${JSON.stringify(i,null,2)}`}]}});k3.registerTool("check_chart",{title:"Check a generated chart locally",description:"Verify a generated chart locally before deploying: draws the chart.js that generate_chart wrote, headless, against its data - the rows query_semantic_model saved, a CSV, or the rows it was generated for - through @bicharts/chart-host the way the app will, and reports in plain words: the marks drawn by kind, anything thrown (with the line), whether it came out blank, anything drawn outside its frame, and whether a click on a mark reaches onSelect with source rows. Takes seconds; local and free (no network, no credentials, nothing sent). Run it after each generate_chart instead of deploying to see whether a chart works, and check at the size the panel will give it: by default it reads minChartWidth and minChartHeight off the app's BicChartPanel around the chart (else the size it was generated for); pass width and height only for another size. A WARNING line is a label cut off at that size even at a narrow font's width; the reply says whether more room fixes it. Never edit the generated chart code by hand to clear a finding: regenerate it (generate_chart, with a prompt describing the fix) or report it. It does not judge colours or real font widths.",inputSchema:{chart:T.string().optional().describe(`The chart's folder name under the project's src/charts (the out_dir given to generate_chart), e.g. "region-map".`),chart_path:T.string().optional().describe("Or the path of the chart.js generate_chart wrote, or the folder it is in."),project_dir:T.string().optional().describe("The app's folder. Defaults to the folder setup_fabric_app used, else the working directory or the one project folder inside it."),data_name:T.string().optional().describe("The name given to query_semantic_model: checks on the rows it saved (.bic/data/<name>.csv). Default: a saved file named like the chart's folder, else the rows the chart was generated for."),csv_path:T.string().optional().describe("Or a CSV with the chart's columns."),width:T.number().positive().optional().describe("The width in px to check at (default: the minChartWidth of the app's BicChartPanel around this chart, else the width it was generated for, else 800)."),height:T.number().positive().optional().describe("The height in px to check at (default: the minChartHeight of the app's BicChartPanel around this chart, else the height it was generated for, else 600).")},annotations:{readOnlyHint:!0,openWorldHint:!1}},async e=>{if(Number(process.versions.node.split(".")[0])<20)return{isError:!0,content:[{type:"text",text:`check_chart needs Node 20 or later to draw a chart (this is Node ${process.versions.node}).`}]};let t=f8(e.project_dir,s8),n=lr(e,{projectDir:t,profileColumns:(u,c)=>B4(u,g8({...c,privacy_level:"20"})).shape.map(d=>({name:d.name,dataType:d.dataType,isMeasure:!!d.isMeasure}))});if(n.ok===!1)return{isError:!0,content:[{type:"text",text:n.error}]};let{job:r,context:o}=n,i=r.geoKind?r7(r.geoKind):void 0,{outcome:s,roomier:l}=await cr(r,u=>ur({...u,geo:i},new URL("./check-chart.mjs",import.meta.url)));return{content:[{type:"text",text:dr(s,o,{width:r.width,height:r.height},l)}]}});PW(k3);k3.registerTool("setup_fabric_app",{title:"Set up a Fabric App for these charts",description:`Run once in a Microsoft Fabric App scaffolded from Rayfin's data app template, before the first chart: installs @bicharts/chart-host (at this server's version) and d3, adds src/lib/bic-chart-options.ts (the app's Fabric theme as chart options) and src/components/bic-chart-panel.tsx (a chart box that measures itself and shows the query's loading, error and empty states) and .agents/skills/regiabi-charts/SKILL.md, ignores .bic/, and puts every @microsoft/rayfin-* package on npm's latest, the release \`rayfin init\` installs (run it before rayfin init; every package.json range it changes is listed, and the reply names the Rayfin version installed). Never overwrites a file; the one template file it edits - a vi.mock that fixes the template's own validate-visual test under ESM - is named in the reply, as is the one line of src/main.tsx it points at src/lib/bic-css-theme.ts (the theme without bundling Vega). With features: ["notes"] and/or ["scenarios"] (once Rayfin's data service is on), it also adds finished notes-on-marks and saved-scenario files - the Rayfin entities, registered in rayfin/data/schema.ts, a hook each, a notes dialog and a scenarios panel - built on chart-host's useBicFilter / useBicControls; write none of that by hand. Replies with a worked example that renders a generated chart from a live query.`,inputSchema:{project_dir:T.string().optional().describe("The Fabric App project folder. Defaults to the working directory, or the one Fabric App folder directly inside it."),skip_install:T.boolean().optional().describe("Add the files only; don't run npm install."),features:T.array(T.enum(["notes","scenarios"])).optional().describe(`Also add finished files for readers' notes on chart marks ("notes") and saved settings of a chart's own controls ("scenarios", e.g. a What-if chart's rate). Needs Rayfin's data service on (rayfin/data/schema.ts); the reply says the command when it isn't. Never overwrites.`)},annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0}},async e=>{let a=f8(e.project_dir,or),t=Ul(a);if(!t.ok)return{isError:!0,content:[{type:"text",text:`Not set up: ${t.why}. Scaffold it first with npx -y @microsoft/create-rayfin --template dataapp, then run this in that folder (or pass project_dir).`}]};let n=[],r=rS(t.has,oS(gu),process.env.BIC_CHART_HOST_SPEC||void 0,t.ranges);if(e.skip_install)(r.deps.length||r.devDeps.length)&&n.push(`still to install: npm install ${r.deps.join(" ")} && npm install -D ${r.devDeps.join(" ")}`.trim());else{for(let[c,d]of[[r.deps,!1],[r.devDeps,!0]]){if(!c.length)continue;let p=["install",...d?["-D"]:[],...c];try{await Pl(a,p),n.push(`installed: npm ${p.join(" ")}`)}catch(m){return{isError:!0,content:[{type:"text",text:`npm ${p.join(" ")} failed in ${a}: ${m?.message??m}
|
|
264
264
|
Nothing else was changed; fix the install and run this again.`}]}}}!r.deps.length&&!r.devDeps.length&&n.push("packages: already installed")}n.push(...await uS(a,{install:!e.skip_install})),n.push(...cS(a));let o=lS(Ul(a));for(let[c,d]of Object.entries(o)){let p=Ge(a,c);if(Wr(p)){n.push(`kept yours: ${c}`);continue}vr(Ge(p,".."),{recursive:!0}),F3(p,d,"utf-8"),n.push(`added: ${c}`)}n.push(...sS(a)),n.push(...fS(a,e.features??[],Rl)),o["src/lib/bic-chart-options.ts"]||n.push("no theme bridge: this project's @microsoft/fabric-visuals has no useCssTheme; pass palette/themeFg/backgroundColor in options yourself"),lW(a);let i=Ge(a,".gitignore"),s=Wr(i)?c7(i,"utf-8"):"",l=dS(s);return l!==s&&(F3(i,l,"utf-8"),n.push("ignored: .bic/ in .gitignore")),{content:[{type:"text",text:["The project is ready for charts:",...n.map(c=>`- ${c}`),"","Per chart: query_semantic_model (its DAX) -> list_eligible_charts -> generate_chart with out_dir src/charts/<name> (writes chart.js and chart.component.tsx), then render it like this:","```tsx",hS,"```"].join(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.73",
|
|
4
4
|
"description": "MCP (Model Context Protocol) stdio server for BIC AI charts: profile a dataset locally, then generate chart code that has passed the BIC backend's render gates. Works with any MCP-capable client (Claude Code, Claude Desktop, Cursor, Copilot Studio). An account pays to author charts; the code it writes is yours and runs with no licence.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -180,6 +180,12 @@ disqualifies it), `renderer` (a JavaScript or TypeScript project gets D3 without
|
|
|
180
180
|
`@bicharts/chart-host` draws), `width`/`height`, and
|
|
181
181
|
`reasoning_mode: "1P"` when you want the cheapest, fastest pass.
|
|
182
182
|
|
|
183
|
+
**Two parameters are the user's to answer, not yours to guess.** `level_of_detail` (how much of
|
|
184
|
+
the data they want to see: every observation, a distribution, or one mark per category) and
|
|
185
|
+
`font_family` (a font or house style they've named) are preferences, not properties of the data.
|
|
186
|
+
When one matters to the request and the user hasn't said, ask them in one short question;
|
|
187
|
+
otherwise leave it unset, which changes nothing.
|
|
188
|
+
|
|
183
189
|
**Generation takes minutes.** Fire the calls for independent charts **in parallel** rather
|
|
184
190
|
than in sequence — it is the difference between ~4 minutes and ~12.
|
|
185
191
|
|