Excalidraw MCP Server
A high-performance, backend only Model Context Protocol (MCP) server for programmatically generating, editing, and managing Excalidraw v2 diagrams with obstacle aware arrow routing, font accurate text containment, and automated layout resolution.
Overview
Excalidraw MCP empowers LLM agents (Claude, Cursor, OpenCode, Codex, and custom AI agents) to create, inspect, manipulate, and export rich Excalidraw diagrams directly from chat and automated workflows.
Built on FastMCP using Hatchling as the build backend, it runs entirely local via standard I/O (stdio). It requires no browser, no headless Chromium, and no third party cloud hosting, rendering deterministic, production grade .excalidraw (schema v2) documents safely and atomically.
Key Capabilities
- Obstacle Aware Orthogonal Arrow Routing: Manhattan A* corridor pathfinding routes connectors cleanly around intermediate boxes, frames, and text with configurable clearance ($15\text{px}$). Arrows attach perpendicularly to cardinal faces (top, bottom, left, right) on rectangles, diamonds, and ellipses, preventing clipped arrowheads and wire collisions.
- Multi Edge Parallel Lanes: Automatically routes multiple connectors between the same components into distinct, parallel tracks ($20\text{px}$ lane spacing) so arrows and labels never superimpose.
- Collision Free Label Positioning: Ranks corridor segments to position connector labels along the longest clear segment away from arrowheads and obstacle boxes.
- Font Aware Text Containment: Accurate FreeType/Pillow font measurement (supporting Excalifont, Helvetica, Cascadia, and Comic Shanns) automatically wraps text and expands shapes outward from center. Rhombus and ellipse bounding geometry ensures labels never escape container borders.
- Two Pass Architecture Layout Engine: Content first sizing calculates text dimensions before positioning containers, automatically aligning multi tier architectures (Main Stack, Side Columns, Banners) with zero overlaps.
- Dynamic 8K Canvas Scaling: Content aware export bounds calculate margins from all visible elements, generating crystal clear high resolution PNGs up to 8192px without downscaling or text truncation.
- Automated Geometry Diagnostics:
check_diagram_geometryautomatically detects shape to shape overlaps, gap violations, frame containment escapes, and connector collisions. - Mermaid & Architecture Shorthand: Directly parse Mermaid flowcharts (
draw_mermaid) or shorthand prompts (draw_architecture) into structured, fully editable Excalidraw scenes. - Syntax Highlighted Code Blocks:
add_code_blockrenders syntax highlighted code with line numbers, dark/light themes, and window controls. - Interactive Live Canvas & WebSocket Sync: Built in HTTP and WebSocket server (
start_live_server) synchronizes canvas changes with browser clients in real time. - End-to-End Encrypted Sharing: Client side 128 bit AES-GCM encryption (
export_to_excalidraw_url) publishes shareable links directly to excalidraw.com. - Atomic Operations & In-Memory Undo: Non blocking transactions with process local undo history (
undo) and memory budgets. - Automatic Agent Discovery: Ships with a root
mcp.jsonanduvxsupport for zero config onboarding across modern AI coding environments.
Architecture & Workflow
flowchart TD
subgraph Agent["AI Agent / Host Client"]
Client["Claude / Cursor / OpenCode / Codex"]
end
subgraph Server["Excalidraw MCP Server (stdio)"]
FastMCP["FastMCP Framework"]
Service["Scene Service & Transaction Guard"]
State[("In-Memory SceneState")]
History[("Undo History Stack")]
subgraph LayoutEngines["Layout & Routing Engines"]
Router["Orthogonal Router (A* Manhattan)"]
Containment["Text Containment & Font Metrics"]
AutoLayout["Two-Pass Architecture Layout"]
Diagnostics["Geometry Diagnostics"]
end
Renderer["PNG Renderer (8K Dynamic Scaling)"]
Storage["Atomic Storage Manager"]
end
subgraph Output["Artifacts & UI"]
Disk[("Local Storage (.excalidraw)")]
PNG["Rendered High-Res PNG"]
WebShare["excalidraw.com (AES-GCM)"]
LiveCanvas["Browser Live Canvas (WebSocket)"]
end
Client <-->|"MCP Protocol (JSON-RPC over stdio)"| FastMCP
FastMCP --> Service
Service <--> State
Service <--> History
Service --> LayoutEngines
Service --> Renderer
Service --> Storage
Storage --> Disk
Renderer --> PNG
Service --> WebShare
Service --> LiveCanvas
Agent Workflow
- Creation: Agent invokes
draw_graph,draw_architecture,draw_mermaid, or primitive shape tools (add_rectangle,add_circle,connect). - Layout & Routing: Layout engine positions components into balanced tiers and routes connectors through obstacle free corridors with parallel lane separation.
- Inspection & Revision: Agent verifies element placement using
get_scene,get_element, orcheck_geometry. If adjustments are needed, it callsmove,resize,update_element,reroute_connectors, orundo. - Persistence or Export: The scene is saved locally with
save(name="arch-v1"), exported as a high resolution image viaexport_png/preview, or published to excalidraw.com viaexport_to_excalidraw_url.
Quick Start
Run instantly without cloning the repository or managing virtual environments using uvx:
uvx --from git+https://github.com/Dharshan2208/excalidraw-mcp.git excalidraw-mcp
Or install it globally to your FastMCP environment:
fastmcp install mcp.json
Client Configuration
Add excalidraw-mcp to your favorite MCP client configuration file:
Claude Desktop
Add this to claude_desktop_config.json:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"excalidraw": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/Dharshan2208/excalidraw-mcp.git",
"excalidraw-mcp"
]
}
}
}
Cursor
Add to .cursor/mcp.json in your project root or user configuration:
{
"mcpServers": {
"excalidraw": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/Dharshan2208/excalidraw-mcp.git",
"excalidraw-mcp"
]
}
}
}
OpenCode
Add to opencode.json (or the MCP configuration section of your OpenCode settings):
{
"mcpServers": {
"excalidraw": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/Dharshan2208/excalidraw-mcp.git",
"excalidraw-mcp"
]
}
}
}
Codex / Generic Agent Configuration
For CLI agents and runners accepting standard mcpServers JSON:
{
"mcpServers": {
"excalidraw": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/Dharshan2208/excalidraw-mcp.git",
"excalidraw-mcp"
],
"env": {
"EXCALIDRAW_HISTORY_LIMIT": "25"
}
}
}
}
Automatic Agent Discovery
This repository comes pre-packaged with a root mcp.json descriptor:
{
"mcpServers": {
"excalidraw": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/Dharshan2208/excalidraw-mcp.git",
"excalidraw-mcp"
]
}
}
}
When an MCP compliant agent tool (e.g. fastmcp list mcp.json, Cursor, or Zed) scans your workspace, it will automatically detect the server and register all 46 tools with zero manual configuration.
MCP Tools Reference
The server exports 46 typed tools designed with strict JSON schema bounds and explicit safety hints (readOnly, destructive, idempotent).
1. High Level Generation & Design Guidance
| Tool | Inputs | Outputs | Description |
|---|---|---|---|
draw_graph |
nodes: list[GraphNode]edges: list[GraphEdge]clusters: list[GraphCluster] = Nonelayout: str = "layered" (layered, horizontal, vertical)direction: str = "horizontal" (horizontal, vertical)elbowed: bool = False`roundness: str |
None = None` | GraphResult{ revision, element_count, node_map, edge_map, cluster_map } |
draw_mermaid |
mermaid_code: str`direction: str |
None = None<br>layout: str = "layered"<br>elbowed: bool = False<br>roundness: str |
None = None` |
draw_architecture |
prompt: str (e.g., "box: Frontend, db: Postgres")wrap_threshold: int = 4Optional styles ( strokeColor, backgroundColor, etc.) |
ArchitectureResult{ revision, element_count, components } |
Rapid diagram generator parsing comma/newline-separated shorthand syntax (box, circle, diamond) with grid-wrapping and automatic obstacle-avoiding connectors. |
read_diagram_guide |
`topic: str | None = None (layout, colors, typography, grouping, arrows, spacing` ) |
DiagramGuideResult{ topic, guide, available_topics } |
2. Primitive Shapes, Text, Code Blocks & Frames
| Tool | Inputs | Outputs | Description |
|---|---|---|---|
add_rectangle |
x: float, y: float, w: float, h: floatlabel: str = "" + styles + roundness: str = None, frame_id: str = None |
CreateResult{ rectangle_id, label_id } |
Adds a rectangle to the active scene with an optional centered bound label, adaptive/round corner roundness, frame membership, and automatic outward expansion. |
add_circle |
x: float, y: float, size: floatlabel: str = "" + styles + frame_id: str = None |
CreateResult{ circle_id, label_id } |
Adds a circle (ellipse) with an optional centered bound label, ellipse inscribed boundary containment, and frame membership. |
add_diamond |
x: float, y: float, w: float, h: floatlabel: str = "" + styles + roundness: str = None, frame_id: str = None |
CreateResult{ diamond_id, label_id } |
Adds a decision diamond with rhombus inscribed boundary containment, adaptive roundness, and frame membership. |
add_arrow |
x: float, y: float, x2: float, y2: floatlabel: str = "" + styles + elbowed: bool = False |
CreateResult{ arrow_id, label_id } |
Adds a directional arrow (straight or elbowed) with an optional corridor positioned label. |
add_text |
x: float, y: float, content: strfont_size: int = 20 + styles + frame_id: str = None |
CreateResult{ text_id } |
Creates a standalone text element with accurate FreeType font metrics and optional frame membership. |
add_code_block |
code: strlanguage: str = "python"x: float = 0, y: float = 0title: str = Nonetheme: str = "dark"font_size: int = 14line_numbers: bool = Truew: float = None, h: float = Noneframe_id: str = None |
CodeBlockResult{ status, container_id, text_id, width, height, revision } |
Adds a syntax highlighted code block with window controls, configurable themes, line numbers, and monospace typography. |
connect |
source_id: str, target_id: strlabel: str = "" + styles + elbowed: bool = False |
ConnectResult{ arrow_id, label_id } |
Binds two existing shapes with obstacle aware orthogonal routing, cardinal face boundary docking, and multi edge lane separation. |
add_frame |
name: str = "", x: float, y: float, w: float, h: floatelement_ids: list[str] = None |
CreateResult{ frame_id } |
Creates an Excalidraw frame container and optionally parents specified elements inside it. |
3. Scene Editing & Manipulation
| Tool | Inputs | Outputs | Description |
|---|---|---|---|
duplicate_elements |
element_ids: list[str]offset_x: float = 20.0offset_y: float = 20.0 |
DuplicateResult{ status, original_ids, duplicated_ids, id_mapping, revision } |
Duplicates multiple elements with spatial offsets, relationship remapping, arrow endpoint retargeting, bound label cloning, and group isolation. |
move |
element_id: str, x: float, y: float |
MoveResult{ status, element_id, x, y } |
Repositions an element. Moving a frame moves all contained elements; moving grouped elements moves group members together; rejects locked elements. |
resize |
element_id: str, width: float, height: float |
ResizeResult{ status, element_id, width, height } |
Resizes a shape or frame, recalculates label centering, and reroutes connected arrows; rejects locked elements. |
update_element |
element_id: str`label: str |
None = None+ styles +roundness, elbowed, frame_id` |
UpdateResult{ status, element_id, label_id } |
delete |
element_id: str |
DeleteResult{ status, deleted_ids } |
Deletes an element and its bound label; unparents frame children if a frame is deleted; cleans up references in connected arrows; rejects locked elements. |
group_elements |
element_ids: list[str] |
GroupResult{ status, group_id, element_ids, revision } |
Groups multiple elements together so that movements and transformations apply cohesively. |
ungroup_elements |
element_ids: list[str] = Nonegroup_id: str = None |
UngroupResult{ status, affected_elements, removed_group_ids, revision } |
Disbands an element group by group_id or removes outermost groups from specified elements. |
lock_elements |
element_ids: list[str] |
LockResult{ status, element_ids, revision } |
Locks elements and bound labels to protect them from unintended moves, resizes, updates, or deletions. |
unlock_elements |
element_ids: list[str] |
UnlockResult{ status, element_ids, revision } |
Unlocks previously locked elements and bound labels for subsequent editing. |
batch_mutate |
operations: list[dict] |
BatchResult{ status, operations_executed, ref_map, results, revision } |
Atomically executes multiple creation, connection, movement, resize, lock, group, duplicate, or update actions in a single revision step with dynamic client_id resolution. |
clear |
None | DeleteResult{ status } |
Wipes the entire scene, active elements, and embedded state. |
undo |
None | UndoResult{ status, revision } |
Rolls back the scene to the state immediately preceding the latest mutation. |
4. Inspection, Viewport, Query, Export & Sharing
| Tool | Inputs | Outputs | Description |
|---|---|---|---|
describe_scene |
detailed: bool = True |
SceneDescriptionResult{ summary, element_count, layout_type, elements, connections, bounds, revision } |
Produces an AI-readable summary of elements, positions, labels, flow connections, topological layout, and bounds. |
get_scene_summary |
None | SceneSummaryResult{ element_count, type_counts, frames, groups, connectors, code_blocks, standalone_elements, bounds, description, revision } |
Provides a structured semantic overview and topological understanding of the entire diagram for AI agent reasoning. |
get_resource |
`uri: str | None = None<br>resource_type: str |
None = None<br>identifier: str |
import_scene |
data: strmode: str = "replace" (replace, merge)offset_x: float = 0.0offset_y: float = 0.0 |
ImportResult{ status, mode, elements_imported, total_elements, id_mapping, revision } |
Imports Excalidraw scenes from raw JSON, .excalidraw files, Obsidian .excalidraw.md files, or embedded PNGs with reference preservation and collision remapping. |
export_to_excalidraw_url |
`name: str | None = None` | ShareUrlResult{ status, url, id, revision } |
get_viewport |
None | ViewportResult{ scroll_x, scroll_y, zoom, view_background_color, revision } |
Inspects current canvas camera scroll coordinates, zoom factor, and background color. |
set_viewport |
scroll_x: float = Nonescroll_y: float = Nonezoom: float = Nonebackground_color: str = None |
ViewportResult{ scroll_x, scroll_y, zoom, view_background_color, revision } |
Updates canvas camera viewport scroll offsets, zoom level, or background canvas color. |
focus_elements |
element_ids: list[str] = Nonepadding: int = 50 |
FocusResult{ status, scroll_x, scroll_y, zoom, bounding_box, focused_element_ids, revision } |
Automatically calculates bounding box, centers camera, and sets fit zoom to frame specified elements or the entire diagram. |
find_elements |
`query: str | None<br>type: str |
None<br>frame_id: str |
list_fonts |
`category: str | None (sans-serif, monospace, serif, handwriting`) |
FontListResult{ fonts, total, defaults } |
get_scene |
offset: int = 0, limit: int = 50 |
ScenePage{ elements, total, offset, limit, has_more, revision } |
Paginated inspection of all elements currently on the canvas. |
get_element |
element_id: str |
ElementResult{ element, revision } |
Fetches details and properties of a single element by its ID. |
list_saved_scenes |
None | SavedScenesResult{ scenes: list[str] } |
Returns a list of all scene files stored in the persistent directory. |
save |
name: str, overwrite: bool = False |
SaveResult{ status, name, path } |
Atomically writes the scene to server disk in .excalidraw JSON format. |
load |
name: str |
LoadResult{ status, name, elements } |
Atomically loads a saved .excalidraw file and sets it as the active scene. |
export_scene |
None | EmbeddedResource( application/vnd.excalidraw+json) |
Emits the entire .excalidraw schema v2 document directly into the MCP message. |
preview |
scale: float = 1.0padding: int = 40background: bool = True |
ImageContent( image/png) |
Renders an inline PNG image preview of the active Excalidraw canvas directly into the chat conversation with dynamic canvas scaling up to 8K. |
get_canvas_screenshot |
scale: float = 1.0padding: int = 40background: bool = True |
ImageContent( image/png) |
Alias for preview. Renders the current canvas into an inline PNG screenshot containing embedded application/vnd.excalidraw+json metadata. |
export_png |
name: str`directory: str |
None = None<br>scale: float = 1.0<br>padding: int = 40<br>background: bool = True<br>overwrite: bool = False` |
PngExportResult{ status, name, path, bytes, width, height } |
5. Live Canvas Server & Browser Synchronization
| Tool | Inputs | Outputs | Description |
|---|---|---|---|
start_live_server |
host: str = "127.0.0.1"port: int = 8080 |
LiveServerResult{ status, url, host, port } |
Launches the interactive browser connected live canvas server with real-time WebSocket synchronization. |
stop_live_server |
None | LiveServerResult{ status } |
Gracefully shuts down the background live canvas server and closes active client connections. |
get_live_server_status |
None | LiveServerResult{ is_running, url, host, port, active_clients } |
Reports operational status, connection URL, and connected client count of the live canvas server. |
MCP Resources
In addition to tools, the server exposes saved scenes via standard MCP resource URIs:
- URI Template:
excalidraw://scenes/{name}.excalidraw - MIME Type:
application/vnd.excalidraw+json
Clients can directly read saved scenes into their context window without altering the active working canvas.
Environment Variables & Configuration
The server operates with secure defaults and zero required environment variables. Fine tune performance and resource limits via the environment:
| Variable | Default | Allowed Values | Description |
|---|---|---|---|
EXCALIDRAW_SCENE_DIR |
Platform User Data | Absolute path | Directory where scenes are persisted. Defaults to $XDG_DATA_HOME/excalidraw-mcp/scenes on Linux, ~/Library/Application Support/excalidraw-mcp/scenes on macOS, and %APPDATA%\excalidraw-mcp\scenes on Windows. |
EXCALIDRAW_EXPORT_DIR |
$EXCALIDRAW_SCENE_DIR/exports |
Absolute path | Configurable directory where PNG diagram exports are saved by default. |
EXCALIDRAW_DEFAULT_FONT |
Platform Sans Font | Font family name | Preferred system font family for regular diagram text and labels. |
EXCALIDRAW_MONO_FONT |
Platform Monospace Font | Font family name | Preferred monospace font family for code blocks and code annotations. |
EXCALIDRAW_FONT_DIR |
None | Directory path | Additional custom directory scanned for TrueType (.ttf) and OpenType (.otf) fonts. |
EXCALIDRAW_HISTORY_LIMIT |
20 |
1–100 |
Maximum number of in-memory undo snapshots retained per session. |
EXCALIDRAW_MAX_HISTORY_BYTES |
104857600 (100 MiB) |
1 KiB–500 MiB | Shared memory ceiling allocated for serialized undo states. |
EXCALIDRAW_MAX_SCENE_BYTES |
26214400 (25 MiB) |
1 KiB–100 MiB | Maximum file size permitted when saving or loading scene files. |
EXCALIDRAW_MAX_EXPORT_BYTES |
26214400 (25 MiB) |
1 KiB–100 MiB | Maximum payload size permitted for export_scene and resource reads. |
EXCALIDRAW_MAX_ELEMENTS |
10000 |
1–100,000 |
Hard cap on the total number of elements allowed per scene. |
Local Development & Debugging
Prerequisites
- Python
>= 3.12 uv(recommended) orpip
1. Clone & Set Up Environment
git clone https://github.com/Dharshan2208/excalidraw-mcp.git
cd excalidraw-mcp
# Synchronize dependencies with uv
uv sync
# Or with pip & virtualenv
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
2. Run the Test Suite
The test suite contains 353 comprehensive unit and contract tests verifying Excalidraw serialization, obstacle aware arrow routing, text containment and font metrics, two pass architecture layout, 8K dynamic scaling, storage safety, PNG export, code blocks, live server synchronization, sharing, and MCP schemas:
uv run --locked python -B -m unittest discover -s tests -v
3. Interactive Debugging with FastMCP Inspector
Inspect schemas, invoke tools interactively, and test stdio communications using the built-in FastMCP web inspector:
uv run fastmcp inspect mcp.json
Or list discovered tools:
uv run fastmcp list mcp.json
4. Packaging Verification
Test source distribution and wheel generation with Hatchling:
uv build
Contributing
Contributions are warmly welcomed! Please follow these guidelines:
- Fork the Repository: Create a descriptive feature branch (
git checkout -b feat/custom-styling). - Adhere to Code Standards: Ensure clean type annotations, preserve existing docstrings, and maintain standard exception hierarchies.
- Verify Tests: Run
uv run --locked python -B -m unittest discover -s tests -vand ensure all 353 tests pass. - Compile Check: Confirm no byte compilation warnings via:
uv run python -B -m compileall -q -f main.py excalidraw_mcp tests
- Submit a Pull Request: Provide a concise summary of the rationale and changes.
Standards & License
- Protocol Specification: Complies with the official Model Context Protocol Specification.
- Document Format: Serializes to standard Excalidraw schema v2.
- License: Released under the MIT License.
Metadata
Release files for exca-mcp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| exca_mcp-0.1.0.tar.gz | 310.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| exca_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 502.3 kB
Release files / exca_mcp-0.1.0.tar.gz
| Download URL | exca_mcp-0.1.0.tar.gz |
|---|---|
| Size | 310.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
064b869d43c204a7b9118eab60c1cd72a60df19255d08920579276c9620726f6
|
|
BLAKE2b-256 checksum How to use checksums |
a4a54465428aa888f11ca46afad4a3b96359269b813af6b05c1992ad140b2285
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.11
|
Release files / exca_mcp-0.1.0-py3-none-any.whl
| Download URL | exca_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 191.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bc1e4259ca79bc6966968b294ca40cf6a1eeb4b57288fae4995808b3b64a2899
|
|
BLAKE2b-256 checksum How to use checksums |
6bc540d53b6fe810c4d4066c9af057e78105985299928fe1b7058ff49b2b221c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.11
|
Security for exca-mcp 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.