Skip to main content
Glama
waqasdaar

d3-visualization-mcp

by waqasdaar

Universal D3 Visualization MCP Server

d3-visualization-mcp is a TypeScript Model Context Protocol server. It lets AI agents analyze data, plan visualizations from natural-language requests, and render, validate, refine, compose and export them with D3.js v7.9.0. The output is standalone interactive HTML, SVG, PNG, PDF, JSON specs and CSV.

The verification status at the bottom of this file comes from real runs of npm run verify (see reports/verification-report.md).

Contents

Related MCP server: Viz MCP Server

Architecture

flowchart TB
  user(["πŸ‘€ User<br/>asks in plain language:<br/>'dashboard of latency by site<br/>from these 3 files'"])

  subgraph CLIENT["AI client (VS Code Copilot, OpenChamber, Claude, …)"]
    llm["LLM agent<br/>picks a tool and fills its arguments"]
  end

  subgraph SERVER["d3-visualization-mcp server"]
    direction TB
    transport["Transport<br/>stdio (default) Β· Streamable HTTP (--http, token, Host/Origin checks)"]
    tools["MCP tools (17) Β· resources Β· prompt<br/>Zod-validated input, structured results, actionable errors"]

    subgraph CORE["Core pipeline"]
      direction LR
      data["1 Β· Data<br/>ingest any format<br/>(JSON, CSV, Excel, YAML,<br/>GeoJSON, graphs, files)<br/>+ profile fields"]
      link["2 Β· Linking<br/>find shared keys,<br/>ask when ambiguous,<br/>join datasets"]
      plan["3 Β· Planning<br/>recommend charts,<br/>build spec v1.0,<br/>refine, KPIs, findings"]
      render["4 Β· Rendering<br/>D3 plugins draw SVG<br/>in a worker pool (jsdom)"]
      validate["5 Β· Validation<br/>structure, safety,<br/>accessibility checks"]
      export["6 Β· Export<br/>standalone HTML (inline d3, CSP)<br/>SVG Β· JSON Β· CSV<br/>PNG/PDF via headless Chromium"]
      data --> link --> plan --> render --> validate --> export
    end

    store[("Artifact store<br/>sandboxed folder, TTL<br/>datasets Β· specs Β· files")]
  end

  files[("Local data files<br/>only inside D3MCP_DATA_DIRS")]
  browser["🌐 Browser opens chart.html / dashboard.html<br/>tooltips · zoom · brush · drag · search ·<br/>click-to-select across charts · detail panel"]

  user -- "1. prompt" --> llm
  llm -- "2. tool call (JSON-RPC)" --> transport --> tools --> data
  files -. "read-only" .-> data
  tools <-. "elicitation: 'which field links A to B?'" .-> llm
  export --> store
  store -- "3. result: summary, spec, findings, file paths" --> tools
  tools --> transport --> llm
  llm -- "4. answer + link to the file" --> user
  user -- "5. opens" --> browser
  store -. "HTML file" .-> browser

How a request flows

  1. You ask the AI assistant for a chart or dashboard and name your data: pasted rows, a file path, a spreadsheet, a network or a map.

  2. The assistant calls one of the server's tools, such as d3_visualize, d3_create_dashboard or d3_link_datasets. The server checks the arguments against a schema.

  3. The core pipeline runs:

    • Data: the input is parsed into a dataset and each field is profiled (numbers, dates, categories, outliers).

    • Linking: with several datasets, the server finds the keys they share and joins them. If a link is ambiguous, it asks you through the assistant.

    • Planning: a deterministic planner chooses the chart types and builds a reusable JSON spec.

    • Rendering: D3 renders the spec to SVG in a background worker.

    • Validation: the output is checked for structure, safety and accessibility.

    • Export: the result is written as HTML/SVG/PNG/PDF/CSV.

  4. The assistant gets back a summary, the spec, findings and absolute file paths, and replies to you.

  5. You open the HTML file in a browser. It works offline and is fully interactive. You can ask the assistant to refine or export it further.

D3 renders the SVG server-side into a jsdom document, so every artifact is checked structurally before it is written. The standalone HTML embeds that SVG plus d3 and a small runtime that adds tooltips, zoom, brush, drag, search, keyboard navigation, linked highlighting and canvas point clouds. Protocol, planning, rendering, validation and export are separate layers. New chart types are added by registering a RendererPlugin; the MCP layer doesn't change. See docs/architecture.md.

Feature matrix

Capability

Status

Evidence

stdio MCP transport (official SDK 1.32.2)

βœ… verified

tests/integration/mcp.test.ts, npm run smoke

Streamable HTTP transport (--http)

βœ… verified

tests/integration/http.test.ts: SDK HTTP client, sessions, bearer auth, Host/Origin checks

17 MCP tools, 2 resources, 1 prompt

βœ… verified

integration tests

32 visualization types (below)

βœ… unit-tested; 9 types browser-tested

tests/unit/renderers.test.ts, tests/browser/

Natural-language planning + refinement

βœ… deterministic, keyword/heuristic based

tests/unit/planning.test.ts

Data input: JSON, JSON Lines, YAML, CSV/TSV/any delimiter, Excel (xlsx/xlsm/xlsb/xls), ODS, GeoJSON, TopoJSON, as text, base64 or local file

βœ… verified

tests/unit/formats.test.ts (25), integration scenarios 12–15 (including an 8-format dashboard)

Automatic dashboards (d3_create_dashboard): chart selection, KPI tiles, per-sheet / multi-dataset

βœ… verified

tests/unit/dashboard.test.ts (16), integration scenarios 16–19

Multi-source linking: key inference across formats, questions via MCP elicitation, joins, cross-chart entity selection, detail panel

βœ… verified

tests/unit/link.test.ts (15), integration scenarios 20–26 (incl. a real elicitation round trip), Chromium click test

Dashboards with shared filter, linked highlight, time brush

