Compare commits

..
6 Commits
Author SHA1 Message Date
sanjibdevnathlabs-release-bot[bot] c16e1f4cc3 chore(release): v1.1.5 2026-03-13 16:05:31 +00:00
sanjibdevnathlabs 8144c7cd6e 📝 docs(skill): add geometric thinking principles and technique catalogs
Excalidraw skill lacked guidance on building complex visual shapes from
basic primitives, leading to flat single-shape attempts for structures
like roofs and walls. Adds coordinate geometry foundations so agents
produce realistic illustrative diagrams alongside technical ones.

🔧 Core geometric principles:
- Compose complex shapes from many small primitives with tessellation
- Eliminate gaps using interleaving offset rows at half-width spacing
- Parametric positioning formulas for centering, circular, triangular, and isometric layouts
- Scale primitive dimensions proportionally to container size

📦 Technique catalog reference:
- Illustrative elements (roofs, walls, clouds, trees, fences, windows)
- Sugiyama-inspired flow diagram layout algorithm
- Zone-grid architecture diagram layout
- Isometric 2.5D projection formulas
- Repeating pattern formulas with brick-pattern offsets

🧹 Consolidate tenants/projects/search into single compact table to
keep SKILL.md under the 500-line skill guideline
2026-03-13 21:33:21 +05:30
sanjibdevnathlabs-release-bot[bot] dcce55d469 chore(release): v1.1.4 2026-03-13 13:36:27 +00:00
Sanjib Devnathandsanjibdevnathlabs 061fa82672 🐛 fix(pkg): remove broken postinstall that blocks npx installation (#9)
The postinstall ran prebuild-install and node-gyp in the wrong context
(our package instead of better-sqlite3), always failing with "binding.gyp
not found". better-sqlite3 handles its own native compilation via its
install script — the package-level postinstall was redundant.

Co-authored-by: sanjibdevnathlabs <devnath.sanjib@gmail.com>
2026-03-13 19:04:10 +05:30
sanjibdevnathlabs-release-bot[bot] ffc922b296 chore(release): v1.1.3 2026-03-13 12:53:33 +00:00
Sanjib Devnath 97d816df4a 🐛 fix(cli): robust npx entry point, Node 20 requirement, setup hardening (#8)
- Use fs.realpathSync for entry point detection to fix npx symlink failures
- Add --help and --version CLI flags with explicit process.exit(0)
- Add TTY detection in setup wizard to prevent non-interactive hangs
- Wrap JSON.parse in mergeJsonConfig with try/catch for malformed configs
- Update engines.node to >=20.0.0 to match better-sqlite3 requirements
- Update Dockerfiles from node:18-slim to node:20-slim
- Remove error-swallowing || true from postinstall script
- Replace deprecated windows-build-tools with VS Build Tools link
2026-03-13 18:21:19 +05:30
10 changed files with 318 additions and 68 deletions
+2 -2
View File
@@ -2,7 +2,7 @@
# Builds the MCP server with SQLite persistence
# Stage 1: Build backend (TypeScript compilation + native modules)
FROM node:18-slim AS builder
FROM node:20-slim AS builder
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
@@ -16,7 +16,7 @@ COPY tsconfig.json ./
RUN npm run build:server
# Stage 2: Production MCP Server
FROM node:18-slim AS production
FROM node:20-slim AS production
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
+3 -3
View File
@@ -2,7 +2,7 @@
# Provides the web interface, REST API, and SQLite persistence
# Stage 1: Build frontend
FROM node:18-slim AS frontend-builder
FROM node:20-slim AS frontend-builder
WORKDIR /app
@@ -14,7 +14,7 @@ COPY vite.config.js ./
RUN npm run build:frontend
# Stage 2: Build backend (TypeScript compilation + native modules)
FROM node:18-slim AS backend-builder
FROM node:20-slim AS backend-builder
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
@@ -28,7 +28,7 @@ COPY tsconfig.json ./
RUN npm run build:server
# Stage 3: Production Canvas Server
FROM node:18-slim AS production
FROM node:20-slim AS production
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
+2 -4
View File
@@ -58,7 +58,7 @@ Click the workspace badge to switch between isolated canvases — each workspace
| Requirement | Why | Check |
|---|---|---|
| **Node.js >= 18** (LTS 20 or 22 recommended) | Runtime | `node --version` |
| **Node.js >= 20** (LTS 20 or 22 recommended) | Runtime | `node --version` |
| **C++ build tools** | `better-sqlite3` compiles native bindings | See below |
| **npm** (bundled with Node.js) | Package manager | `npm --version` |
@@ -75,9 +75,7 @@ sudo apt install build-essential python3
```
**Windows:**
```bash
npm install --global windows-build-tools
```
Install "Desktop development with C++" from [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
> `better-sqlite3` ships prebuilt binaries for most Node LTS versions. The build tools are only needed when a prebuilt binary isn't available for your platform/Node combination.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@sanjibdevnath/mcp-excalidraw-local",
"version": "1.1.2",
"version": "1.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@sanjibdevnath/mcp-excalidraw-local",
"version": "1.1.2",
"version": "1.1.5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@sanjibdevnath/mcp-excalidraw-local",
"version": "1.1.2",
"version": "1.1.5",
"description": "Fully local MCP server for Excalidraw with SQLite persistence, multi-tenancy, auto-sync, real-time canvas, and 32 tools",
"main": "dist/index.js",
"type": "module",
@@ -18,7 +18,6 @@
"dev:server": "npx tsc --watch",
"production": "npm run build && npm run canvas",
"prepublishOnly": "npm run build",
"postinstall": "prebuild-install --runtime napi || node-gyp rebuild --directory node_modules/better-sqlite3 2>/dev/null || true",
"setup": "node dist/index.js setup",
"type-check": "npx tsc --noEmit",
"test": "vitest run",
@@ -107,7 +106,7 @@
]
},
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
},
"publishConfig": {
"access": "public",
+120 -45
View File
@@ -288,6 +288,118 @@ Layout:
Title: fontSize=24 above each zone
```
## Geometric Thinking (Critical — For All Diagram Types)
Excalidraw only offers basic primitives: rectangles, diamonds, ellipses, lines, arrows, text. Building anything beyond simple box-and-arrow diagrams requires **geometric composition** — combining many small primitives using coordinate math to form complex shapes, textures, and layouts.
### Principle 1: Compose Complex Shapes from Many Small Primitives
**Never use one large primitive where many small ones create a better result.**
A single large diamond looks like a flat rhombus. But 45 small diamonds arranged in a triangular grid with tessellating offset rows looks like a tiled roof. The technique:
1. **Identify the target shape** (triangle, circle, arc, wave, etc.)
2. **Choose a small primitive** that can tile/fill that shape (diamond for tiles, rectangle for bricks, ellipse for clouds)
3. **Compute a grid of positions** that fills the target shape's boundary
4. **Apply row-by-row reduction** for tapered shapes (triangles, cones)
5. **Add interleaving offset rows** to eliminate gaps (tessellation)
```
Example — Triangular tiled roof (building width=380, center_x=1090):
Tile size: 52w × 36h
Row spacing: 28px vertical (= tile height)
Interleave offset: 26px horizontal (= tile width / 2)
Interleave rows at: midpoint y between main rows
Main rows (reduce by 2 tiles per row):
Row 1: 9 tiles at y=130 | ◇◇◇◇◇◇◇◇◇
Row 2: 7 tiles at y=102 | ◇◇◇◇◇◇◇
Row 3: 5 tiles at y=74 | ◇◇◇◇◇
Row 4: 3 tiles at y=46 | ◇◇◇
Row 5: 1 tile at y=18 | ◇
Interleaving rows (offset by half tile width, fill gaps):
Inter 1: 8 tiles at y=116 | ◇◇◇◇◇◇◇◇
Inter 2: 6 tiles at y=88 | ◇◇◇◇◇◇
Inter 3: 4 tiles at y=60 | ◇◇◇◇
Inter 4: 2 tiles at y=32 | ◇◇
Total: 45 tiles → seamless triangular roof
```
### Principle 2: Tessellation — Eliminate Gaps with Offset Rows
When same-row primitives leave triangular/pointed gaps, add **interleaving rows** offset by half the primitive width:
```
Gap pattern (diamonds side-by-side): Filled with interleaving row:
/\ /\ /\ /\ /\ /\ /\ /\
/ \/ \/ \/ \ / \/ \/ \/ \
\ /\ /\ /\ / \ /\/\/\/\/\/\ /
\/ \/ \/ \/ ◇◇◇◇◇◇◇◇◇ ← offset row
/\ /\ /\ /\
```
**Formula for interleaving:**
```
main_row_y[n] = base_y - n * row_spacing
inter_row_y[n] = (main_row_y[n] + main_row_y[n+1]) / 2
inter_row_x_offset = tile_width / 2
inter_row_count = main_row_count[n] - 1
```
### Principle 3: Parametric Positioning — Use Formulas, Not Guessing
Compute element positions mathematically. Common formulas:
**Centering N items in a container:**
```
item_x[i] = container_x + (container_width - N * item_width) / (N + 1) * (i + 1) + i * item_width
```
**Circular arrangement (N items around center):**
```
angle[i] = (2π / N) * i + rotation_offset
x[i] = center_x + radius * cos(angle[i]) - item_width / 2
y[i] = center_y + radius * sin(angle[i]) - item_height / 2
```
**Triangular reduction (pyramid/roof):**
```
count[row] = base_count - 2 * row
x_start[row] = center_x - (count[row] * tile_width) / 2
y[row] = base_y - row * row_spacing
```
**Isometric projection (2.5D diagrams):**
```
screen_x = (grid_x - grid_y) * tile_width / 2
screen_y = (grid_x + grid_y) * tile_height / 2
```
### Principle 4: Scale Primitives to Context
When applying a technique to different-sized containers, **scale the primitive size proportionally**:
```
Hut (body width 290px) → tiles 40w × 28h, 9 per base row
Building (body width 380px) → tiles 52w × 36h, 9 per base row
```
Maintain the same count-per-row for visual consistency; adjust individual tile dimensions.
### Technique Catalogs
For detailed recipes and formulas for specific diagram types, see [geometric-thinking.md](references/geometric-thinking.md):
- **Illustrative diagrams**: 11 visual element recipes (roofs, walls, clouds, trees, fences, water, smoke, windows, stairs)
- **Flow diagrams**: Sugiyama-inspired 4-step hierarchical layout (layer assignment → ordering → coordinates → edge routing)
- **Architecture diagrams**: Zone-grid layout with service grid-packing and dependency layering
- **Isometric/2.5D diagrams**: Screen projection formulas for depth illusion
- **Repeating patterns**: Generic repeat and brick-pattern offset formulas
- **Anti-patterns**: 6 common geometric composition mistakes and fixes
## Workflow: Iterative Refinement
```
@@ -303,55 +415,18 @@ create shapes (batch 1)
For multi-diagram canvases, offset each new diagram by 300px+ from the previous one's bounding box.
## Workflow: Multi-Tenancy (Workspaces)
## Workflow: Tenants, Projects & Search
The MCP is multi-tenant. Each Cursor workspace automatically gets its own tenant (identified by a SHA-256 hash of the workspace path). All elements, projects, and snapshots are scoped to the active tenant.
### Automatic Tenant Detection
On MCP startup, the server:
1. Creates a tenant from `process.cwd()` (initial guess)
2. After connecting, calls `server.listRoots()` to get the real workspace path from Cursor
3. If different, re-creates/switches to the correct tenant and notifies the canvas
This means globally-configured MCPs (`~/.cursor/mcp.json`) correctly detect the per-window workspace — no manual setup needed.
### Tenant Operations
Multi-tenant: each Cursor workspace auto-gets its own tenant (SHA-256 hash of workspace path). Globally-configured MCPs detect per-window workspace automatically.
| Task | Tool | Notes |
|------|------|-------|
| See all workspaces | `list_tenants` | Returns id, name, workspace_path, created_at |
| Switch workspace | `switch_tenant` with `tenantId` | Canvas reloads that tenant's elements via WebSocket |
| Check current tenant | (from describe_scene or frontend header) | Shows "Workspace: [name]" in canvas |
### Multiple Cursor Instances
Each instance sends its own `X-Tenant-Id` header on every HTTP/MCP request. SQLite uses `busy_timeout` for concurrent write safety. No state conflicts between windows.
## Workflow: Projects (Within a Tenant)
Projects group diagrams within a tenant. Each tenant has a "Default Project" created automatically. Use projects to organize different diagram sets (e.g., "Architecture", "User Flows", "Sprint Planning").
| Task | Tool | Notes |
|------|------|-------|
| List projects | `list_projects` | Shows all projects in active tenant |
| Switch project | `switch_project` with `projectId` | Elements change to that project's set |
| Create new project | `switch_project` with `createName` | Creates and switches in one call |
## Workflow: Search & History
### Full-Text Search
`search_elements` with `query` — searches across element labels and text content in the active project. Useful for finding specific elements in large diagrams.
### Element Version History
`element_history` — view create/update/delete operations for:
- A specific element: pass `elementId`
- Entire active project: omit `elementId`
- Control result count with `limit` (default 50)
Use history to debug unexpected changes or audit what was modified.
| List workspaces | `list_tenants` | Returns id, name, workspace_path |
| Switch workspace | `switch_tenant` with `tenantId` | Canvas reloads tenant's elements |
| List projects | `list_projects` | Projects group diagrams within a tenant |
| Switch/create project | `switch_project` | Pass `projectId` or `createName` |
| Full-text search | `search_elements` with `query` | Searches labels and text content |
| Version history | `element_history` | Pass `elementId` or omit for project-wide |
## Workflow: Refine An Existing Diagram
@@ -0,0 +1,131 @@
# Geometric Thinking — Detailed Technique Catalogs
## Technique Catalog: Illustrative Diagrams
| Visual Element | Primitive Composition | Key Parameters |
|---------------|----------------------|----------------|
| **Tiled roof** | Grid of diamonds, rows reduce by 2, interleave offset rows | tile_size, row_count, center_x |
| **Brick wall** | Grid of rectangles, alternating rows offset by half-width | brick_w, brick_h, mortar_gap |
| **Cloud** | 5-7 overlapping ellipses of varying sizes | center, radii, overlap% |
| **Tree** | Rectangle trunk + 3-4 overlapping ellipses (canopy) | trunk_w, canopy_r |
| **Fence** | Repeated thin rectangles with pointed-top triangles | post_spacing, post_h |
| **Road/path** | Two parallel lines + dashed center line | width, dash_pattern |
| **Water/waves** | Repeating sine-curve approximated by overlapping ellipses | amplitude, wavelength |
| **Sun rays** | Central ellipse + rotated lines at equal angles | ray_count, ray_length |
| **Smoke/steam** | 3+ ellipses of increasing size, ascending and drifting | size_step, drift_x |
| **Window (classic)** | Rectangle + 2 thin rectangle cross-bars (H and V) | win_w, win_h, bar_thickness=3 |
| **Stairs** | Stacked rectangles, each offset right and down | step_w, step_h, count |
## Technique Catalog: Flow Diagrams (Sugiyama-Inspired Layout)
For flowcharts and directed graphs, use the Sugiyama hierarchical layout algorithm:
**Step 1 — Layer Assignment:**
Assign each node to a horizontal layer. Entry nodes at top (layer 0), each subsequent step increases layer number. Nodes in the same layer share the same Y coordinate.
```
layer_y[n] = start_y + n * (node_height + vertical_gap)
vertical_gap: 120px minimum (for visible arrows)
```
**Step 2 — Ordering Within Layers:**
Position nodes within each layer to minimize edge crossings. Place each node at the average X of its connected neighbors in the layer above.
```
node_x = average(connected_parent_x_positions)
If two nodes overlap: spread by node_width + horizontal_gap
```
**Step 3 — Coordinate Assignment:**
Center nodes around the diagram midpoint. Distribute evenly within each layer.
```
layer_width = count * node_width + (count - 1) * horizontal_gap
first_x = center_x - layer_width / 2
node_x[i] = first_x + i * (node_width + horizontal_gap)
```
**Step 4 — Edge Routing:**
Create arrows after all shapes. Use `startElementId`/`endElementId` for binding.
- Same-layer connections: horizontal arrows
- Cross-layer connections: vertical arrows
- Multi-layer spans: consider adding routing waypoints
**Decision branches:**
```
YES path → horizontal right to answer box (same layer)
NO path → vertical down to next decision (next layer)
```
## Technique Catalog: Architecture Diagrams (Zone-Grid Layout)
**Zone-based layout** for microservices, infrastructure, and system architecture:
**Step 1 — Define zones as large translucent rectangles:**
```
zone_width = max(services_count * (service_w + gap) + padding * 2, 400)
zone_height = rows * (service_h + gap) + padding * 2 + title_height
zone_bg = "#e9ecef", opacity = 30
```
**Step 2 — Grid-pack services within zones:**
```
col = service_index % cols_per_row
row = service_index / cols_per_row (integer division)
service_x = zone_x + padding + col * (service_w + gap)
service_y = zone_y + title_height + padding + row * (service_h + gap)
```
**Step 3 — Connect zones with arrows:**
- Solid arrows for synchronous calls
- Dashed arrows (`strokeStyle: "dashed"`) for async/event-driven
- Label arrows with protocol/method (REST, gRPC, Kafka, etc.)
**Step 4 — Layer zones top-to-bottom by dependency depth:**
```
Client layer: y = 0
API Gateway: y = zone_height + 80
Services: y = 2 * (zone_height + 80)
Data stores: y = 3 * (zone_height + 80)
```
## Technique Catalog: Isometric / 2.5D Diagrams
For infrastructure and deployment diagrams with depth:
```
Isometric grid formulas:
screen_x = origin_x + (col - row) * cell_width / 2
screen_y = origin_y + (col + row) * cell_height / 2
For a server rack at grid position (2, 3):
screen_x = 500 + (2 - 3) * 60 / 2 = 470
screen_y = 100 + (2 + 3) * 30 / 2 = 175
```
Use diamonds for floor tiles, parallelogram-approximated rectangles for side faces. Stack elements vertically (subtract from y) to show height.
## Technique Catalog: Repeating Patterns
For any repeating visual pattern (fences, grids, timelines, Gantt bars):
```
Generic repeat formula:
element_x[i] = start_x + i * (element_width + gap)
element_y[i] = start_y (same for horizontal repeat)
With alternating offset (brick pattern):
offset = (row % 2) * (element_width / 2 + gap / 2)
element_x[i] = start_x + offset + i * (element_width + gap)
```
## Anti-Patterns for Geometric Composition
| Mistake | Why It Fails | Do This Instead |
|---------|-------------|-----------------|
| Single large primitive for complex shape | Looks flat, unrealistic | Compose from many small primitives |
| Same-row diamonds without interleaving | Visible triangular gaps | Add offset rows at midpoint Y |
| Guessing coordinates | Misaligned elements, uneven spacing | Use parametric formulas |
| Same tile size for all containers | Looks wrong at different scales | Scale tile size to container width |
| Too few primitives | Sparse, gappy appearance | Use enough tiles to achieve ≥80% coverage |
| Forgetting z-order | Background elements cover foreground | Create back-to-front: background first, details last |
+29 -3
View File
@@ -2708,13 +2708,39 @@ if (process.env.DEBUG === 'true') {
logger.debug('Debug mode enabled');
}
// Start the server if this file is run directly
if (fileURLToPath(import.meta.url) === process.argv[1]) {
if (process.argv[2] === 'setup') {
function isMainModule(): boolean {
try {
const ourPath = fs.realpathSync(fileURLToPath(import.meta.url));
const argPath = process.argv[1];
if (!argPath) return false;
return ourPath === fs.realpathSync(path.resolve(argPath));
} catch {
return false;
}
}
if (isMainModule()) {
const arg = process.argv[2];
if (arg === 'setup') {
import('./setup.js').then(m => m.runSetup()).catch(error => {
process.stderr.write(`Setup failed: ${(error as Error).message}\n`);
process.exit(1);
});
} else if (arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v') {
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
if (arg === '--help' || arg === '-h') {
process.stdout.write(`${pkg.name} v${pkg.version}\n\nUsage:\n mcp-excalidraw-local Start MCP server (stdio transport)\n mcp-excalidraw-local setup Interactive setup wizard\n mcp-excalidraw-local --help Show this help\n mcp-excalidraw-local --version Show version\n`);
} else {
process.stdout.write(`${pkg.version}\n`);
}
} catch {
process.stderr.write('Could not read package.json\n');
process.exit(1);
}
process.exit(0);
} else {
runServer().catch(error => {
logger.error('Failed to start server:', error);
+13 -1
View File
@@ -2,6 +2,7 @@ import express, { type Application, Request, Response, NextFunction } from 'expr
import cors from 'cors';
import { WebSocketServer } from 'ws';
import { createServer } from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
@@ -1272,7 +1273,18 @@ export function stopCanvasServer(): Promise<void> {
}
// Direct execution: `node dist/server.js` still works standalone
if (fileURLToPath(import.meta.url) === process.argv[1]) {
function isServerMainModule(): boolean {
try {
const ourPath = fs.realpathSync(fileURLToPath(import.meta.url));
const argPath = process.argv[1];
if (!argPath) return false;
return ourPath === fs.realpathSync(path.resolve(argPath));
} catch {
return false;
}
}
if (isServerMainModule()) {
startCanvasServer().catch((err) => {
logger.error('Failed to start canvas server:', err);
process.exit(1);
+14 -5
View File
@@ -107,10 +107,10 @@ async function phaseEnvironment(rl: readline.Interface): Promise<boolean> {
// Node.js version
const nodeVersion = process.version;
const major = parseInt(nodeVersion.slice(1).split('.')[0] ?? '0', 10);
if (major >= 18) {
if (major >= 20) {
ok(`Node.js ${nodeVersion} ${'.' .repeat(Math.max(0, 24 - nodeVersion.length))} OK`);
} else {
fail(`Node.js ${nodeVersion} — requires >= 18.0.0`);
fail(`Node.js ${nodeVersion} — requires >= 20.0.0`);
allOk = false;
}
@@ -133,7 +133,6 @@ async function phaseEnvironment(rl: readline.Interface): Promise<boolean> {
ok('Rebuild successful');
} catch {
fail('Rebuild failed. Try manually:');
info(` cd ${path.resolve(__dirname, '..')}`);
info(' npm rebuild better-sqlite3');
info('');
info('Prerequisites:');
@@ -142,7 +141,8 @@ async function phaseEnvironment(rl: readline.Interface): Promise<boolean> {
} else if (process.platform === 'linux') {
info(' sudo apt install build-essential python3');
} else {
info(' npm install --global windows-build-tools');
info(' Install "Desktop development with C++" from Visual Studio Build Tools');
info(' https://visualstudio.microsoft.com/visual-cpp-build-tools/');
}
allOk = false;
}
@@ -306,7 +306,11 @@ function mergeJsonConfig(configPath: string): void {
let existing: any = {};
if (fs.existsSync(configPath)) {
const raw = fs.readFileSync(configPath, 'utf-8');
existing = JSON.parse(raw);
try {
existing = JSON.parse(raw);
} catch {
throw new Error(`Failed to parse ${configPath} — fix the JSON syntax and try again.`);
}
}
if (!existing.mcpServers) {
@@ -345,6 +349,11 @@ function printManualConfig(): void {
// ── Main ─────────────────────────────────────────────────────
export async function runSetup(): Promise<void> {
if (!process.stdin.isTTY) {
process.stderr.write('Error: Setup requires an interactive terminal. Run this command directly in your terminal (not piped or in CI).\n');
process.exit(1);
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,