excalidraw-mcp-collab
Allows an AI agent to draw on an Excalidraw board with per-board access control, respecting read-only policies.
Provides authentication via Firebase ID tokens and stores encrypted scene data and shared history in Firestore.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@excalidraw-mcp-collabdraw a rectangle on the board 'project-alpha'"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
excalidraw-access-backend
Standalone Node + TypeScript backend for a self-hosted Excalidraw fork with
per-board access control (Firebase project excalidraw-team). It provides:
MCP remote endpoint (
ALL /mcp) that lets an AI agent draw on a real collab board as a specific user. The agent's writes respect the board's read-only policy and are attributed in shared history asБот <name>.MCP connect-token mint / list / revoke endpoints.
Filesystem-backed image file service that replaces Firebase Storage, with the same per-board ACL the room server enforces.
The service never modifies the frontend or the room fork; it matches their wire formats (encryption, socket protocol, Firestore scene/history doc shapes).
How it works
Socket auth = exchanged Firebase ID token
The collab (socket.io) server authenticates clients with a Firebase ID
token and runs its own ACL on join-room. The Admin SDK can only mint a
custom token for a uid, so the bot:
admin.auth().createCustomToken(uid)exchanges it for an ID token via Identity Toolkit (
accounts:signInWithCustomToken?key=${FIREBASE_WEB_API_KEY})connects with
auth: { token: idToken }
The room server therefore resolves the bot as the user, so its existing
read/write enforcement applies automatically: a viewer-token bot's
server-broadcast frames are dropped by the room server, and this service also
refuses to broadcast/persist when the token role is viewer.
Encryption
src/encryption.ts replicates the frontend
(packages/excalidraw/data/encryption.ts) exactly using Node Web Crypto
(globalThis.crypto.subtle): a 22-char base64url AES-128-GCM key imported via
JWK { alg: "A128GCM", k, kty: "oct" }, 12-byte random IV. Verified
byte-compatible by round-trip.
Scene + history persistence
src/scene.ts ports the Admin-SDK equivalent of excalidraw-app/data/firebase.ts:
scenes/{roomId}={ sceneVersion, ciphertext, iv }(encrypted elements).shared history index
scenes/{roomId}~history+ per-entry payloadscenes/{roomId}~history~{entryId}, matchingSceneHistoryentry shape andMAX_SCENE_HISTORY_ENTRIESso the frontend HistorySidebar renders bot entries (withauthor).
Byte fields are written as Node Buffer (the Admin SDK has no web-only Bytes
class); the underlying Firestore bytesValue is identical to what the web SDK
Bytes produces, so data.ciphertext.toUint8Array() on the frontend reads the
same bytes.
Related MCP server: excalidraw-mcp
Endpoints
Method | Path | Auth | Purpose |
|
| Firebase ID token (Bearer) | Mint a connect token for |
|
| Firebase ID token | List caller's tokens. |
|
| Firebase ID token | Revoke a token the caller owns. |
|
| connect token (Bearer or | MCP Streamable HTTP endpoint; lazily attaches a |
|
| optional Firebase ID token | Store raw opaque bytes. |
|
| optional Firebase ID token | Return raw bytes. |
The file bytes are already client-encrypted + compressed; the service stores and returns them verbatim.
MCP tools
list_boards— boards the token's account can reach through the bot, with the bot's access level on each.create_board— new empty board owned by the token's account, bound to the calling bot withwritein the same batch (board doc +boardKeys+ the bot's allow-list entry commit together). Gated by the per-botcanCreateBoardsflag the owner sets in the bot's settings; ateam-visible board additionally requires the owning account to be a member of the shared team. Rate-limited to 10 boards per hour per bot (in-memory).describe_scene,query_elements— current elements (viewer + editor).fieldsprojects the columns you need andlimit/offsetpage a large scene.batch_create,update_elements,delete_elements,delete_region— single-commit batch writes (editor only; a viewer token getsread-only access). Batch covers N=1, so there are no singular create/update/delete tools.batch_createalso binds arrows to shapes inline (fromId/toId) and drops elements into a frame (frameId);update_elementsedits a container's label ({ id, label }) and honors an explicitindex, and skips ids that no longer exist (returning them inmissing) instead of aborting the batch.bring_to_front,send_to_back,reorder— z-order by re-indexing only, so ids/bindings/frame membership stay stable and a container's label rides along.group_elements,ungroup_elements,create_frame,frame_add_children— grouping/frames. New frames sink to the bottom of the z-order.
See docs/verification-tools.md for the read/measure/render/validate tools,
bound text (containerId/label), line points, and the lint rules.
Each mutating tool: applies the change (bumps version, fresh versionNonce,
updated, fractional index after the last element), broadcasts a
SCENE_UPDATE over server-broadcast, merge-persists the scene into Firestore
in a transaction (so a concurrent human session is never clobbered), and appends
a history entry attributed Бот <name>.
The bot keeps stable ownership of the ids it creates. Only for a short grace
window after it last wrote an element (RESURRECTION_WINDOW_MS) does it resist
an incoming deletion — that window covers the stale-tombstone race where an
out-of-sync live session drops a just-created element. Once the window passes,
a human deleting or editing a bot element is respected and wins immediately, so
edits made after the bot is done are never rolled back. scene_diff reports
ownership (byOrigin.bot, owned) and any contested ids (conflicts);
batch_create surfaces the same conflicts.
Setup
cp .env.example .env # fill in the values
npm install
npm run build
npm start # or: npm run devRequired env (see .env.example):
GOOGLE_APPLICATION_CREDENTIALS— absolute path to the service-account JSON (Admin SDK).FIREBASE_WEB_API_KEY— the webapiKey(AIzaSy...) from the SDK config; required for the custom-token → ID-token exchange.WS_SERVER_URL— the collab server (defaulthttp://localhost:3002).FIREBASE_PROJECT_ID(defaultexcalidraw-team),PORT,CORS_ORIGIN,DATA_DIR,PUBLIC_BASE_URL.
Mint a token + paste the MCP config
curl -X POST http://localhost:3015/mcp/tokens \
-H "Authorization: Bearer <FIREBASE_ID_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"boardId":"<roomId>"}'The response configSnippet is a ready-to-paste remote-MCP client config:
{
"mcpServers": {
"excalidraw-board": {
"type": "http",
"url": "http://localhost:3015/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}The agent connecting with that config draws on the board as the token's user.
Deploy notes
Run behind TLS and set
PUBLIC_BASE_URLsomcpUrlin token responses is correct.Mount
DATA_DIRon persistent storage (it replaces Firebase Storage).The service holds in-memory
CollabBotinstances keyed by connect token; it is intended to run as a single process. Horizontal scaling would need a shared bot registry / sticky routing (not implemented).Firestore security rules must allow the service account to read
boards,boardKeys,teamsand read/writescenes*andmcpTokens. (The Admin SDK bypasses rules;create_boardwritesboards,boardKeysand the caller'sbotsdocument.)PUBLIC_APP_ORIGIN— origin the Excalidraw app is served from, used to put an openableurlin thecreate_boardresponse. Falls back toPUBLIC_BASE_URL, which is the same origin in the default stack.
Not yet verified live
End-to-end testing needs real credentials and a running room server, which are not available in this build environment. The following paths are structurally complete and type-checked but not exercised against live infrastructure:
Firebase Admin init with a real service account and
verifyIdToken.Custom-token → ID-token exchange against Identity Toolkit, and the room server accepting that ID token and applying read-only for viewer tokens.
Live socket handshake (
init-room→join-room→first-in-room/new-user/room-user-change) andclient-broadcastdecryption / reconciliation timing. The handshake resolves on the first membership event or after a 4s fallback.Actual Firestore writes to
scenes/{roomId}andscenes/{roomId}~history*and the frontend HistorySidebar rendering theБот <name>entries.The frontend reading files written by
PUT /files/*(path-shape and opaque byte passthrough are implemented; the exactContent-Type/CORS headers the frontend expects onGETwere set permissively but not validated against a live client).Fractional index ordering interop: the public
fractional-indexing@3.3.0package is used; the frontend uses@excalidraw/fractional-indexing@3.3.0(a fork with identical key output), assumed byte-compatible but not co-tested.
This server cannot be deployed
Maintenance
Related MCP Connectors
Collaborative whiteboard MCP server — create objects, connectors, C4 diagrams, and manage boards
Real-time collaborative whiteboard — AI agents and humans edit the same board live over MCP.
Remote MCP server for The Colony — a social network for AI agents (posts, DMs, search, marketplace).
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Related MCP Servers
- AlicenseBqualityFmaintenanceSecurity-hardened MCP server for Excalidraw with API key auth, rate limiting, real-time WebSocket sync, and 14 diagramming tools16718 npm3MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to create and manipulate live visual diagrams on an Excalidraw canvas in real-time via MCP tools.2,102 npm14BSD 3-Clause
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to collaboratively draw and annotate Excalidraw diagrams in real-time via MCP tools, synced to a browser canvas.6Apache 2.0
- FlicenseNot gradedqualityCmaintenanceMCP server for a persistent local whiteboard that lets AI assistants read, add, update, and delete canvas elements, with undo/redo and provenance tracking.-