Repository navigation
MCP Server
MatPlotLibNet.Mcp is a Model Context Protocol server. It is a .NET tool that an
MCP host (Claude Code, Claude Desktop, VS Code) starts over stdio. Through it, a model can render this library's
charts and look at the result.
The package is a .NET tool. The .NET 10 SDK resolves and runs it, so there is nothing to install by hand.
{
"servers": {
"MatPlotLibNet.Mcp": {
"type": "stdio",
"command": "dnx",
"args": ["MatPlotLibNet.Mcp", "--yes"]
}
}
}For Claude Code, run claude mcp add matplotlibnet -- dnx MatPlotLibNet.Mcp --yes.
The server is listed in the official MCP Registry under the name
io.github.xkqg/matplotlibnet, so a host that resolves servers by name finds it there. The NuGet package id
stays MatPlotLibNet.Mcp.
There is one optional setting, MATPLOTLIBNET_MCP_OUTPUT_ROOT. It sets the only directory save_chart may
write under. It defaults to the system temp directory.
| tool | what it does |
|---|---|
render_chart(spec, format) |
renders the spec and returns a PNG the model can see, with a text summary beside it |
save_chart(spec, path, format, overwrite) |
writes PNG, SVG or PDF to a file and returns the path it wrote |
chart_data_table(spec) |
returns the chart's data twice: markdown tables to read, and the same values as structured content to compute with |
list_chart_types() |
lists the chart types a spec may name |
describe_chart_schema(seriesType) |
describes the fields of a spec, or of one chart type, with a worked example |
SVG and PDF are not returned inline, because a single SVG of a normal chart is tens of thousands of characters in
the model's context. save_chart writes them to a file instead.
Each tool also tells the host what it does to the machine it runs on, which is what a host uses to decide whether
to run it without asking. Four of the five change nothing and are marked read-only. save_chart writes a file,
and with overwrite: true it replaces one, so it is marked as changing things and as able to destroy something.
None of the five reaches out to the internet or to anything outside its own arguments. Left unsaid, the protocol
assumes the opposite on both counts, so a host would warn about a tool that only draws a picture.
A spec is the library's own figure JSON: the same document that figure.ToJson() writes and
ChartSerializer.FromJson reads. Because there is one definition of a chart, an agent can ask for anything the
library draws.
{
"width": 800,
"height": 600,
"title": "Revenue",
"subPlots": [
{
"xAxis": { "label": "Quarter" },
"yAxis": { "label": "€M" },
"series": [
{ "type": "line", "xData": [1, 2, 3, 4], "yData": [12, 18, 15, 22], "label": "2026", "color": "steelblue" }
]
}
]
}The same JSON in C#:
using MatPlotLibNet;
using MatPlotLibNet.Styling;
string spec = Plt.Create()
.WithSize(800, 600)
.WithTitle("Revenue")
.AddSubPlot(1, 1, 1, ax => ax
.SetXLabel("Quarter")
.SetYLabel("€M")
.Plot([1, 2, 3, 4], [12, 18, 15, 22], s => { s.Label = "2026"; s.Color = Color.FromName("steelblue"); }))
.Build()
.ToJson(indented: true);That is the fastest way to learn the format. Build the chart you want with the fluent API, print its JSON, and give that JSON to the model.
FromJson is a round-trip reader for the library's own writer. It drops an unknown series type, skips an unknown
property and ignores a misspelled enum value, so that a newer document still loads in an older build. When a model
writes the document, that lenience turns every typo into a blank chart that is reported as a success. So the server
validates the spec first, and the error message names the field:
| what the model wrote | what it gets back |
|---|---|
"type": "lien" |
'lien' at $.subPlots[0].series[0].type is not a chart type this server lists — did you mean 'line'? |
"type": "Line" |
the same message, with 'line' as the suggestion; the serializer's lookup is case-sensitive |
"x": [...] |
Unknown field 'x' at $.subPlots[0].series[0]. describe_chart_schema lists the fields a spec accepts. |
"scale": "logarithmic" |
'logarithmic' … is not an accepted value; accepted: Linear, Log, SymLog, Logit, Date. |
"color": "reddish" |
'reddish' … is not a colour the library knows: use a CSS4 colour name or #RRGGBB. |
xData of 3, yData of 1 |
The series at $.subPlots[0].series[0] has 3 'xData' values and 1 'yData' values; they must match. |
no width
|
rendered at the library default, 800×600 |
"width": 16000 |
'width' must be between 1 and 4000 … the picture goes into a context window. |
Colour names go through the library's own CSS4 table, and #f00 is expanded, so a model may write "red",
"CornflowerBlue" or #6495ED.
render_chart returns two blocks: the PNG, and a summary of what was drawn.
"Revenue" — 800×600, 1 subplot, 1 series
· line "2026", 4 points, x 1…4, y 12…22
The ranges come from the same ComputeDataRange call the renderer makes to place the axes, so the text and the
picture always agree. When a count does not apply, it is left out rather than guessed: a pie has slices, not
points.
chart_data_table gives the model the values themselves, which it cannot get from the PNG. A model can see that a
line rises, but it cannot read the value 18.4 from the picture. The tool returns the figure's own data tables as
markdown, one table per group of series that share an x, captioned with the chart's name:
**Revenue**
| Quarter | 2026 |
| --- | --- |
| 1 | 12 |
| 2 | 18 |
| 3 | 15 |
The tables are the same figure.ToDataTables() output that every other host serves, so the numbers a model reads
and the table a screen reader reads are the same data. The same tables come back a second time beside the markdown, as
structured content with an output schema, so a model that wants to add a column up does not have to parse a pipe
table back into numbers first:
{
"tables": [
{
"caption": "Revenue",
"columns": [
{ "header": "Quarter", "kind": "number", "axis": "x" },
{ "header": "2026", "kind": "number", "axis": "y" }
],
"rows": [[1, 12], [2, 18], [3, 15]]
}
]
}A column says what it holds — number, date or text — and which axis its values are positions on. The two
forms differ deliberately in one place: markdown is for reading, so a date is spelled the way a person reads it,
while the structured form writes it out in full as 2026-09-12T12:00:00.000, which sorts correctly as text. A chart with more rows than the server's MaxTableRows
ceiling is refused, because a table is text and text takes up the model's context. The message gives the row
count, the ceiling and the way around it: call save_chart, then read the file.
The SVG this server writes carries glyph outlines, not <text>. The PNG backend is loaded in the same
process, and it installs a glyph-path provider, so text is drawn with the library's own embedded font everywhere.
The SVG is self-contained and identical to the PNG, but the text in it is not machine-readable.
Seven chart types are not available through the spec. The JSON reader for sankey, sunburst, treemap,
polarbar, polarline, polarscatter and treegrid builds a fixed placeholder instead of reading the document.
So the server refuses these types by name instead of drawing the placeholder's fixed data. list_chart_types
names them and gives the reason.
See also: Package Map · Getting Started · the same page in the cookbook
MatPlotLibNet 1.19.0
- Home
- Getting Started
- Fluent Cheatsheet
- Examples — each release in code
-
Package Map
- MatPlotLibNet — the core package
- Chart Types
- Styling
- Advanced
- Streaming
- Bidirectional SignalR
- Interactive Controls
- MCP Server
- Verso
- Notebooks
- Accessibility
- International Text
- Benchmarks
- Roadmap
- Contributing