Excalidraw MCP Server
by i-tozer
Verified
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import * as drawings from './src/operations/drawings.js';
import * as exportOps from './src/operations/export.js';
import {
ExcalidrawError,
ExcalidrawValidationError,
ExcalidrawResourceNotFoundError,
ExcalidrawAuthenticationError,
ExcalidrawPermissionError,
ExcalidrawRateLimitError,
ExcalidrawConflictError,
isExcalidrawError,
} from './src/common/errors.js';
const server = new Server(
{
name: "excalidraw-mcp-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
function formatExcalidrawError(error: ExcalidrawError): string {
let message = `Excalidraw API Error: ${error.message}`;
if (error instanceof ExcalidrawValidationError) {
message = `Validation Error: ${error.message}`;
if (error.response) {
message += `\nDetails: ${JSON.stringify(error.response)}`;
}
} else if (error instanceof ExcalidrawResourceNotFoundError) {
message = `Not Found: ${error.message}`;
} else if (error instanceof ExcalidrawAuthenticationError) {
message = `Authentication Failed: ${error.message}`;
} else if (error instanceof ExcalidrawPermissionError) {
message = `Permission Denied: ${error.message}`;
} else if (error instanceof ExcalidrawRateLimitError) {
message = `Rate Limit Exceeded: ${error.message}\nResets at: ${error.resetAt.toISOString()}`;
} else if (error instanceof ExcalidrawConflictError) {
message = `Conflict: ${error.message}`;
}
return message;
}
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_drawing",
description: "Create a new Excalidraw drawing",
inputSchema: zodToJsonSchema(drawings.CreateDrawingSchema),
},
{
name: "get_drawing",
description: "Get an Excalidraw drawing by ID",
inputSchema: zodToJsonSchema(drawings.GetDrawingSchema),
},
{
name: "update_drawing",
description: "Update an Excalidraw drawing by ID",
inputSchema: zodToJsonSchema(drawings.UpdateDrawingSchema),
},
{
name: "delete_drawing",
description: "Delete an Excalidraw drawing by ID",
inputSchema: zodToJsonSchema(drawings.DeleteDrawingSchema),
},
{
name: "list_drawings",
description: "List all Excalidraw drawings",
inputSchema: zodToJsonSchema(drawings.ListDrawingsSchema),
},
{
name: "export_to_svg",
description: "Export an Excalidraw drawing to SVG",
inputSchema: zodToJsonSchema(exportOps.ExportToSvgSchema),
},
{
name: "export_to_png",
description: "Export an Excalidraw drawing to PNG",
inputSchema: zodToJsonSchema(exportOps.ExportToPngSchema),
},
{
name: "export_to_json",
description: "Export an Excalidraw drawing to JSON",
inputSchema: zodToJsonSchema(exportOps.ExportToJsonSchema),
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (!request.params.arguments) {
throw new Error("Arguments are required");
}
switch (request.params.name) {
case "create_drawing": {
const args = drawings.CreateDrawingSchema.parse(request.params.arguments);
const result = await drawings.createDrawing(args.name, args.content);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "get_drawing": {
const args = drawings.GetDrawingSchema.parse(request.params.arguments);
const result = await drawings.getDrawing(args.id);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "update_drawing": {
const args = drawings.UpdateDrawingSchema.parse(request.params.arguments);
const result = await drawings.updateDrawing(args.id, args.content);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "delete_drawing": {
const args = drawings.DeleteDrawingSchema.parse(request.params.arguments);
await drawings.deleteDrawing(args.id);
return {
content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }],
};
}
case "list_drawings": {
const args = drawings.ListDrawingsSchema.parse(request.params.arguments);
const result = await drawings.listDrawings(args.page, args.perPage);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "export_to_svg": {
const args = exportOps.ExportToSvgSchema.parse(request.params.arguments);
const result = await exportOps.exportToSvg(args.id);
return {
content: [{ type: "text", text: result }],
};
}
case "export_to_png": {
const args = exportOps.ExportToPngSchema.parse(request.params.arguments);
const result = await exportOps.exportToPng(
args.id,
args.quality,
args.scale,
args.exportWithDarkMode,
args.exportBackground
);
return {
content: [{ type: "text", text: result }],
};
}
case "export_to_json": {
const args = exportOps.ExportToJsonSchema.parse(request.params.arguments);
const result = await exportOps.exportToJson(args.id);
return {
content: [{ type: "text", text: result }],
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error) {
console.error("Error handling request:", error);
if (isExcalidrawError(error)) {
return {
error: formatExcalidrawError(error),
};
}
return {
error: `Error: ${(error as Error).message}`,
};
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});