βœ… verified in Chromium

tests/browser/artifacts.spec.ts

Topology icons (18 built-in + custom path/SVG icons), tiered layout

βœ… verified

tests/unit/icons.test.ts, integration scenario 11, browser drag/search test, 2 pixel baselines

HTML / SVG / JSON / CSV export

βœ… always available

unit + integration tests

PNG / PDF export

βœ… when Chromium is installed

browser tests, smoke test

Canvas rendering for large scatterplots

βœ… verified (50k points)

browser test, benchmark

Safe declarative D3 composition (d3_execute_composition)

βœ… whitelisted ops only, no eval

unit + security tests

React + Vite workbench

βœ… builds, browser-tested

workbench/, tests/browser/workbench.spec.ts

Off-main-thread rendering (worker_threads pool, timeouts)

βœ… verified

tests/unit/pool.test.ts: event loop stays responsive during a 2k-node force layout

Pixel-comparison visual regression (14 baselines)

βœ… verified on macOS

tests/browser/visual.spec.ts

Remote data fetching (d3-fetch)

🚫 disabled by design

security policy

Visualization types: bar (vertical/horizontal), grouped-bar, stacked-bar, waterfall, line, area, stacked-area, cumulative, scatter, bubble, connected-scatter, histogram, boxplot, density, heatmap, pie, donut, treemap, partition (icicle), sunburst, pack, tree (incl. radial), cluster (dendrogram), force, radial-network, chord, sankey (plugin, d3-sankey β€” not core D3), geo (choropleth, GeoJSON, lat/lon points; 12 projections), voronoi, delaunay, contour, custom (declarative shapes). Small multiples and coordinated views are built with d3_compose_visualizations.

Install and run

Prerequisites: Node.js β‰₯ 20.19 (verified on v26.12.1, macOS arm64). PNG/PDF export and browser validation also need Playwright's Chromium.

npm ci
npx playwright install chromium   # optional but recommended
npm run build
npm start                         # stdio server; logs go to stderr
npm run start:http                # Streamable HTTP on http://127.0.0.1:3333/mcp

HTTP transport

node dist/server/index.js --http (or D3MCP_TRANSPORT=http) serves MCP Streamable HTTP at /mcp, plus GET /healthz. Each client session gets its own MCP server instance; all sessions share the render worker pool.

Variable

Default

Meaning

D3MCP_HTTP_HOST

127.0.0.1

Bind address. A non-loopback address is refused unless a token is set.

D3MCP_HTTP_PORT

3333

0 picks a free port. The actual URL is logged to stderr.

D3MCP_HTTP_TOKEN

unset

When set, every request needs Authorization: Bearer <token> (constant-time compare).

D3MCP_HTTP_ALLOWED_ORIGINS

none

Extra comma-separated browser origins. Host and Origin are always validated (DNS-rebinding protection).

The server has no TLS of its own. To expose it beyond localhost, put it behind a TLS-terminating reverse proxy and set a token.

Command

Purpose

npm run dev

Run from source with tsx

npm run verify

Everything below + report (reports/verification-report.md)

npm run smoke

Real MCP round trip: server β†’ client β†’ bar chart β†’ Chromium

npm test

Unit, integration, browser tests

npm run examples

Regenerate examples/

npm run benchmark

Timing for representative dataset sizes

npm run audit:d3 / audit:d3:check

Rebuild D3 inventory / detect drift vs live official index

npm run workbench:dev

Local React workbench at http://127.0.0.1:5179

Configuration is by environment variables (see .env.example; the server does not read .env itself): D3MCP_ARTIFACT_DIR, D3MCP_MAX_ROWS, D3MCP_MAX_INPUT_BYTES, D3MCP_ARTIFACT_TTL_HOURS, D3MCP_BROWSER_TIMEOUT_MS, D3MCP_LOG_LEVEL, D3MCP_RENDER_WORKERS (default: CPUs βˆ’ 1, max 4; 0 renders inline), D3MCP_RENDER_TIMEOUT_MS (default 60000; runaway renders are stopped), and the HTTP variables above.

MCP tools

All tools validate input with Zod, return a text summary plus structuredContent ({ ok: true, ... }), and report failures as isError: true with { ok: false, error: { code, message, hint } }.

Tool

Purpose

d3_visualize

Data + prompt β†’ planned, rendered, validated artifact (spec, mapping, findings, file paths)

d3_recommend_visualizations

Ranked chart options with the question each answers, rationale and trade-offs

d3_analyze_data

Schema, types, missing/unique counts, numeric summaries, Tukey outliers, time span, correlations (descriptive only)

d3_create_spec

Validated reusable spec (dry-run rendered) without writing artifacts

d3_render

Render a spec to artifacts

d3_compose_visualizations

Dashboard with shared category filter, linked highlight, shared time brush

d3_create_dashboard

Automatic dashboard from data + a goal: picks complementary charts, adds KPI tiles, links interactions; with several datasets it links them and cross-filters (see Automatic dashboards, Linking)

d3_link_datasets

Find the shared keys between 2–8 datasets in any formats; asks the user (MCP elicitation) or returns questions when a link is ambiguous

d3_join_datasets

Join two datasets on a key (case-insensitive) into a new dataset_id; enriches tables, graph nodes or map features; reports matched/unmatched

d3_refine_visualization

Natural-language edits that preserve unrelated settings; new artifact linked to parent

d3_query_api

Search the D3 capability registry (status, docs URL, runtime arity)

d3_execute_composition

Declarative pipeline of 55 whitelisted D3 operations; no code evaluation

d3_validate_visualization

Structural/safety/accessibility checks; optional Chromium desktop+mobile checks

d3_export

Add PNG/PDF/CSV/... to an artifact; reports exact paths and per-format failures

d3_get_artifact

Metadata, size-limited content, or list artifacts

d3_list_capabilities

Types, coverage counts, composition ops, export/browser availability, limits

d3_get_test_report

