Troubleshooting
The first render is slow
Expected. The first call initialises jsdom, imports @unovis/ts, and downloads
Inter once (~34MB into ~/.cache/unovis-ssr/fonts/). Later renders are
milliseconds. To avoid the download entirely:
UNOVIS_MCP_NO_DOWNLOAD=1 unovis-mcp # system fonts instead
UNOVIS_MCP_FONTS_DIR=/path/to/fonts unovis-mcp # your own fonts
If a client enforces a strict tool-call timeout, do one warm-up render after starting the server.
Text looks slightly wrong: clipped, trimmed, or oddly spaced
Text measurement drives label trimming, wrapping and axis margins, so the font used for measuring must match the font used for drawing.
- In SVG/PNG output, measurement uses the provisioned Inter. If you replaced
it via
UNOVIS_MCP_FONTS_DIRwith metrics that differ from the declared font stack, labels can be trimmed too eagerly or overflow. - In interactive HTML, the viewer's machine supplies the font. Text may be a few pixels different from the SVG. Nothing to fix — just don't expect them to be pixel-identical.
The client rejects the whole tool list
If a client refuses to start with something like input_schema: JSON schema is invalid. It must match JSON Schema draft 2020-12, one tool schema is invalid —
and it takes down every tool, not just its own, because clients validate the
tool list as a unit. Released versions guard against this, so if you see it,
please report it with the client name and the full error message.
A chart is blank
Work through these in order:
- Is it an interactive chart in a background tab? Browsers pause
requestAnimationFramefor hidden pages, and Unovis renders through it. The chart appears when the tab becomes visible. This also means automated screenshots of hidden pages come out empty — make the page visible first. - Are you calling
renderToSvgwithout wiringonRenderComplete? The render throws with that exact message; passctx.onRenderCompleteinto the container config. - Is the data empty, or are the field names wrong? The tools validate field names and reply with the available fields; a hand-written spec does not.
- Async layout? A
GraphwithlayoutType: 'force'needsctx.requireComponentReady()plus the component'sonRenderComplete.
"Chart rendering did not complete"
The container never reported finishing. Either onRenderComplete isn't wired
(see above), or a component threw during a frame — the error message includes any
frame errors that were captured.
Graph layouts
generate_network_graph offers force, circular, concentric and dagre,
and all four work in static output and interactive output.
ELK is the exception. It renders fine in Node through a hand-written spec:
components: [{ type: 'Graph', config: { layoutType: 'elk', layoutElkSettings: { 'elk.algorithm': 'layered' } } }]
…but it isn't a tool option, because elkjs is a 1.4MB engine loaded through a
dynamic import that the single-file widget bundle would have to inline. Offering
it would mean outputType: "html" silently producing a broken chart.
Dagre required
@unovis/graphlibrary≥ 2.2.0-3 and@unovis/dagre-layout≥ 0.8.8-3. Earlier releases shipped extensionless ESM imports that no standards-compliant Node loader could resolve.
Graph charts time out under the tsx loader
Running your script with tsx breaks the dynamic imports inside Unovis's
graph layouts: the layout promise never settles, so a graph render times out
after 20 seconds with "component layout never completed". Every other chart
type is unaffected. Compile first and run plain Node (vitest also works).
PNG has a transparent background
Pass one. CSS background-color isn't an SVG rendering attribute, so rasterizers
ignore it:
await svgToPng(svg, { width, scale: 2, background: themeBackground('dark') })
The png output type does this for you.
Interactive output doesn't render in my client
outputType: "interactive" depends on the MCP Apps extension
(io.modelcontextprotocol/ui) — official since protocol revision 2026-07-28,
but host adoption is still uneven. Use outputType: "html" — it works
everywhere because it's just a file.
Large datasets
Data arrives inside the tool call, so it passes through the model's context. Hundreds of rows are comfortable; tens of thousands are wasteful; millions are the wrong tool — aggregate first, or use the library directly (Programmatic use) where data never touches a model.
Charts touch the image edges / I want no padding
Static output frames the chart with 16px sides and bottom, 12px top. Via the library you can change it:
await renderToSvg({ width: 800, height: 400, padding: { top: 0, right: 0, bottom: 0, left: 0 } }, build)
Multiple charts in one HTML document collide
They shouldn't — ids are rewritten with a per-render prefix specifically to make
that safe. If you need byte-stable output (snapshot tests), pass a constant
idPrefix.
Something renders differently than in the browser
Report it. The headless environment reimplements browser geometry, so a mismatch is a real bug in this package. Useful details: the chart spec, the output, and what you expected.