Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6db9227b59 | ||
|
|
6791a4171f | ||
|
|
956e33398c | ||
|
|
86abe92fa2 |
@@ -41,6 +41,7 @@ Click the workspace badge to switch between isolated canvases — each workspace
|
||||
- [Quick Start](#quick-start)
|
||||
- [Configuration](#configuration)
|
||||
- [Verify Installation](#verify-installation)
|
||||
- [Updating](#updating)
|
||||
- [How We Differ from the Official Excalidraw MCP](#how-we-differ-from-the-official-excalidraw-mcp)
|
||||
- [What Changed From Upstream](#what-changed-from-upstream)
|
||||
- [Architecture](#architecture)
|
||||
@@ -280,6 +281,119 @@ open http://localhost:3000
|
||||
|
||||
If the health check fails, see [Troubleshooting](#troubleshooting).
|
||||
|
||||
## Updating
|
||||
|
||||
Already installed a previous version? The interactive update wizard is the easiest way to update everything — MCP server **and** agent skills — in one go.
|
||||
|
||||
### Interactive Update (recommended)
|
||||
|
||||
```bash
|
||||
npx @sanjibdevnath/mcp-excalidraw-local@latest update
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Example session</summary>
|
||||
|
||||
```
|
||||
$ npx @sanjibdevnath/mcp-excalidraw-local@latest update
|
||||
|
||||
Excalidraw MCP — Update v1.2.0
|
||||
|
||||
[1/2] Skill Update
|
||||
|
||||
Found 2 existing skill installation(s):
|
||||
[1] Cursor (global) — ~/.cursor/skills/excalidraw-skill
|
||||
[2] Claude Code (global) — ~/.claude/skills/excalidraw-skill
|
||||
|
||||
Update all 2 installation(s) to v1.2.0? [Y/n]: Y
|
||||
✔ Updated Cursor (global) — ~/.cursor/skills/excalidraw-skill
|
||||
✔ Updated Claude Code (global) — ~/.claude/skills/excalidraw-skill
|
||||
|
||||
2/2 skill(s) updated.
|
||||
|
||||
[2/2] MCP Configuration
|
||||
Re-apply MCP server config? (overwrites existing entry) [y/N]: N
|
||||
MCP config unchanged.
|
||||
|
||||
Update complete! Restart your MCP client to pick up changes.
|
||||
```
|
||||
</details>
|
||||
|
||||
The update wizard:
|
||||
1. **Finds all existing skill installations** across Cursor, Claude Code, and Codex CLI (both global and local scopes)
|
||||
2. **Updates them in-place** with the latest skill files (SKILL.md, cheatsheet, geometric-thinking reference, helper scripts)
|
||||
3. **Offers to install** the skill for any detected agent that doesn't have it yet
|
||||
4. **Optionally re-applies MCP config** if needed
|
||||
|
||||
> **Why this matters:** The agent skill contains workflow guidance, sizing rules, color palettes, and anti-patterns that evolve alongside the MCP tools. Updating the MCP server without updating the skill means your AI agent is working with stale instructions.
|
||||
|
||||
### Manual Update by Installation Method
|
||||
|
||||
If you prefer to update manually, follow the steps for your installation method, then restart your MCP client.
|
||||
|
||||
#### npx users
|
||||
|
||||
If your MCP config uses `npx -y @sanjibdevnath/mcp-excalidraw-local`, npx caches the package locally and won't automatically fetch new versions.
|
||||
|
||||
**Option A — Clear the cache (one-time):**
|
||||
```bash
|
||||
npm cache clean --force
|
||||
```
|
||||
Then restart your MCP client. npx will download the latest version on next launch.
|
||||
|
||||
**Option B — Pin to `@latest` in your MCP config (permanent fix):**
|
||||
|
||||
Update the `args` in your MCP config to include `@latest`:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"excalidraw-canvas": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@sanjibdevnath/mcp-excalidraw-local@latest"],
|
||||
"env": { "CANVAS_PORT": "3000" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This ensures npx always checks for the newest published version.
|
||||
|
||||
#### From-source users
|
||||
|
||||
```bash
|
||||
cd mcp-excalidraw-local
|
||||
git pull origin main
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
#### Docker users
|
||||
|
||||
```bash
|
||||
docker pull sanjibdevnath/mcp-excalidraw-local:latest
|
||||
docker pull sanjibdevnath/mcp-excalidraw-local-canvas:latest
|
||||
```
|
||||
|
||||
Then recreate your containers (`docker compose up -d` or `docker run` again).
|
||||
|
||||
#### Updating the agent skill manually
|
||||
|
||||
If you skipped the interactive update, copy the skill files yourself:
|
||||
```bash
|
||||
cp -R skills/excalidraw-skill ~/.cursor/skills/excalidraw-skill
|
||||
cp -R skills/excalidraw-skill ~/.claude/skills/excalidraw-skill
|
||||
```
|
||||
|
||||
### Verify the update
|
||||
|
||||
```bash
|
||||
# Check the running version
|
||||
curl -s http://localhost:3000/health
|
||||
|
||||
# Or check the installed package version
|
||||
npx @sanjibdevnath/mcp-excalidraw-local --version
|
||||
```
|
||||
|
||||
## How We Differ from the Official Excalidraw MCP
|
||||
|
||||
Excalidraw now has an [official MCP](https://github.com/excalidraw/excalidraw-mcp) — it's great for quick, prompt-to-diagram generation rendered inline in chat. We solve a different problem.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@sanjibdevnath/mcp-excalidraw-local",
|
||||
"version": "1.1.5",
|
||||
"version": "1.2.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@sanjibdevnath/mcp-excalidraw-local",
|
||||
"version": "1.1.5",
|
||||
"version": "1.2.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sanjibdevnath/mcp-excalidraw-local",
|
||||
"version": "1.1.5",
|
||||
"version": "1.2.1",
|
||||
"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",
|
||||
@@ -19,6 +19,7 @@
|
||||
"production": "npm run build && npm run canvas",
|
||||
"prepublishOnly": "npm run build",
|
||||
"setup": "node dist/index.js setup",
|
||||
"update": "node dist/index.js update",
|
||||
"type-check": "npx tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
|
||||
+6
-1
@@ -2727,12 +2727,17 @@ if (isMainModule()) {
|
||||
process.stderr.write(`Setup failed: ${(error as Error).message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
} else if (arg === 'update') {
|
||||
import('./setup.js').then(m => m.runUpdate()).catch(error => {
|
||||
process.stderr.write(`Update 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`);
|
||||
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 update Update agent skills and MCP config\n mcp-excalidraw-local --help Show this help\n mcp-excalidraw-local --version Show version\n`);
|
||||
} else {
|
||||
process.stdout.write(`${pkg.version}\n`);
|
||||
}
|
||||
|
||||
+203
-3
@@ -55,6 +55,7 @@ interface AgentDef {
|
||||
skillBasePaths: { global: string; local: string };
|
||||
mcpConfigType: 'json-file' | 'cli-command';
|
||||
mcpConfigPath?: string;
|
||||
mcpCliRemove?: string;
|
||||
mcpCliCommand?: string;
|
||||
}
|
||||
|
||||
@@ -79,7 +80,8 @@ function getAgents(): AgentDef[] {
|
||||
local: path.join(process.cwd(), '.claude', 'skills'),
|
||||
},
|
||||
mcpConfigType: 'cli-command',
|
||||
mcpCliCommand: 'claude mcp add excalidraw-canvas --scope user -e CANVAS_PORT=3000 -- npx -y @sanjibdevnath/mcp-excalidraw-local',
|
||||
mcpCliRemove: 'claude mcp remove excalidraw-canvas --scope user',
|
||||
mcpCliCommand: 'claude mcp add excalidraw-canvas --scope user -e CANVAS_PORT=3000 -- npx -y @sanjibdevnath/mcp-excalidraw-local@latest',
|
||||
},
|
||||
{
|
||||
name: 'Codex CLI',
|
||||
@@ -285,6 +287,9 @@ async function phaseMcpConfig(rl: readline.Interface): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
if (agent.mcpCliRemove) {
|
||||
try { execSync(agent.mcpCliRemove, { stdio: 'pipe' }); } catch { /* ignore if not found */ }
|
||||
}
|
||||
execSync(agent.mcpCliCommand, { stdio: 'inherit' });
|
||||
ok(`Registered 'excalidraw-canvas' via ${agent.name} CLI`);
|
||||
} catch (err) {
|
||||
@@ -299,7 +304,7 @@ async function phaseMcpConfig(rl: readline.Interface): Promise<void> {
|
||||
function mergeJsonConfig(configPath: string): void {
|
||||
const mcpEntry = {
|
||||
command: 'npx',
|
||||
args: ['-y', '@sanjibdevnath/mcp-excalidraw-local'],
|
||||
args: ['-y', '@sanjibdevnath/mcp-excalidraw-local@latest'],
|
||||
env: { CANVAS_PORT: '3000' },
|
||||
};
|
||||
|
||||
@@ -331,6 +336,23 @@ function mergeJsonConfig(configPath: string): void {
|
||||
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
|
||||
function checkJsonConfigStatus(configPath: string): 'up-to-date' | 'needs-update' | 'not-found' {
|
||||
if (!fs.existsSync(configPath)) return 'not-found';
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(configPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
const entry = config?.mcpServers?.['excalidraw-canvas'];
|
||||
if (!entry) return 'not-found';
|
||||
|
||||
const args: string[] = entry.args ?? [];
|
||||
const hasLatest = args.some((a: string) => a.includes('@latest'));
|
||||
return hasLatest ? 'up-to-date' : 'needs-update';
|
||||
} catch {
|
||||
return 'not-found';
|
||||
}
|
||||
}
|
||||
|
||||
function printManualConfig(): void {
|
||||
process.stdout.write(`
|
||||
Manual config (JSON):
|
||||
@@ -338,7 +360,7 @@ function printManualConfig(): void {
|
||||
"mcpServers": {
|
||||
"excalidraw-canvas": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@sanjibdevnath/mcp-excalidraw-local"],
|
||||
"args": ["-y", "@sanjibdevnath/mcp-excalidraw-local@latest"],
|
||||
"env": { "CANVAS_PORT": "3000" }
|
||||
}
|
||||
}
|
||||
@@ -346,6 +368,184 @@ function printManualConfig(): void {
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Update ───────────────────────────────────────────────────
|
||||
|
||||
interface SkillInstallation {
|
||||
agent: AgentDef;
|
||||
scope: 'global' | 'local';
|
||||
path: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
function getPackageVersion(): string {
|
||||
try {
|
||||
const pkgPath = path.resolve(__dirname, '..', 'package.json');
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||
return pkg.version ?? 'unknown';
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function findExistingSkillInstalls(): SkillInstallation[] {
|
||||
const agents = getAgents();
|
||||
const installs: SkillInstallation[] = [];
|
||||
for (const agent of agents) {
|
||||
for (const scope of ['global', 'local'] as const) {
|
||||
const skillDir = path.join(agent.skillBasePaths[scope], 'excalidraw-skill');
|
||||
installs.push({
|
||||
agent,
|
||||
scope,
|
||||
path: skillDir,
|
||||
exists: fs.existsSync(path.join(skillDir, 'SKILL.md')),
|
||||
});
|
||||
}
|
||||
}
|
||||
return installs;
|
||||
}
|
||||
|
||||
export async function runUpdate(): Promise<void> {
|
||||
if (!process.stdin.isTTY) {
|
||||
process.stderr.write('Error: Update requires an interactive terminal.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const version = getPackageVersion();
|
||||
process.stdout.write(`\n ${BOLD}Excalidraw MCP — Update${RESET} ${DIM}v${version}${RESET}\n`);
|
||||
|
||||
try {
|
||||
// ── Phase 1: Detect existing skill installations ──────────
|
||||
heading('1/2', 'Skill Update');
|
||||
|
||||
const allInstalls = findExistingSkillInstalls();
|
||||
const existing = allInstalls.filter(i => i.exists);
|
||||
const missing = allInstalls.filter(i => !i.exists);
|
||||
const detectedAgents = detectInstalledAgents();
|
||||
|
||||
const skillSource = path.resolve(__dirname, '..', 'skills', 'excalidraw-skill');
|
||||
if (!fs.existsSync(skillSource)) {
|
||||
fail(`Skill source not found at ${skillSource}`);
|
||||
fail('This can happen with corrupted installs. Try: npx @sanjibdevnath/mcp-excalidraw-local@latest setup');
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing.length > 0) {
|
||||
process.stdout.write(`\n Found ${CYAN}${existing.length}${RESET} existing skill installation(s):\n`);
|
||||
existing.forEach((inst, i) => {
|
||||
const label = `${inst.agent.name} (${inst.scope})`;
|
||||
process.stdout.write(` ${CYAN}[${i + 1}]${RESET} ${label} — ${DIM}${inst.path}${RESET}\n`);
|
||||
});
|
||||
|
||||
const doUpdate = await confirm(rl, `\n Update all ${existing.length} installation(s) to v${version}?`);
|
||||
if (doUpdate) {
|
||||
let updated = 0;
|
||||
for (const inst of existing) {
|
||||
try {
|
||||
copyDirSync(skillSource, inst.path);
|
||||
ok(`Updated ${inst.agent.name} (${inst.scope}) — ${inst.path}`);
|
||||
updated++;
|
||||
} catch (err) {
|
||||
fail(`Failed to update ${inst.path}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
process.stdout.write(`\n ${GREEN}${updated}/${existing.length}${RESET} skill(s) updated.\n`);
|
||||
} else {
|
||||
info(`${DIM}Skipped skill update.${RESET}`);
|
||||
}
|
||||
} else {
|
||||
info('No existing skill installations found.');
|
||||
}
|
||||
|
||||
// Offer to install for detected agents that don't have the skill
|
||||
const agentsWithoutSkill = detectedAgents.filter(agent =>
|
||||
!existing.some(inst => inst.agent.name === agent.name),
|
||||
);
|
||||
|
||||
if (agentsWithoutSkill.length > 0) {
|
||||
process.stdout.write(`\n Agents without the skill:\n`);
|
||||
agentsWithoutSkill.forEach((a, i) => {
|
||||
process.stdout.write(` ${YELLOW}[${i + 1}]${RESET} ${a.name}\n`);
|
||||
});
|
||||
|
||||
const doInstall = await confirm(rl, 'Install the skill for these agents?');
|
||||
if (doInstall) {
|
||||
for (const agent of agentsWithoutSkill) {
|
||||
const scopeAnswer = await ask(rl, `\n ${agent.name} — scope? [G]lobal / [l]ocal: `);
|
||||
const scope = scopeAnswer.trim().toLowerCase() === 'l' ? 'local' : 'global';
|
||||
const destDir = path.join(agent.skillBasePaths[scope], 'excalidraw-skill');
|
||||
|
||||
try {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
copyDirSync(skillSource, destDir);
|
||||
ok(`Installed to ${destDir}`);
|
||||
} catch (err) {
|
||||
fail(`Failed to install to ${destDir}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 2: MCP config check ────────────────────────────
|
||||
heading('2/2', 'MCP Configuration');
|
||||
|
||||
for (const agent of detectedAgents) {
|
||||
if (agent.mcpConfigType === 'json-file' && agent.mcpConfigPath) {
|
||||
const status = checkJsonConfigStatus(agent.mcpConfigPath);
|
||||
|
||||
if (status === 'up-to-date') {
|
||||
ok(`${agent.name} — already uses @latest, auto-updates on restart.`);
|
||||
} else if (status === 'needs-update') {
|
||||
const doIt = await confirm(rl, `${agent.name} — config uses a pinned version. Migrate to @latest?`);
|
||||
if (doIt) {
|
||||
try {
|
||||
mergeJsonConfig(agent.mcpConfigPath);
|
||||
ok(`Migrated to @latest in ${agent.mcpConfigPath}`);
|
||||
} catch (err) {
|
||||
fail(`Failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const doIt = await confirm(rl, `${agent.name} — no MCP config found. Add it?`);
|
||||
if (doIt) {
|
||||
try {
|
||||
mergeJsonConfig(agent.mcpConfigPath);
|
||||
ok(`Added 'excalidraw-canvas' to ${agent.mcpConfigPath}`);
|
||||
} catch (err) {
|
||||
fail(`Failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (agent.mcpConfigType === 'cli-command' && agent.mcpCliCommand) {
|
||||
info(`${agent.name} — config managed via CLI (cannot auto-detect version).`);
|
||||
const doIt = await confirm(rl, `${agent.name} — re-register with @latest?`, false);
|
||||
if (doIt) {
|
||||
try {
|
||||
if (agent.mcpCliRemove) {
|
||||
try { execSync(agent.mcpCliRemove, { stdio: 'pipe' }); } catch { /* ignore if not found */ }
|
||||
}
|
||||
execSync(agent.mcpCliCommand, { stdio: 'inherit' });
|
||||
ok(`Re-registered 'excalidraw-canvas' via ${agent.name} CLI`);
|
||||
} catch (err) {
|
||||
fail(`CLI registration failed: ${(err as Error).message}`);
|
||||
}
|
||||
} else {
|
||||
info(`${DIM}Skipped.${RESET}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(`\n ${GREEN}${BOLD}Update complete!${RESET} Restart your MCP client to pick up changes.\n\n`);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────
|
||||
|
||||
export async function runSetup(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user