@fusedashlabs/ui9000-mcp
v5.5.5
Published
MCP server that creates charts from your data — bar, line, pie, maps, KPI, network, and more.
Readme
UI9000-MCP
MCP server for creating charts from data. Attach a CSV or pass values in chat, choose a chart type (bar, line, pie, map, KPI, network, and more), and the chart renders in the MCP Apps view — Cursor, Claude Desktop, or any host that supports MCP Apps.
npm: @fusedashlabs/ui9000-mcp
Server key in Cursor / Claude: UI9000-MCP
Add to Cursor, Claude Desktop, or ChatGPT: docs/add-to-chatgpt-cursor-claude.md
Install in Cursor or Claude
Hosted
Streamable HTTP — no local Node process:
{
"mcpServers": {
"UI9000-MCP": {
"url": "https://mcp.ui9000.com/mcp"
}
}
}Restart Cursor after saving ~/.cursor/mcp.json (or this repo's .cursor/mcp.json).
npx (local stdio, hosted data-links)
Node.js 22+. The CLI speaks MCP over stdio. Chart payloads are POSTed to https://mcp.ui9000.com/v1/data-links and widgets GET the signed URL — no mcp.json env, no client token. No clone required.
Cursor
Add to ~/.cursor/mcp.json (or project .cursor/mcp.json):
{
"mcpServers": {
"UI9000-MCP": {
"command": "npx",
"args": ["-y", "@fusedashlabs/ui9000-mcp"]
}
}
}Restart Cursor. All chart types are registered. Optional: "env": { "MCP_BASE_URL": "http://127.0.0.1:8088" } to save/fetch chart JSON on a local MCP HTTP server instead of https://mcp.ui9000.com.
Claude Desktop
Edit claude_desktop_config.json:
{
"mcpServers": {
"UI9000-MCP": {
"command": "npx",
"args": ["-y", "@fusedashlabs/ui9000-mcp"]
}
}
}Optional env:
| Variable | Default (npx CLI) | Meaning |
| --- | --- | --- |
| MCP_BASE_URL | https://mcp.ui9000.com | Hosted save/get. local starts loopback HTTP. A http://127.0.0.1:… URL assumes pnpm mcp:serve is already up |
| MCP_UI_RESOURCE_MIME | mcp-app | Claude / Cursor MCP Apps MIME |
| PORT | 8088 | Local data-link port when MCP_BASE_URL=local |
| STORAGE_DIR | ~/.mcp-ui/data | Local payload files (loopback only) |
| MCP_DATA_LINK_SECRET | dev default locally | Host signing secret; not sent by npx |
Attach a CSV in chat, then ask for a chart (see chart-test-kit/prompts.md in the workspace).
Requirements
- Node.js 22+
- pnpm (via corepack or global install)
- Chart widgets: npm
@fusedashlabs/widgets(aliased as@ui9000/widgetsat build time)
Quick Start (<10 min)
Install:
pnpm install pnpm build:mcp-appEnvironment:
cp env.local .env # or env.prod / .env.exampleHTTP/SSE (signed data-links + local HTTP):
pnpm run mcp:serveDefault port from
.env(often8088or5173). Expect:MCP Server running over SSE on port …Stdio (Claude Desktop / Cursor local MCP):
pnpm run mcp:serve:stdioStdio alone does not serve
/v1/data-links. Keeppnpm mcp:serverunning (or pointMCP_BASE_URLat a deployed HTTP instance) whenever charts need signed payloads. Data-link files live undermcp-ui/.data(package root), notprocess.cwd(), so Cursor/Claude stdio andmcp:serveshare the same store.
Install in Claude Desktop (from a clone)
Prefer npx (see Install in Cursor or Claude (npm)). Clone + stdio is for local development.
Edit claude_desktop_config.json (absolute paths; prefer tsx over pnpm to avoid Corepack version checks):
{
"mcpServers": {
"UI9000-MCP": {
"command": "/absolute/path/to/mcp-ui/node_modules/.bin/tsx",
"args": ["/absolute/path/to/mcp-ui/src/mcp-stdio.ts"],
"env": {
"MCP_BASE_URL": "http://localhost:8088"
}
}
}
}Or install the .mcpb bundle (see MCPB pack) via Claude’s extension install dialog.
Install in Cursor (from a clone)
Prefer npx (see Install in Cursor or Claude (npm)). Clone + tsx is for local development.
Add to ~/.cursor/mcp.json (or project .cursor/mcp.json). Prefer invoking tsx directly (avoids Corepack/pnpm version mismatch when Cursor spawns the process):
{
"mcpServers": {
"UI9000-MCP": {
"command": "/absolute/path/to/mcp-ui/node_modules/.bin/tsx",
"args": ["/absolute/path/to/mcp-ui/src/mcp-stdio.ts"],
"env": {
"MCP_BASE_URL": "http://localhost:5173"
}
}
}
}Run pnpm install once so node_modules/.bin/tsx exists. Keep pnpm mcp:serve running for signed data-links.
Match MCP_BASE_URL / PORT to your running mcp:serve. Cursor may show tool results as links/text; full MCP Apps iframe rendering varies by host (see Known limitations).
Prompt examples (copy-paste)
| # | Prompt | Expected tool / chart |
|---|--------|------------------------|
| 1 | Create a line chart of monthly sales: Jan 10, Feb 20, Mar 15. Title "Sales trend". | generate_simple_single_series_chart → lineChart (inline @ui9000/widgets) |
| 2 | Pie chart of share: Alpha 40, Beta 35, Gamma 25. Title "Market share". | generate_pie_chart → pie (@ui9000/widgets) |
| 3 | Load this CSV as a dataset then chart revenue by region with a bar chart: (paste small CSV or use load_dataset then chart with datasetId) | load_dataset → generate_simple_single_series_chart / bar |
Screenshot
Add a PNG of a rendered chart in Claude (MCP Apps) at docs/screenshots/claude-chart.png when available. Until then, verify locally: tool result _meta.dataUrl in the MCP App View.
Transport support
| Transport | Command / endpoint | FuseDash | Claude Desktop | Cursor |
|-----------|--------------------|----------|----------------|--------|
| Streamable HTTP | POST /mcp via mcp:serve | Yes | Supported | Possible via URL config |
| SSE | GET /sse via mcp:serve | Yes | Supported | Possible via URL config |
| Stdio | pnpm mcp:serve:stdio | No | Primary local path | Primary local path |
P13 verification: automated IT covers list-tools / read ui://ui9000/chart / call chart tool on in-memory, Streamable HTTP, SSE, and stdio (src/mcp/transport.integration.test.ts). Claude Desktop visual check: install via stdio config above (or .mcpb), confirm MCP App View loads; for HTTP use local mcp:serve URL if the host allows remote MCP.
Chart tools advertise _meta.ui.resourceUri: ui://ui9000/chart. CSP connectDomains is MCP_BASE_URL plus Mapbox / PMTiles origins (frameDomains is empty — widgets fetch JSON, no nested charts app). Maps join against GET /geojson/{country,state,county,province,city}.json (centroids) and paint choropleth fills from GET /pmtiles/*.pmtiles (CORS proxy; centroids cannot fill). Country/state/province/county collections are also inlined on the signed data-link so the join does not depend on a second fetch.
Render: @ui9000/widgets in the MCP App View. Unsupported chartType values show the View empty state (no apps/charts iframe).
Rebuild the View after editing src/mcp-app/ or bumping @fusedashlabs/widgets:
pnpm install
pnpm build:mcp-appMCPB pack (P28)
pnpm pack:mcpbProduces dist/mcp-ui.mcpb from manifest.json (stdio entry via pnpm + this directory). Requires Node 22 + pnpm on the install host. User config prompts for MCP_BASE_URL.
GitHub release (P29)
Push a tag mcp-v* or v* (or run Release mcpb workflow dispatch) to:
- Pack
dist/mcp-ui.mcpband attach it to a GitHub Release - Trigger the existing
ci.ymltag path that deploys HTTP/SSE to staging (mcp-v*)
Remote clients: use staging MCP_BASE_URL → GET /sse or POST /mcp, plus /v1/data-links for chart payloads.
Known limitations
| Topic | Detail |
|-------|--------|
| Map charts | Inline <ui9000-map-chart> — token + /geojson + /pmtiles (CORS proxy) |
| Host UI | Claude MCP Apps render ui://; Cursor/VS Code may only show tool text/dataUrl |
| Stdio vs data-links | Chart tools need HTTP MCP_BASE_URL reachable for signed payloads |
| Widget coverage | Unsupported types show empty state in the MCP App View — no charts iframe |
| SSE “reconnect” | GET /sse?sessionId=… returns 400 — clients must open a new SSE session |
Host verification (P31)
Same stdio / HTTP / .mcpb artifacts for every host — no per-host code forks.
| Host | Install path | Tools / data-links | MCP Apps UI (ui://) | Status |
|------|--------------|--------------------|----------------------|--------|
| Claude Desktop | Stdio or .mcpb | Yes (with HTTP MCP_BASE_URL) | Yes (primary) | Verified path (P13 + IT) |
| Cursor | Stdio (~/.cursor/mcp.json) | Yes | Partial / host-dependent | Smoke locally |
| VS Code Insiders (MCP) | Stdio or HTTP URL | Expected yes | Depends on MCP Apps support in extension | Not fully verified — re-test after release |
| ChatGPT (MCP Apps) | Remote HTTP when host allows | Expected yes | Depends on host CSP / Apps support | Not fully verified — use same HTTP artifact |
| Goose | Stdio or HTTP | Expected yes | Host-dependent | Not fully verified — no Goose-specific code |
If a host needs a code branch, fix handshake/CSP in P1 instead of forking.
Production secret check (P04)
# Offline: ensure env.prod (or ENV_FILE) is not the known-dev default — never prints the secret
pnpm check:data-link-secret
ENV_FILE=env.prod pnpm check:data-link-secretHTTP/stdio entrypoints refuse to start when NODE_ENV/MCP_ENV/ENV_FILE look production-like and MCP_DATA_LINK_SECRET is missing, too short, or a known default (dev-secret-key-for-testing, secret, …).
Known limitations
Build the image:
docker build --build-arg ENV_FILE=env.prod -t mcp-ui .The build process will extract the
PORTvalue from yourenv.prodfile and display it in the logs.Run the container:
docker run -p 5173:5173 mcp-uiReplace
5173with the actualPORTvalue from yourenv.prodfile (shown during build).With persistent storage:
# Windows PowerShell docker run -p 5173:5173 -v ${PWD}/.data:/app/.data mcp-ui # macOS/Linux docker run -p 5173:5173 -v $(pwd)/.data:/app/.data mcp-uiRun in background:
docker run -d -p 5173:5173 --name mcp-ui-container mcp-ui
Environment Variables
Create a .env file in the project root with the following variables:
| Variable | Description | Default |
|----------|-------------|---------|
| PORT | Port for the Express MCP server | 8088 |
| MCP_BASE_URL | Base URL for signed data links. npx CLI defaults to https://mcp.ui9000.com | http://localhost:8088 (mcp:serve) |
| MCP_DATA_LINK_SECRET | Secret key for signing data links | dev-secret-key-for-testing |
| CORS_ALLOWED_ORIGINS | Comma-separated list of allowed origins or * | * |
| STORAGE_DIR | Directory for storing chart payloads (relative to project root) | .data |
| DATA_LINK_TTL_HOURS | Time-to-live for data links in hours | 24 |
| MAX_PAYLOAD_SIZE_MB | Maximum payload size in megabytes for data links | 1.5 |
| MCP_UI_RESOURCE_MIME | MCP Apps MIME (mcp-app) | mcp-app (npx) |
Example .env file:
PORT=5173
STORAGE_DIR=.data
MCP_BASE_URL=http://localhost:5173
MCP_DATA_LINK_SECRET=your-secret-key-here
DATA_LINK_TTL_HOURS=24
MAX_PAYLOAD_SIZE_MB=1.5
CORS_ALLOWED_ORIGINS=https://charts.example.com,https://mcp.example.comAPI Endpoints
Data Links
Create a data link:
POST /v1/data-links
Content-Type: application/json
{
"config": { ... },
"data": { ... },
"meta": {
"source": "mcp",
"expiresInHours": 24
}
}Response:
{
"id": "abc123",
"dataUrl": "http://localhost:5173/v1/data-links/abc123?sig=...&exp=..."
}Retrieve a data link:
GET /v1/data-links/:id?sig=...&exp=...Response:
{
"id": "abc123",
"config": { ... },
"data": { ... },
"createdAt": "2024-01-01T00:00:00Z",
"ttlSeconds": 86400,
"meta": { ... }
}Health Checks
GET /health- Detailed health status with statsGET /health/ready- Readiness probeGET /health/live- Liveness probe
MCP Tools (SSE)
The MCP server exposes tools via Server-Sent Events (SSE):
- SSE Endpoint:
GET /sseandPOST /sse/message - Alias:
GET /mcpandPOST /mcp/message
Available Chart Tools
All tools are defined in src/mcp/registerTools.ts and include:
- Basic Charts: Bar, Line, Lollipop, Area (simple, grouped, stacked, cumulative)
- Advanced Charts: Sankey, Treemap, Histogram, Bubble, Scatterplot, Boxplot, Punchcard
- Specialized: SparkLine, SparkArea, KPIs, Map (with multiple layer types)
- ML/Stats: KS Plot, ROC Curve, Gini Impurity & Entropy, Violin, Partial Dependence, Bias-Variance Tradeoff
- Q-Q Plot: Scatterplot with reference line option
All tools return an externalUrl UI resource that embeds the charting application with the signed data link.
Storage
Chart payloads are stored as JSON files in the STORAGE_DIR directory (default: .data/):
- Each data link is saved as
{id}.json - Files are automatically cleaned up when expired (TTL check on access)
- Storage directory is created automatically if it doesn't exist
Location: {STORAGE_DIR}/{id}.json relative to the project root (or container working directory).
Development
Scripts
pnpm run mcp:serve- Run the MCP Node.js server (Express + SSE)pnpm run dev- Run the React Router UI in development modepnpm run build- Build the React Router applicationpnpm run typecheck- Type check the codebasepnpm test- Run testspnpm run lint- Lint codepnpm run lint:fix- Fix linting issues
Project Structure
src/
├── mcp-node.ts # Express server + MCP initialization
├── mcp/
│ └── registerTools.ts # MCP tool definitions
├── api/
│ ├── healthCheck.ts # Health check handlers
│ └── handleDataLink.ts # Data link API handlers
└── utils/
├── dataLinkStore.ts # Filesystem-based TTL storage
├── dataLinkHandlers.ts # CORS, validation, rate limiting
└── createChartData.ts # Helper for chart data linksDocker Details
Build Process
The Dockerfile:
- Extracts the
PORTvalue from the specifiedENV_FILEduring build - Displays the extracted port in build logs
- Copies the environment file as
.envfor runtime use - Runs the MCP server with
pnpm run mcp:serve
Build Arguments
ENV_FILE- Environment file to use (default:env.prod)
Example Build & Run
# Build with custom env file
docker build --build-arg ENV_FILE=env.prod -t mcp-ui .
# Check the PORT value in build logs (look for "PORT extracted from env.prod: XXXX")
# Run with the extracted port
docker run -p 5173:5173 mcp-ui
# Run with persistent storage
docker run -p 5173:5173 -v ${PWD}/.data:/app/.data mcp-ui
# Run in background
docker run -d -p 5173:5173 --name mcp-ui-container mcp-ui
# View logs
docker logs mcp-ui-container
# Stop container
docker stop mcp-ui-containerSecurity Notes
- Use a strong
MCP_DATA_LINK_SECRETin production - Restrict
CORS_ALLOWED_ORIGINSto specific domains instead of* - Adjust
DATA_LINK_TTL_HOURSbased on your data retention requirements - Rate limiting can be enabled in
src/utils/dataLinkHandlers.ts
FAQ
Q: Where are data files stored?
A: In the STORAGE_DIR directory (default: .data/) relative to the project root. Files are named {id}.json.
Q: How are charts rendered?
A: MCP Apps View (ui://ui9000/chart) plus _meta.dataUrl. There is no apps/charts iframe.
Q: How do I enforce strict CORS?
A: Set CORS_ALLOWED_ORIGINS to a comma-separated list of allowed domains: CORS_ALLOWED_ORIGINS=https://example.com,https://other.com
Q: What port does the server use?
A: The port is read from the PORT environment variable (in .env file). Default is 8088.
Q: How do I check if the server is running?
A: Visit http://localhost:<PORT>/health or use the health check endpoints.
Publish to npm
From this repo (must be logged in to an npm user/org that can publish @fusedashlabs/*):
pnpm install
pnpm build:npm
npm publish --access publicCI: push a v* / mcp-v* tag or run the Publish npm workflow. Set GitHub secret NPM_TOKEN.
After publish, Cursor/Claude use npx -y @fusedashlabs/ui9000-mcp with server key UI9000-MCP (see top of this README).
License
Apache-2.0
