Excalidraw Mcp Collab — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Excalidraw Mcp Collab (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Standalone Node + TypeScript backend for a self-hosted Excalidraw fork with per-board access control (Firebase project excalidraw-team). It provides:
ALL /mcp) that lets an AI agent draw on a realcollab board as a specific user. The agent's writes respect the board's read-only policy and are attributed in shared history as Бот <name>.
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).
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)(accounts:signInWithCustomToken?key=${FIREBASE_WEB_API_KEY})
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.
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.
src/scene.ts ports the Admin-SDK equivalent of excalidraw-app/data/firebase.ts:
scenes/{roomId} = { sceneVersion, ciphertext, iv } (encrypted elements).scenes/{roomId}~history + per-entry payloadscenes/{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.
| 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.
describe_scene — current non-deleted elements (viewer + editor).query_elements — filter by type / ids / groupId (viewer + editor).create_element, update_element, delete_element — editor only. A viewertoken gets an MCP error read-only access.
batch_create, update_elements, delete_elements, delete_region —single-commit batch writes (editor only). update_elements skips ids that no longer exist (returning them in missing) instead of aborting the batch.
group_elements, ungroup_elements, create_frame — grouping/frames.clear_canvas — editor only, safe by default (needs confirm:true).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. If a concurrent human session deletes one of them (the live session can broadcast a fresh bot element back as a tombstone), the bot re-asserts its last good copy at a higher version so the element is not silently lost — up to a few times before it yields to a persistent deletion. scene_diff reports ownership (byOrigin.bot, owned) and any contested ids (conflicts); create_element/batch_create surface the same conflicts.
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 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.
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.
PUBLIC_BASE_URL so mcpUrl in token responses iscorrect.
DATA_DIR on persistent storage (it replaces Firebase Storage).CollabBot instances keyed by connect token; itis intended to run as a single process. Horizontal scaling would need a shared bot registry / sticky routing (not implemented).
boards,boardKeys, teams and read/write scenes* and mcpTokens.
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:
verifyIdToken.server accepting that ID token and applying read-only for viewer tokens.
init-room → join-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.
scenes/{roomId} and scenes/{roomId}~history*and the frontend HistorySidebar rendering the Бот <name> entries.
PUT /files/* (path-shape and opaquebyte passthrough are implemented; the exact Content-Type/CORS headers the frontend expects on GET were set permissively but not validated against a live client).
[email protected]package is used; the frontend uses @excalidraw/[email protected] (a fork with identical key output), assumed byte-compatible but not co-tested.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.