Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcce55d469 | ||
|
|
061fa82672 | ||
|
|
ffc922b296 | ||
|
|
97d816df4a | ||
|
|
6a69ac6761 | ||
|
|
ddfd3a73e8 | ||
|
|
19ed8bd1fc |
+98
-60
@@ -36,93 +36,131 @@ jobs:
|
||||
echo "should_test=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build-and-test:
|
||||
name: Build & Test (Node ${{ matrix.node-version }})
|
||||
setup:
|
||||
name: Install & Build
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.should_test == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x, 22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Cache node_modules
|
||||
id: cache-nm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-nm.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Type check
|
||||
run: npm run type-check
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Unit & integration tests
|
||||
- name: Upload build output
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
|
||||
test:
|
||||
name: Unit & Integration Tests
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
lint:
|
||||
name: Lint & Type Check
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Type check
|
||||
run: npm run type-check
|
||||
|
||||
e2e:
|
||||
name: E2E Tests
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Download build output
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- name: Check build artifacts
|
||||
run: |
|
||||
test -f dist/index.js
|
||||
test -f dist/server.js
|
||||
test -d dist/frontend
|
||||
|
||||
- name: Upload build artifacts
|
||||
if: matrix.node-version == '20.x'
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Get Playwright version
|
||||
id: pw-version
|
||||
run: echo "version=$(node -e "console.log(require('./node_modules/@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
id: cache-pw
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: dist/
|
||||
retention-days: 7
|
||||
|
||||
lint-check:
|
||||
name: Lint Check
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.should_test == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check for TypeScript errors
|
||||
run: npm run type-check
|
||||
|
||||
e2e:
|
||||
name: E2E Tests
|
||||
needs: [check-changes, build-and-test]
|
||||
if: needs.check-changes.outputs.should_test == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
||||
|
||||
- name: Install Playwright browsers
|
||||
if: steps.cache-pw.outputs.cache-hit != 'true'
|
||||
run: ./node_modules/.bin/playwright install --with-deps chromium
|
||||
|
||||
- name: Install Playwright system deps
|
||||
if: steps.cache-pw.outputs.cache-hit == 'true'
|
||||
run: ./node_modules/.bin/playwright install-deps chromium
|
||||
|
||||
- name: Run E2E tests
|
||||
run: npm run test:e2e
|
||||
env:
|
||||
@@ -140,7 +178,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: false
|
||||
name: CI Status Check
|
||||
needs: [check-changes, build-and-test, lint-check, e2e]
|
||||
needs: [check-changes, setup, test, lint, e2e]
|
||||
if: always()
|
||||
permissions:
|
||||
statuses: write
|
||||
|
||||
@@ -12,6 +12,10 @@ on:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: docker-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IMAGE_NAME_MCP: sanjibdevnath/mcp-excalidraw-local
|
||||
IMAGE_NAME_CANVAS: sanjibdevnath/mcp-excalidraw-local-canvas
|
||||
|
||||
@@ -6,6 +6,10 @@ on:
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: release-main
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
@@ -185,9 +189,16 @@ jobs:
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache node_modules
|
||||
id: cache-nm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-node20.x-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-nm.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
|
||||
+2
-2
@@ -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
@@ -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/*
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# MCP Excalidraw Local
|
||||
|
||||
[](https://github.com/sanjibdevnathlabs/mcp-excalidraw-local/actions/workflows/ci.yml)
|
||||
[](https://github.com/sanjibdevnathlabs/mcp-excalidraw-local/actions/workflows/docker.yml)
|
||||
[](https://github.com/sanjibdevnathlabs/mcp-excalidraw-local/actions/workflows/release.yml)
|
||||
[](LICENSE)
|
||||
|
||||
A fully local, self-hosted Excalidraw MCP server with **SQLite persistence**, **multi-tenancy**, and **auto-sync** — designed to run entirely on your machine without depending on `excalidraw.com`.
|
||||
@@ -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.
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@sanjibdevnath/mcp-excalidraw-local",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@sanjibdevnath/mcp-excalidraw-local",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+2
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sanjibdevnath/mcp-excalidraw-local",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.4",
|
||||
"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",
|
||||
|
||||
+29
-3
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user