Latest npm run verify report

Resources: d3://schema/visualization-spec (JSON Schema, also in schemas/) and d3://capabilities/coverage. Prompt: visualize-data.

Data formats

Every tool that takes data (d3_visualize, d3_analyze_data, d3_recommend_visualizations, d3_create_spec, d3_render, d3_execute_composition, and each chart in d3_compose_visualizations) accepts all of the following.

Format

Inline text {format, text}

Inline bytes {base64}

Local file {file}

Notes

JSON records / documents

json

βœ“

.json

Arrays of objects; {data:[…]}-style wrappers, records_path, column-oriented and key/value objects are converted (and reported)

JSON Lines

jsonl, ndjson

βœ“

.jsonl .ndjson

One object per line; errors name the line

YAML 1.2

yaml, yml

βœ“

.yaml .yml

Tables, graphs and hierarchies; safe core schema (no code tags, alias-bomb limit, duplicate keys rejected)

CSV

csv

βœ“

.csv

Delimiter auto-detected (, ; tab |); decimal commas parsed in ;/tab/| files

TSV / other delimiters

tsv, dsv (+delimiter)

βœ“

.tsv .tab .psv .txt

Excel / spreadsheets

β€” (binary)

βœ“

.xlsx .xlsm .xlsb .xls .ods .fods

sheet (name, 1-based index, or "all"), header_row; formulas use cached values; dates β†’ ISO text

GeoJSON

geojson

βœ“

.geojson

Or pass the FeatureCollection object directly

TopoJSON

topojson (+object)

βœ“

.topojson

Converted to GeoJSON; or pass the Topology object directly

Graph

{nodes:[…], links:[…]} (also from JSON/YAML files)

Hierarchy

{format:"hierarchy", root:{name, children}} or any {children:[…]} document

[{ "region": "EU", "sales": 10 }]                                          // records
{ "format": "csv", "text": "date;value\n2026-01-01;3,5" }                  // text (any text format)
{ "format": "yaml", "text": "- {site: SJC, latency: 21}\n- {site: NYC, latency: 48}" }
{ "format": "json", "text": "{\"response\":{\"items\":[…]}}", "records_path": "response.items" }
{ "base64": "UEsDBBQABgAIAAAAIQ…", "filename": "report.xlsx", "sheet": "Q3" }  // binary inline
{ "file": "~/Downloads/report.xlsx", "sheet": "all" }                      // local file
{ "file": "data/metrics.csv", "delimiter": ";" }                           // relative to the first data dir
{ "nodes": [{ "id": "a" }], "links": [{ "source": "a", "target": "b" }] } // graph
{ "format": "hierarchy", "root": { "name": "root", "children": [] } }     // hierarchy
{ "type": "FeatureCollection", "features": [] }                            // GeoJSON
{ "dataset_id": "ds_..." }                                                 // reuse stored data

Reading local files

File input is off by default. Set D3MCP_DATA_DIRS to the directories the server may read; separate entries with : on macOS/Linux or ; on Windows. Then pass {"file": "<path>"} as an absolute path, ~/…, a file:// URL, or a path relative to the first directory.

  • The path is resolved with realpath before the check, so symlinks and ../ cannot leave the allowed directories.

  • Only regular files up to D3MCP_MAX_FILE_BYTES (default 25 MB) are read.

  • http(s):// URLs are rejected; nothing is ever downloaded.

  • d3_list_capabilities β†’ data_input.file_access shows the active directories.

Set D3MCP_DATA_DIRS in your client config to the folders you want to allow (for example ~/Downloads and ~/Desktop). The project-level .vscode/mcp.json allows only examples/data-files.

Examples

Sample files in every format are in examples/data-files/ (regenerate with npx tsx scripts/generate-sample-files.ts).

Excel sheet β†’ chart:

{
  "data": { "file": "~/Downloads/kpis.xlsx", "sheet": "KPIs" },
  "prompt": "Latency over time by site",
  "visualization_type": "line",
  "options": {
    "encoding": { "x": { "field": "date" }, "y": { "field": "latency_ms" }, "series": { "field": "site" } }
  }
}

The result's dataset.origin reports the file, the sheet used, and every sheet in the workbook.

Dashboard from several files (d3_compose_visualizations): each chart can read its own file and format. examples/dashboard-from-files/ shows eight charts from xlsx, ods, YAML, JSON Lines, a semicolon CSV with decimal commas, TopoJSON and a YAML graph. Its request.json is the exact tool call.

{
  "title": "Q3 review",
  "charts": [
    {
      "data": { "file": "~/Downloads/q3.xlsx", "sheet": "Revenue" },
      "visualization_type": "bar",
      "prompt": "revenue by region"
    },
    {
      "data": { "file": "~/Downloads/q3.xlsx", "sheet": "KPIs" },
      "visualization_type": "line",
      "prompt": "latency over time by site"
    },
    { "data": { "file": "~/Desktop/inventory.yaml" }, "prompt": "devices by type" },
    { "data": { "file": "~/Downloads/events.jsonl" }, "prompt": "events per hour" }
  ]
}

Natural-language prompts that work once the folder is allowed:

  • "Analyze ~/Downloads/sales.xlsx, sheet Q3, and chart revenue by region."

  • "Combine all sheets in ~/Desktop/budget.ods and compare totals per sheet."

  • "Build a dashboard from ~/Downloads/latency.csv and ~/Downloads/topology.yaml."

Ingestion documents every material transformation, such as delimiter detection, numeric and decimal-comma parsing, unwrapping, sheet selection, date conversion, truncation of over-long text and polygon rewinding. It never imputes, drops or normalizes values without saying so. Aggregation happens only when the spec asks for it, and it is reported in transformations.

Excel and ODS are read with SheetJS CE 0.20.3, vendored in vendor/xlsx-0.20.3.tgz (SHA-256 8dc73fc3…99fe8) from the official CDN. The npm xlsx package is frozen at 0.18.5, which has known vulnerabilities. Macros, external links and formulas are never evaluated.

