Ask your coding agent for a chart and it will happily comply. What you get back is usually a chart-shaped disappointment: a bar chart for a time series, a pie chart with eleven slices, axis labels that overlap into mush, a legend that explains nothing. The model knows the charting library's syntax. It does not know which chart to draw, how to lay it out, or when the thing it just produced is wrong.

Microsoft's Flint is a visualization language built for exactly this gap. Instead of asking the model to emit raw Vega-Lite or ECharts JSON — a large, fiddly schema it will get subtly wrong — you give it one small semantic spec: the data, what each field means, the chart type, a theme, and an interaction. Flint's compiler resolves the rest: scales, axes, formatting, colors, layout. The same spec renders on five backends — Vega-Lite, ECharts, Chart.js, Plotly, and native Excel charts — and ships as an MCP server your agent can call directly.

Flint 1.0 landed on October 19, 2026. This guide wires its MCP server into your agent, renders a first chart from real data, validates the spec the way the server does, and compiles the same spec three ways — library, CLI, and MCP — with every step run on this machine on November 8, 2026. No API key, no account, no model bill: the server renders locally, in process, and your data never leaves your machine.

What you'll need#

  • Node.js 18 or later and npm. This guide used Node v24.22.1 and npm 10.9.4. Check yours with node --version.
  • A text editor and a terminal. About 30 minutes.
  • An MCP client for Step 2 — Claude Code, Claude Desktop, Cursor, or VS Code. Optional: everything else in this guide runs without an agent, through the same protocol, so you can verify the server before you hand it to one.
  • Nothing else. No API key, no account, no paid model. Flint is MIT-licensed, and the MCP server's rendering runs locally in process.

Step 1: Run the MCP server and prove it answers#

Flint ships two npm packages: flint-chart (the library) and flint-chart-mcp (the MCP server). Both are at version 1.0.1. Start with the server — it needs no install:

npx -y flint-chart-mcp

The server starts on stdio and says so on stderr:

flint-chart-mcp 1.0.1 ready on stdio (backends: vegalite, echarts, chartjs; local files readable on request)

Before trusting any MCP server, make it answer a question with no agent in the loop. The MCP protocol over stdio is newline-delimited JSON-RPC, so a short script can drive it directly. Send initialize:

{"jsonrpc": "2.0", "id": 1, "method": "initialize",
 "params": {"protocolVersion": "2025-06-18", "capabilities": {},
            "clientInfo": {"name": "probe", "version": "1.0.0"}}}

The server answers with its identity and its instructions:

{"result": {"protocolVersion": "2025-06-18",
  "serverInfo": {"name": "flint-chart-mcp", "version": "1.0.1"},
  "instructions": "Flint compiles one semantic chart spec (ChartAssemblyInput) into Vega-Lite, ECharts, or Chart.js..."}}

Then list the tools. There are nine:

render_chart, compile_chart, validate_chart, list_chart_types,
list_interaction_presets, get_flint_skill, list_themes, validate_board, show

That list is the whole surface, and it is deliberately small. Most chart MCP servers expose one tool per chart type — 26 or more — because every chart has a different schema. Flint has one schema (ChartAssemblyInput) spanning its chart types, so the server exposes a handful of tools that all take the same spec. show opens a live chart card in hosts with MCP App UIs (Claude Desktop); render_chart is the fallback that returns a static PNG or SVG. The server keeps nothing between calls — every tool rebuilds the chart from the spec it is given — and it renders locally, so the rows you pass never leave your machine.

Step 2: Wire it into your agent#

Registration is the standard MCP client config. In Claude Code, Claude Desktop, Cursor, or VS Code, add a server named flint:

{
  "mcpServers": {
    "flint": {
      "command": "npx",
      "args": ["-y", "flint-chart-mcp"]
    }
  }
}

In Claude Code the same registration is one command:

claude mcp add flint -- npx -y flint-chart-mcp

Restart the client, then ask it something that forces a tool call rather than a guess: List Flint's chart types for the Vega-Lite backend. The server returns a 28,350-character catalog — 39 chart types on Vega-Lite alone, each with its encoding channels and the interaction presets it supports. If the agent answers from memory instead, it has not picked up the server; the tools-list probe in Step 1 is the ground truth that the server works, and a fresh client restart is the usual fix.

Two options worth knowing before you hand the server to an agent. By default the server can read any local file your agent references in data.url — a relative path resolves against the working directory, an absolute path is read as given, and remote URLs are never fetched. For an untrusted deployment, pass --disable-file-reference and it accepts only inline data.values. And you can gate the exposed backends at deploy time with FLINT_MCP_BACKENDS=vegalite,echarts npx -y flint-chart-mcp.

