Agents produce terrible charts. They hallucinate axis labels, guess at scales, and emit verbose JSON that breaks when data changes. Microsoft Research’s Flint solves this by introducing a semantic intermediate language that compiles to five different charting backends without forcing the agent to know about tick marks or padding.
The project hit 2,398 stars in days and includes a Model Context Protocol (MCP) server, which means you can wire it into Claude Desktop or any MCP-compatible agent runtime. The core insight: separate what the chart means from how it renders.
The Problem Flint Solves
Traditional charting libraries require explicit configuration of scales, axes, legends, spacing, and layout. When an LLM generates a Vega-Lite or ECharts spec, it must:
- Infer data types from column names or sample values
- Choose appropriate scales (linear, log, time)
- Set axis domains and tick counts
- Calculate layout dimensions based on label length
- Handle edge cases like missing data or outliers
This bloats the context window and introduces failure modes. The agent either over-specifies (brittle) or under-specifies (broken).
Flint replaces this with a semantic layer. Instead of telling the backend “use a linear scale from 0 to 100,” you tell Flint “this column is a Temperature.” The compiler derives the rest.
Architecture: Semantic Types to Backend Configs
Flint’s compilation pipeline has three stages:
- Semantic inference: Parse the spec and data, assign semantic types
- Layout derivation: Calculate scales, axes, spacing from cardinality and types
- Backend emission: Generate native Vega-Lite, ECharts, Chart.js, Plotly, or Excel config
Semantic Type System
Flint ships with 70+ semantic types organized into categories:
| Category | Examples | Inference Signals |
|---|---|---|
| Quantitative | Price, Temperature, Percentage | Unit suffixes, value ranges |
| Temporal | Date, Year, Month | ISO strings, numeric year ranges |
| Nominal | Country, City, Category | String cardinality, known entity lists |
| Ordinal | Rank, Rating, Priority | Ordered discrete values |
The compiler uses these types to:
- Choose scale types (linear for
Temperature, band forCountry) - Format labels (
$1,234forPrice,23°CforTemperature) - Set default color palettes (diverging for
Sentiment, sequential forRank) - Derive axis titles from semantic names
Multi-Backend Compilation
A single Flint spec compiles to five backends. Here’s the flow:
import { compile } from 'flint-chart';
const spec = {
data: [
{ country: 'USA', gdp: 21427 },
{ country: 'China', gdp: 14343 }
],
mark: 'bar',
encoding: {
x: { field: 'country', type: 'Country' },
y: { field: 'gdp', type: 'Price' }
}
};
// Compile to different backends
const vegaLite = compile(spec, { backend: 'vega-lite' });
const echarts = compile(spec, { backend: 'echarts' });
const chartjs = compile(spec, { backend: 'chartjs' });
const plotly = compile(spec, { backend: 'plotly' });
const excel = compile(spec, { backend: 'excel' });
Each backend has a translation layer that maps Flint’s semantic primitives to native config objects. The compiler maintains a registry of backend-specific quirks:
- Vega-Lite uses
encoding.x.scale.domainfor axis ranges - ECharts uses
xAxis.minandxAxis.max - Chart.js uses
scales.x.minandscales.x.max
Flint abstracts these into a single scale concept during the semantic phase, then emits the correct syntax per backend.
MCP Server Integration
The flint-chart-mcp package exposes three tools:
create_chart: Accept a Flint spec, return a rendered image or JSONvalidate_spec: Check semantic types and data compatibilitylist_semantic_types: Return available types for agent discovery
Server-Side Validation
The MCP server validates specs before compilation. This catches:
- Mismatched semantic types (e.g.,
Countryfield with numeric data) - Invalid mark types for the encoding (e.g.,
linechart with two nominal fields) - Missing required fields
Validation happens server-side to avoid round-trip latency. The agent sends a spec, the server returns either a rendered chart or a structured error with suggestions.
Agent Workflow
- Agent receives user request: “Show me GDP by country”
- Agent calls
list_semantic_typesto discoverCountryandPricetypes - Agent constructs minimal Flint spec with those types
- Agent calls
create_chartwith spec and data - Server validates, compiles to requested backend, returns image or embeddable JSON
The agent never sees Vega-Lite syntax or ECharts options. It only manipulates semantic types and mark names.
Layout Derivation Logic
Flint’s compiler calculates layout from data cardinality and semantic types. Key heuristics:
- Axis label rotation: If x-axis has >8 categories or max label length >12 characters, rotate 45 degrees
- Chart dimensions: Base width on category count (min 400px, +40px per category up to 1200px)
- Legend placement: Right side if <6 categories, bottom if >6
- Tick density: Quantitative axes get 5-10 ticks depending on range and precision
These rules are encoded in a layout engine that runs after semantic inference but before backend emission. You can override them in the spec:
const spec = {
// ... data and encoding
config: {
layout: {
width: 800,
height: 400,
axisTitleFontSize: 14
}
}
};
Overrides merge with derived values, so you only specify what differs from the defaults.
Failure Modes and Observability
Flint’s compilation can fail at three points:
- Semantic inference failure: Data doesn’t match declared type (e.g., strings in a
Temperaturefield) - Layout constraint violation: Derived dimensions exceed backend limits (e.g., ECharts max 10,000px width)
- Backend emission error: Semantic construct has no equivalent in target backend
The compiler returns structured errors with:
- Failure stage (inference, layout, emission)
- Affected field or encoding
- Suggested fix (e.g., “Use
Nominalinstead ofCountryfor non-geographic categories”)
For production deployments, instrument these error paths:
- Log inference failures to catch data quality issues
- Alert on layout violations (indicates unusual data cardinality)
- Track backend emission errors by target (some backends support fewer mark types)
Deployment Shape
Flint runs in three modes:
- Client-side: Compile in the browser, render with Vega-Lite or Chart.js
- Server-side: Compile in Node.js, return static images or JSON
- MCP server: Long-running process that agents call via stdio or HTTP
For agent workflows, the MCP server is the natural choice. It maintains a warm compiler instance and caches semantic type metadata. Startup time is ~50ms, compilation is 5-20ms depending on data size.
If you’re embedding Flint in a web app, client-side compilation keeps the server stateless. The tradeoff: you ship the compiler (~120KB gzipped) and one backend library (~200KB for Vega-Lite, ~800KB for ECharts).
Security Boundaries
Flint specs are JSON, so the usual injection risks apply:
- Data injection: User-supplied data can contain script tags if rendered in HTML without escaping
- Spec injection: Malicious specs can specify extremely large dimensions or nested data structures
The MCP server mitigates this by:
- Validating data types before compilation
- Capping chart dimensions (max 2000x2000px)
- Rejecting specs with >100 fields or >10,000 data points
If you expose Flint compilation to untrusted input, add:
- Content Security Policy headers to block inline scripts
- Resource limits on data size and compilation time
- Output sanitization if embedding in HTML
When to Use Flint
Use Flint when:
- Agents generate charts and you want consistent output without verbose specs
- You need to support multiple charting backends from a single spec
- You want semantic types to drive formatting and layout automatically
- You’re building a data analysis agent that needs reliable visualization
Avoid Flint when:
- You need pixel-perfect control over every visual element
- Your charts require custom interactions or animations not covered by semantic types
- You’re working with domain-specific visualizations (network graphs, geographic projections) that don’t map to Flint’s mark types
- Your backend is not one of the five supported libraries
Technical Verdict
Flint is infrastructure for agent-generated visualizations. It solves the context bloat problem by moving rendering decisions into a compiler that understands semantic types. The MCP server makes it trivial to wire into any agent runtime.
The multi-backend compilation is the real win. You can switch from Vega-Lite to ECharts without rewriting agent prompts or specs. The tradeoff is less control: if you need custom scales or interactions, you’ll drop down to native backend config.
For production agent systems that generate charts, Flint is the most practical option available. It’s narrow, well-scoped, and solves a real problem without introducing orchestration complexity.