Example prompts

Type these into any MCP-enabled chat (VS Code Copilot Chat in Agent mode, OpenChamber, …). The agent turns them into tool calls.

  • Sample files: prompts naming a file under examples/data-files/ work as-is when that folder is in D3MCP_DATA_DIRS. The project's .vscode/mcp.json already allows it. With your own data, use the path to your own file.

  • Checked by tests: the single-chart and refinement tables are verified by tests/unit/readme-prompts.test.ts. It sends each prompt or instruction to the real planner and checks the chart type or the change described, so these tables can't drift from actual behavior.

Single charts (d3_visualize)

Prompt

Data

Chart chosen

Compare total revenue by region

sales.csv

bar

Which region has the highest revenue? Rank them.

sales.csv

bar

What share of units does each product have? Show a donut chart.

sales.csv

donut

Compare revenue by region and product side by side

sales.csv

grouped-bar

Stacked bar of revenue by region and product

sales.csv

stacked-bar

Heatmap of revenue by region and product

sales.csv

heatmap

Is revenue related to units?

sales.jsonl

scatter

Bubble chart of units vs revenue sized by revenue

sales.csv

bubble

Show latency over time for each site

telemetry.csv

line

Show the distribution of latency

telemetry.csv

histogram

Box plot of throughput by site to spot outliers

telemetry.csv

boxplot

Density of packet loss

telemetry.csv

density

Stacked area of throughput over time by site

telemetry.csv

stacked-area

Running total of throughput over time

telemetry.csv

cumulative

Contour density of latency vs throughput

telemetry.csv

contour

Voronoi diagram of latency vs throughput

telemetry.csv

voronoi

Show the network topology

topology.yaml

force

Radial network of the topology grouped by tier

topology.yaml

radial-network

Chord diagram of the links

topology.yaml

chord

Treemap of the organisation

org.yaml

treemap

Sunburst of the organisation

org.yaml

sunburst

Org chart as a tree

org.yaml

tree

Circle packing of the organisation

org.yaml

pack

Choropleth map of value

regions.geojson

geo

Map the regions

regions.topojson

geo

Compare total by region

workbook.xlsx sheet Totals

bar

Revenue by region

sales-semicolon.csv

bar

Compare revenue by region

sales.yaml

bar

In chat, name the file, e.g. "Box plot of throughput by site to spot outliers using examples/data-files/telemetry.csv". The agent passes {"file": …} and your words as prompt. Every result explains why the chart was chosen, which fields were mapped, the aggregation used (sum only for additive measures such as revenue; mean for latency or rates), and descriptive findings.

Refining a chart (d3_refine_visualization)

After any chart, say what to change. Unrelated settings are kept, and the new artifact links to the previous one. Several instructions can be combined in one sentence, separated by ; or and.

Instruction

Starting chart

What changes

use a logarithmic y scale

bar of sales.csv

y scale β†’ log (symlog if data has zeros or negatives)

add a threshold line at 100

bar of sales.csv

reference rule at y = 100

highlight the top 2

bar of sales.csv

the two largest bars are emphasised

highlight East

bar of sales.csv

the named category is emphasised

use a dark theme

bar of sales.csv

dark theme

sort descending

bar of sales-semicolon.csv

bars sorted by value

make it horizontal

bar of sales.csv

horizontal bars

change to a line chart

bar of sales.csv

chart type β†’ line

color by product

scatter of sales.csv

color encodes product

title "Q3 revenue"

bar of sales.csv

new title

use the viridis palette

heatmap of sales.csv

color scheme β†’ Viridis

hide the legend

scatter of sales.csv

legend hidden

width 1000

bar of sales.csv

1000 px wide

enable zoom and pan

scatter of sales.csv

zoom/pan in the HTML

20 bins

histogram of telemetry.csv

20 histogram bins

smooth the lines

line of telemetry.csv

monotone curve

size by degree

force of topology.yaml

node size = number of links

show labels

force of topology.yaml

node labels on

make it radial

tree of org.yaml

radial tree layout

Combined example: "use a dark theme; add a threshold line at 100 and highlight the top 2". Instructions the server doesn't understand are returned in unrecognized_instructions rather than guessed.

Automatic dashboards (d3_create_dashboard)

  • "Build a dashboard of SD-WAN health by site from examples/data-files/telemetry.csv: latency, packet loss and throughput." Gives KPI tiles, one trend per measure with a shared time brush, per-site comparisons and a relationship chart.

  • "Create a dashboard from examples/data-files/workbook.xlsx, one section per sheet." Uses split_sheets; every sheet gets labelled charts and KPIs.

  • "Dashboard for a quarterly review using sales.yaml (label Sales), topology.yaml (label Network) and regions.geojson (label Regions), at most 6 charts, dark theme." Uses datasets, so every source gets at least one chart.

  • "Dashboard of the distribution and outliers of revenue in sales.csv." The wording pulls distribution charts into the mix.

  • "Same dashboard without KPI tiles, 3 columns."

Linking several data sources (d3_link_datasets, d3_join_datasets, d3_create_dashboard)

