Compare commits

..
2 Commits
Author SHA1 Message Date
sanjibdevnathlabs-release-bot[bot] fac1c7a267 chore(release): v1.3.0 2026-03-13 18:19:16 +00:00
sanjibdevnathlabs 9311561227 feat(skill): add auto-triggering for excalidraw-skill via CLAUDE.md directives
The excalidraw-skill was not being auto-invoked when users prompted
Claude to draw diagrams, despite being installed. Claude would call
Excalidraw MCP tools directly, bypassing the skill's critical sizing
formulas and verification workflow — producing broken diagrams with
invisible arrows, truncated text, and overlapping elements. The root
cause is that Claude Code's skill system is advisory: when direct MCP
tools or built-in Bash instructions are available, Claude skips skill
consultation entirely.

🔧 Skill description rewrite:
- Lead with "MANDATORY prerequisite" to assert priority over raw MCP tools
- Add user-intent trigger keywords (draw, visualize, sketch, diagram)
- Name specific consequences of skipping the skill
- List common diagram types for semantic matching

🏗️ Setup/update auto-directives:
- Write marked CLAUDE.md sections during skill install and update
- Write Cursor .mdc rules with alwaysApply for Cursor users
- Use HTML comment markers for idempotent append-or-replace on updates
- Non-fatal directive writing — skill installs even if directive fails

