* chore(ci): release fires on workflow_dispatch, not on every CI pass Replaced workflow_run trigger (fired automatically when CI completed on main) with workflow_dispatch. The full semantic-release automation is preserved — bump detection, version commit, tag, GitHub Release, NPM + Docker publish — but now runs only when explicitly triggered. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(transport): add Streamable HTTP mode — one shared process for all MCP clients (v1.2.0) Add MCP_TRANSPORT=http mode backed by StreamableHTTPServerTransport. Each Claude Code session connects to the shared long-lived process via HTTP (port 3031 by default) instead of spawning a new stdio process per session, eliminating per-session process multiplication. Sessions are isolated by mcp-session-id header. Also refactor src/index.ts to extract createMcpServer()/registerHandlers() for clean per-session server instantiation, and upgrade fs.writeFileSync/readFileSync calls to fs.promises async variants in export/import tool handlers. 9 new tests cover transport resolution, session isolation, teardown, and startMcpHttpServer. All 528 tests pass. Bumps v1.1.0 → v1.2.0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: newblacc <newblacc@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
143 lines
7.5 KiB
Markdown
143 lines
7.5 KiB
Markdown
# 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](https://github.com/sanjibdevnathlabs/mcp-excalidraw-local) (itself from [yctimlin/mcp_excalidraw](https://github.com/yctimlin/mcp_excalidraw)).
|
|
|
|
## Build & Development Commands
|
|
|
|
```bash
|
|
# 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
|
|
```
|
|
|
|
477 tests across unit, API, WebSocket, e2e, 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
|
|
- 477/477 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.1`)
|
|
- [ ] Run `npm test` — must be 446/446
|
|
- [ ] 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
|
|
|
|
## Strict Installation Decoupling
|
|
|
|
Once installed (e.g., to ~/.local/bin), the project binary must NEVER depend on the local repository path (~/DevOpsSec) for execution, configuration, or data. All paths must be relative to the installation root or use standard system config paths (~/.config).
|