mermaid-mcp
Fast Mermaid diagram validation and rendering (PNG/SVG) using jsdom and sharp, no Chromium needed.
README
mermaid-mcp
Custom Mermaid MCP server — fast validation + PNG/SVG rendering via jsdom + sharp, no Chromium needed.
Replaces the broken @rtuin/mcp-mermaid-validator that consistently timed out (MCP error -32001) for every invocation, even on trivially small diagrams. The root cause was shell + npx overhead spawning Chromium/Puppeteer. This server eliminates Chromium entirely — smaller image, faster renders, no browser crash risk.
Why It Exists
The original @rtuin/mcp-mermaid-validator@0.7.0 used @mermaid-js/mermaid-cli under the hood, which spawns a headless Chromium browser for every render. This caused:
- Timeouts — Chromium startup takes 2-5 seconds, exceeding MCP request timeouts
- Large image — ~1 GB Docker image (Chromium + Node.js)
- Crash risk — Headless browser crashes on certain diagram types
This server replaces Chromium with jsdom (fake DOM) + sharp (SVG → PNG rasterization), resulting in:
- Instant validation — ~50 ms (pure Node.js,
mermaid.parse()) - Fast rendering — ~30-100 ms (jsdom + sharp, no browser)
- Tiny image — ~150-300 MB (no Chromium)
- Reliable — No browser crash risk
Features
| Feature | Description |
|---|---|
validate |
Parse-only validation — instant, no rendering, no DOM needed |
render |
Full rendering to PNG (default) or SVG via jsdom + sharp, no browser |
| Stateless | Per-request McpServer + transport — no cross-request state |
| HTTP-native | Node.js built-in http module — no Express dependency |
| Docker-ready | Multi-arch (amd64 + arm64), small image, puma-net deployment |
| Structured logging | JSON logs to stderr, debug level via LOG_LEVEL env var |
Architecture
graph TD
subgraph "opencode"
A["agent<br/>calls tool via HTTP"]
end
subgraph "puma-net"
subgraph "mermaid-mcp container"
B["http.createServer"]
C["POST /mcp"]
D["McpServer (per request)"]
E["validate tool"]
F["render tool"]
G["mermaid.parse()"]
H["mermaid.render() + jsdom"]
I["sharp → PNG"]
end
end
A -->|"HTTP POST /mcp"| B
B --> C --> D
D --> E --> G
D --> F --> G
F -->|"if valid"| H --> I
I -->|"PNG buffer"| D
style G fill:#e8f5e9
style H fill:#e3f2fd
style I fill:#fff4e1
Key design decisions:
| Concern | Decision | Rationale |
|---|---|---|
| Transport | HTTP (StreamableHTTPServerTransport, port 3000) | Remote opencode access, follows hugging-kreuzberg pattern |
| HTTP server | Node.js built-in http |
No Express dependency, per user request |
| Validation | mermaid.parse() |
Pure Node.js, instant, no DOM |
| Rendering | jsdom + sharp (no Chromium) | Mermaid v11+ render() works with jsdom; sharp rasterizes SVG→PNG |
| Docker base | node:26.3.0-slim |
Latest stable, no Chromium — tiny image, fast pulls |
| Output | PNG default, SVG optional | PNG for inline chat; SVG for editable diagrams |
Quick Start
Prerequisites
- Node.js 26+ (see
.nvmrc) - Docker Desktop with Docker Compose
puma-netDocker network (created automatically bystart.sh)
Local Development
# Clone and install
cd ~/www/misc/mermaid-mcp
npm install
# Run locally (port 3000)
npm start
# Run tests (60 pass, 1 skip, 0 fail)
npm test
Docker Compose
# Build and start
./start.sh
# Stop
./stop.sh
# Smoke test (7 HTTP tests via curl)
./test.sh
Build and Push to Docker Hub
# Build + push (latest)
./build-and-push.sh
# Tag with version
./build-and-push.sh --tag v1.0.0
# Build only (skip push)
./build-and-push.sh --build-only
# ARM64 only
./build-and-push.sh --platform linux/arm64
# Dry run (show commands, don't execute)
./build-and-push.sh --dry-run
Tool Reference
validate
Validate a Mermaid diagram definition without rendering. Returns the detected diagram type on success or a parse error with line number on failure. Pure Node.js — instant, no browser needed.
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
diagram |
string |
Yes | Mermaid diagram definition text |
Output (valid):
{
"content": [
{
"type": "text",
"text": "Valid: flowchart"
}
]
}
Output (invalid):
{
"content": [
{
"type": "text",
"text": "Invalid: Parse error on line 3: ..."
}
],
"isError": true
}
Example (MCP JSON-RPC):
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "validate",
"arguments": {
"diagram": "graph TD\n A[Start] --> B[End]"
}
}
}'
render
Render a Mermaid diagram to PNG or SVG. Validates first, then renders via jsdom (no browser needed). PNG output uses 2x DPI (density 144) for crisp images.
Input:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
diagram |
string |
Yes | — | Mermaid diagram definition text |
format |
enum |
No | png |
Output format — png (raster) or svg (vector) |
backgroundColor |
string |
No | transparent |
Background color for PNG (CSS color value, e.g. "white" or "transparent") |
Output (PNG):
{
"content": [
{
"type": "text",
"text": "Rendered as PNG (12345 bytes)"
},
{
"type": "image",
"data": "<base64-encoded PNG>",
"mimeType": "image/png"
}
]
}
Output (SVG):
{
"content": [
{
"type": "text",
"text": "Rendered as SVG (5678 bytes)"
},
{
"type": "image",
"data": "<base64-encoded SVG>",
"mimeType": "image/svg+xml"
}
]
}
Output (error):
{
"content": [
{
"type": "text",
"text": "Error: Parse error on line 3: ..."
}
],
"isError": true
}
Example (MCP JSON-RPC):
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "render",
"arguments": {
"diagram": "graph TD\n A[Start] --> B[End]",
"format": "png",
"backgroundColor": "white"
}
}
}'
Development
Running Locally
# Start the server (port 3000)
npm start
# Start with debug logging
LOG_LEVEL=debug npm start
# Start on a different port
MCP_PORT=8080 npm start
Running Tests
# All tests (60 pass, 1 skip, 0 fail)
npm test
# Specific test file
node --test src/renderer.test.js
node --test src/tools.test.js
node --test src/mcp-server.test.js
Test Coverage
| Test File | Tests | Coverage |
|---|---|---|
src/renderer.test.js |
36 | Validate (6 diagram types), render SVG (6 types), render PNG (5 types + 1 skipped), error paths |
src/tools.test.js |
4 | Tool schema registration, validate handler, render handler (PNG + SVG) |
src/mcp-server.test.js |
21 | MCP protocol (initialize, tools/list, tools/call), HTTP error codes (405, 415, 413, 400) |
| Total | 60 | 60 pass, 1 skip, 0 fail |
Smoke Tests (Docker)
# Start compose, wait for server, run 7 HTTP tests
./start.sh && ./test.sh
The smoke test suite (test.sh) tests via curl against the running container:
- MCP initialize handshake
tools/listreturns 2 tools (validate, render)validate— valid flowchartvalidate— invalid diagramrender— PNG formatrender— SVG formatrender— invalid diagram
Build and Push
# Build + push to tuiteraz/mermaid-mcp:latest (multi-arch: amd64 + arm64)
./build-and-push.sh
# Build only (local image)
./build-and-push.sh --build-only
# Push with version tag
./build-and-push.sh --tag v1.0.0
# ARM64 only
./build-and-push.sh --platform linux/arm64
# Dry run (preview commands)
./build-and-push.sh --dry-run
Deployment
Local Compose
# Start (creates puma-net if missing, builds image, runs container)
./start.sh
# Stop
./stop.sh
Puma-LAN (puma-net)
The deployment lives at ~/www/olho/puma-lan/lite-llm/mcp/mermaid/ and uses the Infisical pattern for secret management.
# Deploy to puma-net
cd ~/www/olho/puma-lan/lite-llm/mcp/mermaid
./start.sh
# Stop from puma-net
./stop.sh
# Pin image digest (for reproducible deployments)
./pin-image-digest.sh
# Build and push (copies source from misc dir, builds, pushes)
./build-and-push.sh
opencode Integration
The server is configured in ~/.config/opencode/opencode.jsonc:
"mermaid": {
"type": "remote",
"url": "https://lite-llm.lan/mcp/mermaid",
"enabled": true,
"category": "validation",
"enabledTools": ["validate", "render"]
}
This replaces the old mermaid-validator entry that used @rtuin/mcp-mermaid-validator via stdio transport.
Project Structure
mermaid-mcp/
├── Dockerfile # node:26.3.0-slim + fonts-dejavu + fonts-liberation
├── docker-compose.yml # Local compose (puma-net, port 3000)
├── package.json # Pinned deps, ESM, npm scripts
├── package-lock.json # Lockfile
├── .nvmrc # Node.js 26
├── .gitignore
├── .husky/ # Git hooks
├── start.sh # Start local compose (creates puma-net if needed)
├── stop.sh # Stop local compose
├── build-and-push.sh # Build + push to Docker Hub (multi-arch)
├── test.sh # Smoke tests (7 HTTP tests via curl)
└── src/
├── mcp-server.mjs # Entry: http.createServer + StreamableHTTPServerTransport
├── tools.js # validate + render tool definitions (Zod schemas)
├── renderer.js # Render pipeline: mermaid.parse → mermaid.render → sharp
├── polyfills.js # jsdom + browser polyfills (rAF, ResizeObserver, CSSStyleSheet, SVG)
├── config.js # Frozen config object from env vars
├── logger.js # Structured JSON logging to stderr
├── mcp-server.test.js # Server tests (21: MCP protocol + HTTP error codes)
├── tools.test.js # Tool tests (4: schema + handler)
└── renderer.test.js # Renderer tests (36: validate + render, 1 skip)
Key Files
| File | Purpose |
|---|---|
src/mcp-server.mjs |
HTTP server entry point. Uses Node.js built-in http.createServer (no Express). Each request gets its own stateless McpServer + StreamableHTTPServerTransport pair. Handles POST /mcp, validates Content-Type, enforces body size limit (10 MB), graceful shutdown on SIGINT. |
src/tools.js |
MCP tool definitions. validate — parse-only validation via mermaid.parse(). render — validate + render pipeline. Input schemas use Zod. Returns MCP-formatted responses with text + image content. |
src/renderer.js |
Core rendering pipeline. validate(diagram) — uses mermaid.parse() with suppressErrors: false, returns diagram type or parse error. render(diagram, format, backgroundColor) — validates first, then mermaid.render() for SVG, then sharp for PNG conversion (density 144, compression 9). |
src/polyfills.js |
Browser environment for Mermaid in Node.js. Creates a single JSDOM instance, attaches window/document/Element to global, polyfills requestAnimationFrame, ResizeObserver, CSSStyleSheet, SVG getBBox/getCTM. Lazy-loads DOMPurify + Mermaid via dynamic import() after globals are set. |
src/config.js |
Configuration — single frozen object loaded from environment variables at startup. No process.env access outside this module. |
src/logger.js |
Structured JSON logging to stderr (stdout reserved for HTTP in Docker). logInfo always emits; logDebug only when LOG_LEVEL=debug. |
Configuration
All configuration is loaded from environment variables at startup via src/config.js. No process.env access outside this module.
| Variable | Default | Description |
|---|---|---|
MCP_PORT |
3000 |
HTTP server port |
MERMAID_DEFAULT_FORMAT |
png |
Default output format (png or svg) |
MERMAID_BACKGROUND_COLOR |
transparent |
Default background color for PNG rendering (CSS color value) |
MERMAID_SCALE |
1 |
Render scale factor |
LOG_LEVEL |
info |
Log level (info or debug) |
MCP_MAX_BODY_SIZE |
10485760 |
Maximum request body size in bytes (10 MB) |
Example:
# Development with debug logging
LOG_LEVEL=debug MCP_PORT=8080 npm start
# Production with white background
MERMAID_BACKGROUND_COLOR=white MCP_PORT=3000 npm start
Docker
Image
FROM node:26.3.0-slim
├── fonts-dejavu (~20 MB, proper Mermaid text rendering)
├── fonts-liberation (~20 MB, Arial compatibility)
├── npm ci --omit=dev (production deps only)
└── node src/mcp-server.mjs
Image size: ~150-300 MB (vs ~1 GB for mermaid-cli with Chromium)
Registry
Images are pushed to docker.io/tuiteraz/mermaid-mcp via build-and-push.sh. Supports multi-arch builds (amd64 + arm64) via docker buildx.
Known Issues
Gantt PNG Rendering Fails
Symptom: Rendering a Gantt diagram to PNG fails with an image conversion error.
Root cause: Mermaid produces viewBox="0 0 0 124" (zero width) for Gantt diagrams when rendered via jsdom. This is a mermaid rendering bug, not an issue with this server. The SVG rendering path works fine for Gantt diagrams.
Workaround: Use format: "svg" for Gantt diagrams.
Test: The test render PNG — gantt diagram is skipped (test.skip) with this note. All other diagram types render to PNG successfully.
Affected diagram type: gantt only. Flowchart, sequence, class, state, ER, and pie diagrams all render to PNG correctly.
License
This project is part of the internal tooling for the Puma LAN infrastructure.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.