The files are in examples/data-files/linked/: telemetry (CSV), sites (Excel), incidents (JSON), WAN topology (YAML) and regions (GeoJSON).

  • "How do wan-telemetry.csv, sites.xlsx, incidents.json, wan-topology.yaml and regions.geojson relate?" β†’ d3_link_datasets finds telemetry.site = sites.site_code, incidents.site = sites.site_code (lower-case sjc matches SJC), topology.id = sites.site_code and sites.region = regions.name.

  • "Build one interactive dashboard of WAN health by site and region from those five files: latency, packet loss and incidents. Show the number of incidents by severity." β†’ d3_create_dashboard with datasets (per-dataset prompt for incidents). Clicking a site in any chart selects it everywhere; clicking a region on the map selects all its sites. (examples/linked-dashboard/)

  • "Compare average latency by region; the region is in sites.xlsx, the latency in wan-telemetry.csv." β†’ d3_join_datasets (telemetry + sites), then d3_visualize with the returned dataset_id.

  • "Add the site's city and tier to every incident in incidents.json." β†’ d3_join_datasets (left join; unmatched incidents are kept and counted).

  • "Color the regions map by the average latency of the sites in each region." β†’ part of the linked dashboard: maps get an avg_<measure> from the linked fact table.

  • "Link devices.csv and cpu.csv and build a dashboard of CPU by model." When both id and hostname match cpu.device, the server asks "How should devices be linked to cpu?" with the options, instead of guessing.

  • "Use devices.hostname = cpu.device and build the dashboard." β†’ explicit links; or "…just pick the best match" β†’ link_mode: "best".

  • "Build a dashboard from these three files but keep them independent." β†’ link_mode: "none".

Dashboards with charts you choose (d3_compose_visualizations)

  • "Compose a dashboard titled 'Branch health' with: latency over time by site, packet loss over time by site, a box plot of throughput by site, and the topology from topology.yaml. Link them with a shared time brush."

  • "Put a revenue-by-region bar chart and a units-vs-revenue scatter side by side, filterable by region." Clicking a region or a legend entry highlights it in both charts.

Network topologies and icons

  • "Draw examples/data-files/topology.yaml as a network with device icons, colored by tier, layered from WAN to endpoints (tier order wan, edge, core, distribution, access, endpoint); enable zoom, drag and search." Uses icons auto-detected from type, plus tierField/tierOrder.

  • "Same topology, plain icon style, sized by degree."

  • "Use this SVG as the icon for nodes of type nic: <svg …>…</svg>" Becomes options.icons.nic.svg, sanitized to plain shapes.

  • "Map group hub β†’ router, branch β†’ switch, datacenter β†’ server for the network I pasted." Becomes options.iconMap.

Any data format, inline or from files

  • "Chart revenue by region from ~/Downloads/q3.xlsx, sheet Revenue." or "…combine all sheets" (sheet: "all", adds a _sheet column).

  • "Read report.xlsx; the headers are on row 3." (header_row: 3)

  • "Here is YAML: - {site: SJC, latency: 21} - {site: NYC, latency: 48}. Compare latency by site." ({format: "yaml", text})

  • "Plot the items array inside response.json." (records_path: "response.items")

  • "Use metrics.txt; it is pipe-delimited." (delimiter: "|")

  • "Map the counties object in us.topojson." (object: "counties")

  • "Visualize this JSON Lines log: …" ({format: "jsonl", text})

European CSVs (; delimiters with 1.234,5 numbers) are detected automatically. Every conversion is listed in transformations.

Explore, inspect and export

  • "Profile telemetry.csv: types, missing values, outliers, time range and correlations." β†’ d3_analyze_data (correlations need at least 10 values).

  • "What charts would answer questions about sales.csv? Give me the top 3 with trade-offs." β†’ d3_recommend_visualizations

  • "Which D3 functions does this server support for force layouts?" or "Is d3.scaleSymlog implemented?" β†’ d3_query_api

  • "Using D3, compute the quartiles and a 10-bin histogram of latency in telemetry.csv." β†’ d3_execute_composition (whitelisted D3 operations; no code is run)

  • "Validate the last chart in a real browser at desktop and mobile sizes." β†’ d3_validate_visualization

  • "Export the last dashboard as PNG and PDF." or "Give me the cleaned data as CSV." β†’ d3_export

  • "List my recent charts." or "Show me the SVG of artifact art_…" β†’ d3_get_artifact

  • "What visualization types, data formats and limits does the server support?" β†’ d3_list_capabilities

  • "Did the last verification pass?" β†’ d3_get_test_report

Tips for good prompts

  • Name the fields you care about (latency, throughput). Unit suffixes are optional: latency matches latency_ms.

  • Name a chart type to force it (box plot, sunburst, chord). Otherwise intent words steer the choice: over time, distribution, outliers, share of, related to, compare, rank, map, topology.

  • State the aggregation if it matters (average revenue). Otherwise the server averages non-additive measures and says so.

  • Ask for formats explicitly (export PNG and PDF). HTML, SVG and the JSON spec are always produced.

Ready-made requests and outputs are in examples/. Each folder has a request.json plus the generated chart.html, chart.svg, chart.png and spec.json; the dashboard folders hold dashboard.html and dashboard.png.

Network topologies with icons

force and radial-network graphs can draw a device icon on every node and lay the graph out in tiers. Icons work in the HTML, SVG and PNG outputs, and in the HTML they move when you drag a node and dim during search or filtering.

Choosing icons

You don't need to configure anything if your nodes already describe themselves:

How

Example

An icon field on nodes (used automatically)

{ "id": "r1", "icon": "router" }

A type / role / device_type / kind / category field whose values are mostly known icon names (detected automatically and reported in warnings)

{ "id": "fw1", "type": "firewall" }

options.iconField: use any field

"options": { "iconField": "model" }

options.iconMap: map field values (or the color/group value) to icon names

"iconMap": { "hub": "router", "branch": "switch" }

Built-in icons: router, switch, firewall, server, cloud, database, laptop, desktop, phone, access-point, load-balancer, internet, user, vpn, controller, storage, camera, iot.

Aliases: rtr, edge, wan-edge β†’ router; sw, l2-switch, l3-switch β†’ switch; fw β†’ firewall; host, vm β†’ server; db β†’ database; pc, workstation β†’ desktop; mobile β†’ phone; ap, wifi, wireless β†’ access-point; lb β†’ load-balancer; globe, wan β†’ internet; saas β†’ cloud; nas, san β†’ storage; sensor β†’ iot.

Matching ignores case, and spaces or underscores count as -. Values with no matching icon fall back to a plain circle, and the warning lists the available names.

options.iconStyle:

  • "badge" (default): white icon on a disc in the node's color.

  • "plain": icon drawn in the node's color on a light disc.

Custom icons

Add your own icons under options.icons. Custom names override built-ins with the same name. There are two forms.

  1. Path data. Safest; validated by the schema:

"icons": {
  "k8s": {
    "viewBox": [0, 0, 100, 100],
    "paths": [
      { "d": "M50 5L90 27v46L50 95 10 73V27z", "fill": "#326ce5" },
      { "d": "M50 30a20 20 0 1 0 0 40a20 20 0 1 0 0-40z", "fill": "none", "stroke": "white", "strokeWidth": 8 }
    ]
  }
}
  1. Pasted SVG markup, such as an icon exported from a design tool or an icon library:

"icons": {
  "nic": { "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><rect x=\"2\" y=\"6\" width=\"20\" height=\"12\" rx=\"2\"/><circle cx=\"7\" cy=\"12\" r=\"2\"/></svg>" }
}

The SVG is converted, not embedded:

  • path, circle, ellipse, rect, line, polyline and polygon become sanitized path data. Inherited fill/stroke/stroke-width/linecap/linejoin and transform chains are kept.

  • Scripts, event handlers, links, <image>, <use>, <foreignObject>, <style>, text, gradients (url(...)) and DOCTYPE/ENTITY are dropped or rejected, and every drop is listed in warnings.

  • currentColor takes the icon color (white in badge style, the node color in plain style).

  • Limits: 50 custom icons, 50 KB of markup and 64 shapes per icon.

Tiered layout

options.tierField places each tier on its own horizontal row; options.tierOrder sets the top-to-bottom order. Labels go under the nodes so links on the same row don't cross them.

Full example (d3_visualize)

{
  "data": {
    "nodes": [
      { "id": "internet", "label": "Internet", "type": "internet", "tier": "wan" },
      { "id": "fw-1", "label": "FW-1", "type": "firewall", "tier": "edge" },
      { "id": "core-1", "label": "Core-1", "type": "router", "tier": "core" },
      { "id": "dist-1", "label": "Dist-1", "type": "l3-switch", "tier": "distribution" },
      { "id": "acc-1", "label": "Acc-1", "type": "switch", "tier": "access" },
      { "id": "ap-1", "label": "AP-1", "type": "ap", "tier": "endpoint" },
      { "id": "k8s-1", "label": "Cluster", "type": "k8s", "tier": "endpoint" }
    ],
    "links": [
      { "source": "internet", "target": "fw-1" },
      { "source": "fw-1", "target": "core-1" },
      { "source": "core-1", "target": "dist-1" },
      { "source": "dist-1", "target": "acc-1" },
      { "source": "acc-1", "target": "ap-1" },
      { "source": "acc-1", "target": "k8s-1" }
    ]
  },
  "prompt": "Network topology with device icons, layered from WAN to endpoints; enable zoom, drag and search",
  "visualization_type": "force",
  "options": {
    "encoding": { "color": { "field": "tier" } },
    "interaction": { "zoom": true, "drag": true, "search": true },
    "options": {
      "iconField": "type",
      "tierField": "tier",
      "tierOrder": ["wan", "edge", "core", "distribution", "access", "endpoint"],
      "icons": {
        "k8s": {
          "viewBox": [0, 0, 100, 100],
          "paths": [{ "d": "M50 5L90 27v46L50 95 10 73V27z", "fill": "#326ce5" }]
        }
      }
    }
  },
  "output_formats": ["html", "svg", "png"]
}

A natural-language prompt is enough when the nodes carry a type field:

Draw this network topology with device icons, colored by site, layered by tier (wan, edge, core, distribution, access, endpoint). Enable zoom, drag and search.

To change icons on an existing chart, edit options.options.icons / iconMap in the returned spec and pass it to d3_render. d3_refine_visualization handles text instructions such as "dark theme" or "enable zoom", but not icon definitions.

Rendered examples:

Automatic dashboards

d3_create_dashboard builds a whole dashboard from data and a goal. You don't list charts; use d3_compose_visualizations when you want to choose each chart yourself.

{
  "data": { "file": "~/Downloads/telemetry.csv" },
  "prompt": "SD-WAN health by site: latency, packet loss and throughput",
  "output_formats": ["html", "png"]
}

Result (examples/auto-dashboard/):

  • KPI tiles: records, average latency/loss/throughput with min–max, time range, number of sites.

  • One trend chart per measure (by site), sharing a time brush.

  • Per-site comparisons of mean latency and loss.

  • A latency-vs-loss scatterplot.

  • A shared category filter.

  • Descriptive findings.

Input

Meaning

data

One dataset in any supported format (records, text, base64, file, graph, GeoJSON, …)

datasets

Up to 8 {data, label?, prompt?} entries; every dataset gets at least one chart, titled with its label

split_sheets: true

With a spreadsheet in data, each sheet becomes its own labelled dataset (examples/auto-dashboard-excel/)

prompt

The goal. Field names and words such as trend, distribution, outliers, share, map, network steer the chart mix. The first sentence becomes the title unless title is set

max_charts

1–12 (default 6)

kpis

KPI tiles on/off (default on)

columns, theme, output_formats, validate_in_browser

Layout and output

{
  "prompt": "Quarterly review",
  "datasets": [
    { "data": { "file": "~/Downloads/q3.xlsx", "sheet": "Revenue" }, "label": "Revenue" },
    { "data": { "file": "~/Desktop/inventory.yaml" }, "label": "Network", "prompt": "topology by tier" },
    { "data": { "file": "~/Downloads/sites.geojson" }, "label": "Sites" }
  ],
  "max_charts": 6
}