Step 3: Write your first spec#

Every Flint chart is one JSON object with four parts: the data, the semantic types, the chart spec, and optional theme and interaction specs. Here is the one this guide renders, saved as chart.json. The data is real: GitHub star counts, in thousands, for the most-starred AI repositories in the November 6, 2026 GitHub AI Radar report (Caveman's count cross-checked against its repository page, 118,194 stars).

{
  "data": {"values": [
    {"Repo": "ECC", "Stars": 298.3},
    {"Repo": "hermes-agent", "Stars": 261.1},
    {"Repo": "firecrawl", "Stars": 199.2},
    {"Repo": "ComfyUI", "Stars": 145.4},
    {"Repo": "graphify", "Stars": 143.4},
    {"Repo": "MoneyPrinterTurbo", "Stars": 140.1},
    {"Repo": "caveman", "Stars": 118.2},
    {"Repo": "claude-mem", "Stars": 113.8}
  ]},
  "semantic_types": {"Repo": "Category", "Stars": "Quantity"},
  "chart_spec": {
    "chartType": "Bar Chart",
    "encodings": {"x": "Stars", "y": "Repo"}
  },
  "theme_spec": "economist",
  "interaction_spec": {"interactions": [{"type": "click-highlight"}]}
}

Read it field by field, because this is the whole mental model:

  • data.values — the rows, inline. You can also point at a local .json, .csv, or .tsv file with data.url; the server never fetches remote URLs.
  • semantic_types — what each field means. Repo is a Category, Stars is a Quantity. There are 44 registered semantic types (YearMonth, Temperature, Delta, Rank, and more), and the compiler uses them to infer parsing, scales, axes, formatting, and colors. This is the part the model usually gets wrong when it writes raw chart JSON — and the part Flint makes explicit.
  • chart_spec — the chart type (Bar Chart) and the encodings (x is Stars, y is Repo). The compiler adapts sizing, spacing, labels, and legends to the data; the same spec renders across backends.
  • theme_spec — economist, one of 14 preset themes. Geometry, typography, and color are reusable specs that apply across chart types; you can extend a preset and override only your brand color (Step 7).
  • interaction_spec — click-highlight: click a bar to emphasize it and mute the rest. One of 23 interaction presets — but not every preset fits every chart, which is what Step 4 is for.
Illustration of one document flowing into a central compiler hub that fans out to five chart panels
AI-generated illustration: one Flint spec in, a compiled chart out on any of five backends — the compiler, not the model, resolves scales, axes, and layout.

The point of the exercise is the direction of the work. Without Flint, your agent writes a 200-line Vega-Lite spec and you debug it. With Flint, your agent writes a 20-line semantic spec and the compiler does the 200 lines — deterministically, the same way every time.

Step 4: Validate before you render#

Flint's most useful habit is one most chart tools skip: it validates the spec and tells you what is wrong before anything is drawn. Call validate_chart with the spec's fields and the backend:

{"jsonrpc": "2.0", "id": 5, "method": "tools/call",
 "params": {"name": "validate_chart",
            "arguments": {"data": {"values": [...]}, "semantic_types": {"Repo": "Category", "Stars": "Quantity"},
                          "chart_spec": {"chartType": "Bar Chart", "encodings": {"x": "Stars", "y": "Repo"}},
                          "theme_spec": "economist",
                          "interaction_spec": {"interactions": [{"type": "click-highlight"}]},
                          "backend": "vegalite"}}}

Note the shape: the tool takes the spec's fields spread at the top level of arguments — data, chart_spec, theme_spec — not nested under a spec key. Passing a nested spec object fails with MCP error -32602: Required at data, Required at chart_spec, which is the mistake I made on the first attempt. The response, when the shape is right:

{"backend": "vegalite", "chartType": "Bar Chart", "valid": true,
 "warnings": [], "errors": [],
 "computedSize": {"width": 395, "height": 272}}

Valid, no warnings, and the compiler even tells you the size it computed. The validator also catches the kind of mistake that silently produces a bad chart. My first draft of this spec used inspect-index — hover to read values at an index — as the interaction. The validator returned valid: true but with a warning:

Interaction "inspect-index" requires an index axis shared by the series;
Bar Chart has none. The interaction was dropped.

That is the guardrail working: inspect-index belongs on line and area charts, where an index axis exists. A bar chart's supported interactions are a different set — click-highlight, axis-highlight, select, lasso-select, brush-x, and 16 more, 21 in total — which is exactly what list_chart_types reports per chart type. Swapping in click-highlight cleared the warning. Treat the validator as the spec's linter: run it first, read the warnings, and only then render.

Step 5: Render it#

With a clean validation, render. render_chart takes the same arguments plus a format:

{"jsonrpc": "2.0", "id": 7, "method": "tools/call",
 "params": {"name": "render_chart",
            "arguments": {"data": {"values": [...]}, "semantic_types": {"Repo": "Category", "Stars": "Quantity"},
                          "chart_spec": {"chartType": "Bar Chart", "encodings": {"x": "Stars", "y": "Repo"}},
                          "theme_spec": "economist",
                          "interaction_spec": {"interactions": [{"type": "click-highlight"}]},
                          "backend": "vegalite", "format": "svg"}}}

The SVG came back as 10,418 characters of self-contained markup, 522 by 349 pixels. Asking for format: "png" instead returned a 17,555-byte PNG with valid PNG magic bytes. Both renders happened in process, on this machine, with no network call. This is the SVG the server produced from the spec in Step 3 — the actual artifact, not a mockup:

Horizontal bar chart of GitHub stars for eight AI repositories, rendered by Flint in the Economist theme
Rendered by Flint's Vega-Lite backend from the spec in Step 3: eight repositories, values printed on the bars, Economist theme. The small red mark at the top left is in the server's own output.

Eight bars, values printed on the marks, the Economist theme's typography and grid. The chart is not beautiful by accident — the theme decided the fonts (Helvetica Neue), the grid color, and the label placement. Change one string in the spec and the whole look changes; that is Step 7.

Step 6: Compile the same spec three ways — no agent needed#

The MCP server is the convenient path, but the compiler underneath is a plain npm library, and it also ships as a CLI. All three take the same chart.json. Use the library when you are building an app; the CLI when you want a file; the MCP server when you want an agent to do it.

The library. Install it and compile in Node:

npm install flint-chart
import { assembleVegaLite } from 'flint-chart/vegalite';
import { assembleECharts } from 'flint-chart/echarts';
import { validateChart } from 'flint-chart/validate';
import { readFileSync } from 'node:fs';

const spec = JSON.parse(readFileSync('chart.json', 'utf8'));
console.log(validateChart(spec, 'vegalite'));
// { backend: 'vegalite', chartType: 'Bar Chart', valid: true,
//   warnings: [], errors: [], computedSize: { width: 395, height: 272 } }

const vl = assembleVegaLite(spec);   // a layered Vega-Lite spec, 8 data values
const ec = assembleECharts(spec);    // an ECharts option: series[0].type === 'bar'

The Vega-Lite output is a layered spec carrying the Economist theme in its config — fonts, grid colors, label sizes — and the ECharts output is a ready-to-mount option object with a bar series and a value x-axis. One spec, two backends, one function call apart. The package also exports assembleChartjs, assemblePlotly, and assembleExcel for the other backends, plus listThemePresets() (14 themes) and listInteractionPresets() (23 presets) for discovery.

The CLI. The MCP package also installs a flint-chart command that compiles a saved spec to an image with no agent and no code:

flint-chart compile chart.json --format svg
flint-chart compile chart.json --backend echarts --format png --output chart.png
cat chart.json | flint-chart compile - --format svg > chart.svg

The first command reports Wrote vegalite · svg · 522×349px → chart.svg and writes a 10,418-byte SVG — the same bytes the MCP server returned. But there is a catch, and it cost me twenty minutes: in version 1.0.1, the documented command silently does nothing when run through npx or npm's .bin symlink. It exits 0, prints nothing, and writes no file. The cause is in the CLI's entry check in dist/flint-chart.js: it compares the invoked path against its own real path, and the symlink npm creates does not resolve to it, so main() never runs. The workaround is to invoke the real file directly:

node ./node_modules/flint-chart-mcp/dist/flint-chart.js compile chart.json --format svg --output chart.svg

That is the one genuine bug I hit in this guide, and it only affects the CLI binary — the MCP server works exactly as documented through npx (Step 1 proved it). If you run the documented command and nothing happens, this is why; use the real path until a fixed release lands. The Chart.js backend, note, renders PNG only — asking it for SVG fails, because its engine has no SVG output.

Step 7: Themes and interactions#

With the pipeline working, the fun part is cheap: change strings, not code. list_themes returns the 14 shipped presets:

nyt, economist, swiss, nature, mckinsey, datawrapper, powerbi,
powerbi-light, pop, cartoon, apple, de-stijl, bbc, material

Swap theme_spec to nyt and re-validate — it validates clean — and the same eight bars come back in New York Times style: a headline that states the finding, values printed on the marks, series named at their own ends. A brand adjustment is an extension, not a rewrite:

{
  "theme_spec": {
    "extends": "economist",
    "id": "our-brand",
    "ink": { "series": { "single": "#6b3fa0" } }
  }
}

That spec — extending economist with a single purple series color — also validates clean and assembles to the same eight values. Keep the override small; the preset does the rest.

Interactions work the same way, with one rule: the preset has to fit the chart. list_interaction_presets returns all 23, each with what it does, what it needs from the chart, and the options it accepts. On this bar chart, click-highlight and brush-x (drag an interval along the x-axis to select a range of star counts) both fit; inspect-index does not, and the validator says so instead of silently drawing nothing. For a line chart with a time axis, inspect-index is the right call — hover to read values at each month. When a chart needs more than one view, validate_board checks a whole dashboard of charts — headline, layout, and the links between them — without drawing anything.

Which approach should you use?#

PathUse it whenWhat you get
MCP serverYou live in an agent chat and want charts on demandNine tools; the agent authors, validates, and renders the spec. show opens a live card in MCP App hosts.
Library (flint-chart)You are building an app or dashboardassembleVegaLite / assembleECharts / assemblePlotly / assembleChartjs / assembleExcel — one spec, five backends, plus React and vanilla JS embedding.
CLI (flint-chart compile)You want a file, no agent and no codeSVG or PNG from a saved spec JSON. Mind the 1.0.1 symlink bug in Step 6.
Excel backendThe chart belongs in a report or spreadsheetassembleExcel produces a native, editable Excel chart spec. Rendering it needs an Office.js host, so no .xlsx was produced in this guide.

If you only do one of these, do the MCP server. It is the path where Flint's design pays off most: the agent stops writing chart-library JSON it half-remembers, and starts writing a twenty-line semantic spec the compiler can check.

Troubleshooting: the failures you will actually hit#

  • The CLI exits 0 and writes nothing. You ran it through npx or node_modules/.bin — the 1.0.1 symlink bug from Step 6. Invoke the real file with node and the path.
  • MCP error -32602: Required at data, Required at chart_spec. You nested the spec under a spec key. The tools take the spec's fields spread at the top level of arguments.
  • A warning says an interaction was dropped. Read it — it names the preset and the reason, usually that the chart type has no axis the preset needs. Check list_chart_types for the presets your chart type actually supports.
  • Chart.js refuses SVG. By design: that backend renders PNG only. Ask for format: "png".
  • The agent answers from memory instead of calling the tools. Restart the client after adding the server, and ask for something only the tools can answer — list Flint's chart types or what themes ship with Flint?
  • Your CSV is not being read. data.url accepts local files only; remote URLs are never fetched. Pass the rows inline with data.values, or point at a local file.

The takeaway#

Agents are going to draw most of the charts from now on — in notebooks, in dashboards, in chat. The question is whether the chart they draw is a guess in a big schema or a checked spec in a small one. Flint 1.0 makes it the second: one semantic spec, a compiler that resolves the details, a validator that catches the mismatches, and five backends that all agree on what the chart is. Wire the MCP server into your agent, keep validate_chart in the loop, and the charts stop being the part of the agent's output you quietly fix by hand.

Frequently asked questions

Do I need an API key or an account to use Flint?

No. Both packages are free and MIT-licensed, and the MCP server renders locally in process. Your data goes from your machine to the rendering code on your machine — nothing is uploaded.

Which charting backends does one spec support?

Five: Vega-Lite, ECharts, Chart.js, Plotly, and native Excel charts. The MCP server exposes three of them (Vega-Lite, ECharts, Chart.js); the library adds Plotly and Excel. The project reports 52 chart types across the five backends; the Vega-Lite backend lists 39.

Can Flint read my spreadsheet or CSV?

Yes, as a local file: pass data.url pointing at a .json, .csv, or .tsv file, or paste the rows inline as data.values. Remote URLs are never fetched. Pass --disable-file-reference to the server to reject file references entirely.

Why did the CLI do nothing when I ran it?

Version 1.0.1 has a bug: run through npx or npm's .bin symlink, the flint-chart CLI exits 0 without running, because its entry check compares the invoked path against its real path. Run the real file directly with node ./node_modules/flint-chart-mcp/dist/flint-chart.js compile …. The MCP server is unaffected.

Does Flint replace Vega-Lite or ECharts?

No — it compiles to them. Flint is the authoring layer: you write the semantic spec, and the compiler produces the backend-native spec. If you already have Vega-Lite or ECharts code, Flint is a way to let an agent generate it reliably rather than a replacement for it.

Sources