Public log

@bicharts/chart-mcp

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 ac

npm · fingerprint 17e835b720c25fa01e5c4862 · repository · RSS

10tools
7recorded versions
45dtracked
changed 2d ago

What changed

Changed 2026-10-15 11:28 UTC

3 descriptions rewritten, 2 schema or annotation changes with the description left byte-identical.

--- pinned/check_chart
+++ observed/check_chart
  {
    "annotations": {
      "openWorldHint": false,
-   "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.",
+   "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, 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. The app's own legend and frame fit is NOT run in the check, so a label it flags may already be handled in the app. 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 colors or real font widths.",
    "execution": {
      "taskSupport": "forbidden"
    },
--- pinned/generate_chart
+++ observed/generate_chart
  {
    "annotations": {
      "destructiveHint": false,
-   "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.",
+   "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. The arguments chart_style, level_of_detail and culture are the person's choices, and the guide's 'settings' topic lists every choice a person can make with its options.",
    "execution": {
      "taskSupport": "forbidden"
    },
+       "chart_style": {
+         "description": "Narrows the charts considered to one family: 'traditional' = the familiar forms (bar, stacked and grouped bar, line, area, pie, donut, card, gauge), 'non_traditional' = every other chart type. A chart named in chart_type is still honored unless the data shape disqualifies it, and 'traditional' also turns chart_variety off. Unset considers both families.",
+         "enum": [
+           "",
+           "traditional",
+           "non_traditional"
+         ],
+         "type": "string"
+       },
        "chart_type": {
          "description": "Preferred chart type by name (honored unless the data shape disqualifies it).",
          "type": "string"
+       "chart_variety": {
+         "description": "How much variety the person asks for: the probability, from 0 to 1, that the choice among the charts that fit this data is nudged toward a less conventional one, when the engine runs its planning pass. The nudge is among already-valid charts only: when only the obvious chart honestly fits, it is kept. It has no effect with an explicit chart_type, chart_style 'traditional' or reasoning_mode 1P or CP. Unset: the server's own default.",
+         "maximum": 1,
+         "minimum": 0,
+         "type": "number"
+       },
+       "compactness": {
+         "description": "How roomy the person wants the chart: 'spacious' = roomier margins and more aggressive aggregation of a high-cardinality column, 'balanced' = standard margins and label density, 'compact' = tighter margins and denser axes, aggregating only when cardinality clearly exceeds what fits. Unset: a level is drawn at random for each generation.",
+         "enum": [
+           "",
+           "spacious",
+           "balanced",
+           "compact"
+         ],
+         "type": "string"
+       },
        "csv_path": {
          "description": "Path to a CSV file (header row required).",
          "type": "string"
+       "data_labels": {
+         "description": "Whether the person wants the value printed on each mark: 'include' asks for the labels, 'exclude' for none. Per-mark labels are suppressed when more than 30 marks would carry one. Unset: the engine decides.",
+         "enum": [
+           "",
+           "include",
+           "exclude"
+         ],
+         "type": "string"
+       },
+       "date_grain": {
+         "description": "The time grain the person asks for on the time axis: day, week, month, quarter, year or decade. It is passed on as a strong request, and a different bucket the engine plans is rewritten to it when the dates give that grain 2 or more periods. Unset: the engine chooses, from the number of periods the dates give at each grain.",
+         "enum": [
+           "",
+           "day",
+           "week",
+           "month",
+           "quarter",
+           "year",
+           "decade"
+         ],
+         "type": "string"
+       },
+       "derived_measures": {
+         "description": "Whether the person allows derived measures (ratios, z-scores, percent change, rank, index): 'discourage' keeps the chart to the raw columns (aggregations and sorts still apply); 'encourage' keeps them allowed and rules out the draw that would turn them off, without asking for more of them. Unset: a weighted draw for each generation. 'encourage' is not sent with forbid_derived_columns.",
+         "enum": [
+           "",
+           "encourage",
+           "discourage"
+         ],
+         "type": "string"
+       },
        "descriptions": {
          "additionalProperties": {
            "type": "string"
-         "description": "CSS font family for the chart's own text — 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.",
+         "description": "The font family the person names for the chart's own text (titles, axis labels, tick labels, legends), or the one their house style says: a family such as 'Segoe UI' or a stack such as 'Segoe UI, Arial, sans-serif'. A font isn't a property of the data or the project. It applies to whichever renderer is chosen, and an unavailable family falls back to a web-safe stack rather than failing. Unset: the engine decides.",
          "type": "string"
        },
        "forbid_derived_columns": {
+       "group_others": {
+         "description": "Whether the person allows the categories beyond a limit to be grouped into one 'Others' bucket. True is the same as unset: under a limit the rest is grouped and the total stays whole; with no limit the engine decides whether to group. False: the rest is dropped and nothing is grouped. Unset: grouping allowed. group_others true is not sent with rows_policy 'all', which draws every row.",
+         "type": "boolean"
+       },
        "height": {
          "type": "number"
        },
+       "in_chart_caption": {
+         "description": "Whether the person asks for a one-line subtitle under the chart title naming the chart type, any derived measure used and any non-default aggregation. Unset or false: none.",
+         "type": "boolean"
+       },
        "include_code": {
          "description": "Include the full code in the response — 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.",
          "type": "boolean"
+       "industry": {
+         "description": "The industry the person names for the data, one of the listed values. A recognized metric name is used only when the columns are that industry's inputs for it. Unset: no industry context is added.",
+         "enum": [
+           "",
+           "Retail & e-commerce",
+           "Logistics & shipping",
+           "Manufacturing",
+           "Healthcare & life sciences",
+           "Finance & banking",
+           "Insurance",
+           "Marketing & advertising",
+           "SaaS & technology",
+           "Telecom",
+           "Energy & utilities",
+           "Real estate",
+           "Education",
+           "Government & public sector",
+           "Hospitality & travel"
+         ],
+         "type": "string"
+       },
        "language": {
          "description": "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.",
          "enum": [
+       "legend_placement": {
+         "description": "Where the person wants the legend: 'outside' = in the chart margin, 'inside' = within the plot area where it overlaps the data least, 'none' = no standalone legend (the chart's own labels carry the names). A single-series chart draws no legend whichever is chosen. Unset: outside on a chart at least 500 px wide, the renderer's default on a narrower one.",
+         "enum": [
+           "",
+           "outside",
+           "inside",
+           "none"
+         ],
+         "type": "string"
+       },
        "level_of_detail": {
          "description": "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.",
          "enum": [
+       "limit_count": {
+         "description": "How many groups or categories the person asks to keep, from 1 to 10000. The engine keeps exactly that many by the measure, and its own Top-N, 'Other'-cap and cardinality rules yield to it. Unset: no limit from the person (the engine's own Top-N rules still apply).",
+         "maximum": 10000,
+         "minimum": 1,
+         "type": "integer"
+       },
+       "limit_to": {
+         "description": "The subset the person asks for, passed on as an authoritative instruction to limit to or focus on exactly it: 'latest', 'top' (top / greatest), 'bottom' (bottom / smallest), 'most_common' or 'outliers'. limit_count says how many are kept. Unset: the engine decides.",
+         "enum": [
+           "",
+           "latest",
+           "top",
+           "bottom",
+           "most_common",
+           "outliers"
+         ],
+         "type": "string"
+       },
        "measures": {
          "description": "Columns to treat as MEASURES (the PBI 'Values' well — fields the chart aggregates). Numeric non-identifier columns are measures by default.",
          "items": {
+       "others_label": {
+         "description": "The name the person gives the 'Others' bucket, at most 40 characters; quotes, backticks and control characters are removed and longer text is cut. Unset: 'Other'. Not sent when group_others is false.",
+         "type": "string"
+       },
        "out_dir": {
          "description": "Optional directory to write chart.js/py, a data.sample.json, and (if preview_html) preview.html.",
          "type": "string"
+       "overlay": {
+         "description": "The analytical layer the person asks for: 'moving_avg' = moving average, 'trend_line' = trend / regression line, 'anomaly' = anomaly highlights, 'confidence_band' = confidence band, 'forecast' = forecast / projection, 'yoy' = year-over-year comparison. It is strongly preferred where it applies to the chosen chart and data (the engine adds the derived measures it needs) and left out where it cannot apply. Unset: the engine decides.",
+         "enum": [
+           "",
+           "moving_avg",
+           "trend_line",
+           "anomaly",
+           "confidence_band",
+           "forecast",
+           "yoy"
+         ],
+         "type": "string"
+       },
        "preview_html": {
          "description": "Also emit a standalone D3 preview (needs out_dir). Default false — the code is meant to be merged, not run as an index.html.",
          "type": "boolean"
+       "show_title": {
+         "description": "Whether the person wants a title inside the chart: true asks for a concise one, false asks for none. A yes or no, not the title's text. Unset: a chart narrower than 400 px or shorter than 300 px gets none, and a larger one draws at random whether to have one.",
+         "type": "boolean"
+       },
        "width": {
          "type": "number"
        },
+       "capabilities": {
+         "additionalProperties": false,
+         "description": "What the server declares the host may do with this chart, beside chartName. Absent when the chart has no capability: a chart type that declares none, a language other than D3, or an older server.",
+         "properties": {
+           "lasso": {
+             "additionalProperties": false,
+             "description": "Present when this chart type takes chart-host's lasso: a drag around marks selects the ones inside, through the selection a click makes. The component written with a D3 chart passes it as options.lasso ({ capable: true, enabled: true }), and the app's own options can switch it off with lasso: { enabled: false }.",
+             "properties": {
+               "rowCap": {
+                 "description": "The most rows in view that still get a lasso; above it the host turns the lasso off. Absent: chart-host's own default.",
+                 "type": "number"
+               }
+             },
+             "type": "object"
+           }
+         },
+         "type": "object"
+       },
        "chartName": {
          "description": "The chart type the backend produced.",
          "type": "string"
-         "description": "Ties this generation to the server-side log — quote it in any support request.",
+         "description": "Ties this generation to the server-side log; it is what a support request quotes.",
          "type": "string"
        },
        "creditCost": {
-             "description": "chart.component.tsx: the React host written with a D3 chart - the group, the binding, the plugins and the click contract wired, plus rowsFromTable for a query result. Render it instead of writing one.",
+             "description": "chart.component.tsx: the React host written with a D3 chart - the group, the binding, the plugins, the legend and frame fit (layoutPass) and the click contract wired, plus rowsFromTable for a query result. Render it instead of writing one.",
              "type": "string"
            },
            "data": {
-         "description": "The option names THIS chart's code reads (sorted). A chart honours nothing it does not read, so a live restyle - host.setOptions({...}), never a regeneration - can only move these. Some are the host's to supply (width, height, geo, geoPoint, uiState, setUiState, themeFg/themeBg); the integration contract says what each one does. Empty for a non-D3 language.",
+         "description": "The option names THIS chart's code reads (sorted). A chart honors nothing it does not read, so a live restyle - host.setOptions({...}), never a regeneration - can only move these. Some are the host's to supply (width, height, geo, geoPoint, uiState, setUiState, themeFg/themeBg); the integration contract says what each one does. Empty for a non-D3 language.",
          "items": {
            "type": "string"
          },
-         "description": "The server's identity for this exact code: the same code always carries the same hash. Keep it with the chart; it names the delivered code in a support request or a later report. Absent when the server sent none.",
+         "description": "The server's identity for this exact code: the same code always carries the same hash, which names the delivered code in a support request or a later report. Absent when the server sent none.",
          "type": "string"
        },
        "recoveredAfterMs": {
+       "settings": {
+         "additionalProperties": false,
+         "description": "WHICH SETTINGS APPLIED to this chart and where each value came from, so a person can be told what was assumed. Present on every generation.",
+         "properties": {
+           "applied": {
+             "description": "Every setting that applies to THIS chart and took effect, with its value and where that value came from.",
+             "items": {
+               "additionalProperties": false,
+               "properties": {
+                 "adjusted": {
+                   "description": "Present when the value was changed on the way: cut to length, held to a range, written in canonical form.",
+                   "type": "string"
+                 },
+                 "key": {
+                   "description": "The catalog row: the engine's name for the setting.",
+                   "type": "string"
+                 },
+                 "source": {
+                   "description": "Whose choice it is: 'user' = an argument of this call; 'app' = a live value already chosen for this app; 'default' = nothing set; 'server-roll' = the server drew it at random; 'from-prompt' = the server read it out of the request's own words. A later layer may add a source.",
+                   "type": "string"
+                 },
+                 "value": {
+                   "description": "The value in effect, or the default's wording ('the engine decides') when nothing was set.",
+                   "type": "string"
+                 },
+                 "via": {
+                   "description": "How the setting is carried: 'argument <name>' (of generate_chart) for the next chart, or 'chart-host option <name>' for a live restyle.",
+                   "type": "string"
+                 }
+               },
+               "required": [
+                 "key",
+                 "via",
+                 "value",
+                 "source"
+               ],
+               "type": "object"
+             },
+             "type": "array"
+           },
+           "askable": {
+             "description": "The settings that apply to THIS chart and that nothing of the person's is on yet: every next-generate row, and a live row only when this chart's code reads its option.",
+             "items": {
+               "additionalProperties": false,
+               "properties": {
+                 "ask": {
+                   "const": "offer",
+                   "description": "Present on a row worth a question, which then also carries values or range and default (and askWhen, on a live row). Absent on a row passed only when the person has said it; the guide's settings topic has its choices.",
+                   "type": "string"
+                 },
+                 "askWhen": {
+                   "description": "For a live offer row, when the question is worth putting to the person.",
+                   "type": "string"
+                 },
+                 "default": {
+                   "type": "string"
+                 },
+                 "how": {
+                   "description": "How an answer is carried: 'argument <name>' (of generate_chart) or 'chart-host option <name>' (the component's options prop).",
+                   "type": "string"
+                 },
+                 "key": {
+                   "type": "string"
+                 },
+                 "label": {
+                   "type": "string"
+                 },
+                 "phase": {
+                   "description": "Where the setting is carried: before-generate rows are arguments of generate_chart; after-generate rows are render options of the generated chart.",
+                   "enum": [
+                     "before-generate",
+                     "after-generate"
+                   ],
+                   "type": "string"
+                 },
+                 "range": {
+                   "additionalProperties": false,
+                   "properties": {
+                     "integer": {
+                       "type": "boolean"
+                     },
+                     "max": {
+                       "type": "number"
+                     },
+                     "min": {
+                       "type": "number"
+                     }
+                   },
+                   "required": [
+                     "min"
+                   ],
+                   "type": "object"
+                 },
+                 "timing": {
+                   "description": "next-generate = an argument of the next generate_chart call; live = a chart-host render option, set in the host code.",
+                   "enum": [
+                     "next-generate",
+                     "live"
+                   ],
+                   "type": "string"
+                 },
+                 "values": {
+                   "items": {
+                     "additionalProperties": false,
+                     "properties": {
+                       "label": {
+                         "type": "string"
+                       },
+                       "value": {
+                         "type": [
+                           "string",
+                           "boolean"
+                         ]
+                       }
+                     },
+                     "required": [
+                       "value",
+                       "label"
+                     ],
+                     "type": "object"
+                   },
+                   "type": "array"
+                 }
+               },
+               "required": [
+                 "key",
+                 "label",
+                 "timing",
+                 "phase",
+                 "how"
+               ],
+               "type": "object"
+             },
+             "type": "array"
+           },
+           "conflicts": {
+             "description": "Arguments of this call that were not sent, each with its reason: a contradiction with an authoritative argument, no effect, or nothing left once cleaned or checked.",
+             "items": {
+               "additionalProperties": false,
+               "properties": {
+                 "because": {
+                   "description": "Why it was not sent.",
+                   "type": "string"
+                 },
+                 "dropped": {
+                   "description": "The argument that was not sent.",
+                   "type": "string"
+                 }
+               },
+               "required": [
+                 "dropped",
+                 "because"
+               ],
+               "type": "object"
+             },
+             "type": "array"
+           },
+           "engineChoices": {
+             "description": "reported = the server said what it picked, so a row left at default is a default; unavailable = it did not (an older server, or a generation recovered by a poll), so the engine may have picked where a row reads default and picks cannot be listed.",
+             "enum": [
+               "reported",
+               "unavailable"
+             ],
+             "type": "string"
+           }
+         },
+         "required": [
+           "applied",
+           "askable",
+           "conflicts",
+           "engineChoices"
+         ],
+         "type": "object"
+       },
        "version": {
          "description": "Server-side version number of this generation.",
          "type": "number"
--- pinned/get_guide
+++ observed/get_guide
  {
    "annotations": {
      "openWorldHint": false,
-   "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.",
+   "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. 'settings' lists the choices a person can make about a chart, with their options and defaults.",
    "execution": {
      "taskSupport": "forbidden"
    },
-         "description": "'fabric-app' (default), 'all', or 'chart-shapes'.",
+         "description": "'fabric-app' (default), 'all', 'chart-shapes', or 'settings'.",
          "enum": [
            "fabric-app",
            "all",
-           "chart-shapes"
+           "chart-shapes",
+           "settings"
          ],
          "type": "string"
        }
--- pinned/list_eligible_charts
+++ observed/list_eligible_charts
  {
    "annotations": {
      "openWorldHint": true,
+       "chart_style": {
+         "description": "The user's own preference for which family of charts the list covers, as they've stated it. 'traditional' lists only the familiar forms (bar, stacked and grouped bar, line, area, pie, donut, card, gauge), 'non_traditional' lists only the other chart types; a type outside the chosen family is left out. Unset when they haven't stated one; unset changes nothing.",
+         "enum": [
+           "traditional",
+           "non_traditional",
+           ""
+         ],
+         "type": "string"
+       },
        "csv_path": {
          "description": "Path to a CSV file (header row required).",
          "type": "string"
-         "description": "Project source language → 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.",
+         "description": "Project source language → the server ranks across that language's renderers (javascript/typescript = D3, python = Plotly/matplotlib). Omit to AUTO-DETECT from the working dir; `renderer` (if given) overrides this.",
          "enum": [
            "javascript",
            "typescript",
-           "VEGA",
            ""
          ],
          "type": "string"
--- pinned/preview_request
+++ observed/preview_request
  {
    "annotations": {
      "openWorldHint": false,
+       "chart_style": {
+         "description": "Narrows the charts considered to one family: 'traditional' = the familiar forms (bar, stacked and grouped bar, line, area, pie, donut, card, gauge), 'non_traditional' = every other chart type. A chart named in chart_type is still honored unless the data shape disqualifies it, and 'traditional' also turns chart_variety off. Unset considers both families.",
+         "enum": [
+           "",
+           "traditional",
+           "non_traditional"
+         ],
+         "type": "string"
+       },
        "chart_type": {
          "description": "Preferred chart type by name (honored unless the data shape disqualifies it).",
          "type": "string"
+       "chart_variety": {
+         "description": "How much variety the person asks for: the probability, from 0 to 1, that the choice among the charts that fit this data is nudged toward a less conventional one, when the engine runs its planning pass. The nudge is among already-valid charts only: when only the obvious chart honestly fits, it is kept. It has no effect with an explicit chart_type, chart_style 'traditional' or reasoning_mode 1P or CP. Unset: the server's own default.",
+         "maximum": 1,
+         "minimum": 0,
+         "type": "number"
+       },
+       "compactness": {
+         "description": "How roomy the person wants the chart: 'spacious' = roomier margins and more aggressive aggregation of a high-cardinality column, 'balanced' = standard margins and label density, 'compact' = tighter margins and denser axes, aggregating only when cardinality clearly exceeds what fits. Unset: a level is drawn at random for each generation.",
+         "enum": [
+           "",
+           "spacious",
+           "balanced",
+           "compact"
+         ],
+         "type": "string"
+       },
        "csv_path": {
          "description": "Path to a CSV file (header row required).",
          "type": "string"
+       "data_labels": {
+         "description": "Whether the person wants the value printed on each mark: 'include' asks for the labels, 'exclude' for none. Per-mark labels are suppressed when more than 30 marks would carry one. Unset: the engine decides.",
+         "enum": [
+           "",
+           "include",
+           "exclude"
+         ],
+         "type": "string"
+       },
+       "date_grain": {
+         "description": "The time grain the person asks for on the time axis: day, week, month, quarter, year or decade. It is passed on as a strong request, and a different bucket the engine plans is rewritten to it when the dates give that grain 2 or more periods. Unset: the engine chooses, from the number of periods the dates give at each grain.",
+         "enum": [
+           "",
+           "day",
+           "week",
+           "month",
+           "quarter",
+           "year",
+           "decade"
+         ],
+         "type": "string"
+       },
+       "derived_measures": {
+         "description": "Whether the person allows derived measures (ratios, z-scores, percent change, rank, index): 'discourage' keeps the chart to the raw columns (aggregations and sorts still apply); 'encourage' keeps them allowed and rules out the draw that would turn them off, without asking for more of them. Unset: a weighted draw for each generation. 'encourage' is not sent with forbid_derived_columns.",
+         "enum": [
+           "",
+           "encourage",
+           "discourage"
+         ],
+         "type": "string"
+       },
        "descriptions": {
          "additionalProperties": {
            "type": "string"
-         "description": "CSS font family for the chart's own text — 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.",
+         "description": "The font family the person names for the chart's own text (titles, axis labels, tick labels, legends), or the one their house style says: a family such as 'Segoe UI' or a stack such as 'Segoe UI, Arial, sans-serif'. A font isn't a property of the data or the project. It applies to whichever renderer is chosen, and an unavailable family falls back to a web-safe stack rather than failing. Unset: the engine decides.",
          "type": "string"
        },
        "forbid_derived_columns": {
+       "group_others": {
+         "description": "Whether the person allows the categories beyond a limit to be grouped into one 'Others' bucket. True is the same as unset: under a limit the rest is grouped and the total stays whole; with no limit the engine decides whether to group. False: the rest is dropped and nothing is grouped. Unset: grouping allowed. group_others true is not sent with rows_policy 'all', which draws every row.",
+         "type": "boolean"
+       },
        "height": {
          "type": "number"
        },
+       "in_chart_caption": {
+         "description": "Whether the person asks for a one-line subtitle under the chart title naming the chart type, any derived measure used and any non-default aggregation. Unset or false: none.",
+         "type": "boolean"
+       },
        "include_code": {
          "description": "Include the full code in the response — 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.",
          "type": "boolean"
+       "industry": {
+         "description": "The industry the person names for the data, one of the listed values. A recognized metric name is used only when the columns are that industry's inputs for it. Unset: no industry context is added.",
+         "enum": [
+           "",
+           "Retail & e-commerce",
+           "Logistics & shipping",
+           "Manufacturing",
+           "Healthcare & life sciences",
+           "Finance & banking",
+           "Insurance",
+           "Marketing & advertising",
+           "SaaS & technology",
+           "Telecom",
+           "Energy & utilities",
+           "Real estate",
+           "Education",
+           "Government & public sector",
+           "Hospitality & travel"
+         ],
+         "type": "string"
+       },
        "language": {
          "description": "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.",
          "enum": [
+       "legend_placement": {
+         "description": "Where the person wants the legend: 'outside' = in the chart margin, 'inside' = within the plot area where it overlaps the data least, 'none' = no standalone legend (the chart's own labels carry the names). A single-series chart draws no legend whichever is chosen. Unset: outside on a chart at least 500 px wide, the renderer's default on a narrower one.",
+         "enum": [
+           "",
+           "outside",
+           "inside",
+           "none"
+         ],
+         "type": "string"
+       },
        "level_of_detail": {
          "description": "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.",
          "enum": [
+       "limit_count": {
+         "description": "How many groups or categories the person asks to keep, from 1 to 10000. The engine keeps exactly that many by the measure, and its own Top-N, 'Other'-cap and cardinality rules yield to it. Unset: no limit from the person (the engine's own Top-N rules still apply).",
+         "maximum": 10000,
+         "minimum": 1,
+         "type": "integer"
+       },
+       "limit_to": {
+         "description": "The subset the person asks for, passed on as an authoritative instruction to limit to or focus on exactly it: 'latest', 'top' (top / greatest), 'bottom' (bottom / smallest), 'most_common' or 'outliers'. limit_count says how many are kept. Unset: the engine decides.",
+         "enum": [
+           "",
+           "latest",
+           "top",
+           "bottom",
+           "most_common",
+           "outliers"
+         ],
+         "type": "string"
+       },
        "measures": {
          "description": "Columns to treat as MEASURES (the PBI 'Values' well — fields the chart aggregates). Numeric non-identifier columns are measures by default.",
          "items": {
+       "others_label": {
+         "description": "The name the person gives the 'Others' bucket, at most 40 characters; quotes, backticks and control characters are removed and longer text is cut. Unset: 'Other'. Not sent when group_others is false.",
+         "type": "string"
+       },
        "out_dir": {
          "description": "Optional directory to write chart.js/py, a data.sample.json, and (if preview_html) preview.html.",
          "type": "string"
+       "overlay": {
+         "description": "The analytical layer the person asks for: 'moving_avg' = moving average, 'trend_line' = trend / regression line, 'anomaly' = anomaly highlights, 'confidence_band' = confidence band, 'forecast' = forecast / projection, 'yoy' = year-over-year comparison. It is strongly preferred where it applies to the chosen chart and data (the engine adds the derived measures it needs) and left out where it cannot apply. Unset: the engine decides.",
+         "enum": [
+           "",
+           "moving_avg",
+           "trend_line",
+           "anomaly",
+           "confidence_band",
+           "forecast",
+           "yoy"
+         ],
+         "type": "string"
+       },
        "preview_html": {
          "description": "Also emit a standalone D3 preview (needs out_dir). Default false — the code is meant to be merged, not run as an index.html.",
          "type": "boolean"
+       "show_title": {
+         "description": "Whether the person wants a title inside the chart: true asks for a concise one, false asks for none. A yes or no, not the title's text. Unset: a chart narrower than 400 px or shorter than 300 px gets none, and a larger one draws at random whether to have one.",
+         "type": "boolean"
+       },
        "width": {
          "type": "number"
        },

Changed 2026-10-09 13:19 UTC

4 schema or annotation changes with the description left byte-identical.

--- pinned/assess_data_shape
+++ observed/assess_data_shape
  {
    "annotations": {
      "openWorldHint": false,
+       "culture": {
+         "description": "The caller's language and region as a BCP 47 tag such as 'de-DE', 'fr' or 'ja-JP'. Default 'en-US'. It is the language the chart's own text is written in (titles, axis labels, legends, notes), and it breaks the tie when a CSV's numbers or dates are ambiguous: 12.500 as twelve thousand five hundred or twelve and a half, 05.03.2024 as 5 March or 3 May. Set it from the end user's locale when you know it; it does not translate the data.",
+         "maxLength": 35,
+         "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$",
+         "type": "string"
+       },
        "data": {
          "additionalProperties": false,
          "description": "A pre-typed {columns, rows} table — 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.",
--- pinned/generate_chart
+++ observed/generate_chart
  {
    "annotations": {
      "destructiveHint": false,
-         "description": "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.",
+         "description": "The caller's language and region as a BCP 47 tag such as 'de-DE', 'fr' or 'ja-JP'. Default 'en-US'. It is the language the chart's own text is written in (titles, axis labels, legends, notes), and it breaks the tie when a CSV's numbers or dates are ambiguous: 12.500 as twelve thousand five hundred or twelve and a half, 05.03.2024 as 5 March or 3 May. Set it from the end user's locale when you know it; it does not translate the data.",
          "maxLength": 35,
          "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$",
          "type": "string"
-         "description": "Present when the response stream was cut after the server accepted the request and this chart was then fetched back by its correlation: milliseconds from the start of the call to delivery. creditCost is null then, because the generation was charged as usual and the fetch that delivered it added the cached-serve fee.",
+         "description": "Present when the response stream was cut after the server accepted the request and this chart was then fetched back by its correlation: milliseconds from the start of the call to delivery. creditCost is null then: the fetch that delivered it reports only its own cost (a recovery is free), not the generation's, which was charged once, as usual.",
          "type": "number"
        },
        "requiredD3Plugins": {
--- pinned/list_eligible_charts
+++ observed/list_eligible_charts
  {
    "annotations": {
      "openWorldHint": true,
+       "culture": {
+         "description": "The caller's language and region as a BCP 47 tag such as 'de-DE', 'fr' or 'ja-JP'. Default 'en-US'. It is the language the chart's own text is written in (titles, axis labels, legends, notes), and it breaks the tie when a CSV's numbers or dates are ambiguous: 12.500 as twelve thousand five hundred or twelve and a half, 05.03.2024 as 5 March or 3 May. Set it from the end user's locale when you know it; it does not translate the data.",
+         "maxLength": 35,
+         "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$",
+         "type": "string"
+       },
        "data": {
          "additionalProperties": false,
          "description": "A pre-typed {columns, rows} table — 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.",
--- pinned/preview_request
+++ observed/preview_request
  {
    "annotations": {
      "openWorldHint": false,
-         "description": "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.",
+         "description": "The caller's language and region as a BCP 47 tag such as 'de-DE', 'fr' or 'ja-JP'. Default 'en-US'. It is the language the chart's own text is written in (titles, axis labels, legends, notes), and it breaks the tie when a CSV's numbers or dates are ambiguous: 12.500 as twelve thousand five hundred or twelve and a half, 05.03.2024 as 5 March or 3 May. Set it from the end user's locale when you know it; it does not translate the data.",
          "maxLength": 35,
          "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$",
          "type": "string"

Changed 2026-10-08 13:32 UTC

1 description rewritten.

--- pinned/list_eligible_charts
+++ observed/list_eligible_charts
  {
    "annotations": {
      "openWorldHint": true,
-   "description": "The AUTHORITATIVE list of chart types that can render this dataset — 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 — 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'/…) 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.",
+   "description": "The AUTHORITATIVE list of chart types that can render this dataset — 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 — 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'/…) 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. The reply carries a text block and a structured table: one row per chart type with its rank, name, preview flag, fit verdict, description, language and renderer. Rows flagged alsoWorthALook are a few picks from below the top fits, drawn so an offer can include something different; the fit order itself never changes.",
    "execution": {
      "taskSupport": "forbidden"
    },
+   "outputSchema": {
+     "$schema": "http://json-schema.org/draft-07/schema#",
+     "additionalProperties": false,
+     "properties": {
+       "charts": {
+         "description": "One row per chart type, in the order issued.",
+         "items": {
+           "additionalProperties": false,
+           "properties": {
+             "alsoWorthALook": {
+               "description": "A pick drawn from below the top fits, so an offer can include something different. Never a type the fit order rates poor.",
+               "type": "boolean"
+             },
+             "description": {
+               "type": "string"
+             },
+             "fit": {
+               "description": "fits = the chart ontology judges it a fit for this data; can-also-render = it can draw this, as a weaker fit.",
+               "enum": [
+                 "fits",
+                 "can-also-render"
+               ],
+               "type": "string"
+             },
+             "language": {
+               "type": [
+                 "string",
+                 "null"
+               ]
+             },
+             "name": {
+               "type": "string"
+             },
+             "preview": {
+               "description": "A newer, less proven type (still selectable by name).",
+               "type": "boolean"
+             },
+             "rank": {
+               "description": "Position in the order asked for; null for a type the server did not rank.",
+               "type": [
+                 "number",
+                 "null"
+               ]
+             },
+             "renderer": {
+               "type": [
+                 "string",
+                 "null"
+               ]
+             }
+           },
+           "required": [
+             "rank",
+             "name",
+             "preview",
+             "fit",
+             "description",
+             "language",
+             "renderer",
+             "alsoWorthALook"
+           ],
+           "type": "object"
+         },
+         "type": "array"
+       },
+       "eligibleCount": {
+         "description": "How many chart types can render this data before any name_filter or top.",
+         "type": "number"
+       },
+       "filter": {
+         "anyOf": [
+           {
+             "additionalProperties": false,
+             "properties": {
+               "text": {
+                 "type": "string"
+               },
+               "tier": {
+                 "description": "Which match tier answered: name, namePart, desc or none.",
+                 "type": "string"
+               }
+             },
+             "required": [
+               "text",
+               "tier"
+             ],
+             "type": "object"
+           },
+           {
+             "type": "null"
+           }
+         ],
+         "description": "The name_filter applied, or null."
+       },
+       "metering": {
+         "description": "What this call cost, in the words the text block states it in.",
+         "type": "string"
+       },
+       "order": {
+         "description": "The order the rows are in: by fit to this data, or the picker's selection order.",
+         "enum": [
+           "fit",
+           "variety"
+         ],
+         "type": "string"
+       },
+       "shown": {
+         "description": "How many rows this answer carries.",
+         "type": "number"
+       }
+     },
+     "required": [
+       "order",
+       "eligibleCount",
+       "shown",
+       "filter",
+       "metering",
+       "charts"
+     ],
+     "type": "object"
+   },
    "title": "List eligible chart types"
  }

Changed 2026-10-07 13:26 UTC

4 descriptions rewritten, 5 schema or annotation changes with the description left byte-identical, 1 tool added.

--- pinned/assess_data_shape
+++ observed/assess_data_shape
  {
+   "annotations": {
+     "openWorldHint": false,
+     "readOnlyHint": true,
+     "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.",
    "execution": {
      "taskSupport": "forbidden"

+ tool added: check_app

--- pinned/check_chart
+++ observed/check_chart
  {
+   "annotations": {
+     "openWorldHint": false,
+     "readOnlyHint": true,
+     "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.",
    "execution": {
      "taskSupport": "forbidden"
--- pinned/generate_chart
+++ observed/generate_chart
  {
-   "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. Requires a trial or paid BIC account (license env vars / credentials file). 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.",
+   "annotations": {
+     "destructiveHint": false,
+     "idempotentHint": false,
+     "openWorldHint": true,
+     "readOnlyHint": false,
+     "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.",
    "execution": {
      "taskSupport": "forbidden"
    },
-         "description": "CSS font family for the chart's own text — titles, axis labels, tick labels, legends (e.g. 'Segoe UI' or a stack like 'Segoe UI, Arial, sans-serif'). Pass this ONLY when the user has named a font or you are matching a stated house style — do NOT infer one from the data, the project, or your own preference. Omitting it is a proven no-op: 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.",
+         "description": "CSS font family for the chart's own text — 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.",
          "type": "string"
        },
        "forbid_derived_columns": {
-         "description": "How much of the data the user wants to SEE. ASK THEM — do not infer it from the data or from your own preference; it is a question about what they want, and they can answer it in one word. '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). Omit ONLY if the user has genuinely expressed no preference — omitting it 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.",
+         "description": "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.",
          "enum": [
            "High",
            "Medium",
--- pinned/get_guide
+++ observed/get_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.",
+   "annotations": {
+     "openWorldHint": false,
+     "readOnlyHint": true,
+     "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.",
    "execution": {
      "taskSupport": "forbidden"
    },
--- pinned/list_eligible_charts
+++ observed/list_eligible_charts
  {
-   "description": "The AUTHORITATIVE list of chart types that can render this dataset — 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 — 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 + RANKED by the picker's chart-type×renderer weights (best first), each tagged with its source language and renderer. Pass `language` ('javascript'|'python') to force it, or `renderer` ('D3'/'PLOTLY'/…) 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. REQUIRES an active license (BIC_LICENSEE + BIC_LICENSE_KEY): eligibility IS the engine, so programmatic access to it is licensed. Free within a generous hourly allowance; beyond it a call may draw usage credits, so avoid calling it in a tight loop. The refusal names what to set.",
+   "annotations": {
+     "openWorldHint": true,
+     "readOnlyHint": true,
+     "title": "List eligible chart types"
+   },
+   "description": "The AUTHORITATIVE list of chart types that can render this dataset — 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 — 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'/…) 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.",
    "execution": {
      "taskSupport": "forbidden"
    },
-         "description": "How much of the data the user wants to SEE — 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.",
+         "description": "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.",
          "enum": [
            "High",
            "Medium",
+       "order": {
+         "description": "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.",
+         "enum": [
+           "fit",
+           "variety"
+         ],
+         "type": "string"
+       },
        "privacy_level": {
          "description": "How much detail to compute/share: 0 names only, 10 +counts, 20 +detailed stats (default), 30 +obfuscated sample rows.",
          "enum": [
--- pinned/preview_request
+++ observed/preview_request
  {
-   "description": "Shows exactly what generate_chart 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 licence: it is local and free. Takes the same arguments as generate_chart. Use it to answer 'what data leaves my machine?' before generating; list_eligible_charts sends a subset of the same shape.",
+   "annotations": {
+     "openWorldHint": false,
+     "readOnlyHint": true,
+     "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.",
    "execution": {
      "taskSupport": "forbidden"
    },
-         "description": "CSS font family for the chart's own text — titles, axis labels, tick labels, legends (e.g. 'Segoe UI' or a stack like 'Segoe UI, Arial, sans-serif'). Pass this ONLY when the user has named a font or you are matching a stated house style — do NOT infer one from the data, the project, or your own preference. Omitting it is a proven no-op: 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.",
+         "description": "CSS font family for the chart's own text — 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.",
          "type": "string"
        },
        "forbid_derived_columns": {
-         "description": "How much of the data the user wants to SEE. ASK THEM — do not infer it from the data or from your own preference; it is a question about what they want, and they can answer it in one word. '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). Omit ONLY if the user has genuinely expressed no preference — omitting it 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.",
+         "description": "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.",
          "enum": [
            "High",
            "Medium",
--- pinned/query_semantic_model
+++ observed/query_semantic_model
  {
+   "annotations": {
+     "destructiveHint": false,
+     "idempotentHint": true,
+     "openWorldHint": true,
+     "readOnlyHint": false,
+     "title": "Query the semantic model for a chart"
+   },
    "description": "In a Microsoft Fabric App built on Rayfin's data app template: runs one chart's DAX query through the project's own `fabric-app-data` CLI, renames the `Table[Column]` and `[Measure]` headers to plain names (the names the chart.component.tsx that generate_chart writes will match on live rows), saves EVERY row as CSV, reads each column's role, format and description from the model, and profiles the result. Replaces the query, rename, save and profile steps with one call, and returns the arguments to pass to list_eligible_charts and generate_chart. Write the query at the chart's grain (one row per country for a country map). Local: the data goes only where the project's CLI already sends it, and nothing reaches the chart server.",
    "execution": {
      "taskSupport": "forbidden"
--- pinned/setup_fabric_app
+++ observed/setup_fabric_app
  {
+   "annotations": {
+     "destructiveHint": false,
+     "idempotentHint": true,
+     "openWorldHint": true,
+     "readOnlyHint": false,
+     "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.",
    "execution": {
      "taskSupport": "forbidden"
--- pinned/suggest_charts
+++ observed/suggest_charts
  {
+   "annotations": {
+     "destructiveHint": false,
+     "idempotentHint": true,
+     "openWorldHint": true,
+     "readOnlyHint": false,
+     "title": "Suggest charts for a semantic model"
+   },
    "description": "Which charts suit this data? Answers 'what are good chart types for this dataset / model', 'what can I chart here', 'recommend some charts' for a semantic model in a Microsoft Fabric App built on Rayfin's data app template, in ONE call, from the model's real rows: reads its tables, columns, measures and relationships through the project's own `fabric-app-data` CLI, finds its natural views (measures by place, flows between places, measures over time, measures by category), queries each at its grain, keeps the measures that vary there, and asks the server's eligibility engine what fits each view. Replies view by view with the real counts (rows, places, periods, categories) and the chart types that fit, and saves each view's rows and DAX under .bic/data so generate_chart can build any of them next. Pass `views` to ask about your own groupings. Answer the person from this reply rather than from general knowledge. One eligibility call per view: metered like list_eligible_charts (free within an hourly allowance, usage credits beyond it) and needs the same licence. For one table you already hold, use list_eligible_charts.",
    "execution": {
      "taskSupport": "forbidden"

Changed 2026-10-06 14:57 UTC

2 schema or annotation changes with the description left byte-identical.

--- pinned/generate_chart
+++ observed/generate_chart
  {
    "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. Requires a trial or paid BIC account (license env vars / credentials file). 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.",
    "execution": {
-         "description": "Project source language: with no concrete `renderer`, the backend auto-picks a renderer WITHIN this language (javascript/typescript → D3/Vega, python → Plotly/matplotlib). Omit to AUTO-DETECT from the working dir.",
+         "description": "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.",
          "enum": [
            "javascript",
            "typescript",
--- pinned/preview_request
+++ observed/preview_request
  {
    "description": "Shows exactly what generate_chart 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 licence: it is local and free. Takes the same arguments as generate_chart. Use it to answer 'what data leaves my machine?' before generating; list_eligible_charts sends a subset of the same shape.",
    "execution": {
-         "description": "Project source language: with no concrete `renderer`, the backend auto-picks a renderer WITHIN this language (javascript/typescript → D3/Vega, python → Plotly/matplotlib). Omit to AUTO-DETECT from the working dir.",
+         "description": "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.",
          "enum": [
            "javascript",
            "typescript",

Changed 2026-10-05 14:32 UTC

1 description rewritten, 1 schema or annotation change with the description left byte-identical, 6 tools added.

+ tool added: check_chart

--- pinned/generate_chart
+++ observed/generate_chart
  {
    "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. Requires a trial or paid BIC account (license env vars / credentials file). 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.",
    "execution": {
+       "culture": {
+         "description": "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.",
+         "maxLength": 35,
+         "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$",
+         "type": "string"
+       },
        "data": {
          "additionalProperties": false,
          "description": "A pre-typed {columns, rows} table — 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.",
-         "description": "Include the full code in the response (default true) — governs BOTH the text block and structuredContent.code. Set false when you only want it written to out_dir.",
+         "description": "Include the full code in the response — 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.",
          "type": "boolean"
        },
        "language": {
-         "description": "Directory to auto-detect the project language from (default: the MCP server's working directory).",
+         "description": "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).",
          "type": "string"
        },
        "prompt": {
+       "binding": {
+         "additionalProperties": false,
+         "description": "How `data` was built from your rows. Hand all three to @bicharts/chart-host - <BicChartGroup geo point destination> or createChartGroup(columns, rows, { geo, point, destination }) - and it builds the payload again from LIVE rows with the same columns; never rebuild __rowIdx__ or __geo* columns by hand.",
+         "properties": {
+           "destination": {
+             "anyOf": [
+               {
+                 "additionalProperties": {},
+                 "type": "object"
+               },
+               {
+                 "type": "null"
+               }
+             ],
+             "description": "The same for a route's far end (an origin-destination flow map); point is then the origin. Null otherwise."
+           },
+           "geo": {
+             "anyOf": [
+               {
+                 "additionalProperties": false,
+                 "properties": {
+                   "column": {
+                     "type": "string"
+                   },
+                   "kind": {
+                     "type": "string"
+                   }
+                 },
+                 "required": [
+                   "column",
+                   "kind"
+                 ],
+                 "type": "object"
+               },
+               {
+                 "type": "null"
+               }
+             ],
+             "description": "The choropleth join: the column matched to the geometry and its kind. Null when nothing joins."
+           },
+           "point": {
+             "anyOf": [
+               {
+                 "additionalProperties": {},
+                 "type": "object"
+               },
+               {
+                 "type": "null"
+               }
+             ],
+             "description": "Which columns place a point (lat/lon, city, state, zip, country) and the map's gazetteer scope. Null when the chart places no points."
+           }
+         },
+         "required": [
+           "geo",
+           "point",
+           "destination"
+         ],
+         "type": "object"
+       },
        "build": {
          "description": "MCP server build stamp — the bundle actually serving this call.",
          "type": "string"
+       "dropLines": {
+         "additionalProperties": false,
+         "description": "A 3D scatter's optional hover drop lines (a line from a hovered point to each back wall). requested: on, because the request asked; on/off: the user's answer, already applied to the generated component; ask: put askUser to the user ONCE before finishing, then pass hoverDropLines: true in the chart's options on a yes. Absent for every other chart.",
+         "properties": {
+           "askUser": {
+             "type": "string"
+           },
+           "state": {
+             "enum": [
+               "requested",
+               "on",
+               "off",
+               "ask"
+             ],
+             "type": "string"
+           }
+         },
+         "required": [
+           "state"
+         ],
+         "type": "object"
+       },
        "files": {
          "additionalProperties": false,
          "description": "Absolute paths written, when out_dir was given.",
+           "component": {
+             "description": "chart.component.tsx: the React host written with a D3 chart - the group, the binding, the plugins and the click contract wired, plus rowsFromTable for a query result. Render it instead of writing one.",
+             "type": "string"
+           },
            "data": {
              "type": "string"
            },
+               "ambiguousExamples": {
+                 "items": {
+                   "type": "string"
+                 },
+                 "type": "array"
+               },
                "ambiguousRows": {
-                 "description": "Rows placed by a largest-match tie-break rather than an exact one.",
+                 "description": "Rows whose place name matched several places and was therefore NOT placed.",
                  "type": "number"
                },
                "placed": {
-               "ambiguousRows"
+               "ambiguousRows",
+               "ambiguousExamples"
              ],
              "type": "object"
            },
-         "description": "Point-geocoding outcome; null when the chart geocodes no points."
+         "description": "Point-geocoding outcome (a route's ORIGIN end when geoPointDest is present). `placed` counts only rows with a coordinate: neither an unplaced nor an ambiguous name is plotted. Null when the chart geocodes no points."
        },
+       "geoPointDest": {
+         "additionalProperties": false,
+         "description": "A route's DESTINATION end (an origin-destination flow map), geocoded apart from geoPoint, which is then the origin. `placed` counts only rows with a destination coordinate: neither an unplaced nor an ambiguous name is plotted. Present only for a route.",
+         "properties": {
+           "ambiguousExamples": {
+             "items": {
+               "type": "string"
+             },
+             "type": "array"
+           },
+           "ambiguousRows": {
+             "description": "Rows whose destination name matched several places and was therefore NOT placed.",
+             "type": "number"
+           },
+           "placed": {
+             "type": "number"
+           },
+           "precision": {
+             "type": [
+               "string",
+               "null"
+             ]
+           },
+           "total": {
+             "type": "number"
+           },
+           "unplaced": {
+             "type": "number"
+           },
+           "unplacedExamples": {
+             "items": {
+               "type": "string"
+             },
+             "type": "array"
+           }
+         },
+         "required": [
+           "placed",
+           "total",
+           "precision",
+           "unplaced",
+           "unplacedExamples",
+           "ambiguousRows",
+           "ambiguousExamples"
+         ],
+         "type": "object"
+       },
        "hostContract": {
          "description": "Host contract version the code targets; a host on a different MAJOR may not interoperate.",
          "type": "string"
+       "integrationContract": {
+         "description": "HOW TO RUN THIS CHART IN YOUR APP - the same integration contract the text block carries, here because a host that shows the model only this structured result would otherwise never see it: the payload and option shapes, the reference host (@bicharts/chart-host) with its React bindings AND its framework-free group (createChartGroup + createChartHost + attach) for a plain page or a UI framework reached through interop, this chart's d3 plugins and geometry, and the host-page prerequisites. Read it before wiring the chart in.",
+         "type": "string"
+       },
        "language": {
          "description": "Renderer/language of the code (D3, PYTHON, …).",
          "type": "string"
+       "optionsRead": {
+         "description": "The option names THIS chart's code reads (sorted). A chart honours nothing it does not read, so a live restyle - host.setOptions({...}), never a regeneration - can only move these. Some are the host's to supply (width, height, geo, geoPoint, uiState, setUiState, themeFg/themeBg); the integration contract says what each one does. Empty for a non-D3 language.",
+         "items": {
+           "type": "string"
+         },
+         "type": "array"
+       },
+       "outputHash": {
+         "description": "The server's identity for this exact code: the same code always carries the same hash. Keep it with the chart; it names the delivered code in a support request or a later report. Absent when the server sent none.",
+         "type": "string"
+       },
+       "recoveredAfterMs": {
+         "description": "Present when the response stream was cut after the server accepted the request and this chart was then fetched back by its correlation: milliseconds from the start of the call to delivery. creditCost is null then, because the generation was charged as usual and the fetch that delivered it added the cached-serve fee.",
+         "type": "number"
+       },
        "requiredD3Plugins": {
-         "description": "D3 plugin PACKAGES this code calls (e.g. [\"d3-sankey\"]) — install and Object.assign onto the same d3 you pass the host, or the chart throws mid-render. Empty for core-only d3 and for non-D3 languages.",
+         "description": "D3 plugin PACKAGES this code calls (e.g. [\"d3-sankey\"]) — install them and pass the host ONE d3 that carries them, built with assembleD3(d3, { ...plugins }) from @bicharts/chart-host (assigning onto an ES-module d3 namespace throws), or the chart throws mid-render. Empty for core-only d3 and for non-D3 languages.",
          "items": {
            "type": "string"
          },

+ tool added: get_guide

--- pinned/list_eligible_charts
+++ observed/list_eligible_charts
  {
-   "description": "The AUTHORITATIVE list of chart types that can render this dataset — the server's own eligibility engine (ChartFilter.GetCharts), 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 — 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 + RANKED by the picker's chart-type×renderer weights (best first), each tagged with its source language and renderer. Pass `language` ('javascript'|'python') to force it, or `renderer` ('D3'/'PLOTLY'/…) 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. REQUIRES an active license (BIC_LICENSEE + BIC_LICENSE_KEY): eligibility IS the engine, so programmatic access to it is licensed. Free within a generous hourly allowance; beyond it a call may draw usage credits, so avoid calling it in a tight loop. The refusal names what to set.",
+   "description": "The AUTHORITATIVE list of chart types that can render this dataset — 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 — 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 + RANKED by the picker's chart-type×renderer weights (best first), each tagged with its source language and renderer. Pass `language` ('javascript'|'python') to force it, or `renderer` ('D3'/'PLOTLY'/…) 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. REQUIRES an active license (BIC_LICENSEE + BIC_LICENSE_KEY): eligibility IS the engine, so programmatic access to it is licensed. Free within a generous hourly allowance; beyond it a call may draw usage credits, so avoid calling it in a tight loop. The refusal names what to set.",
    "execution": {
      "taskSupport": "forbidden"
    },
+       "name_filter": {
+         "description": "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`.",
+         "type": "string"
+       },
+       "name_filter_match": {
+         "description": "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.",
+         "enum": [
+           "loose",
+           "exact"
+         ],
+         "type": "string"
+       },
        "non_additive": {
          "description": "MEASURE columns that must NOT be summed (a ratio/average/rate — use mean, never stack). Usually auto-detected; this is an authoritative override.",
          "items": {
-         "description": "Directory to auto-detect the project language from (default: the MCP server's working directory).",
+         "description": "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).",
          "type": "string"
        },
        "renderer": {

+ tool added: preview_request

+ tool added: query_semantic_model

+ tool added: setup_fabric_app

+ tool added: suggest_charts

Current tools

Show all 10 tool fingerprints
assess_data_shape
8b80c59d0f6b5340
check_app
ad6169d0b1f28c35
check_chart
aeb7c461af50572d
generate_chart
4db070f4e04dfc89
get_guide
4934bac68cebb079
list_eligible_charts
ce193ea7f67c8f57
preview_request
7aa6323b1d9e57c9
query_semantic_model
3e3bb9e57e6613cf
setup_fabric_app
eb118b6b9d969ce4
suggest_charts
5bb4081c3085f586

Watch this server yourself

If you run this server, put the proxy in front of it. It pins these exact fingerprints on first connect and stops the session if they move.

npx -y mcp-pin@0.2.3 -- <your @bicharts/chart-mcp command>

Or subscribe to this page's RSS feed to be told when it changes.

Badge

mcp-pin status badge for @bicharts/chart-mcp

The badge states one fact about time and nothing else. It never claims a server is safe.

[![mcp-pin](https://mcp-pin.gautamkhosla.com/badge/a27a9e9b379a5af4.svg)](https://mcp-pin.gautamkhosla.com/servers/a27a9e9b379a5af4.html)