🎯 Two-layer defense ensures reliable skill triggering: the description
catches semantic matching, while the CLAUDE.md directive provides an
authoritative instruction that Claude cannot deprioritize in favor of
raw tool access.
2026-03-13 23:46:59 +05:30
4 changed files with 125 additions and 4 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@sanjibdevnath/mcp-excalidraw-local",
"version": "1.2.1",
"version": "1.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@sanjibdevnath/mcp-excalidraw-local",
"version": "1.2.1",
"version": "1.3.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@sanjibdevnath/mcp-excalidraw-local",
"version": "1.2.1",
"version": "1.3.0",
"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",
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: excalidraw-skill
description: Programmatic canvas toolkit for creating, editing, and refining Excalidraw diagrams via MCP tools (32 tools) or REST API with real-time canvas sync, multi-tenant workspace isolation, SQLite persistence, project management, full-text search, and element version history. Use when an agent needs to draw or lay out diagrams on a live canvas, iteratively refine diagrams using screenshots, manage workspaces/tenants and projects, export/import .excalidraw files or PNG/SVG images, search elements, view change history, save/restore canvas snapshots, or perform element-level CRUD. Canvas server port is configurable via CANVAS_PORT env var (default 3000).
description: MANDATORY prerequisite for ALL Excalidraw MCP tool usage. Read this skill BEFORE calling any Excalidraw tool (batch_create_elements, create_element, create_from_mermaid, update_element, etc.) — without this skill's sizing formulas, two-batch ordering (shapes first, arrows second), and write-check-review verification cycle, diagrams will have invisible arrows, truncated text, and overlapping elements. Use whenever the user asks to draw, create, visualize, sketch, or diagram anything — flowcharts, architecture diagrams, system designs, org charts, sequence flows, decision trees, network topologies, ER diagrams, mind maps, or any visual on Excalidraw canvas. Also covers diagram refinement, PNG/SVG export, project/workspace management, and all canvas interactions.
---
# Excalidraw Skill
+121
View File
@@ -57,6 +57,11 @@ interface AgentDef {
mcpConfigPath?: string;
mcpCliRemove?: string;
mcpCliCommand?: string;
instructionConfig?: {
global: string;
local: string;
format: 'claude-md' | 'cursor-mdc';
};
}
function getAgents(): AgentDef[] {
@@ -71,6 +76,11 @@ function getAgents(): AgentDef[] {
},
mcpConfigType: 'json-file',
mcpConfigPath: path.join(home, '.cursor', 'mcp.json'),
instructionConfig: {
global: path.join(home, '.cursor', 'rules', 'excalidraw.mdc'),
local: path.join(process.cwd(), '.cursor', 'rules', 'excalidraw.mdc'),
format: 'cursor-mdc',
},
},
{
name: 'Claude Code',
@@ -82,6 +92,11 @@ function getAgents(): AgentDef[] {
mcpConfigType: 'cli-command',
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',
instructionConfig: {
global: path.join(home, '.claude', 'CLAUDE.md'),
local: path.join(process.cwd(), 'CLAUDE.md'),
format: 'claude-md',
},
},
{
name: 'Codex CLI',
@@ -226,6 +241,17 @@ async function phaseSkillInstall(rl: readline.Interface): Promise<void> {
fs.mkdirSync(destDir, { recursive: true });
copyDirSync(skillSource, destDir);
ok(`Installed to ${destDir}`);
// Write instruction directive to ensure skill auto-triggers
if (agent.instructionConfig) {
const instrPath = agent.instructionConfig[scope];
try {
writeInstructionDirective(instrPath, agent.instructionConfig.format);
ok(`Skill directive added to ${instrPath}`);
} catch (instrErr) {
warn(`Could not write directive to ${instrPath}: ${(instrErr as Error).message}`);
}
}
} catch (err) {
fail(`Failed to install to ${destDir}: ${(err as Error).message}`);
}
@@ -245,6 +271,79 @@ function copyDirSync(src: string, dest: string): void {
}
}
// ── Instruction Directives ──────────────────────────────────
const DIRECTIVE_MARKER_START = '<!-- excalidraw-mcp:start -->';
const DIRECTIVE_MARKER_END = '<!-- excalidraw-mcp:end -->';
const EXCALIDRAW_DIRECTIVE_BODY =
'When Excalidraw MCP tools are available (batch_create_elements, create_element, ' +
'create_from_mermaid, etc.), ALWAYS invoke the `/excalidraw-skill` skill FIRST ' +
'before calling any Excalidraw tool. The skill contains essential sizing formulas, ' +
'batch ordering rules (shapes first, arrows second), and a mandatory ' +
'write-check-review verification cycle. Using Excalidraw tools without consulting ' +
'this skill produces broken diagrams — invisible arrows, truncated text, ' +
'overlapping elements.';
function buildClaudeMdSection(): string {
return [
DIRECTIVE_MARKER_START,
'## Excalidraw Canvas — Skill Directive',
'',
EXCALIDRAW_DIRECTIVE_BODY,
DIRECTIVE_MARKER_END,
].join('\n');
}
function buildCursorMdc(): string {
return [
'---',
'description: Always consult excalidraw-skill before using Excalidraw MCP tools',
'globs:',
'alwaysApply: true',
'---',
'',
'## Excalidraw Canvas — Skill Directive',
'',
EXCALIDRAW_DIRECTIVE_BODY,
'',
].join('\n');
}
function writeInstructionDirective(filePath: string, format: 'claude-md' | 'cursor-mdc'): void {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (format === 'cursor-mdc') {
// Cursor .mdc files are standalone — write/overwrite the whole file
fs.writeFileSync(filePath, buildCursorMdc(), 'utf-8');
return;
}
// For claude-md: append or replace the marked section
let content = '';
if (fs.existsSync(filePath)) {
content = fs.readFileSync(filePath, 'utf-8');
}
const section = buildClaudeMdSection();
const startIdx = content.indexOf(DIRECTIVE_MARKER_START);
const endIdx = content.indexOf(DIRECTIVE_MARKER_END);
if (startIdx !== -1 && endIdx !== -1) {
// Replace existing section
content = content.slice(0, startIdx) + section + content.slice(endIdx + DIRECTIVE_MARKER_END.length);
} else {
// Append with spacing
const trimmed = content.trimEnd();
content = trimmed + (trimmed ? '\n\n' : '') + section + '\n';
}
fs.writeFileSync(filePath, content, 'utf-8');
}
// ── Phase 3: MCP Configuration ──────────────────────────────
async function phaseMcpConfig(rl: readline.Interface): Promise<void> {
@@ -450,6 +549,17 @@ export async function runUpdate(): Promise<void> {
copyDirSync(skillSource, inst.path);
ok(`Updated ${inst.agent.name} (${inst.scope}) — ${inst.path}`);
updated++;
// Update instruction directive
if (inst.agent.instructionConfig) {
const instrPath = inst.agent.instructionConfig[inst.scope];
try {
writeInstructionDirective(instrPath, inst.agent.instructionConfig.format);
ok(`Skill directive updated in ${instrPath}`);
} catch (instrErr) {
warn(`Could not update directive in ${instrPath}: ${(instrErr as Error).message}`);
}
}
} catch (err) {
fail(`Failed to update ${inst.path}: ${(err as Error).message}`);
}
@@ -484,6 +594,17 @@ export async function runUpdate(): Promise<void> {
fs.mkdirSync(destDir, { recursive: true });
copyDirSync(skillSource, destDir);
ok(`Installed to ${destDir}`);
// Write instruction directive
if (agent.instructionConfig) {
const instrPath = agent.instructionConfig[scope];
try {
writeInstructionDirective(instrPath, agent.instructionConfig.format);
ok(`Skill directive added to ${instrPath}`);
} catch (instrErr) {
warn(`Could not write directive to ${instrPath}: ${(instrErr as Error).message}`);
}
}
} catch (err) {
fail(`Failed to install to ${destDir}: ${(err as Error).message}`);
}