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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。