Files
excalidraw-mcp-sentinel/CLAUDE.md
T
newblaccandClaude Opus 4.6 9fb8ce34ec chore: rename project to excalidraw-mcp-sentinel
- Package name: @sanjibdevnath/mcp-excalidraw-local → excalidraw-mcp-sentinel
- GitHub repo: celstnblacc/mcp-excalidraw-local → celstnblacc/excalidraw-mcp-sentinel
- Docker images, CLI binary, CI workflows, docs all updated
- Version reset to 1.0.0 for independent release track
- Added "Why this fork?" section to README
- Removed superseded planning docs (PLAN.md, PLAN_v2.md, REVIEW.md, HANDOFF.md)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 20:00:27 +02:00

7.2 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What This Is

A hardened, self-hosted Excalidraw MCP server (excalidraw-mcp-sentinel). Single Node.js process that runs an MCP server (stdio, 32 tools), an embedded Express+WebSocket canvas server, and SQLite persistence with multi-tenancy. Forked from sanjibdevnathlabs/mcp-excalidraw-local (itself from yctimlin/mcp_excalidraw).

Build & Development Commands

# Install dependencies (pnpm preferred, npm works too)
pnpm install
pnpm rebuild better-sqlite3 esbuild

# Full build (frontend + server)
pnpm run build

# Build only server (TypeScript)
pnpm run build:server        # npx tsc

# Build only frontend (Vite/React)
pnpm run build:frontend      # vite build

# Type check without emit
pnpm run type-check           # npx tsc --noEmit

# Dev mode (watch server + Vite dev server on :5173)
pnpm run dev

# Run the MCP server (starts MCP stdio + canvas on :3000)
node dist/index.js

# Run canvas server standalone
node dist/server.js

# Health check
curl http://localhost:3000/health

369 tests across unit, API, WebSocket, and regression suites. Run npm test or pnpm test. CI runs type-check then build then test across Node 18/20/22.

Architecture

Single process, three subsystems:

src/index.ts   ── MCP Server (stdio) ── 32 tools, connects to canvas via HTTP
  ├── imports server.ts  ── Canvas Server (Express + WebSocket on CANVAS_PORT)
  ├── imports db.ts      ── SQLite layer (better-sqlite3, WAL mode)
  └── imports types.ts   ── Shared types, element validation, ID generation

frontend/      ── React + Excalidraw UI (Vite build → dist/frontend/)
  ├── src/App.tsx   ── Main component, WS connection, auto-sync, workspace switcher
  └── src/main.tsx  ── Entry point

Data flow: MCP tool call → index.ts handler → HTTP to canvas REST API (server.ts) → SQLite (db.ts) + WebSocket broadcast → frontend updates.

Key design decisions:

  • Canvas server is embedded in the MCP process — startCanvasServer() is called from runServer(). If port is taken by an existing healthy instance, it reuses it instead of crashing.
  • Multi-tenancy: workspace path → SHA-256 hash (12 chars) → tenant ID. Each tenant has isolated projects/elements. Tenant auto-detected via server.listRoots() after MCP connection.
  • All element data stored as JSON blobs in SQLite elements.data column. FTS5 virtual table for full-text search on labels.
  • Logging goes to file (excalidraw.log) at debug level, only warn+error to stderr (to avoid breaking stdio JSON protocol).

Source Files

File Purpose
src/index.ts (~2540 lines) MCP server entry point. Tool definitions, tool handlers, tenant bootstrap, server lifecycle.
src/server.ts (~1155 lines) Express canvas server. REST API, WebSocket, Zod schemas, arrow binding resolution, image export relay.
src/db.ts (~510 lines) SQLite persistence. Migrations, CRUD, FTS, versioning, snapshots, tenants, projects.
src/types.ts (~315 lines) TypeScript interfaces for elements, WebSocket messages, API responses. generateId() and validateElement().
src/utils/logger.ts Winston logger config (file + stderr).
frontend/src/App.tsx React Excalidraw wrapper with WS sync, auto-sync, workspace switcher.
vite.config.js Frontend build config. Root=frontend/, output=dist/frontend/. Dev proxy to :3000.

Environment Variables

Variable Default Notes
CANVAS_PORT 3000 Canvas server port
EXCALIDRAW_DB_PATH ~/.excalidraw-mcp/excalidraw.db SQLite database location
EXCALIDRAW_EXPORT_DIR process.cwd() Allowed directory for file exports (path traversal protection)
EXPRESS_SERVER_URL http://localhost:{CANVAS_PORT} Only needed if running canvas separately
LOG_FILE_PATH excalidraw.log Winston log file
LOG_LEVEL info Winston log level

TypeScript Configuration

  • ESM modules ("type": "module" in package.json, "module": "ESNext" in tsconfig)
  • Strict mode enabled with noUncheckedIndexedAccess
  • Target ES2022, output to dist/
  • All .js imports in source use .js extension (ESM requirement)

Key Patterns

  • Canvas sync is fire-and-forget: MCP tool handlers call canvas REST API but don't fail if canvas is unavailable. The syncToCanvas() helper catches errors and returns null.
  • Tenant-scoped operations: Every REST endpoint resolves tenant via X-Tenant-Id header → resolveTenantProject() → project ID. Browser requests (no header) fall back to global active state.
  • Arrow binding: startElementId/endElementId on arrows are resolved to edge-point coordinates in resolveArrowBindings() (server.ts). The server computes intersection points for rectangle/ellipse/diamond shapes.
  • Image export relay: MCP → REST /api/export/image → WebSocket broadcast → frontend renders → POST back to /api/export/image/result → resolves pending promise.
  • Element versioning: Every create/update/delete records a version in element_versions table. Soft-delete pattern (is_deleted flag).

Docker

Two Dockerfiles: Dockerfile (MCP server only), Dockerfile.canvas (canvas with frontend). docker-compose.yml orchestrates both with a full profile.

Publish Readiness

Last hardened: 2026-03-29 — gauntlet all-green, PR #1 merged.

Security posture (as of 1.6.3)

  • src/security.ts: helmet, CORS allowlist, timing-safe API key auth, prototype pollution guard, 3-tier rate limiting, WS challenge-response auth, Mermaid input size cap
  • 369/369 tests passing; 4 regression tests cover previously crash-able sync paths
  • Docker: non-root user, resource limits, hardened .dockerignore

Before running npm publish

  • Bump version in package.json to match CHANGELOG.md entry (currently 1.0.0)
  • Run npm test — must be 369/369
  • Run npm run build — must be zero TS errors
  • Run shipguard scan . — must be 0 CRITICAL findings
  • Verify CHANGELOG.md has an entry for the version being published
  • npm publish --dry-run to confirm only dist/, skills/, README.md, LICENSE are included

Safe to push to GitHub?

Yes — as of PR #1, the repo is clean for public visibility:

  • No secrets, hardcoded paths, or private identifiers in tracked files
  • Auth is opt-in (EXCALIDRAW_API_KEY unset = dev mode, by design)
  • Docker images run non-root with resource limits

Code Search Optimization

When exploring or understanding code in supported languages (JS, TS, Python, Go, Rust, Java, C, C++, Ruby):

  • Use smart_search(query, path) instead of Grep+Glob chains for discovering functions/classes/symbols
  • Use smart_outline(file_path) instead of Read to understand file structure (~1-2K tokens vs ~12K+)
  • Use smart_unfold(file_path, symbol_name) instead of Read for viewing specific functions (~400-2K tokens)
  • Fall back to Grep for exact string/regex searches, Read for non-code files and files under 100 lines