Skip to main content
Glama

Excalidraw MCP Server

by yctimlin
server.ts14.1 kB
import express, { Request, Response, NextFunction } from 'express'; import cors from 'cors'; import { WebSocketServer } from 'ws'; import { createServer } from 'http'; import path from 'path'; import { fileURLToPath } from 'url'; import dotenv from 'dotenv'; import logger from './utils/logger.js'; import { elements, generateId, EXCALIDRAW_ELEMENT_TYPES, ServerElement, ExcalidrawElementType, WebSocketMessage, ElementCreatedMessage, ElementUpdatedMessage, ElementDeletedMessage, BatchCreatedMessage, SyncStatusMessage, InitialElementsMessage } from './types.js'; import { z } from 'zod'; import WebSocket from 'ws'; // Load environment variables dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const server = createServer(app); const wss = new WebSocketServer({ server }); // Middleware app.use(cors()); app.use(express.json()); // Serve static files from the build directory const staticDir = path.join(__dirname, '../dist'); app.use(express.static(staticDir)); // Also serve frontend assets app.use(express.static(path.join(__dirname, '../dist/frontend'))); // WebSocket connections const clients = new Set<WebSocket>(); // Broadcast to all connected clients function broadcast(message: WebSocketMessage): void { const data = JSON.stringify(message); clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(data); } }); } // WebSocket connection handling wss.on('connection', (ws: WebSocket) => { clients.add(ws); logger.info('New WebSocket connection established'); // Send current elements to new client const initialMessage: InitialElementsMessage = { type: 'initial_elements', elements: Array.from(elements.values()) }; ws.send(JSON.stringify(initialMessage)); // Send sync status to new client const syncMessage: SyncStatusMessage = { type: 'sync_status', elementCount: elements.size, timestamp: new Date().toISOString() }; ws.send(JSON.stringify(syncMessage)); ws.on('close', () => { clients.delete(ws); logger.info('WebSocket connection closed'); }); ws.on('error', (error) => { logger.error('WebSocket error:', error); clients.delete(ws); }); }); // Schema validation const CreateElementSchema = z.object({ id: z.string().optional(), // Allow passing ID for MCP sync type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES) as [ExcalidrawElementType, ...ExcalidrawElementType[]]), x: z.number(), y: z.number(), width: z.number().optional(), height: z.number().optional(), backgroundColor: z.string().optional(), strokeColor: z.string().optional(), strokeWidth: z.number().optional(), roughness: z.number().optional(), opacity: z.number().optional(), text: z.string().optional(), label: z.object({ text: z.string() }).optional(), fontSize: z.number().optional(), fontFamily: z.string().optional() }); const UpdateElementSchema = z.object({ id: z.string(), type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES) as [ExcalidrawElementType, ...ExcalidrawElementType[]]).optional(), x: z.number().optional(), y: z.number().optional(), width: z.number().optional(), height: z.number().optional(), backgroundColor: z.string().optional(), strokeColor: z.string().optional(), strokeWidth: z.number().optional(), roughness: z.number().optional(), opacity: z.number().optional(), text: z.string().optional(), label: z.object({ text: z.string() }).optional(), fontSize: z.number().optional(), fontFamily: z.string().optional() }); // API Routes // Get all elements app.get('/api/elements', (req: Request, res: Response) => { try { const elementsArray = Array.from(elements.values()); res.json({ success: true, elements: elementsArray, count: elementsArray.length }); } catch (error) { logger.error('Error fetching elements:', error); res.status(500).json({ success: false, error: (error as Error).message }); } }); // Create new element app.post('/api/elements', (req: Request, res: Response) => { try { const params = CreateElementSchema.parse(req.body); logger.info('Creating element via API', { type: params.type }); // Prioritize passed ID (for MCP sync), otherwise generate new ID const id = params.id || generateId(); const element: ServerElement = { id, ...params, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), version: 1 }; elements.set(id, element); // Broadcast to all connected clients const message: ElementCreatedMessage = { type: 'element_created', element: element }; broadcast(message); res.json({ success: true, element: element }); } catch (error) { logger.error('Error creating element:', error); res.status(400).json({ success: false, error: (error as Error).message }); } }); // Update element app.put('/api/elements/:id', (req: Request, res: Response) => { try { const { id } = req.params; const updates = UpdateElementSchema.parse({ id, ...req.body }); if (!id) { return res.status(400).json({ success: false, error: 'Element ID is required' }); } const existingElement = elements.get(id); if (!existingElement) { return res.status(404).json({ success: false, error: `Element with ID ${id} not found` }); } const updatedElement: ServerElement = { ...existingElement, ...updates, updatedAt: new Date().toISOString(), version: (existingElement.version || 0) + 1 }; elements.set(id, updatedElement); // Broadcast to all connected clients const message: ElementUpdatedMessage = { type: 'element_updated', element: updatedElement }; broadcast(message); res.json({ success: true, element: updatedElement }); } catch (error) { logger.error('Error updating element:', error); res.status(400).json({ success: false, error: (error as Error).message }); } }); // Delete element app.delete('/api/elements/:id', (req: Request, res: Response) => { try { const { id } = req.params; if (!id) { return res.status(400).json({ success: false, error: 'Element ID is required' }); } if (!elements.has(id)) { return res.status(404).json({ success: false, error: `Element with ID ${id} not found` }); } elements.delete(id); // Broadcast to all connected clients const message: ElementDeletedMessage = { type: 'element_deleted', elementId: id! }; broadcast(message); res.json({ success: true, message: `Element ${id} deleted successfully` }); } catch (error) { logger.error('Error deleting element:', error); res.status(500).json({ success: false, error: (error as Error).message }); } }); // Query elements with filters app.get('/api/elements/search', (req: Request, res: Response) => { try { const { type, ...filters } = req.query; let results = Array.from(elements.values()); // Filter by type if specified if (type && typeof type === 'string') { results = results.filter(element => element.type === type); } // Apply additional filters if (Object.keys(filters).length > 0) { results = results.filter(element => { return Object.entries(filters).every(([key, value]) => { return (element as any)[key] === value; }); }); } res.json({ success: true, elements: results, count: results.length }); } catch (error) { logger.error('Error querying elements:', error); res.status(500).json({ success: false, error: (error as Error).message }); } }); // Get element by ID app.get('/api/elements/:id', (req: Request, res: Response) => { try { const { id } = req.params; if (!id) { return res.status(400).json({ success: false, error: 'Element ID is required' }); } const element = elements.get(id); if (!element) { return res.status(404).json({ success: false, error: `Element with ID ${id} not found` }); } res.json({ success: true, element: element }); } catch (error) { logger.error('Error fetching element:', error); res.status(500).json({ success: false, error: (error as Error).message }); } }); // Batch create elements app.post('/api/elements/batch', (req: Request, res: Response) => { try { const { elements: elementsToCreate } = req.body; if (!Array.isArray(elementsToCreate)) { return res.status(400).json({ success: false, error: 'Expected an array of elements' }); } const createdElements: ServerElement[] = []; elementsToCreate.forEach(elementData => { const params = CreateElementSchema.parse(elementData); const id = generateId(); const element: ServerElement = { id, ...params, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), version: 1 }; elements.set(id, element); createdElements.push(element); }); // Broadcast to all connected clients const message: BatchCreatedMessage = { type: 'elements_batch_created', elements: createdElements }; broadcast(message); res.json({ success: true, elements: createdElements, count: createdElements.length }); } catch (error) { logger.error('Error batch creating elements:', error); res.status(400).json({ success: false, error: (error as Error).message }); } }); // Sync elements from frontend (overwrite sync) app.post('/api/elements/sync', (req: Request, res: Response) => { try { const { elements: frontendElements, timestamp } = req.body; logger.info(`Sync request received: ${frontendElements.length} elements`, { timestamp, elementCount: frontendElements.length }); // Validate input data if (!Array.isArray(frontendElements)) { return res.status(400).json({ success: false, error: 'Expected elements to be an array' }); } // Record element count before sync const beforeCount = elements.size; // 1. Clear existing memory storage elements.clear(); logger.info(`Cleared existing elements: ${beforeCount} elements removed`); // 2. Batch write new data let successCount = 0; const processedElements: ServerElement[] = []; frontendElements.forEach((element: any, index: number) => { try { // Ensure element has ID, generate one if missing const elementId = element.id || generateId(); // Add server metadata const processedElement: ServerElement = { ...element, id: elementId, syncedAt: new Date().toISOString(), source: 'frontend_sync', syncTimestamp: timestamp, version: 1 }; // Store to memory elements.set(elementId, processedElement); processedElements.push(processedElement); successCount++; } catch (elementError) { logger.warn(`Failed to process element ${index}:`, elementError); } }); logger.info(`Sync completed: ${successCount}/${frontendElements.length} elements synced`); // 3. Broadcast sync event to all WebSocket clients broadcast({ type: 'elements_synced', count: successCount, timestamp: new Date().toISOString(), source: 'manual_sync' }); // 4. Return sync results res.json({ success: true, message: `Successfully synced ${successCount} elements`, count: successCount, syncedAt: new Date().toISOString(), beforeCount, afterCount: elements.size }); } catch (error) { logger.error('Sync error:', error); res.status(500).json({ success: false, error: (error as Error).message, details: 'Internal server error during sync operation' }); } }); // Serve the frontend app.get('/', (req: Request, res: Response) => { const htmlFile = path.join(__dirname, '../dist/frontend/index.html'); res.sendFile(htmlFile, (err) => { if (err) { logger.error('Error serving frontend:', err); res.status(404).send('Frontend not found. Please run "npm run build" first.'); } }); }); // Health check endpoint app.get('/health', (req: Request, res: Response) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), elements_count: elements.size, websocket_clients: clients.size }); }); // Sync status endpoint app.get('/api/sync/status', (req: Request, res: Response) => { res.json({ success: true, elementCount: elements.size, timestamp: new Date().toISOString(), memoryUsage: { heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), // MB heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), // MB }, websocketClients: clients.size }); }); // Error handling middleware app.use((err: Error, req: Request, res: Response, next: NextFunction) => { logger.error('Unhandled error:', err); res.status(500).json({ success: false, error: 'Internal server error' }); }); // Start server const PORT = parseInt(process.env.PORT || '3000', 10); const HOST = process.env.HOST || 'localhost'; server.listen(PORT, HOST, () => { logger.info(`POC server running on http://${HOST}:${PORT}`); logger.info(`WebSocket server running on ws://${HOST}:${PORT}`); }); export default app;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yctimlin/mcp_excalidraw'

If you have feedback or need assistance with the MCP directory API, please join our Discord server