Featured

Deploy OpenClaw in 60 seconds — 20% off logoDeploy OpenClaw in 60 seconds — 20% off

Launch OpenClaw on Hostinger in about 60 seconds and keep your agent live 24/7. Our referral link gives you 20% off, no coupon code needed.

Launch on Hostinger
Run your Hermes agent on Hostinger, fully managed logoRun your Hermes agent on Hostinger, fully managed

Launch Hermes on Hostinger in one click, fully managed, no VPS knowledge needed. Use code ZACAARON10 for 10% off.

Launch on Hostinger
Crawl and scrape any site into clean data, 10% off logoCrawl and scrape any site into clean data, 10% off

Firecrawl crawls and scrapes any site into clean markdown for your agent. Get 1,000 free credits, and new users get 10% off their first purchase.

Try Firecrawl free
6,000+ web scrapers for your AI agent, start free logo6,000+ web scrapers for your AI agent, start free

Apify gives your agent live web data: 6,000+ prebuilt scrapers and actors, MCP-ready. Sign up free with $5 in usage credits.

Try Apify free
One API to scrape, enrich, and extract the internet. logoOne API to scrape, enrich, and extract the internet.

Context.dev gives your agents a single API to scrape, enrich, and extract live web data — no proxies, no parsers, no maintenance.

Start building free
SetupClaw: done-for-you OpenClaw for founders & exec teams logoSetupClaw: done-for-you OpenClaw for founders & exec teams

White-glove OpenClaw for founders and exec teams (4–50+ employees): we install, harden, integrate your tools, and maintain it — secured from day one.

Get it set up for you
SEO data APIs for your agent, $1 free credit logoSEO data APIs for your agent, $1 free credit

DataForSEO gives your agent live access to SERP results, keyword data, backlinks, and on-page SEO data through one API. New accounts get a $1 credit, good for up to 20,000 keyword or backlink lookups.

Try DataForSEO free
Reach 48,000+ AI builders

A flat monthly placement in front of developers actively installing AI tools. No lock-in, cancel anytime.

Advertise here

Works with

Claude CodeClaude DesktopCursorVS CodeClineCodex CLIOpenClaw+ any MCP client

Install to Claude Code

This server doesn't publish a one-line install command. Follow the setup in the source repository.

Summary

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.md

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:

  1. admin.auth().createCustomToken(uid)
  2. exchanges it for an ID token via Identity Toolkit

(accounts:signInWithCustomToken?key=${FIREBASE_WEB_API_KEY})

  1. 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 payload

scenes/{roomId}~history~{entryId}, matching SceneHistory entry shape and MAX_SCENE_HISTORY_ENTRIES so the frontend HistorySidebar renders bot entries (with author).

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, query_elements — current elements (viewer + editor).

fields projects the columns you need and limit/offset page a large scene.

  • batch_create, update_elements, delete_elements, delete_region

single-commit batch writes (editor only; a viewer token gets read-only access). Batch covers N=1, so there are no singular create/update/delete tools. batch_create also binds arrows to shapes inline (fromId/toId) and drops elements into a frame (frameId); update_elements edits a container's label ({ id, label }) and honors an explicit index, and skips ids that no longer exist (returning them in missing) 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 dev

Required env (see .env.example):

  • GOOGLE_APPLICATION_CREDENTIALS — absolute path to the service-account JSON

(Admin SDK).

  • FIREBASE_WEB_API_KEY — the web apiKey (AIzaSy...) from the SDK config;

required for the custom-token → ID-token exchange.

  • WS_SERVER_URL — the collab server (default http://localhost:3002).
  • FIREBASE_PROJECT_ID (default excalidraw-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_URL so mcpUrl in token responses is

correct.

  • Mount DATA_DIR on persistent storage (it replaces Firebase Storage).
  • The service holds in-memory CollabBot instances 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, teams and read/write scenes* and mcpTokens.

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-roomjoin-room

first-in-room/new-user/room-user-change) and client-broadcast decryption / reconciliation timing. The handshake resolves on the first membership event or after a 4s fallback.

  • Actual Firestore writes to scenes/{roomId} and scenes/{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 exact Content-Type/CORS headers the frontend expects on GET were set permissively but not validated against a live client).

  • Fractional index ordering interop: the public fractional-indexing@3.3.0

package is used; the frontend uses @excalidraw/fractional-indexing@3.3.0 (a fork with identical key output), assumed byte-compatible but not co-tested. ```

See related servers & alternatives →

Related MCP servers

Browse all →

Related guides

Hand-picked reading to help you choose and use AI & ML servers.