diff --git a/README.md b/README.md
index b21f6a0..e4fc9d5 100644
--- a/README.md
+++ b/README.md
@@ -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
+```
+
+
+Example session
+
+```
+$ 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.
+```
+
+
+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.
diff --git a/package.json b/package.json
index 37bd5b4..4a23a60 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/index.ts b/src/index.ts
index 791ab53..36f6f80 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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`);
}
diff --git a/src/setup.ts b/src/setup.ts
index 4457eec..643f142 100644
--- a/src/setup.ts
+++ b/src/setup.ts
@@ -346,6 +346,167 @@ 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 {
+ 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');
+
+ const wantConfig = await confirm(rl, 'Re-apply MCP server config? (overwrites existing entry)', false);
+ if (wantConfig) {
+ for (const agent of detectedAgents) {
+ if (agent.mcpConfigType === 'json-file' && agent.mcpConfigPath) {
+ const doIt = await confirm(rl, `${agent.name} — update ${agent.mcpConfigPath}?`);
+ if (doIt) {
+ try {
+ mergeJsonConfig(agent.mcpConfigPath);
+ ok(`Updated 'excalidraw-canvas' in ${agent.mcpConfigPath}`);
+ } catch (err) {
+ fail(`Failed: ${(err as Error).message}`);
+ }
+ }
+ } else if (agent.mcpConfigType === 'cli-command' && agent.mcpCliCommand) {
+ const doIt = await confirm(rl, `${agent.name} — re-register via CLI?`);
+ if (doIt) {
+ try {
+ 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}MCP config unchanged.${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 {