How charts are chosen:

  • Ranking: each dataset is profiled and the single-chart recommender ranks candidates. The dashboard adds one trend and one comparison per extra measure.

  • Variety caps: at most 3 trends, 2 comparisons, 2 network or hierarchy views, and 1 each of share, distribution, relationship, matrix and map, so views don't repeat.

  • Dry render: every chosen chart is rendered once in advance. If one fails, the next candidate replaces it, and the reason is listed in skipped_candidates.

  • Shared time brush: enabled automatically when two or more time-series charts are present.

The response includes, for every chart, the question it answers, why it was chosen, and its encoding.

Analytical safeguards (each covered by tests):

  • Sums vs. averages: values are summed only for additive-sounding fields (revenue, units, count, …). Rates, latencies and percentages are averaged.

  • Part-to-whole charts: shares (pie/donut), stacked bars, stacked areas and running totals are offered only for additive measures.

  • Small samples:

    • Trend charts are not chosen for series with fewer than 4 points.

    • Scatterplots are not chosen for fewer than 5 rows.

    • Correlation coefficients are reported only with at least 10 pairs; otherwise the findings say there are too few values.

  • KPI tiles: descriptive only, and shared evenly across datasets rather than cut off.

In chat, a request like this is enough:

Build a dashboard from ~/Downloads/q3.xlsx, one section per sheet.

Linking multiple data sources

Real questions span several files: metrics in a CSV export, the site inventory in Excel, tickets as JSON, the network in YAML, regions as GeoJSON. The server works out how they connect, asks when it can't tell, and builds charts and dashboards that work across all of them.

1. Find the links. For every pair of datasets the server compares the distinct values of every key-like field:

  • Values are matched after trimming and ignoring case, so SJC, sjc and Sjc are the same key.

  • Each candidate gets a score from value overlap (how much of each side matches), name similarity (site ~ site_code ~ siteId) and cardinality (1:1, 1:N, N:M).

  • Dates and numeric measures are never used as keys; numeric id/code/no fields are.

  • A pair already connected through other links (telemetry β†’ sites β†’ incidents) needs no link of its own.

2. Ask when it is ambiguous. A link is used without asking only when it is clearly the best candidate and won't multiply rows. Otherwise:

  • If the MCP client supports elicitation, the user gets a form: "How should devices be linked to cpu?", with options such as devices.id = cpu.device, devices.hostname = cpu.device and do not link.

  • If not, or if the user declines, the tool returns status: "needs_input" with the questions, the options as ready-to-use links, and dataset_ids, so the agent can ask in chat and call again without re-reading files.

  • link_mode: "best" takes the top candidate without asking; link_mode: "none" keeps datasets independent.

  • Coincidental matches, such as a device type of "router" against an order product of "Router", are reported in notes but don't block.

3. Build linked views. d3_create_dashboard with several datasets:

  • Joined views: a detail table gets a descriptive attribute from its lookup (telemetry by region), so it can be compared by something only the other file has. Only non-numeric attributes are attached, so lookup measures are never double-counted.

  • Linked map colour: maps are coloured by the fact table's measure averaged over the linked entities (regions.avg_latency_ms). Network nodes get the value in their tooltips.

  • Every chart carries the shared entity key (e.g. site_code) on its marks, including aggregated bars, lines, stacked bars and areas, pies, box plots, histograms, networks, maps and hierarchies.

4. Interact. In the HTML:

  • Click any mark to select its entities in every chart. A region selects all its sites, a bar for Gold selects every Gold site, and a network node selects one site. Click again, or press Reset selection, to clear.

  • Detail panel: lists the selected entity's records from every source, built with safe DOM text and up to 500 embedded rows per source.

  • The shared category filter, legend highlighting and tooltips still work.

{
  "prompt": "WAN health by site and region: latency, packet loss and incidents",
  "datasets": [
    { "data": { "file": "linked/wan-telemetry.csv" }, "label": "telemetry" },
    { "data": { "file": "linked/sites.xlsx" }, "label": "sites" },
    {
      "data": { "file": "linked/incidents.json" },
      "label": "incidents",
      "prompt": "number of incidents by severity"
    },
    { "data": { "file": "linked/wan-topology.yaml" }, "label": "topology" },
    { "data": { "file": "linked/regions.geojson" }, "label": "regions" }
  ],
  "max_charts": 8
}

The result (examples/linked-dashboard/) has eight charts:

  • latency trend per site;

  • circuit size per site;

  • incidents by severity;

  • the WAN topology;

  • a region map coloured by linked latency;

  • latency and packet loss by region (joined views);

  • incident resolution time by region.

The response's linking block lists links, entity (sites.site_code), joined_views, enriched_fields, linked_charts, unlinked_sources, asked_user and notes.

To set links yourself, pass links by dataset label (labels default to the file name):

"links": [
  { "left": "telemetry", "left_field": "site", "right": "sites", "right_field": "site_code" },
  { "left": "sites", "left_field": "region", "right": "regions", "right_field": "name" }
]

d3_join_datasets produces a combined dataset_id for any other tool:

  • how: left (default; keeps every left row and counts the unmatched ones) or inner.

  • fields: limits which right-side fields are brought over.

  • Clashing names are prefixed with the dataset label (sites.city).

  • Graph nodes and GeoJSON features can be enriched from a table while keeping their structure.

  • Refused: joins that would put several rows on one node or feature, or exceed the row limit (an N:M key). The error says how to fix it.

Artifacts and export

Each render writes to <D3MCP_ARTIFACT_DIR>/<session_id>/<artifact_id>/: chart.svg, chart.html, spec.json and metadata.json, plus chart.png, chart.pdf and data.csv when requested. The default directory is <project>/artifacts. Tool results return absolute paths, file:// URIs, byte sizes and SHA-256 hashes, and never a path for a format that failed. Failures appear in export_errors.

The HTML is fully offline: d3 7.9.0 is embedded inline (~280 KB), and a strict CSP (default-src 'none', scripts pinned by SHA-256) blocks all network access. MCP clients usually can't display HTML inline, so open the file in a browser. Artifacts expire after D3MCP_ARTIFACT_TTL_HOURS (default 72).

Client setup

  • VS Code: docs/vscode.md. The project folder ships a .vscode/mcp.json, so the server is available when you open it.

  • OpenChamber: docs/openchamber.md. Add the server in Settings β†’ MCP or in ~/.config/opencode/opencode.jsonc; opencode mcp list should report d3-visualization connected.

D3 API coverage

docs/d3-api-coverage.md (machine-readable: data/d3-api-coverage.json) is generated from the official API index for d3 7.9.0. It counts one entry per bullet and cross-checks against the installed package's exports.

Status

Entries

Share

implemented-and-tested (used by a renderer)

211

18.9%

implemented-with-constraints (HTML runtime only)

21

1.9%

available-through-safe-composition

244

21.8%

documented-but-not-yet-implemented

630

56.4%

unsupported-by-current-runtime (d3-fetch)

11

1.0%

total

1117

This is not full D3 coverage. 476 of 1117 entries (42.6%) are reachable. npm run audit:d3:check compares the committed snapshot with the live index and fails on drift (last run: 1117 = 1117, no changes).

Security and privacy

Summary (details in docs/security.md):

  • No eval/new Function/user code.

  • No shell or network tools, and d3-fetch is disabled.

  • Strict Zod schemas plus row, byte, cell, node, mark and feature limits.

  • Prototype-pollution keys are rejected.

  • All text is escaped; SVG/HTML are validated, so scripts, handlers, external references, javascript:, DOCTYPE/ENTITY and CSS @import are rejected.

  • CSP-locked HTML.

  • Chromium renders in an offline context where every network request is aborted.

  • Writes are confined to the artifact directory, and IDs are validated against path traversal.

  • Logs go to stderr and never contain user data.

Testing and verification

Last full run: npm run verify on 2026-10-28, Node v26.12.1, macOS 27.0.1 arm64, Chromium 156.

Suite

Result

ESLint, Prettier, tsc --noEmit (strict)

pass

Unit + security (Vitest)

212 / 212, including the render worker pool, topology icons, data formats, automatic dashboards, multi-source linking and 50 README prompt checks

MCP integration (Vitest + SDK client)

46 / 46: 37 over stdio (10 end-to-end scenarios, icons, README example, 4 file-format, 4 auto-dashboard and 7 multi-source linking scenarios, including a real MCP elicitation round trip) and 9 over Streamable HTTP

Browser (Playwright, Chromium)

27 / 27: 11 functional (including linked cross-chart selection) and 16 pixel-comparison

Smoke (npm run smoke)

12 / 12 checks

D3 drift check against live index

no drift

See docs/testing.md. Screenshots from browser tests are written to reports/screenshots/.

Performance

npm run benchmark measures a single wall-clock run on an Apple-silicon Mac. Each run covers ingest, profiling, planning, render, validation and file writes; these are not capacity guarantees.

Case

Time

HTML size

bar, 50 categories

0.13 s

0.32 MB

line, 10k points Γ— 4 series

0.10 s

0.46 MB

scatter, 10k points (SVG)

0.16 s

1.52 MB

scatter, 50k points (canvas)

0.63 s

6.37 MB

histogram, 50k values

0.14 s

0.31 MB

force, 500 nodes / ~1k links

0.59 s

0.66 MB

force, 2k nodes / ~4k links

2.66 s

1.77 MB

treemap, 5k leaves

0.89 s

1.97 MB

contour, 20k points

0.10 s

0.46 MB

Default limits: 50k rows, 20k SVG marks, 3k force nodes / 10k links, 20k hierarchy nodes and 5k geo features. The planner automatically uses the canvas renderer for scatterplots with more than 5,000 rows. Requests beyond these limits get an actionable LIMIT_EXCEEDED error.

All D3 rendering runs in a worker_threads pool. A 2k-node force layout still takes about 2.6 s of worker time, but the MCP event loop stays responsive and other requests proceed in parallel. In the unit test, the longest event-loop stall during that layout stays under 250 ms. Renders exceeding D3MCP_RENDER_TIMEOUT_MS are terminated and reported as LIMIT_EXCEEDED.

Limitations and roadmap

Limitations:

  • Planning is deterministic and heuristic, not an LLM; unusual phrasing is reported under unrecognized_instructions rather than guessed.

  • Link inference matches exact keys (after trimming and ignoring case). It doesn't do fuzzy matching (San Jose vs San JosΓ©), date-range joins or composite keys; pass links, or prepare the key, for those.

  • Static SVG/PNG have no interactivity.

  • Some D3 module families are only partly exposed (d3-time, d3-scale-chromatic schemes, d3-selection/transition internals).

  • Visual baselines exist for macOS only. On other platforms the pixel tests skip until npm run test:visual:update is run there.

  • The HTTP transport has no built-in TLS or OAuth; use a reverse proxy and a bearer token.

Next:

  1. Expose more d3-scale and d3-time APIs through composition.

  2. Commit Linux baselines from CI so pixel tests also gate pull requests.

  3. Add OAuth for the HTTP transport.

  4. Add transitions with reduced-motion support.

License and contributing

MIT β€” see LICENSE. Contributions: add a renderer as a RendererPlugin (see src/plugins/sankey.ts), add unit and browser tests for it, run npm run verify and include the updated docs/d3-api-coverage.md.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Enables AI agents to create data visualizations like bar charts, line charts, pie charts, scatter plots, and histograms, returning inline SVG or PNG files.
    5
    MIT
  • A
    license
    Not graded
    quality
    Not graded
    maintenance
    Enables AI agents to generate beautiful, presentation-ready charts (SVG + PNG) with zero setup, supporting various chart types and styling options.
    25 npm
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to create interactive charts, diagrams, and tables displayed on a real-time dashboard, supporting multiple data sources with privacy-focused local execution.
    1
    MIT