Skip to main content
Glama
CanvasXpress MCP Server

CanvasXpress MCP Server

by neuhausi

CanvasXpress MCP Server

Natural language → CanvasXpress JSON configs, served over HTTP on port 8100.

Describe a chart in plain English. Get back a ready-to-use CanvasXpress JSON config object ready to pass directly to new CanvasXpress(). No CanvasXpress expertise required.

"Clustered heatmap with RdBu colors and dendrograms on both axes"
"Volcano plot with log2 fold change on x-axis and -log10 p-value on y-axis"
"Violin plot of gene expression by cell type, Tableau colors"
"Survival curve for two treatment groups"
"PCA scatter plot colored by Treatment with regression ellipses"

Supports seven LLM backends: Anthropic API, Amazon Bedrock, Ollama (local, no outbound call), OpenAI, OpenAI-compatible corporate gateways, a corporate AI gateway fronting both Anthropic and OpenAI endpoints, and Google Gemini. The key lives on this server; the CanvasXpress browser client only knows llmServiceURL, and a page can block outbound inference entirely with <meta name="canvasxpress-llm-outbound" content="off">.


How it works

  1. Your description is matched against few-shot examples using semantic vector search (sqlite-vec)

  2. The top 6 most relevant examples are included as context (RAG)

  3. A tiered system prompt is assembled from the canvasxpress-LLM knowledge base — only the content relevant to your request is included

  4. The configured LLM generates a validated CanvasXpress JSON config

  5. Hallucinated parameter names are stripped against the known schema

  6. If headers/data are provided, all column references are validated against them

  7. The config is returned ready to pass to new CanvasXpress()


Related MCP server: Plotting MCP Server

Project structure

canvasxpress-mcp/
│
├── src/
│   ├── server.py           — FastMCP HTTP server (main entry point)
│   ├── llm_providers.py    — Unified LLM backend (Anthropic, Bedrock, Ollama, OpenAI)
│   ├── cx_knowledge.py     — Parameter knowledge skill (fetch, parse, validate, inject)
│   ├── cx_survival.py      — Kaplan-Meier skill (generate, detect columns, validate, annotate)
│   └── cx_selector.py      — Chart type selection skill (deterministic scorer + optional LLM tiebreaker)
│
├── data/
│   ├── few_shot_examples.json    — RAG examples (add more to improve accuracy)
│   ├── few_shot_supplement.json  — curated examples appended when the corpus is regenerated
│   ├── archive/                  — older corpus snapshots (not read by the server)
│   └── embeddings.db             — sqlite-vec vector index (built by build_index.py)
│
├── build_index.py          — builds the vector index (--examples, --output, --model)
├── scripts/
│   ├── generate_few_shots.py — regenerate the corpus with the engine's generateExamples
│   ├── verify_live.py        — check a running endpoint and screenshot the dashboard view
│   └── smoke_mcp_apps.sh     — MCP Apps smoke test
├── tests/                  — offline test suite (pytest)
├── pyproject.toml          — dependency metadata and extras (requirements.txt is the pinned reference)
├── CHANGELOG.md
├── .github/workflows/      — ci.yml (tests + index smoke build), release.yml (v* tags)
│
├── test_client.py          — Python test client
├── test_client.pl          — Perl test client
├── test_client.mjs         — Node.js test client (Node 18+)
│
├── USAGE.md                — usage guide (production, SSH tunnel, local)
├── requirements.txt
└── README.md

Setup

1. Python environment

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

requirements.txt is the full pinned reference environment. For a smaller install, pyproject.toml declares only what the server imports, with an extra per LLM provider and feature (from a clone, editable):

pip install -e ".[bedrock]"        # or anthropic, openai, gemini, ollama; add render for headless PNG
canvasxpress-mcp                   # same as: python src/server.py

On Linux, install the CPU build of PyTorch first, or sentence-transformers pulls several GB of CUDA libraries: pip install torch --index-url https://download.pytorch.org/whl/cpu. fastmcp 3.2.3 (the pin in requirements.txt) and 3.3.1 are both verified (test suite and scripts/verify_live.py); pyproject.toml allows >=3.2.3,<3.4. 4.x is not supported yet.

2. Build the vector index (one-time)

python build_index.py

Options: --examples PATH, --output PATH, --model NAME (default $EMBEDDING_MODEL). The index records its model, dimension, example count and a hash of the examples file; the server warns when they disagree with its settings.

Each record in data/few_shot_examples.json has the keys id, data, header, description and config; only description (embedded) and config (returned) are used for retrieval. Older snapshots are in data/archive/. To add your own examples, append records with that shape and rebuild the index.

Regenerating the corpus. The records are the output of the engine's own generator, generateExamples. scripts/generate_few_shots.py runs it in headless Chromium against a released engine (Playwright required), with a seeded random generator so the same --seed gives the same corpus:

python scripts/generate_few_shots.py --list-graphs                  # the 64 valid graph names
python scripts/generate_few_shots.py --per-graph 60 --seed 1        # 60 per graph type, 3-sentence prompts
python scripts/generate_few_shots.py --per-graph 0 --sentences 8     # the shipped corpus's settings (auto coverage)
python scripts/generate_few_shots.py --graphs "Bar graph,Boxplot" --engine ./canvasXpress.min.js

When all graph types are generated (the default), the curated records in data/few_shot_supplement.json are appended after the generated ones, so hand-written cases the generator cannot phrase (calculated fields, drill-down, meter and bullet KPIs) survive a regeneration. Ids are assigned on merge. Pass --no-supplement to skip them, or --supplement PATH to use another file. Add a record there (description, config, optional data and header) to keep it across regenerations.

Output goes to data/few_shot_examples.generated.json; it never overwrites few_shot_examples.json. Copy it into place and run build_index.py to use it. Graph names are the engine's display names (Bar graph, not Bar).

Re-run whenever you add or change data/few_shot_examples.json. If you skip this step the server still works — it falls back to text-similarity matching and logs a warning.

3. Configure your LLM provider

# Quickstart — Anthropic (default)
export ANTHROPIC_API_KEY="sk-ant-..."

See the LLM providers section for all four backends.

4. Start the server

python src/server.py

Server starts at http://localhost:8100. The MCP protocol endpoint is at /mcp. REST endpoints are at /generate, /modify, /km, etc.

Debug mode — full reasoning trace per request:

CX_DEBUG=1 python src/server.py

Run in background (production):

nohup python src/server.py > /tmp/cx-server.log 2>&1 &

REST endpoints

GET /health returns {status, retrieval, examples} without calling the LLM or fetching anything; use it for load-balancer checks.

All endpoints accept GET (query parameters) or POST (JSON body). Endpoints that interact with the LLM also support JSONP via a callback= parameter for direct integration with CanvasXpress's askLLM() function.

Endpoint

Tool

Required params

GET /health

Liveness and retrieval status (no LLM call, no fetch)

—

GET /generate

Generate a new config

description

GET /modify

Modify an existing config

config, instruction

GET /km

Kaplan-Meier config

at least one of: description, headers, data, config

GET /params

Query parameter schema

none (optional: graph_type, param_name, refresh)

GET /axes

Axis assignment rules

graph_type

GET /select

Recommend a chart type (deterministic + LLM)

intent, column_types (optional: llm_first)

GET /explain

Explain a config property

property

GET /explain-r

CanvasXpress in R

none (optional: topic)

GET /explain-ggplot

CanvasXpress ggplot2 bridge

none (optional: topic)

GET /minimal-params

Minimal required parameters

graph_type

GET /map

Map visualization config

map_id

POST /feedback

Rate a tool call thumbs up/down

request_id, rating

GET /feedback/export

Export call log (admin key required with CX_STRICT_REST=1)

— (optional: tool, rated_only, limit)

POST /feedback/purge

Delete call log rows (admin key required)

— (optional: tool, rated_only)

GET /ui

Browser form UI

—

POST /embed

Embed text(s) into vectors

texts (array) or text

POST /ask

Answer a question from supplied sources with [n] citations

question, sources

Site-search endpoints (/embed, /ask)

These power the internal search on canvasxpress.org (they are not MCP tools — plain REST):

  • POST /embed — returns L2-normalized sentence-transformer vectors (all-MiniLM-L6-v2, 384-dim) for a batch of texts, reusing the already-loaded embedding model: {"texts": ["..."]} → {"model": "...", "dim": 384, "vectors": [[...], ...]}. Used to build the search index and to embed queries for semantic ranking.

  • POST /ask — answers a question using only the provided sources, with inline [n] citations, via the shared LLM provider: {"question": "...", "sources": [{"title": "...", "url": "...", "text": "..."}]} → {"answer": "... [1] ...", "model": "..."}. Retrieval happens in the caller; this only generates the cited answer.

Common parameters (all LLM endpoints)

Parameter

Description

description

Plain English chart description. Alias: prompt, q

headers

