# Directory Structure
```
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── index.ts
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│ ├── common
│ │ └── errors.ts
│ └── operations
│ ├── drawings.ts
│ └── export.ts
├── test.js
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependency directories
node_modules/
# Build output
dist/
# Storage directory
storage/
# Environment variables
.env
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS specific
.DS_Store
Thumbs.db
# Inspiration directory
inspiration/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Excalidraw MCP Server
This is a Model Context Protocol (MCP) server for Excalidraw, providing API functionality for operating on Excalidraw drawings.
## Features
- Create, read, update, and delete Excalidraw drawings
- Export drawings to SVG, PNG, and JSON formats
- Simple file-based storage system
## Installation
```bash
# Clone the repository
git clone https://github.com/yourusername/excalidraw-mcp.git
cd excalidraw-mcp
# Install dependencies
npm install
# Build the project
npm run build
```
## Usage
### Starting the Server
```bash
npm start
```
### API Endpoints
The server provides the following tools:
#### Drawing Management
- `create_drawing`: Create a new Excalidraw drawing
- `get_drawing`: Get an Excalidraw drawing by ID
- `update_drawing`: Update an Excalidraw drawing by ID
- `delete_drawing`: Delete an Excalidraw drawing by ID
- `list_drawings`: List all Excalidraw drawings
#### Export Operations
- `export_to_svg`: Export an Excalidraw drawing to SVG
- `export_to_png`: Export an Excalidraw drawing to PNG
- `export_to_json`: Export an Excalidraw drawing to JSON
## Development
### Project Structure
```
excalidraw-mcp/
├── src/
│ ├── common/
│ │ └── errors.ts
│ └── operations/
│ ├── drawings.ts
│ └── export.ts
├── index.ts
├── package.json
├── tsconfig.json
└── README.md
```
### Building
```bash
npm run build
```
### Running in Development Mode
```bash
npm run dev
```
## License
MIT
```
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
```yaml
version: '3'
services:
excalidraw-mcp:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./storage:/app/storage
restart: unless-stopped
# If we add an HTTP server in the future, we can uncomment this
# ports:
# - "3000:3000"
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"skipLibCheck": true
},
"include": ["src/**/*", "index.ts"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM node:18-alpine
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application
COPY . .
# Build the application
RUN npm run build
# Create storage directory
RUN mkdir -p storage
# Set permissions for storage directory
RUN chmod -R 777 storage
# Expose port (if needed for HTTP server in the future)
# EXPOSE 3000
# Run the server
CMD ["npm", "start"]
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "excalidraw-mcp",
"version": "0.1.0",
"description": "MCP server for Excalidraw",
"main": "index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc-watch --onSuccess \"node dist/index.js\"",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"excalidraw",
"mcp",
"drawing"
],
"author": "",
"license": "MIT",
"dependencies": {
"@excalidraw/excalidraw": "^0.17.0",
"@modelcontextprotocol/sdk": "^1.6.1",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.3"
},
"devDependencies": {
"@types/node": "^20.10.5",
"typescript": "^5.3.3",
"tsc-watch": "^6.0.4"
}
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
name: excalidraw-mcp
version: 0.1.0
description: MCP server for Excalidraw
author: Your Name
license: MIT
repository: https://github.com/yourusername/excalidraw-mcp
homepage: https://github.com/yourusername/excalidraw-mcp
entrypoint:
type: stdio
command: node dist/index.js
capabilities:
tools:
- name: create_drawing
description: Create a new Excalidraw drawing
- name: get_drawing
description: Get an Excalidraw drawing by ID
- name: update_drawing
description: Update an Excalidraw drawing by ID
- name: delete_drawing
description: Delete an Excalidraw drawing by ID
- name: list_drawings
description: List all Excalidraw drawings
- name: export_to_svg
description: Export an Excalidraw drawing to SVG
- name: export_to_png
description: Export an Excalidraw drawing to PNG
- name: export_to_json
description: Export an Excalidraw drawing to JSON
```
--------------------------------------------------------------------------------
/src/common/errors.ts:
--------------------------------------------------------------------------------
```typescript
export class ExcalidrawError extends Error {
constructor(message: string) {
super(message);
this.name = 'ExcalidrawError';
}
}
export class ExcalidrawValidationError extends ExcalidrawError {
response?: any;
constructor(message: string, response?: any) {
super(message);
this.name = 'ExcalidrawValidationError';
this.response = response;
}
}
export class ExcalidrawResourceNotFoundError extends ExcalidrawError {
constructor(message: string) {
super(message);
this.name = 'ExcalidrawResourceNotFoundError';
}
}
export class ExcalidrawAuthenticationError extends ExcalidrawError {
constructor(message: string) {
super(message);
this.name = 'ExcalidrawAuthenticationError';
}
}
export class ExcalidrawPermissionError extends ExcalidrawError {
constructor(message: string) {
super(message);
this.name = 'ExcalidrawPermissionError';
}
}
export class ExcalidrawRateLimitError extends ExcalidrawError {
resetAt: Date;
constructor(message: string, resetAt: Date) {
super(message);
this.name = 'ExcalidrawRateLimitError';
this.resetAt = resetAt;
}
}
export class ExcalidrawConflictError extends ExcalidrawError {
constructor(message: string) {
super(message);
this.name = 'ExcalidrawConflictError';
}
}
export function isExcalidrawError(error: any): error is ExcalidrawError {
return error instanceof ExcalidrawError;
}
```
--------------------------------------------------------------------------------
/src/operations/export.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { ExcalidrawResourceNotFoundError } from '../common/errors.js';
import { getDrawing } from './drawings.js';
// Schema for exporting a drawing to SVG
export const ExportToSvgSchema = z.object({
id: z.string().min(1),
});
// Schema for exporting a drawing to PNG
export const ExportToPngSchema = z.object({
id: z.string().min(1),
quality: z.number().min(0).max(1).optional().default(0.92),
scale: z.number().min(0.1).max(5).optional().default(1),
exportWithDarkMode: z.boolean().optional().default(false),
exportBackground: z.boolean().optional().default(true),
});
// Schema for exporting a drawing to JSON
export const ExportToJsonSchema = z.object({
id: z.string().min(1),
});
// Export a drawing to SVG
export async function exportToSvg(id: string): Promise<string> {
try {
// Get the drawing
const drawing = await getDrawing(id);
// Return the SVG content
// Note: In a real implementation, we would use the Excalidraw API to convert the drawing to SVG
// For now, we'll just return a placeholder
return `<svg>
<text x="10" y="20">Drawing: ${drawing.name}</text>
<text x="10" y="40">This is a placeholder for the SVG export.</text>
</svg>`;
} catch (error) {
if (error instanceof ExcalidrawResourceNotFoundError) {
throw error;
}
throw new Error(`Failed to export drawing to SVG: ${(error as Error).message}`);
}
}
// Export a drawing to PNG
export async function exportToPng(
id: string,
quality: number = 0.92,
scale: number = 1,
exportWithDarkMode: boolean = false,
exportBackground: boolean = true
): Promise<string> {
try {
// Get the drawing
const drawing = await getDrawing(id);
// Return the PNG content as a base64 string
// Note: In a real implementation, we would use the Excalidraw API to convert the drawing to PNG
// For now, we'll just return a placeholder
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
} catch (error) {
if (error instanceof ExcalidrawResourceNotFoundError) {
throw error;
}
throw new Error(`Failed to export drawing to PNG: ${(error as Error).message}`);
}
}
// Export a drawing to JSON
export async function exportToJson(id: string): Promise<string> {
try {
// Get the drawing
const drawing = await getDrawing(id);
// Return the JSON content
return drawing.content;
} catch (error) {
if (error instanceof ExcalidrawResourceNotFoundError) {
throw error;
}
throw new Error(`Failed to export drawing to JSON: ${(error as Error).message}`);
}
}
```
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function main() {
// Create a transport connected to the server process
const transport = new StdioClientTransport({
command: 'node',
args: ['dist/index.js'],
});
// Create a client
const client = new Client(
{
name: 'excalidraw-mcp-test',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
try {
// Connect to the server
await client.connect(transport);
console.log('Connected to server');
// List available tools
const tools = await client.listTools();
console.log('Available tools:', tools);
// Create a drawing
const createResult = await client.callTool({
name: 'create_drawing',
arguments: {
name: 'Test Drawing',
content: JSON.stringify({
type: 'excalidraw',
version: 2,
source: 'excalidraw-mcp-test',
elements: [
{
id: 'rectangle1',
type: 'rectangle',
x: 100,
y: 100,
width: 200,
height: 100,
angle: 0,
strokeColor: '#000000',
backgroundColor: '#ffffff',
fillStyle: 'solid',
strokeWidth: 1,
strokeStyle: 'solid',
roughness: 1,
opacity: 100,
groupIds: [],
strokeSharpness: 'sharp',
seed: 123456,
version: 1,
versionNonce: 1,
isDeleted: false,
boundElementIds: null,
},
],
appState: {
viewBackgroundColor: '#ffffff',
},
}),
},
});
console.log('Created drawing:', createResult);
// Get the drawing ID from the result
const drawingId = JSON.parse(createResult.content[0].text).id;
// Get the drawing
const getResult = await client.callTool({
name: 'get_drawing',
arguments: {
id: drawingId,
},
});
console.log('Retrieved drawing:', getResult);
// Export the drawing to SVG
const svgResult = await client.callTool({
name: 'export_to_svg',
arguments: {
id: drawingId,
},
});
console.log('SVG export:', svgResult);
// Export the drawing to PNG
const pngResult = await client.callTool({
name: 'export_to_png',
arguments: {
id: drawingId,
quality: 0.9,
scale: 2,
exportWithDarkMode: true,
exportBackground: true,
},
});
console.log('PNG export:', pngResult);
// List all drawings
const listResult = await client.callTool({
name: 'list_drawings',
arguments: {
page: 1,
perPage: 10,
},
});
console.log('List of drawings:', listResult);
// Update the drawing
const updateResult = await client.callTool({
name: 'update_drawing',
arguments: {
id: drawingId,
content: JSON.stringify({
type: 'excalidraw',
version: 2,
source: 'excalidraw-mcp-test',
elements: [
{
id: 'rectangle1',
type: 'rectangle',
x: 100,
y: 100,
width: 200,
height: 100,
angle: 0,
strokeColor: '#ff0000', // Changed to red
backgroundColor: '#ffffff',
fillStyle: 'solid',
strokeWidth: 2, // Increased width
strokeStyle: 'solid',
roughness: 1,
opacity: 100,
groupIds: [],
strokeSharpness: 'sharp',
seed: 123456,
version: 2,
versionNonce: 2,
isDeleted: false,
boundElementIds: null,
},
],
appState: {
viewBackgroundColor: '#ffffff',
},
}),
},
});
console.log('Updated drawing:', updateResult);
// Delete the drawing
const deleteResult = await client.callTool({
name: 'delete_drawing',
arguments: {
id: drawingId,
},
});
console.log('Deleted drawing:', deleteResult);
console.log('All tests passed!');
} catch (error) {
console.error('Error:', error);
} finally {
// Close the client connection
await client.close();
}
}
main().catch((error) => {
console.error('Test error:', error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/operations/drawings.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import fs from 'fs/promises';
import path from 'path';
import { ExcalidrawResourceNotFoundError, ExcalidrawValidationError } from '../common/errors.js';
// Define the storage directory for drawings
const STORAGE_DIR = path.join(process.cwd(), 'storage');
// Ensure storage directory exists
async function ensureStorageDir() {
try {
await fs.mkdir(STORAGE_DIR, { recursive: true });
} catch (error) {
console.error('Failed to create storage directory:', error);
throw error;
}
}
// Schema for creating a drawing
export const CreateDrawingSchema = z.object({
name: z.string().min(1),
content: z.string().min(1),
});
// Schema for getting a drawing
export const GetDrawingSchema = z.object({
id: z.string().min(1),
});
// Schema for updating a drawing
export const UpdateDrawingSchema = z.object({
id: z.string().min(1),
content: z.string().min(1),
});
// Schema for deleting a drawing
export const DeleteDrawingSchema = z.object({
id: z.string().min(1),
});
// Schema for listing drawings
export const ListDrawingsSchema = z.object({
page: z.number().int().min(1).optional().default(1),
perPage: z.number().int().min(1).max(100).optional().default(10),
});
// Create a new drawing
export async function createDrawing(name: string, content: string): Promise<{ id: string, name: string }> {
await ensureStorageDir();
// Generate a unique ID for the drawing
const id = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
// Create the drawing file
const filePath = path.join(STORAGE_DIR, `${id}.json`);
// Save the drawing content
await fs.writeFile(filePath, content, 'utf-8');
// Create a metadata file for the drawing
const metadataPath = path.join(STORAGE_DIR, `${id}.meta.json`);
const metadata = {
id,
name,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
return { id, name };
}
// Get a drawing by ID
export async function getDrawing(id: string): Promise<{ id: string, name: string, content: string, metadata: any }> {
await ensureStorageDir();
// Get the drawing file path
const filePath = path.join(STORAGE_DIR, `${id}.json`);
const metadataPath = path.join(STORAGE_DIR, `${id}.meta.json`);
try {
// Read the drawing content
const content = await fs.readFile(filePath, 'utf-8');
// Read the metadata
const metadataStr = await fs.readFile(metadataPath, 'utf-8');
const metadata = JSON.parse(metadataStr);
return {
id,
name: metadata.name,
content,
metadata,
};
} catch (error) {
throw new ExcalidrawResourceNotFoundError(`Drawing with ID ${id} not found`);
}
}
// Update a drawing by ID
export async function updateDrawing(id: string, content: string): Promise<{ id: string, name: string }> {
await ensureStorageDir();
// Get the drawing file path
const filePath = path.join(STORAGE_DIR, `${id}.json`);
const metadataPath = path.join(STORAGE_DIR, `${id}.meta.json`);
try {
// Check if the drawing exists
await fs.access(filePath);
// Read the metadata
const metadataStr = await fs.readFile(metadataPath, 'utf-8');
const metadata = JSON.parse(metadataStr);
// Update the drawing content
await fs.writeFile(filePath, content, 'utf-8');
// Update the metadata
metadata.updatedAt = new Date().toISOString();
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
return { id, name: metadata.name };
} catch (error) {
throw new ExcalidrawResourceNotFoundError(`Drawing with ID ${id} not found`);
}
}
// Delete a drawing by ID
export async function deleteDrawing(id: string): Promise<void> {
await ensureStorageDir();
// Get the drawing file path
const filePath = path.join(STORAGE_DIR, `${id}.json`);
const metadataPath = path.join(STORAGE_DIR, `${id}.meta.json`);
try {
// Check if the drawing exists
await fs.access(filePath);
// Delete the drawing file
await fs.unlink(filePath);
// Delete the metadata file
await fs.unlink(metadataPath);
} catch (error) {
throw new ExcalidrawResourceNotFoundError(`Drawing with ID ${id} not found`);
}
}
// List all drawings
export async function listDrawings(page: number = 1, perPage: number = 10): Promise<{ drawings: any[], total: number }> {
await ensureStorageDir();
try {
// Get all files in the storage directory
const files = await fs.readdir(STORAGE_DIR);
// Filter metadata files
const metadataFiles = files.filter(file => file.endsWith('.meta.json'));
// Calculate pagination
const start = (page - 1) * perPage;
const end = start + perPage;
const paginatedFiles = metadataFiles.slice(start, end);
// Read metadata for each drawing
const drawings = await Promise.all(
paginatedFiles.map(async (file) => {
const metadataPath = path.join(STORAGE_DIR, file);
const metadataStr = await fs.readFile(metadataPath, 'utf-8');
return JSON.parse(metadataStr);
})
);
return {
drawings,
total: metadataFiles.length,
};
} catch (error) {
console.error('Failed to list drawings:', error);
return {
drawings: [],
total: 0,
};
}
}
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/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);
});
```