Matplotlib MCP Server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Matplotlib MCP Servercreate a bar chart of monthly sales: Jan 10, Feb 15, Mar 8"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Matplotlib MCP Server
A stateless MCP server that renders Matplotlib charts from data supplied with each call. It exposes exactly two tools: list_chart_types and create_chart.
Install and run
Requires Python 3.11 or newer. Install with pip install ., then set an API key and run:
export MCP_API_KEY='replace-with-a-long-random-secret'
matplotlib-mcpThe server listens on http://127.0.0.1:8000/mcp by default. Set MCP_HOST and MCP_PORT to change the bind address and port. Set MCP_ALLOWED_HOSTS to a comma-separated list of accepted HTTP Host header values when clients connect through another hostname or reverse proxy. Put it behind HTTPS for remote clients. The proxy must forward Authorization and should disable request-body logs and disk buffering. Disable swap and core dumps where the deployment handles sensitive data. The server itself does not write chart data or images to disk and disables Uvicorn access logs.
Application access logs follow a FastAPI/Uvicorn style: UTC timestamp and level, logger name, client address, HTTP method/path/version, status, tool/protocol action, and duration. They do not include request bodies, chart labels, data, or API keys. Behind a reverse proxy, the client address is the proxy unless it terminates the connection directly at the application host.
Every HTTP request must include Authorization: Bearer <MCP_API_KEY>. Keep the key at least 16 characters long. LOG_LEVEL controls Uvicorn's non-access logs.
sh start.sh uses the same environment settings and refuses to start without a key. Keep local secrets in environment variables; .env files are ignored by Git but are not automatically loaded.
Related MCP server: Datawrapper MCP
Internal MCP clients
Use a client that supports Streamable HTTP and MCP image content. Configure the /mcp URL reachable from the client backend and the bearer token there. A container's localhost points to that container; use the service's network hostname when the client runs in another container, and add that hostname and port to MCP_ALLOWED_HOSTS.
One MCP process supervises one renderer subprocess. Rendering is serialized, while protocol requests remain responsive. A renderer deadline, crash, or memory failure affects that chart; the supervisor reaps the process and starts a replacement for subsequent charts. Waiting HTTP charts retain serialized request bodies and are bounded by both count and aggregate buffered bytes. The shared API key authenticates the client service; there are no per-user accounts or quotas. Run the sample client from the same network as your client backend to check connectivity, authentication, and PNG output before connecting your chat interface.
Docker
The included Compose configuration provides defaults to get you up and running quickly. From the repository root, generate a local API key and start the service:
export MCP_API_KEY="$(openssl rand -hex 32)"
docker compose up -d --buildThe supplied defaults are:
Setting | Compose default |
Image | Built from the local repository ( |
Restart policy |
|
Process management |
|
Host port |
|
Server bind address |
|
API key | Required through |
Allowed HTTP hosts |
|
Memory | 512 MB (512,000,000 bytes) for the server and renderer together, with no additional swap |
Core dumps | Disabled ( |
Logs | Docker |
These are starting defaults; adjust compose.yaml for your deployment. The
service uses one application process and the default chart and renderer limits
documented below. The endpoint is http://localhost:8000/mcp. Configure your
client with the generated key as its bearer token.
Compose also loads MCP_API_KEY from a local, Git-ignored .env file if you
prefer to retain it across shell sessions; keep it at least 16 characters long.
MCP_ALLOWED_HOSTS can be overridden through the environment or .env; it
defaults to localhost:8000,127.0.0.1:8000,matplotlib-mcp:8000. Clients on the
Compose network can use http://matplotlib-mcp:8000/mcp. For remote access, use
an HTTPS reverse proxy and add its public hostname to MCP_ALLOWED_HOSTS.
Use docker compose logs -f to view logs and docker compose down to stop and
remove the service.
Alternatively, run the image directly:
docker build -t matplotlib-mcp .
export MCP_API_KEY='replace-with-a-long-random-secret'
docker run --rm -p 127.0.0.1:8000:8000 -e MCP_API_KEY matplotlib-mcpConnect to http://localhost:8000/mcp with the same bearer token. The image
runs as a non-root user and listens on 0.0.0.0:8000 inside the container.
Pass other settings with -e, including MCP_ALLOWED_HOSTS when accessing
the server through a different hostname or reverse proxy. Supply the API key
at runtime; do not include it in the image.
Corporate themes and fonts
The server optionally loads /etc/matplotlib-mcp/theme.yaml once at startup.
Set MCP_THEME_PATH to select another YAML file. An absent default file keeps
the existing appearance; an explicitly selected missing file or an invalid
theme prevents startup. Theme and font changes require a container restart
(or a server restart when running locally). Keep mounted theme files and fonts
unchanged while the service runs: replacement renderer processes read them again.
The example theme uses DejaVu Sans, which ships
with the installed Matplotlib package in its mpl-data/fonts/ttf/ directory.
Those default fonts are already available locally and in the Docker image;
you do not need to copy them into this repository.
Custom corporate fonts are not included. Supply your own TTF/OTF files beside
the theme YAML, using this layout on the host (create examples/theme/fonts/
if needed):
examples/theme/
├── theme.yaml
└── fonts/
├── CorporateSans-Regular.ttf
└── CorporateSans-Bold.ttfMount that entire theme directory read-only:
docker run --rm -p 127.0.0.1:8000:8000 -e MCP_API_KEY \
--mount type=bind,src="$(pwd)/examples/theme",dst=/etc/matplotlib-mcp,readonly \
matplotlib-mcpWith this mount, examples/theme/fonts/CorporateSans-Regular.ttf on the host
is available at /etc/matplotlib-mcp/fonts/CorporateSans-Regular.ttf inside
the container. In theme.yaml, list paths relative to that YAML file and set
the font's internal family name (which can differ from its filename):
colors:
palette: ["#003B71", "#00A6A6", "#F4B400"]
figure_background: "#FFFFFF"
axes_background: "#FFFFFF"
text: "#243746"
axes: "#64748B"
grid: "#E2E8F0"
font:
family: "Corporate Sans"
size: 11
weight: normal
style: normal
files:
- fonts/CorporateSans-Regular.ttf
- fonts/CorporateSans-Bold.ttfFor a local server without Docker, the same layout works: set
MCP_THEME_PATH=examples/theme/theme.yaml before starting matplotlib-mcp.
Font paths still resolve relative to the YAML file's directory.
All settings are optional; omitted settings retain Matplotlib defaults.
palette must be a non-empty list of Matplotlib color strings; quote hex colors
in YAML. It supplies the default series color cycle, leaving scalar-data
colormaps unchanged. Surface colors style figure/axes backgrounds; text styles
text, axis labels, and ticks; axes styles borders; grid styles grid lines
when enabled by chart calls. Legends and colorbars inherit the corresponding defaults.
Font size must be finite and positive. Weight accepts standard Matplotlib names
(such as normal, medium, or bold) or a number from 0 to 1000; style accepts
normal, italic, or oblique. Font files resolve relative to the YAML file
and must be readable by container UID 10001. Supply separate files for desired
weights/styles. Without files, family must identify an installed font or a
Matplotlib generic family such as sans-serif. Unknown settings, invalid colors,
unreadable fonts, or an unavailable family prevent startup. Arbitrary rcParams
are not accepted.
Every chart uses the installed theme by default. Set "theme": false inside
config to use the standard appearance. Explicit options such as
figure.facecolor, kwargs.color, kwargs.colors, kwargs.cmap, or a label's
fontdict override their corresponding defaults. Use native options supported
by the particular method; for example:
{"method": "errorbar", "args": [[1, 2], [3, 4]], "kwargs": {"color": "#003B71"}}list_chart_types reports the active theme's colors/font settings, opt-out
instructions, and each method's color options. It does not expose theme or font
file paths. Chart requests cannot load font files or supply a theme file.
Tools
list_chart_types returns every exposed Matplotlib plotting method, its expected input shape, accepted keyword options, an example, the shared configuration format, all setup methods, and effective resource limits. Pass method="scatter" to inspect one chart method.
create_chart accepts a config and one data object. Config supports optional theme (boolean, default true), figure, and required axes. Its configuration uses Matplotlib method names and their args/kwargs:
{
"config": {
"figure": {"figsize": [8, 5], "dpi": 100, "suptitle": "Quarterly sales"},
"axes": [{
"subplot": [1, 1, 1],
"calls": [
{"method": "bar", "args": [{"$data": "quarter"}, {"$data": "sales"}], "kwargs": {"color": "steelblue", "label": "Sales"}},
{"method": "set_title", "args": ["Sales by quarter"]},
{"method": "set_ylabel", "args": ["Revenue"]},
{"method": "legend"}
]
}]
},
"data": {"format": "columns", "values": {"quarter": ["Q1", "Q2", "Q3"], "sales": [12, 18, 9]}}
}The example creates a 20.32 × 12.7 cm figure. To specify a size in centimetres, divide each dimension by 2.54 for Matplotlib's figsize values.
Data may be named vectors/matrices ({"format":"columns","values":...}), JSON objects ({"format":"rows","rows":[...]}), or CSV text ({"format":"csv","csv":"..."}). Use an exact {"$data":"column"} object anywhere in call args or kwargs to refer to a named column. Matrices are useful for imshow, contour, and grid based methods. Every call supplies fresh data; the server keeps no chart or dataset state.
Named columns may have different lengths, for example histogram edges and values or independent grid coordinates. Plot arguments must still have compatible shapes for the selected Matplotlib method. The returned rows metadata is the largest first dimension among supplied columns. Setup methods accept required arguments by position or by their documented keyword, such as {"method":"set_title","kwargs":{"label":"Sales"}}.
For example, a stairs chart uses one more edge than value:
{
"config": {"axes": [{"calls": [{"method": "stairs", "args": [{"$data": "values"}, {"$data": "edges"}]}]}]},
"data": {"format": "columns", "values": {"values": [3, 2], "edges": [0, 1, 2]}}
}The renderer dispatches only registered 2D Axes methods and setup methods. It accepts ordinary JSON values only; it does not evaluate Python, import modules, resolve object attributes, or accept arbitrary Matplotlib method names. TeX and math text are disabled. Nested usetex, parse_math, fontproperties, and fname options are rejected; use fontdict with ordinary family, size, weight, and color settings instead. Callable options and artist objects cannot be supplied through JSON. PNG bytes are returned as MCP image content, with dimensions and request metadata in a companion text content block.
Limits
Each limit is configured by the corresponding environment variable below. Positive values enforce the limit, 0 disables that one limit, and negative or non-integer values prevent startup. Structural validation and authentication always remain enabled.
Environment variable | Default |
| 4194304 |
| 1048576 |
| 10000 |
| 50 |
| 250000 |
| 500 |
| 1600 each |
| 1000000 |
| 10000 |
| 250000 |
| 5000000 |
| 16 |
| 128 |
| 200 each |
Figure defaults are 20.32 × 12.7 cm / 8 x 5 inch at 100 DPI. Dimensions must remain finite and positive, and configured pixel limits apply before rendering.
MAX_AXES also bounds the total subplot grid cells, including unused cells. MAX_ARRAY_ELEMENTS bounds data and configuration elements separately, streamplot density grid cells, and violin sampling points. MAX_BINS applies to histogram counts, explicit bin edges, each hist2d axis, and hexbin grid dimensions. With that limit enabled, histogram bins must be an explicit count or edge vector; automatic strategies such as "auto" are rejected. MAX_CONTOUR_LEVELS applies to counts and explicit level vectors. These checks also cover positional arguments and resolved $data references.
Charts are preflighted before creating figures or artists. Repeated references share one resolved array, but each call is charged against the chart-wide budgets. Estimates conservatively cover broadcasting, artist counts, geometry, contour levels, histogram grids, vector fields, and violin sample/evaluation combinations. These are admission estimates, not exact measurements of Matplotlib internals. A chart within the estimates may still fail its enforced renderer allocation limit or deadline.
Barb increments must be finite and positive; vector magnitudes and increments contribute to generated geometry. Pie autopct accepts one numeric percent-format conversion (d, i, u, e, E, f, F, g, or G) and escaped percent signs. Field width, precision, and estimated expanded length are checked before formatting. The formatting safety ceiling remains 500 characters when MAX_LABEL_CHARS=0.
The total pixel budget applies in addition to width and height. Default 800 × 500 charts fit; 1600 × 1600 charts are rejected. A 1600 × 625 chart fits the default pixel budget, although complex content may still exceed other limits.
Renderer supervision
Production requires Linux with RLIMIT_AS. These settings must be positive integers; zero cannot disable supervision. Their effective values are included in list_chart_types under rendering.
Environment variable | Default | Meaning |
| 50 | Maximum waiting chart/large-body requests (plus one active request) |
| 33554432 | Aggregate retained HTTP request-body bytes, including partial bodies and active requests |
| 30 | Maximum wait before admission |
| 10 | Total deadline to receive the HTTP body after authentication and reader admission; partial chunks do not reset it |
| 10 | Deadline including renderer startup, validation, rendering, and IPC |
| 268435456 | Renderer virtual address-space cap, applied before importing plotting libraries |
| 50 | Completed requests before replacing the renderer |
| 250000 | HTTP JSON structural-token budget, checked before decoding |
| 64 | HTTP JSON nesting limit, checked before decoding |
OpenBLAS, OpenMP, MKL, and NumExpr thread counts are set to one in the renderer environment. Memory allocation failures, native process exits, and deadlines return safe MCP tool errors. Cancellation kills and reaps an active renderer; cancelled waiting requests release their queue capacity. Disconnect detection depends on transport delivery, so the deadline also bounds work after an undetected disconnect. Shutdown reaps the renderer. PNG responses have a separate 12 MiB encoded IPC ceiling.
Queue or body-buffer saturation returns HTTP 429, and queue expiration returns HTTP 503, each with Retry-After: 1. These failures occur before MCP tool dispatch. Clients must check HTTP errors as well as MCP isError, allow more than 40 seconds for the configured maximum queue-plus-render interval, and retry transient busy errors with backoff. Reducing chart complexity is appropriate for validation, memory, or render-deadline errors. Small ping and catalog calls bypass the chart queue. All bodies over 8 KiB share admission before audit or MCP JSON parsing, so large control requests cannot retain concurrent decoded trees. A linear scan bounds JSON containers, separators, string starts, and nesting before either decoder allocates objects; exceeding that structural budget returns HTTP 413. Quoted contents and escaped quotes are handled without constructing decoded strings. Total in-flight requests are bounded to RENDER_QUEUE_SIZE + 8, with at most RENDER_QUEUE_SIZE + 4 simultaneous body readers.
Incomplete uploads return HTTP 408 when RENDER_BODY_SECONDS expires, releasing reader capacity and retained bytes before MCP dispatch. This total upload deadline also bounds clients that keep sending partial chunks. Budget client timeouts for upload time in addition to queue and rendering time. Configure equivalent request-body deadlines on a reverse proxy, if used.
HTTP request bodies must use valid UTF-8 JSON; a UTF-8 BOM is accepted. Invalid UTF-8, raw NUL bytes, and UTF-16/32 bodies (with or without a BOM) return HTTP 400 before structural scanning, audit parsing, or MCP decoding. Charset headers do not enable other encodings. Unicode strings and JSON-escaped NUL characters (\u0000) remain supported. Encoding validation follows the same admission rules as the structural scan, including queue admission for bodies over 8 KiB.
Use one application process with the 500 MB deployment profile. Increasing process count multiplies renderer memory and admission capacity. Changing memory, request-body, queue, or complexity settings requires repeating the container load test; RLIMIT_AS bounds the child, while the container limit includes the parent, queued bodies, shared libraries, and other charged memory. Keep the chart limits enabled.
Development
pip install -e '.[dev]'
pytestWith the server running and MCP_API_KEY set to the same value in your client terminal, run the sample client:
python examples/test_client.pyUse --url to select another MCP endpoint or --output to choose where the client saves its PNG preview.
Use the repeatable load client against a disposable container with MCP_API_KEY set in your client environment:
python examples/load_test.py --url http://127.0.0.1:18973/mcp --sessions 50 --charts 10
python examples/load_test.py --url http://127.0.0.1:18973/mcp --mode rate --rate 1.74 --duration 120
python examples/load_test.py --url http://127.0.0.1:18973/mcp --width 1600 --height 625
python examples/load_test.py --url http://127.0.0.1:18973/mcp --mode probes--mode probes submits all four historical failure payloads and checks for MCP rejection followed by working pings and charts. Other modes require every chart to succeed and verify PNG dimensions and repeatable hashes. Use --output /tmp/load-results.json to retain individual timings. The client does not measure container memory; inspect cgroup memory.peak and memory.events separately. See LOADTEST.md for the measured container results.
This server cannot be deployed
Maintenance
Related MCP Connectors
Create, inspect, manage, and render charts and data visualizations as SVG/PNG or interactive embeds.
Turn rows into bar, line, pie and scatter chart images plus an optional PDF or HTML report.
Render HTML and CSS to PNG images over HTTP. Send HTML and CSS and get a PNG back.
Create choropleth, category and pin maps of countries, states, counties and ZIP codes as images.
Related MCP Servers
- AlicenseBqualityDmaintenanceEnables AI clients to generate over 26 chart types via MCP, automatically starting a private rendering service.271MIT
- AlicenseNot gradedqualityDmaintenanceEnables creating, managing, and publishing Datawrapper charts.MIT
- FlicenseNot gradedqualityDmaintenanceConnects to SQL databases to execute queries and return results as chart images (PNG) visible in MCP clients.-
- AlicenseBqualityDmaintenanceEnables AI agents to render branded charts as inline images and persistent hosted URLs, supporting explicit chart types and automatic chart suggestion from data.234 npmMIT