excalidraw-mcp-collab
Standalone backend for a self-hosted Excalidraw fork with per-board access control, providing an MCP remote endpoint that lets AI agents draw on real collaboration boards as specific users.
README
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.
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/mcp/tokens |
Firebase ID token (Bearer) | Mint a connect token for { boardId }. Returns { token, mcpUrl, role, configSnippet }. Caller must canRead; role = editor if canWrite else viewer. |
GET |
/mcp/tokens?boardId= |
Firebase ID token | List caller's tokens. |
DELETE |
/mcp/tokens/:token |
Firebase ID token | Revoke a token the caller owns. |
ALL |
/mcp |
connect token (Bearer or ?token=) |
MCP Streamable HTTP endpoint; lazily attaches a CollabBot. |
PUT |
/files/* |
optional Firebase ID token | Store raw opaque bytes. files/rooms/{roomId}/... requires canWrite; files/shareLinks/... open. |
GET |
/files/* |
optional Firebase ID token | Return raw bytes. files/rooms/{roomId}/... requires canRead; files/shareLinks/... open. |
The file bytes are already client-encrypted + compressed; the service stores and returns them verbatim.
MCP tools
describe_scene— current non-deleted elements (viewer + editor).query_elements— filter bytype/ids(viewer + editor).create_element,update_element,delete_element,clear_canvas— editor only. A viewer token gets an MCP errorread-only access.
Each mutating tool: applies the change (bumps version, fresh versionNonce,
updated, fractional index after the last element), broadcasts a
SCENE_UPDATE over server-broadcast, persists the full scene, and appends a
history entry attributed Бот <name>.
Setup
cp .env.example .env # fill in the values
npm install
npm run build
npm start # or: npm run dev
Required 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.
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.
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.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
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.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.