Comma-separated column names: Gene, Expression, Treatment

column_types

Column types: Gene=string, Expression=numeric, Treatment=factor

data

JSON array of arrays (first row = headers): [["Gene","Expr"],["BRCA1",1.2]]

temperature

LLM creativity 0.0–1.0 (default 0.0 = deterministic)

callback

JSONP callback name — set automatically by CanvasXpress

target

CanvasXpress chart target ID — passed through to JSONP response

client_id

CanvasXpress client ID — passed through to JSONP response

Examples

# Generate a config
curl -s "http://localhost:8100/generate?description=Violin+plot+of+expression+by+treatment\
&headers=Expression,Treatment&column_types=Expression=numeric,Treatment=factor"

# Modify an existing config
curl -s "http://localhost:8100/modify?\
config=%7B%22graphType%22%3A%22Heatmap%22%7D\
&instruction=change+colorScheme+to+Spectral+and+add+a+title"

# Kaplan-Meier config from headers
curl -s "http://localhost:8100/km?\
description=OS+curve+by+treatment+arm\
&headers=PatientID,OS_Time,OS_Status,Treatment"

# Query all parameters for a graph type
curl -s "http://localhost:8100/params?graph_type=Heatmap"

# Look up a single parameter
curl -s "http://localhost:8100/params?param_name=colorScheme"

# Axis assignment rules for a chart type
curl -s "http://localhost:8100/axes?graph_type=Scatter2D"

# Recommend a chart type (deterministic scorer + automatic LLM tiebreaker)
curl -s "http://localhost:8100/select?\
intent=show+expression+distribution+by+cell+type\
&column_types=Expression=numeric,CellType=factor"

# Let the LLM pick the chart type from the full catalogue first (llm_first)
curl -s "http://localhost:8100/select?\
intent=show+expression+distribution+by+cell+type\
&column_types=Expression=numeric,CellType=factor&llm_first=true"

# Explain a config property
curl -s "http://localhost:8100/explain?property=groupingFactors"

# Minimal required parameters

# Map config — world choropleth
curl -s "http://localhost:8100/map?map_id=World&color_scheme=Blues&title=World+GDP"

# Map config — US states pie chart
curl -s "http://localhost:8100/map?map_id=USAStates\
&color_by=Winner&size_by=Total\
&topo_json=https://www.canvasxpress.org/data/json/usa-albers-states.json"
curl -s "http://localhost:8100/minimal-params?graph_type=KaplanMeier"

# Map config — world choropleth
curl -s "http://localhost:8100/map?map_id=World&color_scheme=Blues&title=World+GDP"

# Map config — US states pie chart
curl -s "http://localhost:8100/map?map_id=USAStates\
&color_by=Winner&size_by=Total\
&topo_json=https://www.canvasxpress.org/data/json/usa-albers-states.json"

CanvasXpress integration (JSONP)

Set llmServiceURL in your CanvasXpress config. CanvasXpress will append generate and add all required JSONP parameters automatically:

Production (canvasxpress.org):

cx.llmServiceURL = "https://www.canvasxpress.org/";

Local development via SSH tunnel:

# Run this once in a terminal and leave it open
ssh -L 8100:127.0.0.1:8100 canvasxpress@canvasxpress.org -N
cx.llmServiceURL = "http://localhost:8100/";

Local server:

cx.llmServiceURL = "http://localhost:8100/";

Apache proxy configuration

To expose the server through Apache on a production host, add this to your VirtualHost include directory. The ProxyPass / ! line must be last.

# /etc/apache2/conf.d/userdata/ssl/2_4/canvasxpress/canvasxpress.org/mcp-proxy.conf
<Location /mcp>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/mcp
    ProxyPassReverse http://127.0.0.1:8100/mcp
</Location>
<Location /generate>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/generate
    ProxyPassReverse http://127.0.0.1:8100/generate
</Location>
<Location /modify>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/modify
    ProxyPassReverse http://127.0.0.1:8100/modify
</Location>
<Location /km>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/km
    ProxyPassReverse http://127.0.0.1:8100/km
</Location>
<Location /params>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/params
    ProxyPassReverse http://127.0.0.1:8100/params
</Location>
<Location /axes>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/axes
    ProxyPassReverse http://127.0.0.1:8100/axes
</Location>
<Location /select>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/select
    ProxyPassReverse http://127.0.0.1:8100/select
</Location>
<Location /explain-r>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/explain-r
    ProxyPassReverse http://127.0.0.1:8100/explain-r
</Location>
<Location /explain-ggplot>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/explain-ggplot
    ProxyPassReverse http://127.0.0.1:8100/explain-ggplot
</Location>
<Location /explain>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/explain
    ProxyPassReverse http://127.0.0.1:8100/explain
</Location>
<Location /minimal-params>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/minimal-params
    ProxyPassReverse http://127.0.0.1:8100/minimal-params
</Location>
<Location /map>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/map
    ProxyPassReverse http://127.0.0.1:8100/map
</Location>
<Location /feedback>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/feedback
    ProxyPassReverse http://127.0.0.1:8100/feedback
</Location>
<Location /ui>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/ui
    ProxyPassReverse http://127.0.0.1:8100/ui
</Location>
<Location /favicon.ico>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/favicon.ico
    ProxyPassReverse http://127.0.0.1:8100/favicon.ico
</Location>

Note: explain-r and explain-ggplot must appear before explain — Apache matches first-wins, so more specific paths go first. PassengerEnabled Off and ProxyPass must be inside the same <Location> block; top-level ProxyPass directives are intercepted by Passenger before they can fire.

To write the file in one shot on the production server (run as root):

cat > /etc/apache2/conf.d/userdata/ssl/2_4/canvasxpress/canvasxpress.org/mcp-proxy.conf << 'EOF'
<Location /mcp>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/mcp
    ProxyPassReverse http://127.0.0.1:8100/mcp
</Location>
<Location /generate>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/generate
    ProxyPassReverse http://127.0.0.1:8100/generate
</Location>
<Location /modify>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/modify
    ProxyPassReverse http://127.0.0.1:8100/modify
</Location>
<Location /km>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/km
    ProxyPassReverse http://127.0.0.1:8100/km
</Location>
<Location /params>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/params
    ProxyPassReverse http://127.0.0.1:8100/params
</Location>
<Location /axes>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/axes
    ProxyPassReverse http://127.0.0.1:8100/axes
</Location>
<Location /select>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/select
    ProxyPassReverse http://127.0.0.1:8100/select
</Location>
<Location /explain-r>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/explain-r
    ProxyPassReverse http://127.0.0.1:8100/explain-r
</Location>
<Location /explain-ggplot>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/explain-ggplot
    ProxyPassReverse http://127.0.0.1:8100/explain-ggplot
</Location>
<Location /explain>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/explain
    ProxyPassReverse http://127.0.0.1:8100/explain
</Location>
<Location /minimal-params>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/minimal-params
    ProxyPassReverse http://127.0.0.1:8100/minimal-params
</Location>
<Location /map>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/map
    ProxyPassReverse http://127.0.0.1:8100/map
</Location>
<Location /feedback>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/feedback
    ProxyPassReverse http://127.0.0.1:8100/feedback
</Location>
<Location /ui>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/ui
    ProxyPassReverse http://127.0.0.1:8100/ui
</Location>
<Location /favicon.ico>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/favicon.ico
    ProxyPassReverse http://127.0.0.1:8100/favicon.ico
</Location>
EOF

apachectl configtest && service httpd restart

MCP tools

The server exposes the following tools over the MCP protocol (used by AI assistants such as Claude Desktop) and as REST endpoints (used by web pages and scripts directly).

generate_canvasxpress_config

Generate a new CanvasXpress config from a plain English description.

Argument

Type

Required

Description

description

string

✅

Plain English chart description

headers

string[]

❌

Column names from your dataset

data

array[][]

❌

Full data array — first row = headers. Overrides headers

column_types

object

❌

Map of column → type (string/numeric/factor/date)

temperature

float

❌

LLM creativity 0–1 (default 0.0)

Response:

{
  "config":         { "graphType": "Violin", "xAxis": ["Expression"], "groupingFactors": ["Treatment"] },
  "valid":          true,
  "warnings":       [],
  "invalid_refs":   {},
  "headers_used":   ["Expression", "Treatment"],
  "types_used":     { "Expression": "numeric", "Treatment": "factor" },
  "removed_params": [],
  "success":        true,
  "datetime":       "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":     "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}

Field

Description

config

The CanvasXpress JSON config — pass to new CanvasXpress()

valid

true if all column references exist in the provided headers

warnings

Column reference or parameter value warnings

removed_params

Parameter names the LLM invented that were stripped

success

Same as valid — included for CanvasXpress callbackLLM compatibility

request_id

UUID for this call — use with POST /feedback to submit a rating


modify_canvasxpress_config

Modify an existing config using a plain English instruction.

Argument

Type

Required

Description

config

object

✅

The existing CanvasXpress JSON config to modify

instruction

string

✅

Plain English description of the change to apply

headers

string[]

❌

Column names for validating new column references

data

array[][]

❌

Full data array. Overrides headers

column_types

object

❌

Map of column → type

temperature

float

❌

LLM creativity 0–1 (default 0.0)

Response:

{
  "config":         { "graphType": "Heatmap", "colorScheme": "Spectral", "title": "My Heatmap", "xAxis": ["Gene"] },
  "prompt":         "change colorScheme to Spectral and add a title",
  "valid":          true,
  "warnings":       [],
  "invalid_refs":   {},
  "headers_used":   ["Gene"],
  "types_used":     { "Gene": "string" },
  "removed_params": [],
  "changes":        { "added": ["title"], "removed": [], "changed": ["colorScheme"] },
  "success":        true,
  "datetime":       "Fri, 10 Apr 2026 19:00:00 GMT",
  "tool":           "modify_canvasxpress_config",
  "request_id":     "7c9e6679-7425-40c3-bba5-0ee65d4ef6a4"
}

Field

Description

config

The updated CanvasXpress JSON config

prompt

The instruction echoed back

changes

Keys added, removed, and changed relative to the input config

request_id

UUID for this call — use with POST /feedback to submit a rating

Example instructions:

"add a title My Heatmap"
"change the color scheme to Tableau"
"remove the legend"
"switch to dark theme"
"add groupingFactors for the Treatment column"
"set y-axis min to 0 and max to 100"
"add a horizontal reference line at y = 1.5"

generate_km_config

Generate, validate, and detect columns for Kaplan-Meier survival plots. Accepts any combination of description, headers, data, and existing config.

Argument

Type

Required

Description

description

string

❌

Plain English KM description

headers

string[]

❌

Column names from your dataset

data

array[][]

❌

Full data array — enables column detection

config

object

❌

Existing KM config to validate and fix

temperature

float

❌

LLM creativity 0–1 (default 0.0)

At least one argument must be provided.

Response:

{
  "config": {
    "graphType": "KaplanMeier",
    "xAxis": ["OS_Time"],
    "yAxis": ["OS_Status"],
    "groupingFactors": ["Treatment"],
    "xAxisTitle": "Time (months)",
    "yAxisTitle": "Survival Probability",
    "colorScheme": "Tableau",
    "showLegend": true
  },
  "valid": true,
  "errors": [],
  "warnings": [],
  "suggestions": [],
  "column_detection": {
    "time_col":   "OS_Time",
    "event_col":  "OS_Status",
    "group_cols": ["Treatment"],
    "confidence": "high",
    "notes": []
  },
  "valid":      true,
  "success":    true,
  "datetime":   "Fri, 10 Apr 2026 19:00:00 GMT",
  "tool":       "generate_km_config",
  "request_id": "2b9e6679-9c0b-4ef8-bb6b-6bb9bd380a55"
}

query_canvasxpress_params

Query the CanvasXpress parameter knowledge base — fetched live from the canvasxpress-LLM GitHub repo with automatic local cache fallback.

Argument

Type

Required

Description

graph_type

string

❌

Chart type — returns all parameters for this type

param_name

string

❌

Parameter name — returns full definition and valid values

refresh

boolean

❌

Force re-fetch from GitHub (default false)

Pass either, both, or neither (returns full schema summary).

Env var

Default

Description

CX_SCHEMA_TTL

3600

Schema cache TTL in seconds

CX_SKIP_FETCH

0

Set to 1 to always use bundled schema, no GitHub fetch

Response — single parameter (param_name=colorScheme):

{
  "found":        true,
  "param":        "colorScheme",
  "description":  "Color palette applied to the chart.",
  "type":         "string",
  "valid_values": ["Blues", "Reds", "RdBu", "Tableau", "CanvasXpress", "..."],
  "graph_types":  ["Bar", "Heatmap", "Scatter2D", "Violin", "..."],
  "schema_source": "github",
  "tool":         "get_chart_parameters",
  "valid":        true,
  "datetime":     "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":   "a0eebc99-9c0b-4ef8-bb6b-6bb9bd380a11"
}

Response — graph type (graph_type=Heatmap):

{
  "graph_type":   "Heatmap",
  "param_count":  42,
  "params": {
    "colorScheme":          { "description": "Color palette.", "type": "string", "valid_values": ["RdBu", "Blues", "..."] },
    "samplesClustered":     { "description": "Cluster columns with dendrogram.", "type": "boolean", "valid_values": [] },
    "variablesClustered":   { "description": "Cluster rows with dendrogram.", "type": "boolean", "valid_values": [] }
  },
  "schema_source": "github",
  "tool":         "get_chart_parameters",
  "valid":        true,
  "datetime":     "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":   "b1ddbc00-1d1c-5fg9-cc7c-7cc0ce491b22"
}

get_axes_info

Return axis assignment rules for a given graph type: which axes are valid, which are forbidden, and which axis title parameter to use.

Argument

Type

Required

Description

graph_type

string

✅

CanvasXpress chart type e.g. Bar, Scatter2D, BarLine

Response:

{
  "graph_type":       "Scatter2D",
  "category":         "multi_dim",
  "valid_axes":       ["xAxis", "yAxis"],
  "invalid_axes":     [],
  "axis_title_param": "xAxisTitle / yAxisTitle",
  "notes":            "Scatter2D requires both xAxis (numeric) and yAxis (numeric). Use xAxisTitle and yAxisTitle for axis labels. Never use smpTitle.",
  "schema_snippet":   "xAxis — columns for x-axis; yAxis — columns for y-axis ...",
  "tool":             "suggest_axes",
  "valid":            true,
  "datetime":         "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":       "c2eecd11-2e2d-6gh0-dd8d-8dd1df502c33"
}

Field

Description

category

single_dim, multi_dim, or combined

valid_axes

Axis keys that apply to this chart type

invalid_axes

Axis keys that must NOT be used

axis_title_param

Correct axis title parameter (smpTitle vs xAxisTitle/yAxisTitle)


select_canvasxpress_chart

The chart-type recommender ("wizard" / Tufte mode). Given column metadata and a plain English intent, it recommends the most appropriate chart type, returns a ranked list of candidates with rationale, attaches a ready-to-use minimal_config to each, and emits a generate_hint you can pipe straight into generate_canvasxpress_config.

The recommendation blends three tiers, fastest first:

  1. Deterministic scorer (no LLM, no API key) — a structured first pass over the 53-chart catalogue, scoring each candidate on (a) column-type counts + row count + cardinality, (b) semantic regex over the column names, and (c) keyword boosts derived from the intent string. This always runs and is the default result.

  2. LLM tiebreaker (automatic) — when the top two candidates score within 0.15 of each other, the configured LLM disambiguates among the top three and its choice is promoted to the top. The one-sentence rationale is returned in the tiebreak field. If the LLM is unavailable or errors, the deterministic ranking stands.

  3. LLM-first mode (opt-in via llm_first=true) — the LLM freely chooses the best chart type from the full catalogue (with descriptions) given the columns, intent, and row count; its pick becomes top_recommendation and the deterministic scorer fills the alternatives.

No raw data is required — just column names, types, an optional row count, and the intent string.

Argument

Type

Required

Description

intent

string

✅

Plain English description of what you want to show

column_types

object

✅

Map of column name → type (string/numeric/factor/date)

n_samples

integer

❌

Optional number of rows — used to refine recommendations

llm_first

boolean

❌

When true, the LLM chooses the top chart from the full catalogue before the scorer runs (default false)

Response:

{
  "intent": "show expression distribution by cell type",
  "column_summary": { "n_factor": 1, "n_numeric": 1, "n_time": 0, "n_bool": 0, "n_text": 0 },
  "top_recommendation": {
    "graphType":       "Violin",
    "score":           0.9,
    "description":     "Kernel density distribution of a numeric variable split by a categorical grouping factor.",
    "clinical_use":    "Gene expression by cell type, biomarker distribution by cohort.",
    "next_step":       "generate_canvasxpress_config with description='Violin chart of Expression grouped by CellType'",
    "scoring_factors": ["1 numeric + 1 factor — ideal for distribution by group"],
    "minimal_config":  { "graphType": "Violin", "xAxis": ["Expression"], "groupingFactors": ["CellType"] }
  },
  "alternatives": [
    {
      "graphType":      "Boxplot",
      "score":          0.78,
      "description":    "Box-and-whisker summary statistics by group.",
      "clinical_use":   "Quick distribution summary when n is sufficient.",
      "scoring_factors": ["1 numeric + 1 factor — suitable for grouped distribution"],
      "minimal_config": { "graphType": "Boxplot", "xAxis": ["Expression"], "groupingFactors": ["CellType"] }
    }
  ],
  "generate_hint": "Violin chart of Expression grouped by CellType — columns: Expression, CellType",
  "tiebreak":       { "used": false, "chosen": null, "reason": "" },
  "valid":          true,
  "warnings":       [],
  "llm_first":      false,
  "type_source":    "explicit",
  "tool":           "select_canvasxpress_chart",
  "datetime":       "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":     "d3ffde22-3f3e-7hi1-ee9e-9ee2eg613d44"
}

Field

Description

top_recommendation

Best chart type with score, rationale, and minimal_config

alternatives

Up to 4 other ranked candidates, each also with minimal_config (up to 3 in llm_first mode)

generate_hint

Ready-made description to pass to generate_canvasxpress_config

minimal_config

Minimal axis config ready to use — attach to the generate call

tiebreak

Present when the LLM tiebreaker ran: { used, chosen, reason }

llm_first

Echoes whether LLM-first mode was requested

type_source

How column types were resolved: explicit, inferred, or merged


explain_config_property

Return a plain English explanation of any CanvasXpress configuration property.

Argument

Type

Required

Description

property

string

✅

Config property name e.g. colorScheme, groupingFactors, decorations

Response:

{
  "property":    "groupingFactors",
  "explanation": "**`groupingFactors`** — Array of column names used to group/color data. e.g. ['Treatment', 'CellType']",
  "tool":        "explain_canvasxpress_property",
  "valid":       true,
  "datetime":    "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":  "e4ggef33-4g4f-8ij2-ff0f-0ff3fh724e55"
}

explain_canvasxpress_r

Usage guide for CanvasXpress in R — installation, basic usage, data formats, Shiny integration, R Markdown, and the ggplot2 bridge.

Argument

Type

Required

Description

topic

string

❌

Filter by topic: installation, basic, shiny, rmarkdown, data, config

Response (no topic — full guide):

{
  "overview":          "The canvasXpress R package wraps the CanvasXpress JavaScript library as an htmlwidget ...",
  "sections": {
    "installation": { "title": "Installation", "content": "Install from CRAN: install.packages('canvasXpress') ..." },
    "basic":        { "title": "Basic Usage",   "content": "The main function is canvasXpress() ..." },
    "data":         { "title": "Data Format",   "content": "CanvasXpress in R expects data in one of two orientations ..." },
    "config":       { "title": "Configuration Parameters", "content": "All CanvasXpress JSON config parameters map directly ..." },
    "shiny":        { "title": "Using CanvasXpress in Shiny", "content": "CanvasXpress integrates with Shiny via canvasXpressOutput() ..." },
    "rmarkdown":    { "title": "Using CanvasXpress in R Markdown / Quarto", "content": "..." }
  },
  "available_topics": ["installation", "basic", "data", "config", "shiny", "rmarkdown"],
  "tool":             "explain_canvasxpress_r",
  "valid":            true,
  "datetime":         "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":       "f5hhfg44-5h5g-9jk3-gg1g-1gg4gi835f66"
}

Response (with topic=shiny):

{
  "topic":   "shiny",
  "section": { "title": "Using CanvasXpress in Shiny", "content": "CanvasXpress integrates with Shiny via canvasXpressOutput() ..." },
  "available_topics": ["installation", "basic", "data", "config", "shiny", "rmarkdown"],
  "tool":             "explain_canvasxpress_r",
  "valid":            true,
  "datetime":         "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":       "g6iigh55-6i6h-0kl4-hh2h-2hh5hj946g77"
}

explain_canvasxpress_ggplot

Usage guide for the CanvasXpress ggplot2 bridge — convert any ggplot2 object to an interactive CanvasXpress widget with a single function call.

Argument

Type

Required

Description

topic

string

❌

Filter by topic: installation, geoms, example

Response (no topic — full guide):

{
  "sections": {
    "overview":      { "title": "Overview",      "content": "canvasXpress() accepts a ggplot2 object directly ..." },
    "installation":  { "title": "Installation",  "content": "install.packages('canvasXpress') ..." },
    "geoms":         { "title": "Supported Geoms", "content": "geom_point → Scatter2D, geom_bar → Bar ..." },
    "example":       { "title": "Full Example",   "content": "library(ggplot2); library(canvasXpress) ..." }
  },
  "available_topics": ["overview", "installation", "geoms", "example"],
  "tool":             "explain_ggplot_to_canvasxpress",
  "valid":            true,
  "datetime":         "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":       "h7jjih66-7j7i-1lm5-ii3i-3ii6ik057h88"
}

get_minimal_parameters

Return the minimal set of required parameters for a specific chart type.

Argument

Type

Required

Description

graph_type

string

✅

CanvasXpress chart type e.g. Scatter2D, Heatmap

Response:

{
  "graphType":           "KaplanMeier",
  "required_parameters": ["graphType", "xAxis", "yAxis"],
  "tool":                "get_minimal_params",
  "valid":               true,
  "datetime":            "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":          "i8kkji77-8k8j-2mn6-jj4j-4jj7jl168i99"
}

Field

Description

graphType

The requested chart type

required_parameters

Minimum set of parameters that must be populated for a valid config


create_map_config

Generate a CanvasXpress map (choropleth, pie, or marker) config without an LLM call. Supports world maps, continent maps, country maps, US state/county maps, and custom topoJSON maps. Optionally overlays pie charts per region, proportional sizing, and geocoded marker pins.

Argument

Type

Required

Description

map_id

string

✅

Map identifier — see table below

data

array[][]

❌

CSV-style data; first row = headers, first column = geographic IDs

title

string

❌

Chart title

color_scheme

string

❌

Color palette e.g. Blues, RdBu, YlOrRd

color_by

string

❌

Column name to color regions/symbols by

size_by

string

❌

Column name whose values scale symbol/pie size per region

decorations

object

❌

Decoration overlays — currently supports pie (see below)

topo_json

string

❌

URL to a custom topoJSON file

legend_order

object

❌

Map of column → ordered list of values for legend display

markers

array

❌

List of marker pin dicts (see below)

map_id values:

map_id

Coverage

First-column ID format

World

All countries

ISO 3-letter codes (USA, FRA, CHN, …)

WorldContinents

6 continents

Continent names (Africa, Asia, Europe, …)

Africa, Asia, Europe, NorthAmerica, SouthAmerica, Oceania

One continent

ISO 3-letter codes

USAStates

50 US states + DC

2-letter codes (CA, TX, NY, …)

USACounties

US counties

5-digit FIPS codes (06037, 48113, …)

albersStatesPie

US states, Albers projection

2-letter codes — use when Albers projection is explicitly requested

ISO 3-letter code (CAN, GBR, AUS, …)

Country sub-regions

Feature property values — set mapPropertyId in config

2-letter US state code (CA, TX, NY, …)

State counties

County names or FIPS codes

Pie overlay (decorations.pie):

"decorations": {
  "pie": {
    "smps":   ["Democrat", "Republican", "Libertarian", "Other"],
    "colors": ["blue", "red", "yellow", "green"],
    "size":   2.5
  }
}

decorations.pie.size is a float multiplier that controls how large each pie is drawn. size_by (top-level) is a column name that scales each pie proportionally to its data value. These two work independently — both can be used together.

Marker pins (markers list):

Each marker dict supports three ways to specify location:

Key

Description

lat + lng

Explicit decimal-degree coordinates

zip

US ZIP code — resolved via free zippopotam.us API

location

Any city, address, or landmark — geocoded via Nominatim (OpenStreetMap)

Optional fields: label (string), color (default "red"), shape (teardrop|circle|star|square, default teardrop), size (int 1–10, default 4).

Response:

{
  "config": {
    "graphType":    "Map",
    "mapId":        "USAStates",
    "colorBy":      "Winner",
    "sizeBy":       "Total",
    "decorations":  { "pie": [{"smps": ["Democrat","Republican","Libertarian","Other"], "colors": ["blue","red","yellow","green"], "size": 2.5}] },
    "legendOrder":  { "Winner": ["Republican","Democrat"] },
    "title":        "2000 Presidential Elections"
  },
  "valid":        true,
  "warnings":     [],
  "map_id":       "USAStates",
  "headers_used": ["Id","Total","Democrat","Republican","Libertarian","Other","State","Winner"],
  "tool":         "create_map_config",
  "datetime":     "Fri, 10 Apr 2026 19:00:00 GMT",
  "request_id":   "j9lllk88-9l9k-3no7-kk5k-5kk8km279j00"
}

Example prompts for generate_canvasxpress_config:

"Show GDP by country on a world map using Blues color scheme"
"US states map colored by unemployment rate"
"Create a map of France showing population by region"
"Pie map of the 2000 US Presidential Election — Democrat, Republican, Libertarian, Other slices per state"
"World map with markers at Paris, Tokyo, New York, and Sydney"
"USA states map with markers at ZIP codes 10001, 90210, and 60601"

compose_canvasxpress_dashboard

Assemble a multi-panel linked dashboard spec for the canvasxpress-dashboards npm package from a list of CanvasXpress charts — each a {data, config} pair, typically produced by generate_canvasxpress_config. Panels sharing the same data are linked automatically (the engine's broadcast mechanism): clicking a bar, slice or row in one panel selects the matching samples in the others. Deterministic and instant (no LLM call).

Input: title (string), panels ([{data, config, title?, width?}]), description? (intro text panel), columns? (panels per row, 1–4, default 2), theme? (light | dark | auto).

Response: {"spec": {...}, "valid": true, "errors": [], "usage": "<html snippet>"} — the validated dashboard spec plus a ready-to-paste snippet that renders it with CanvasXpressDashboards.renderDashboard.

suggest_canvasxpress_dashboard

Build a whole linked dashboard from one dataset in a single call — the one-call path for "make a dashboard of X". Pass a bundled dataset by name (penguins, iris, mtcars), or data: a CanvasXpress data object or a headers+rows array. The tool returns the validated dashboard spec — a Filters panel plus the panels the data calls for, all bound to the one dataset so a selection in one panel selects the same rows in the others.

Exactly one of data or dataset is required. Dataset names are case-insensitive and take aliases ("palmer penguins"); an unknown name returns an error listing the available datasets.

Shape recognised

Dashboard

Patient cohort with survival columns (a time column such as time, os_months, PFS.days plus a numeric 0/1 event column)

Filters, Kaplan–Meier curve by the best grouping column, a measure by group (boxplot) and against time (scatter), and a case card

Per-sample sequencing run QC (three or more of: reads, mapping, quality, duplication, GC, insert size, coverage, adapter, contamination)

Filters, depth per sample, mapping vs duplication, quality spread by lane / flowcell / batch, and a case card

Anything else with at least two numeric columns and one factor column (the explore template, the fallback)

Filters (one per factor), a scatter of the two numeric columns that separate the main factor best, a boxplot, a histogram grouped by the main factor, and counts by the second factor stacked by the main one

Headers+rows input. A first row of headers followed by data rows, the same shape generate_canvasxpress_config takes. A column is numeric when more than 90% of its non-missing values parse as numbers (it becomes a measure); anything else is a factor. NA is missing: null in a numeric column, a literal "NA" level in a factor (no row is ever dropped). year is always a factor.

How the explorer picks factors. The main factor has the fewest levels (counting NA), ties going to the factor that separates the numeric columns most. The second is the next fewest, ties going to the one least associated with the main factor (so it adds a second view instead of repeating the first). Year and date columns are never chosen, though they stay filters. For penguins that is species and sex.

Every dashboard ends with a case card: click a patient's point (or a sample's bar) and a one-row table loads that row's record, while the charts keep the whole dataset and pick the case out. It is plain spec — a case parameter, a second source narrowed to it, and a setParam click action — so it works in any dashboard. Only data stored with rows as samples and variables as measures is handled; a table stored the other way round, or a survival table with a text event (Dead/Alive), gets no suggestion.

It is a suggestion, never a silent rewrite: when nothing fits it answers {"suggested": false, ...} with a message that says what to correct. Deterministic and instant (no LLM call), and the data is never altered.

When a call does not work: retry, then force. The tool description tells the model to read the message, correct the arguments and call again, up to 5 attempts, and never to draw the dashboard itself in an HTML widget. If the 5th attempt still fails it calls once more with force=true, which skips the templates and builds a Filters panel plus bar charts from whatever the table has:

Table

Forced dashboard

Has a numeric column

Filters and one Bar of the first numeric column, one bar per row, coloured by the first small factor

Only discrete columns

Filters and a count bar per discrete column (up to 4: fewest levels first, columns whose values are all different last), each a Bar of a column of 1s summed per level

Getting the data in: one rule. The slow part of a dashboard request is the model writing the rows into the tool call (about 40,000 tokens for 1,500 rows, 6-8 minutes), and no chat client hands a file to an MCP server (a file attached to the chat, or made by an analysis, lives in a sandbox that cannot reach the server). So the tool text is one rule: pass dataset (penguins, iris, mtcars) or url (any link to a CSV/TSV: a GitHub blob, Dropbox, Google Drive or Google Sheets share link — turned into the direct-download link — or a https://canvasxpress.org/mcp/file/<id> link) when there is one; otherwise call the tool with no data at all. The view then shows a drop zone ("Drag your file here"); the person drops the file (or 2 to 6 related files), the view uploads it within its own origin (POST /mcp/upload, a 30-minute link), calls the tool again with the link (url, or urls for several files) through the host and draws the dashboard. No rows pass through the model, so a file on the computer, an attachment and analysis output all take seconds. A small table already in the chat (under 300 rows) may still be passed as csv/data; a file_id from curl -s --data-binary @FILE "https://canvasxpress.org/mcp/upload?name=FILE", and the local upload_csv(path) tool in tools/local-upload/, remain for scripted use. GET /mcp/upload is the same drop page as a stand-alone web page; a dropped file is served at GET /mcp/file/<id>. Uploads are held in memory for 30 minutes, up to 50 MB, raw body or multipart; the routes sit under /mcp, the path a reverse proxy already forwards. A fetched url must be http(s) on a public host (private and loopback addresses are refused); CX_PUBLIC_URL (default https://canvasxpress.org) is the base the file links are built with.

Getting the data in: the eight cases, as you type them. Verified in Claude Desktop; each is one message.

Where the data is

What to type

What happens

A bundled dataset

Create a dashboard from the penguins dataset

One call, under a second (penguins, iris, mtcars).

A link

Create a dashboard from https://github.com/mwaskom/seaborn-data/blob/master/penguins.csv

The server fetches it. GitHub, Dropbox, Google Drive and Google Sheets share links are converted to direct downloads.

A file on your computer

Create a dashboard from my QC file

The panel says Drag your file here; drag the file from any folder onto it. Seconds, whatever its size.

A file attached with +

attach it, then Create a dashboard from this file

Same drop zone: drag the same file from Finder onto the panel (a chat attachment cannot be forwarded to a server).

Data produced by an analysis

Analyze ~/data/trial-patients.csv: add a BMI category column and months on study per patient, save the result as a CSV in ~/data, then create a dashboard from it

The assistant saves the result; drag that file onto the panel.

Several related files

Create a dashboard from my trial files: patients, sites and arms

Select the 2 to 6 files and drag them together; one dashboard is built from the joined table (the header lists the files).

A small table already in the chat

Create a dashboard from this table: followed by the CSV lines (under 300 rows)

Passed as csv, about a minute at most.

A file path, with the local uploader installed

Create a dashboard from ~/data/trial-patients.csv

A consent question in the chat (accept), or the drop zone; both are correct. See tools/local-upload/.

Behind the drop zone: the tool, called with no data, returns {drop: true}; the view reads the dropped file and sends it to the server in 60 KB pieces through the host (upload_canvasxpress_file), gets a 30-minute link, calls the tool again with url (or urls) and draws the result. The iframe makes no network request of its own (Claude Desktop blocks that), and no row ever passes through the model.

Large results. The result carries the complete table, so it can be hundreds of KB, and Claude replaces a tool result that large with an error text. suggest_canvasxpress_dashboard therefore returns a short result (template, panels, validity, result_id, parts) and holds the full one on the server for 30 minutes; the dashboard view fetches it in pieces through the host with get_canvasxpress_dashboard_result (a tool a model never needs to call) and draws it. A result the host mangled still shows one line in the panel.

Always the complete data. A dashboard is drawn from every row of the table and never from a sample: the tool text tells the model to send every row, and a call that declares more rows in total_rows (the size of the full table) than it sends is refused with a message that says so. The view has no file loader and no banner: it draws what the tool returned.

Several related files. Pass tables instead of data: [{"name": "orders", "data": [[headers], [row], ...]}, {"name": "customers", "data": [...]}, {"name": "products", "data": [...]}] (2 to 6 complete files). The file with the most rows is the base and keeps every row; each other file is a lookup joined on a column name they share, and that column must identify one row of the lookup file (a one-to-many relation is read from the many side, so a patient file is never multiplied by its visits). Existing names are kept and a clashing column is renamed <column> (<file>). The template detection then runs on the joined table, the header lists the files, and the insights strip skips columns that came from a lookup file (they are the same for every row of a group, so they cannot be a finding). The result carries joined (base, rows per file, the key of each join, matched and unmatched rows).

Header. A full-width text panel across the top: the title in bold, larger text, then a short description (kind of table, the complete row and column count, the charts it holds). Like the Cohort Explorer example.

KPI row. Every suggested dashboard starts with ONE row of up to four round (ring) cards: Rows (the count) and the average of up to three measures: for sequencing QC the depth, mapping and quality columns; for a survival cohort the follow-up time, then the first other measure; otherwise the numeric columns in order (a column whose average rounds to nothing on a whole-number ring is used only when nothing larger exists). Card headers are the humanized column names (pct_q30 is "% Q30"); the ring is bound to the real column. The cards follow the filters like every other panel (the row count is a calculated field of 1s). The row is added after the template is chosen, so the survival and sequencing-QC recognition below is unaffected.

Over time. A table with an ISO date column (YYYY-MM-DD, optionally with a time; a day/month order is never guessed, so 03/04/2025 is not read) also gets a panel of its leading measure over time, coloured by the first small factor, right under the KPI row, and a date-period filter: by month (shown as 01 Jan, or 2025-01 when the data spans years) when the dates cover at most 12 months, by year when they cover 2 to 12 years, none when finer than that. The date becomes a numeric variable named <column> (time) (UTC milliseconds) with axis limits taken from the data's own range plus a 2% margin. The result's derived entry tells the view how to repeat this for a complete file it loads. Added after the template is chosen, like the KPI row.

What stands out. An insights strip above the KPI row, computed in the view from the complete data, never from a sample: how many rows break each limit you declared (thresholds); a group whose average is at least 0.5 SD from the overall average (z of 4 or more, at least 5 rows), such as "Lane L3: % Mapped averages 84.5, 8.18 below the overall 92.7"; and values more than 3.5 SD from the mean. At most five lines, and no strip when nothing stands out.

The recognition is pinned. tests/test_detection_pinned.py fails if the survival / QC / batch / group vocabulary, the thresholds, or the template chosen for the reference tables (the cohort, QC, generic and edge cases of the MCP app test set) change by accident; the JavaScript parity test covers the template chooser itself.

An input that is not a table at all (for example an object with headers and rows, or a list of row objects) gets an error that names the accepted shapes: a list of lists with the column names first, or a {y, x} CanvasXpress object. A table with no numeric columns gets an error that points straight at force=true, because no template can ever match it. The same recognition drives the "whole dashboard" option in the canvasxpress-dashboards data wizard.

Each dashboard also opts in to an insights strip — captions above the charts for what stands out (a group that differs, outliers, and any limit you declare), each carrying its figures and computed by fixed deterministic checks, not a model. Pass thresholds to flag failures: limits are never inferred, only the ones you declare are checked, e.g. [{"field": "q30", "op": "lt", "value": 85, "label": "Q30 below 85%"}] (op is lt, le, gt or ge).

Input: data? (CanvasXpress data object or headers+rows array), dataset? (penguins | iris | mtcars), title?, theme? (light | dark | auto), thresholds? (limits to flag in the insights strip), tables? (several related files, see above, instead of data), total_rows? (the rows in the full table; fewer sent is refused), force? (only after 5 failed attempts: the filter-plus-bars dashboard above). Exactly one of data / dataset / tables.

Response: {"suggested": true, "template": "cohort" | "ngs-qc" | "explore" | "minimal", "label", "reason", "panels": [{id, title, graphType}], "spec": {...}, "valid": true, "errors": [], "usage": "<html snippet>"}. When nothing fits, or the input is wrong, it is {"suggested": false, "reason": "..."} (plus "error" for bad input).

validate_dashboard_spec

Validate a canvasxpress-dashboards spec: structure plus every internal reference (layout items → panels, panel dataRef → data sources, source kinds, $param tokens → declared params, joins/relationships, calculated fields, pushdown queries, filters, controls, and self-referencing join/function cycles). Returns {"valid": bool, "errors": [...]} — structured errors an agent can fix before rendering. Use after hand-editing a composed spec.

publish_canvasxpress_dashboard

Publish a composed spec to a running canvasxpress-dashboards install so it opens in the no-code Builder. Saves the spec (from compose_canvasxpress_dashboard / suggest_canvasxpress_dashboard) to the install named by CXD_DASHBOARDS_URL, authenticating with CXD_DASHBOARDS_API_KEY (sent as X-Cxd-Api-Key, matched against the server's CXD_API_KEY). Returns builder_url — a link that opens the dashboard straight in the Builder via the server's ?open=<id> deep link — plus a share_url when visibility is auth or public (default private). Never throws: returns {"published": false, "errors": [...]} when unconfigured, the spec is invalid, or the server rejects it. Configure with:

CXD_DASHBOARDS_URL=https://canvasxpress.org/dashboards
CXD_DASHBOARDS_API_KEY=<matches CXD_API_KEY on the dashboards server>
# CXD_DASHBOARDS_AUTH=Bearer ...   # optional, for an authenticating proxy

On the dashboards server, set CXD_API_KEY (the shared secret) and CXD_API_USER (an admin account so publishing and public sharing are permitted); published dashboards are owned by that account, so sign into the Builder as the same user to see them.

explain_canvasxpress_dashboards

Documentation for composing linked dashboards: installation (npm / UMD / the self-hosted server with its no-code builder and share links), spec anatomy, cross-chart linking (broadcast, relationships, drill-through actions, controls), and the recommended MCP workflow. Optional topic: overview, install, spec, linking, workflow.


Call logging & feedback

Every tool call is automatically logged to data/call_log.db (a separate SQLite database from the vector index). Each response includes a request_id UUID that can be used to submit thumbs-up/down feedback.

What each logged call contains

Besides the request and response bodies, every stored call carries:

Field

Description

timing.duration_ms

Wall-clock time of the whole request

llm_calls[]

One record per model call made while serving it (empty for tools that never call a model): provider, model, temperature, max_tokens, ms, ok/error, input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, stop_reason, cost_usd (from the configured rates), the stable system prompt by size + hash (system_chars, system_sha1), and — verbatim — the per-request system_suffix and user_prompt

These are recorded at the single LLM chokepoint (llm_providers.complete), so they cover all tools uniformly and are stored only in the call log — they are not added to the API response. A failed model call is recorded with its error before the exception propagates. The manage_call_log.py export command and the /feedback/export endpoint return them with each row.

Submit feedback

curl -X POST http://localhost:8100/feedback \
  -H "Content-Type: application/json" \
  -d '{"request_id": "<uuid from response>", "rating": 1, "comment": "Perfect"}'

Field

Type

Required

Description

request_id

string

✅

UUID from the tool response

rating

integer

✅

1 = thumbs up, -1 = thumbs down

comment

string

❌

Optional free-text note

Export call log

# All calls (latest 500)
curl "http://localhost:8100/feedback/export"

# Only rated calls for one tool
curl "http://localhost:8100/feedback/export?tool=select_canvasxpress_chart&rated_only=true"

Parameter

Description

tool

Filter by tool name (optional)

rated_only

true — return only rows that have a rating

limit

Max rows to return (default 500)

The export returns every logged request and response. With CX_STRICT_REST=1 it requires the X-Admin-Key header like the purge endpoint (see Hosting behind a firewall).

Purge call log (admin only)

Requires the X-Admin-Key header. See ADMIN_KEY below.

# Delete all rows
curl -X POST http://localhost:8100/feedback/purge \
  -H "X-Admin-Key: your-secret-key"

# Delete only rated rows for one tool
curl -X POST http://localhost:8100/feedback/purge \
  -H "X-Admin-Key: your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"tool": "generate_canvasxpress_config", "rated_only": true}'

Body field

Description

tool

Delete only rows for this tool name (optional)

rated_only

true — delete only rows that have a rating (optional)

Omitting both deletes all rows. Returns {"success": true, "deleted": N}.

Security — without a correct X-Admin-Key header the endpoint returns 403 Forbidden. The comparison uses hmac.compare_digest to prevent timing-based attacks. Set ADMIN_KEY in .env to enable the endpoint. If it is not set, the admin endpoints are disabled and always answer 403; no key is generated, printed or logged.

Manage the call log locally (CLI)

manage_call_log.py is a standalone CLI script for managing data/call_log.db directly on the server — no HTTP key required.

Note — data/call_log.db is in .gitignore and is never overwritten by git pull. The server uses CREATE TABLE IF NOT EXISTS, so the file is only ever opened and appended to, never recreated on restart.

# Summary: row counts by tool, rated/unrated, 👍/👎
python manage_call_log.py stats

# Export everything to a timestamped JSON file
python manage_call_log.py export --out backup_$(date +%Y%m%d).json

# Export only rated calls as CSV
python manage_call_log.py export --rated-only --format csv --out rated.csv

# Export only calls for one tool
python manage_call_log.py export --tool generate_canvasxpress_config

# Purge only rated calls (keeps unrated history)
python manage_call_log.py purge --rated-only

# Purge everything without a prompt (e.g. in a cron job after export)
python manage_call_log.py purge --yes

# Use a different database path
python manage_call_log.py --db /path/to/other/call_log.db stats

stats — prints a per-tool breakdown table with total, rated, 👍, 👎 counts and first-call timestamp.

export options:

Option

Description

--tool TOOL

Filter by tool name (partial match)

--rated-only

Only rows that have a rating

--limit N

Max rows to export (default: all)

--format json|csv

Output format (default: json)

--out FILE

Write to file instead of stdout

purge options:

Option

Description

--tool TOOL

Delete only rows for this tool (partial match)

--rated-only

Delete only rows that have a rating

--yes

Skip the confirmation prompt


LLM providers

Anthropic (default)

export LLM_PROVIDER=anthropic          # optional — this is the default
export ANTHROPIC_API_KEY="sk-ant-..."
export LLM_MODEL=claude-sonnet-4-20250514  # optional
python src/server.py

Amazon Bedrock

pip install boto3
export LLM_PROVIDER=bedrock
export AWS_REGION=us-east-1
# Uses your existing AWS credentials (IAM role, SSO profile, or explicit keys)
python src/server.py

The server calls the Converse API, which needs bedrock:InvokeModel on the model (or inference profile) named by LLM_MODEL. Some newer Claude models reject an explicit temperature; the server retries the call once without it and remembers that model, so no configuration is needed. Install the SDK with pip install -e ".[bedrock]".

Ollama (local, no API key)

ollama serve
ollama pull llama3.2
export LLM_PROVIDER=ollama
export LLM_MODEL=llama3.2
python src/server.py

OpenAI / corporate gateway

pip install openai
export LLM_PROVIDER=openai
export OPENAI_API_KEY="your-key"
export OPENAI_BASE_URL="https://api.your-company.com/openai/v1"
export LLM_MODEL=gpt-4o
python src/server.py

Environment variables

Variable

Default

Description

LLM_PROVIDER

anthropic

LLM backend: anthropic, bedrock, ollama, openai

LLM_MODEL

provider default

Model name / ID

ANTHROPIC_API_KEY

—

Anthropic API key

AWS_REGION

us-east-1

AWS region for Bedrock

OLLAMA_BASE_URL

http://localhost:11434

Ollama server URL

OPENAI_API_KEY

—

OpenAI / gateway API key

OPENAI_BASE_URL

https://api.openai.com/v1

OpenAI-compatible endpoint

MCP_HOST

0.0.0.0

Server bind host

MCP_PORT

8100

Server port

CORS_ORIGINS

*

Comma-separated allowed origins

ADMIN_KEY

—

Secret key required for POST /feedback/purge (and /feedback/export in strict mode). If unset, the admin endpoints are disabled; no key is generated or printed

CX_STRICT_REST

0

Set to 1 for hosted deployments: no CORS middleware, no Access-Control-Allow-Origin on JSONP responses, and /feedback/export (every logged request and response) requires X-Admin-Key. CORS_ORIGINS is ignored

CX_DEBUG

0

Set to 1 for full debug trace

CX_SCHEMA_TTL

3600

Schema cache TTL in seconds

CX_SKIP_FETCH

0

Set to 1 to skip GitHub schema fetch

CX_KB_BASE_URL

canvasxpress-LLM main on GitHub

Base URL for SCHEMA.md / RULES.md / CONTEXT.md. Point at a release tag or an internal mirror to pin the knowledge base

CX_STATE_DIR

data/

Writable state directory: the call log (call_log.db) and the knowledge-base cache ($CX_STATE_DIR/kb_cache; the old data/kb_cache is still read as a fallback, and cache write failures are logged, not fatal). The code directory can then be read-only

CX_PUBLIC

0

Public profile. Set to 1 for an internet-facing deployment: bearer-token auth is required and the server refuses to start without a token (fail-closed). Leave unset for local/tunnel installs — nothing changes

CX_AUTH_TOKENS

—

Comma-separated bearer tokens accepted on Authorization: Bearer <token>. When set (even without CX_PUBLIC), auth is enforced on every route except the exempt prefixes and CORS preflight

CX_AUTH_TOKENS_FILE

—

Path to a file of bearer tokens, one per line (# comments allowed). Merged with CX_AUTH_TOKENS

CX_REQUIRE_AUTH

0

Set to 1 to require auth (fail-closed) without the full public profile

CX_AUTH_EXEMPT_PATHS

/health

Comma-separated path prefixes served without a token (for load-balancer probes)

CX_RATE_LIMIT_PER_MIN

0

Per-token fixed-window request cap per 60s (0 = unlimited). Over the cap returns 429

CX_DAILY_COST_LIMIT_USD

0

Per-UTC-day LLM cost ceiling. Past it, generation is paused until 00:00 UTC (0 = no ceiling)

CX_DISABLE_GENERATION

0

Manual kill switch: set to 1 to pause all generation immediately

EMBEDDING_MODEL

all-MiniLM-L6-v2

Sentence transformer model for vector search

CX_REQUIRE_VECTOR

0

Set to 1 to refuse to start if vector retrieval is unavailable (default: fall back to text similarity). GET /health always reports retrieval: vector|fallback

CX_DISABLED_TOOLS

—

Comma-separated tool names to remove, e.g. render_canvasxpress_config,publish_canvasxpress_dashboard on hosts without Playwright or a dashboards install

CX_STRICT_TOOLS

0

Set to 1 to fail startup if CX_DISABLED_TOOLS names an unknown tool

All variables can also be set in a .env file in the project root.


Hosting behind a firewall

Everything below is opt-in; the defaults suit local use and a public instance behind a proxy. These settings are for a shared or corporate deployment (for example an ALB in front of a container).

Setting

Effect

GET /health

Liveness for load balancers: {"status":"ok","retrieval":"vector","examples":3783}. Makes no LLM call and no network fetch. retrieval is fallback (with a retrieval_reason) when the vector index did not load.

CX_REQUIRE_VECTOR=1

Refuse to start unless vector retrieval works. Without it a missing or broken index silently falls back to text similarity (lower quality). The index is probed with a real query at startup, so a broken DB or sqlite-vec is caught, not just a missing file.

CX_DISABLED_TOOLS

Comma-separated tools to remove, e.g. render_canvasxpress_config,publish_canvasxpress_dashboard on a host with no Chromium and no dashboards install.

CX_STRICT_TOOLS=1

Fail startup if CX_DISABLED_TOOLS names a tool that does not exist, so an upstream rename is not missed.

CX_STRICT_REST=1

No CORS middleware and no Access-Control-Allow-Origin header on JSONP responses; /feedback/export requires X-Admin-Key. It does not remove the REST routes (/generate, /ask, /ui, ...): expose only /mcp and /health at your proxy or load balancer if that is what you want. CORS_ORIGINS is ignored in this mode.

ADMIN_KEY

Enables /feedback/purge (and /feedback/export in strict mode). Unset means the admin endpoints are disabled.

CX_STATE_DIR

Where the server writes (call log, knowledge-base cache). Point it at a writable volume and the application directory can be read-only.

CX_KB_BASE_URL

Where SCHEMA.md / RULES.md / CONTEXT.md are fetched from (default: canvasxpress-LLM main on GitHub). Point at a tag or an internal mirror to pin the knowledge base.

CX_CONFIG_SCHEMA_URL

Where the config JSON Schema is fetched from (default: canvasxpress.org). A bundled copy is the fallback.

CX_SKIP_FETCH=1

Never fetch; use the cache and the bundled schema. This also drops the SCHEMA.md / RULES.md layer, so graph-type applicability is less detailed.

CX_PUBLIC_URL

The address clients reach this server at (default https://canvasxpress.org). It is the base of the file links (<CX_PUBLIC_URL>/mcp/file/<id>) and of the drop page and curl upload address the dashboard tool text tells the model to use, so a self-hosted instance must set it to its own address. The local uploader (tools/local-upload/) points at it with CX_UPLOAD_URL=<CX_PUBLIC_URL>/mcp/upload.

Network. At runtime the server reaches the LLM provider, GitHub (raw) for the knowledge base and canvasxpress.org for the config schema. Building the index downloads the embedding model from Hugging Face once; bake it into your image and set HF_HUB_OFFLINE=1 to stop lookups at runtime. The knowledge base is refreshed on a one-hour TTL (CX_SCHEMA_TTL), so behaviour can change without a deploy unless you pin CX_KB_BASE_URL.

Container build. Install the CPU build of PyTorch first (the default Linux wheel pulls several GB of CUDA libraries), then pip install -e ".[bedrock]" (or your provider's extra), then python build_index.py so the index is part of the image. embeddings.db is gitignored, so it is always built, never fetched.

Pinning. Pin a release tag (v0.1.0 and later) rather than a commit SHA; see Releases.

A typical hosted configuration:

LLM_PROVIDER=bedrock
LLM_MODEL=us.anthropic.claude-sonnet-5-5
AWS_REGION=us-east-1
CX_REQUIRE_VECTOR=1
CX_STRICT_REST=1
CX_DISABLED_TOOLS=render_canvasxpress_config,publish_canvasxpress_dashboard
CX_STRICT_TOOLS=1
CX_STATE_DIR=/var/lib/canvasxpress-mcp
HF_HUB_OFFLINE=1

Troubleshooting

Updating the production server from the repo

Run these steps every time you pull a new version:

cd canvasxpress-mcp/
./server.sh stop
git fetch origin
git reset --hard origin/main
git apply --whitespace=nowarn canvasxpress-ctypes-fix.patch
source .venv/bin/activate
pip install -r requirements.txt
./server.sh start

git reset --hard discards any local changes and makes the working tree match the remote exactly. If you have local config files (.env, data/call_log.db) they are untracked and will not be touched.

If src/server.py is missing after the patch step (patch did not restore it), recover it with:

git restore src/server.py

No module named 'dotenv'

pip install python-dotenv

No module named 'starlette'

pip install starlette fastmcp

Port 8100 already in use

lsof -ti :8100 | xargs kill -9   # macOS/Linux

Homepage shows "It works! Python 3.12" Remove the Passenger configuration from /home/canvasxpress/public_html/.htaccess. Delete the lines between CLOUDLINUX PASSENGER CONFIGURATION BEGIN and CLOUDLINUX PASSENGER CONFIGURATION END.

404 from the browser but curl works Check that llmServiceURL does not include the port number in the path. Correct: "https://www.canvasxpress.org/" — incorrect: "https://www.canvasxpress.org:8100/".

500 error on invalid descriptions Upgrade to the latest server.py — graceful error handling was added so invalid prompts return a 200 with valid: false and a helpful message instead of a 500.

removed_params is non-empty The LLM generated parameter names not in the CanvasXpress schema. They were automatically stripped. The config is still valid — refine the description if needed.

New tool added — Apache returns 404 for the new endpoint Every time a new tool (and its REST endpoint) is added to server.py, the Apache proxy config must be updated as root. Add a new <Location> block for the new endpoint, keeping PassengerEnabled Off and ProxyPass together inside it:

<Location /new-endpoint>
    PassengerEnabled Off
    ProxyPass http://127.0.0.1:8100/new-endpoint
    ProxyPassReverse http://127.0.0.1:8100/new-endpoint
</Location>

Then reload Apache:

apachectl configtest && service httpd restart

See the Apache proxy configuration section above for the complete current config to overwrite the file from scratch if needed.

MCP Apps support

MCP Apps (SEP-1865) is an MCP extension that lets hosts embed a server-supplied iframe directly inside the conversation UI, enabling inline interactive chart and dashboard rendering.

Which tools declare a UI resource

Four chart-producing tools carry _meta.ui.resourceUri = "ui://canvasxpress/chart":

Tool

Purpose

generate_canvasxpress_config

Generate a new chart config from plain English

modify_canvasxpress_config

Modify an existing chart config

generate_km_config

Generate a Kaplan-Meier survival plot config

create_map_config

Generate a choropleth / map config

When a supporting host calls any of these tools it will load the ui://canvasxpress/chart resource, render it in a sandboxed iframe, and post the tool result to the iframe via the ui/notifications/tool-result postMessage notification. The iframe parses the JSON config and calls new CanvasXpress() to display the chart inline.

Two dashboard-composing tools carry _meta.ui.resourceUri = "ui://canvasxpress/dashboard":

Tool

Purpose

compose_canvasxpress_dashboard

Compose a linked multi-panel dashboard from chart panels

suggest_canvasxpress_dashboard

One call for any dashboard request: a bundled dataset (penguins, iris, mtcars) or your own data

These render the returned spec inline as a live, interactive dashboard — a grid of linked CanvasXpress panels — via ui://canvasxpress/dashboard, which loads the canvasxpress-dashboards UMD bundle and calls renderDashboard(spec, element). Selections propagate across panels in the conversation, no server or sign-in needed. (To instead persist a dashboard into an install and open it in the no-code Builder, use publish_canvasxpress_dashboard above.)

The dashboard view: Save, Open in Builder, errors

Above the rendered dashboard the view shows two buttons.

  • Save downloads the dashboard spec as <id>.spec.json (host ui/download-file). If the host does not allow downloads, a box shows the JSON with a Copy JSON button. The file can be put on any canvasxpress-dashboards install; in its Builder, drop it on the Data page and it opens at once.

  • Open in Builder opens the dashboard in the Builder at CXD_DASHBOARDS_URL (default https://canvasxpress.org/dashboards). When CXD_DASHBOARDS_URL and CXD_DASHBOARDS_API_KEY are both set it first saves through publish_canvasxpress_dashboard; saves land in the key's account (CXD_API_USER, or the first admin), so use that for private servers.

The theme follows the chat (auto), a tool call that yields no dashboard shows no error banner (the reason goes to the browser console and the assistant retries), and the corner stamp canvasxpress-mcp view <commit>/<hash> identifies the build. Clients cache the tool list and view HTML: after a deploy, quit all chats and restart the client. The same walk-through is in the dashboards README ("Dashboards in Claude (MCP App)") and on the Builder Home.

The dashboard view: the drop zone

When the tool is called with no data the view shows Drag your file here instead of a dashboard. Drop a CSV/TSV (or 2 to 6 related files together), or click to choose; the view sends it to the server in pieces through the host and draws the dashboard from the link it gets back. The eight ways to get data in, with the exact messages, are under suggest_canvasxpress_dashboard.

Known supporting hosts

  • Claude Desktop (Anthropic)

  • ChatGPT / OpenAI Apps SDK

  • VS Code (with MCP Apps extension)

  • Goose (Block)

Testing locally

# 1. Import check
.venv/bin/python -c "import sys; sys.path.insert(0, 'src'); import server; print('OK')"

# 2. Unit tests (offline, no LLM calls, < 10 s)
.venv/bin/python -m pytest tests/ -x -q

# 3. Verify resource content in-process
.venv/bin/python -c "
import sys, asyncio; sys.path.insert(0, 'src')
import server
async def main():
    r = await server.mcp.read_resource('ui://canvasxpress/chart')
    print(r.contents[0].mime_type, len(r.contents[0].content), 'bytes')
asyncio.run(main())
"

All existing REST endpoints (/generate, /modify, /select, /km, etc.) and any MCP clients that do not support MCP Apps are completely unaffected — the _meta.ui.resourceUri annotation is silently ignored by non-supporting hosts.

CDN versioning note

The HTML view loads CanvasXpress from https://www.canvasxpress.org/dist/canvasXpress.min.js. No versioned CDN URL is offered by CanvasXpress upstream (verified 2026-05-28). Users who require reproducibility should self-host a pinned copy of canvasXpress.min.js and update the <script src> in src/ui/cx_chart_view.html.

Development and releases

pip install -e ".[dev]"        # on Linux, install CPU torch first (see Setup)
python -m pytest               # offline; the render test skips itself without Playwright

tests/test_generate_few_shots.py also runs the real generator when CX_TEST_ENGINE points at a canvasXpress.min.js (URL or path); without it those tests skip, so CI needs no network for them. tests/test_chart_view_host.py loads the chart view in a stand-in strict MCP Apps host (one that delivers the tool result only after the view sends ui/notifications/initialized, as Claude does); it needs Playwright and CX_TEST_ENGINE set to a canvasXpress.min.js file, which it serves in place of the CDN copy.

CI. .github/workflows/ci.yml installs the package, builds the vector index as a smoke test and runs the tests on every push and pull request. pyproject.toml allows fastmcp>=3.2.3,<3.4, so CI runs on the newest allowed version while requirements.txt stays pinned to the version used in production.

Releases. Pushing a tag such as v0.2.0 runs .github/workflows/release.yml (tests, then a GitHub release with generated notes). Changes are listed in CHANGELOG.md.

Regenerating the few-shot corpus is described under Setup: scripts/generate_few_shots.py runs the engine's own generateExamples with a seed and appends the curated data/few_shot_supplement.json.

The shipped few_shot_examples.json was made with the generator's auto-coverage mode at 8 sentences per prompt (--per-graph 0 --sentences 8). That setting reproduces its per-graph counts for 63 of 64 graph types (3,768 records against 3,774; Oncoprint was added in a separate run). The wording differs because the original run was unseeded. The script's defaults (--per-graph 60 --sentences 3) are different: auto mode at --sentences 3 yields about 12,200 records of short prompts, which retrieve differently. In a retrieval test with held-out synthetic requests, the short-prompt corpus was slightly better on one- or two-sentence requests (top-1 graph type 0.91 against 0.88) and clearly worse on long multi-parameter ones (parameter recall 0.69 against 0.90), while putting about a third less text in front of the LLM. Concatenating both corpora kept most of each advantage. This used synthetic requests from the same templates, not a live LLM evaluation, so check eval/ before changing the shipped corpus.


License

MIT — see LICENSE. The MCP server code is MIT-licensed; it generates and validates configurations for the CanvasXpress JavaScript library, which is distributed separately under the CanvasXpress Community License (see https://www.canvasxpress.org/license.html).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers