From 1be9f3b47bef82089cbace0aceb1283a4ab62aa3 Mon Sep 17 00:00:00 2001 From: ycsahara Date: Fri, 11 Jul 2025 17:49:07 +0000 Subject: [PATCH 01/13] Add frontend React app and server components - Add React frontend with Excalidraw integration (App.jsx, main.jsx) - Add Express server with MCP protocol support (server.js) - Update CLI with new functionality (cli.js) - Add Vite configuration for frontend build - Update package.json with new dependencies - Add public assets and build files - Update .gitignore to exclude build artifacts --- .gitignore | 1 + frontend/index.html | 240 ++++++++++++ frontend/src/App.jsx | 523 +++++++++++++++++++++++++++ frontend/src/main.jsx | 9 + package.json | 22 +- public/dist/frontend/index.html | 241 +++++++++++++ public/dist/index.html | 241 +++++++++++++ public/index.html | 621 ++++++++++++++++++++++++++++++++ src/cli.js | 0 src/server.js | 382 ++++++++++++++++++++ vite.config.js | 27 ++ 11 files changed, 2303 insertions(+), 4 deletions(-) create mode 100644 frontend/index.html create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/main.jsx create mode 100644 public/dist/frontend/index.html create mode 100644 public/dist/index.html create mode 100644 public/index.html mode change 100644 => 100755 src/cli.js create mode 100644 src/server.js create mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore index 4e17dba..be5020e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules package-lock.json .cursor *.excalidraw +public/dist/assets/ \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..00593be --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,240 @@ + + + + + + Excalidraw POC - Backend API Integration + + + +
+ + + \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..30003db --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,523 @@ +import React, { useState, useEffect, useRef } from 'react' +import { Excalidraw, convertToExcalidrawElements } from '@excalidraw/excalidraw' +import '@excalidraw/excalidraw/index.css' + +function App() { + const [excalidrawAPI, setExcalidrawAPI] = useState(null) + const [isConnected, setIsConnected] = useState(false) + const [elementCount, setElementCount] = useState(0) + const [apiPanelVisible, setApiPanelVisible] = useState(true) + const [notifications, setNotifications] = useState([]) + const websocketRef = useRef(null) + + // Form state + const [formData, setFormData] = useState({ + type: 'rectangle', + x: 100, + y: 100, + width: 100, + height: 100, + text: '', + backgroundColor: '#ffffff', + strokeColor: '#000000', + strokeWidth: 2 + }) + + // WebSocket connection + useEffect(() => { + connectWebSocket() + return () => { + if (websocketRef.current) { + websocketRef.current.close() + } + } + }, []) + + // Load existing elements when Excalidraw API becomes available + useEffect(() => { + if (excalidrawAPI) { + console.log('ExcalidrawAPI ready, setting up real-time sync') + + // Load existing elements immediately + loadExistingElements() + + // Ensure WebSocket is connected for real-time updates + if (!isConnected) { + console.log('ExcalidrawAPI ready but WebSocket not connected, connecting...') + connectWebSocket() + } else { + console.log('Both ExcalidrawAPI and WebSocket are ready for real-time sync') + } + } + }, [excalidrawAPI, isConnected]) + + const loadExistingElements = async () => { + try { + const response = await fetch('/api/elements') + const result = await response.json() + + if (result.success && result.elements && result.elements.length > 0) { + console.log('Loading existing elements from API:', result.elements.length) + const convertedElements = convertToExcalidrawElements(result.elements) + console.log('Converted existing elements:', convertedElements) + excalidrawAPI.updateScene({ elements: convertedElements }) + } + } catch (error) { + console.error('Error loading existing elements:', error) + } + } + + const connectWebSocket = () => { + if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) { + console.log('WebSocket already connected') + return + } + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const wsUrl = `${protocol}//${window.location.host}` + + console.log('Connecting to WebSocket:', wsUrl) + websocketRef.current = new WebSocket(wsUrl) + + websocketRef.current.onopen = () => { + console.log('WebSocket connected successfully') + setIsConnected(true) + + // Request existing elements when WebSocket connects and API is ready + if (excalidrawAPI) { + console.log('WebSocket connected and ExcalidrawAPI ready - requesting refresh') + setTimeout(loadExistingElements, 100) // Small delay to ensure connection is stable + } + } + + websocketRef.current.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + handleWebSocketMessage(data) + } catch (error) { + console.error('Error parsing WebSocket message:', error, event.data) + } + } + + websocketRef.current.onclose = (event) => { + console.log('WebSocket disconnected:', event.code, event.reason) + setIsConnected(false) + + // Reconnect after 3 seconds if not a clean close + if (event.code !== 1000) { + console.log('Attempting to reconnect in 3 seconds...') + setTimeout(connectWebSocket, 3000) + } + } + + websocketRef.current.onerror = (error) => { + console.error('WebSocket error:', error) + setIsConnected(false) + } + } + + const handleWebSocketMessage = (data) => { + console.log('WebSocket message received:', data.type, data) + + if (!excalidrawAPI) { + console.log('ExcalidrawAPI not ready, skipping message:', data.type) + return + } + + try { + const currentElements = excalidrawAPI.getSceneElements() + console.log('Current elements count:', currentElements.length) + + switch (data.type) { + case 'initial_elements': + if (data.elements && data.elements.length > 0) { + console.log('Loading initial elements:', data.elements.length) + const convertedElements = convertToExcalidrawElements(data.elements) + console.log('Converted initial elements:', convertedElements.length) + excalidrawAPI.updateScene({ elements: convertedElements }) + } + break + + case 'element_created': + console.log('Processing element_created:', data.element) + const newElement = convertToExcalidrawElements([data.element]) + console.log('Converted new element:', newElement) + const updatedElementsAfterCreate = [...currentElements, ...newElement] + console.log('Total elements after create:', updatedElementsAfterCreate.length) + excalidrawAPI.updateScene({ elements: updatedElementsAfterCreate }) + showNotification('Element created successfully!', 'success') + break + + case 'element_updated': + console.log('Processing element_updated:', data.element.id) + const convertedUpdatedElement = convertToExcalidrawElements([data.element])[0] + const updatedElements = currentElements.map(el => + el.id === data.element.id ? convertedUpdatedElement : el + ) + excalidrawAPI.updateScene({ elements: updatedElements }) + showNotification('Element updated successfully!', 'success') + break + + case 'element_deleted': + console.log('Processing element_deleted:', data.elementId) + const filteredElements = currentElements.filter(el => el.id !== data.elementId) + excalidrawAPI.updateScene({ elements: filteredElements }) + showNotification('Element deleted successfully!', 'success') + break + + case 'elements_batch_created': + console.log('Processing elements_batch_created:', data.elements.length) + const batchElements = convertToExcalidrawElements(data.elements) + console.log('Converted batch elements:', batchElements.length) + const updatedElementsAfterBatch = [...currentElements, ...batchElements] + console.log('Total elements after batch:', updatedElementsAfterBatch.length) + excalidrawAPI.updateScene({ elements: updatedElementsAfterBatch }) + showNotification(`${data.elements.length} elements created!`, 'success') + break + + default: + console.log('Unknown WebSocket message type:', data.type) + } + } catch (error) { + console.error('Error processing WebSocket message:', error, data) + showNotification('Error processing real-time update', 'error') + } + } + + const showNotification = (message, type = 'success') => { + const id = Date.now() + const notification = { id, message, type } + setNotifications(prev => [...prev, notification]) + + setTimeout(() => { + setNotifications(prev => prev.filter(n => n.id !== id)) + }, 3000) + } + + const handleExcalidrawChange = (elements, appState, files) => { + setElementCount(elements.length) + } + + const handleFormSubmit = async (e) => { + e.preventDefault() + + const elementData = { + type: formData.type, + x: parseInt(formData.x), + y: parseInt(formData.y), + width: parseInt(formData.width) || undefined, + height: parseInt(formData.height) || undefined, + backgroundColor: formData.backgroundColor, + strokeColor: formData.strokeColor, + strokeWidth: parseInt(formData.strokeWidth) + } + + if (formData.text) { + elementData.text = formData.text + } + + // Remove undefined values + Object.keys(elementData).forEach(key => { + if (elementData[key] === undefined) { + delete elementData[key] + } + }) + + try { + const response = await fetch('/api/elements', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(elementData) + }) + + const result = await response.json() + + if (result.success) { + showNotification('Element created via API!', 'success') + // Reset form positions + setFormData(prev => ({ + ...prev, + x: Math.floor(Math.random() * 400) + 50, + y: Math.floor(Math.random() * 300) + 50 + })) + } else { + showNotification(`Error: ${result.error}`, 'error') + } + } catch (error) { + console.error('Error creating element:', error) + showNotification('Failed to create element', 'error') + } + } + + const createSampleElements = async () => { + const sampleElements = [ + { + type: 'rectangle', + x: 50, + y: 50, + width: 150, + height: 100, + backgroundColor: '#ffeaa7', + strokeColor: '#2d3436' + }, + { + type: 'ellipse', + x: 250, + y: 50, + width: 120, + height: 120, + backgroundColor: '#74b9ff', + strokeColor: '#0984e3' + }, + { + type: 'diamond', + x: 50, + y: 200, + width: 100, + height: 100, + backgroundColor: '#fd79a8', + strokeColor: '#e84393' + }, + { + type: 'text', + x: 250, + y: 220, + text: 'Hello from API!', + fontSize: 20, + strokeColor: '#2d3436' + } + ] + + try { + const response = await fetch('/api/elements/batch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ elements: sampleElements }) + }) + + const result = await response.json() + + if (result.success) { + showNotification(`${result.count} sample elements created!`, 'success') + } else { + showNotification(`Error: ${result.error}`, 'error') + } + } catch (error) { + console.error('Error creating sample elements:', error) + showNotification('Failed to create sample elements', 'error') + } + } + + const clearCanvas = () => { + if (excalidrawAPI) { + excalidrawAPI.updateScene({ elements: [] }) + showNotification('Canvas cleared!', 'success') + } + } + + const refreshElements = async () => { + console.log('Manual refresh requested') + if (excalidrawAPI) { + await loadExistingElements() + showNotification('Elements refreshed!', 'success') + } else { + showNotification('Canvas not ready yet', 'error') + } + } + + const forceReconnectWebSocket = () => { + console.log('Force reconnecting WebSocket') + if (websocketRef.current) { + websocketRef.current.close() + } + setTimeout(connectWebSocket, 100) + } + + const toggleApiPanel = () => { + setApiPanelVisible(!apiPanelVisible) + } + + const handleInputChange = (e) => { + const { name, value } = e.target + setFormData(prev => ({ + ...prev, + [name]: value + })) + } + + return ( +
+ {/* Header */} +
+

Excalidraw POC - Backend API Integration

+
+
+
+ {isConnected ? 'Connected' : 'Disconnected'} +
+
+ Elements: {elementCount} +
+ + + + {!isConnected && ( + + )} +
+
+ + {/* API Panel Toggle */} + + + {/* API Panel */} + {apiPanelVisible && ( +
+

Create Element via API

+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + +
+
+ )} + + {/* Canvas Container */} +
+ setExcalidrawAPI(api)} + onChange={handleExcalidrawChange} + initialData={{ + elements: [], + appState: { + theme: 'light', + viewBackgroundColor: '#ffffff' + } + }} + /> +
+ + {/* Notifications */} + {notifications.map(notification => ( +
+ {notification.message} +
+ ))} +
+ ) +} + +export default App \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..aad322f --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,9 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) \ No newline at end of file diff --git a/package.json b/package.json index 0afd752..222d603 100644 --- a/package.json +++ b/package.json @@ -10,25 +10,39 @@ "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js", + "server": "node src/server.js", + "server:dev": "nodemon src/server.js", + "frontend:dev": "vite", + "frontend:build": "vite build", + "frontend:preview": "vite preview", + "build": "npm run frontend:build", + "poc": "concurrently \"npm run server\" \"npm run frontend:dev\"", "test": "jest", "lint": "eslint src/**/*.js", - "build": "tsc", "docker:build": "docker build -t mcp/excalidraw .", "docker:run": "docker run -p 3000:3000 mcp/excalidraw", "prepare": "node -e \"try { require('fs').chmodSync('./src/cli.js', '755') } catch(e) { console.log(e) }\"" }, "dependencies": { + "@excalidraw/excalidraw": "^0.18.0", "@modelcontextprotocol/sdk": "latest", "cors": "^2.8.5", "dotenv": "^16.3.1", + "express": "^4.18.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "winston": "^3.11.0", + "ws": "^8.14.2", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.3" }, "devDependencies": { - "nodemon": "^3.0.2", + "@vitejs/plugin-react": "^4.6.0", + "concurrently": "^9.2.0", + "eslint": "^8.56.0", "jest": "^29.7.0", - "eslint": "^8.56.0" + "nodemon": "^3.0.2", + "vite": "^6.3.5" }, "keywords": [ "mcp", @@ -50,4 +64,4 @@ "README.md", "LICENSE" ] -} \ No newline at end of file +} diff --git a/public/dist/frontend/index.html b/public/dist/frontend/index.html new file mode 100644 index 0000000..709f2ae --- /dev/null +++ b/public/dist/frontend/index.html @@ -0,0 +1,241 @@ + + + + + + Excalidraw POC - Backend API Integration + + + + + +
+ + \ No newline at end of file diff --git a/public/dist/index.html b/public/dist/index.html new file mode 100644 index 0000000..709f2ae --- /dev/null +++ b/public/dist/index.html @@ -0,0 +1,241 @@ + + + + + + Excalidraw POC - Backend API Integration + + + + + +
+ + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..b99c9c9 --- /dev/null +++ b/public/index.html @@ -0,0 +1,621 @@ + + + + + + Excalidraw POC - Backend API Integration + + + +
+

Excalidraw POC - Backend API Integration

+
+
+
+ Connecting... +
+
+ Elements: 0 +
+ + +
+
+ + + +
+

Create Element via API

+
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + +
+
+ +
+
+
+
+
+
Loading Excalidraw...
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/src/cli.js b/src/cli.js old mode 100644 new mode 100755 diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..a3d379b --- /dev/null +++ b/src/server.js @@ -0,0 +1,382 @@ +import express 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 +} from './types.js'; +import { z } from 'zod'; + +// 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 for production +// or from the old public directory for development +const staticDir = process.env.NODE_ENV === 'production' + ? path.join(__dirname, '../public/dist') + : path.join(__dirname, '../public'); +app.use(express.static(staticDir)); + +// WebSocket connections +const clients = new Set(); + +// Broadcast to all connected clients +function broadcast(message) { + const data = JSON.stringify(message); + clients.forEach(client => { + if (client.readyState === client.OPEN) { + client.send(data); + } + }); +} + +// WebSocket connection handling +wss.on('connection', (ws) => { + clients.add(ws); + logger.info('New WebSocket connection established'); + + // Send current elements to new client + ws.send(JSON.stringify({ + type: 'initial_elements', + elements: Array.from(elements.values()) + })); + + 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({ + type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES)), + 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(), + fontSize: z.number().optional(), + fontFamily: z.string().optional() +}); + +const UpdateElementSchema = z.object({ + id: z.string(), + type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES)).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(), + fontSize: z.number().optional(), + fontFamily: z.string().optional() +}); + +// API Routes + +// Get all elements +app.get('/api/elements', (req, res) => { + 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.message + }); + } +}); + +// Create new element +app.post('/api/elements', (req, res) => { + try { + const params = CreateElementSchema.parse(req.body); + logger.info('Creating element via API', { type: params.type }); + + const id = generateId(); + const element = { + id, + ...params, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + version: 1 + }; + + elements.set(id, element); + + // Broadcast to all connected clients + broadcast({ + type: 'element_created', + element: element + }); + + res.json({ + success: true, + element: element + }); + } catch (error) { + logger.error('Error creating element:', error); + res.status(400).json({ + success: false, + error: error.message + }); + } +}); + +// Update element +app.put('/api/elements/:id', (req, res) => { + try { + const { id } = req.params; + const updates = UpdateElementSchema.parse({ id, ...req.body }); + + const existingElement = elements.get(id); + if (!existingElement) { + return res.status(404).json({ + success: false, + error: `Element with ID ${id} not found` + }); + } + + const updatedElement = { + ...existingElement, + ...updates, + updatedAt: new Date().toISOString(), + version: existingElement.version + 1 + }; + + elements.set(id, updatedElement); + + // Broadcast to all connected clients + broadcast({ + type: 'element_updated', + element: updatedElement + }); + + res.json({ + success: true, + element: updatedElement + }); + } catch (error) { + logger.error('Error updating element:', error); + res.status(400).json({ + success: false, + error: error.message + }); + } +}); + +// Delete element +app.delete('/api/elements/:id', (req, res) => { + try { + const { id } = req.params; + + 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 + broadcast({ + type: 'element_deleted', + elementId: id + }); + + 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.message + }); + } +}); + +// Get element by ID +app.get('/api/elements/:id', (req, res) => { + try { + const { id } = req.params; + 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.message + }); + } +}); + +// Query elements with filters +app.get('/api/elements/search', (req, res) => { + try { + const { type, ...filters } = req.query; + let results = Array.from(elements.values()); + + // Filter by type if specified + if (type) { + 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[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.message + }); + } +}); + +// Batch create elements +app.post('/api/elements/batch', (req, res) => { + 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 = []; + + elementsToCreate.forEach(elementData => { + const params = CreateElementSchema.parse(elementData); + const id = generateId(); + const element = { + id, + ...params, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + version: 1 + }; + + elements.set(id, element); + createdElements.push(element); + }); + + // Broadcast to all connected clients + broadcast({ + type: 'elements_batch_created', + elements: createdElements + }); + + 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.message + }); + } +}); + +// Serve the frontend +app.get('/', (req, res) => { + const htmlFile = process.env.NODE_ENV === 'production' + ? path.join(__dirname, '../public/dist/index.html') + : path.join(__dirname, '../public/index.html'); + res.sendFile(htmlFile); +}); + +// Health check endpoint +app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + elements_count: elements.size, + websocket_clients: clients.size + }); +}); + +// Error handling middleware +app.use((err, req, res, next) => { + logger.error('Unhandled error:', err); + res.status(500).json({ + success: false, + error: 'Internal server error' + }); +}); + +// Start server +const PORT = process.env.PORT || 3000; +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; \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..14eeede --- /dev/null +++ b/vite.config.js @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'public/dist', + rollupOptions: { + input: { + main: './frontend/index.html', + }, + }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + '/health': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, +}) \ No newline at end of file From c5ba87db6e2fd8f9c448dfa91c445ca14d6c71f8 Mon Sep 17 00:00:00 2001 From: ycsahara Date: Fri, 11 Jul 2025 18:41:39 +0000 Subject: [PATCH 02/13] Simplify frontend UI and build system --- frontend/src/App.jsx | 28 ++++- package.json | 1 + src/index.js | 262 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 284 insertions(+), 7 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 30003db..beb8e2d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -312,10 +312,32 @@ function App() { } } - const clearCanvas = () => { + const clearCanvas = async () => { if (excalidrawAPI) { - excalidrawAPI.updateScene({ elements: [] }) - showNotification('Canvas cleared!', 'success') + try { + // First, get all current elements + const response = await fetch('/api/elements') + const result = await response.json() + + if (result.success && result.elements) { + // Delete all elements from backend + const deletePromises = result.elements.map(element => + fetch(`/api/elements/${element.id}`, { method: 'DELETE' }) + ) + + await Promise.all(deletePromises) + console.log('All elements deleted from backend') + } + + // Clear the frontend canvas + excalidrawAPI.updateScene({ elements: [] }) + showNotification('Canvas cleared completely!', 'success') + } catch (error) { + console.error('Error clearing canvas:', error) + // Still clear frontend even if backend fails + excalidrawAPI.updateScene({ elements: [] }) + showNotification('Canvas cleared (frontend only)', 'warning') + } } } diff --git a/package.json b/package.json index 222d603..a19326f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "node-fetch": "^3.3.2", "react": "^18.3.1", "react-dom": "^18.3.1", "winston": "^3.11.0", diff --git a/src/index.js b/src/index.js index ec502e8..a95c248 100644 --- a/src/index.js +++ b/src/index.js @@ -17,10 +17,105 @@ import { generateId, EXCALIDRAW_ELEMENT_TYPES } from './types.js'; +import fetch from 'node-fetch'; // Load environment variables dotenv.config(); +// Express server configuration +const EXPRESS_SERVER_URL = process.env.EXPRESS_SERVER_URL || 'http://localhost:3000'; +const ENABLE_CANVAS_SYNC = process.env.ENABLE_CANVAS_SYNC !== 'false'; // Default to true + +// Helper functions to sync with Express server (canvas) +async function syncToCanvas(operation, data) { + if (!ENABLE_CANVAS_SYNC) { + logger.debug('Canvas sync disabled, skipping'); + return null; + } + + try { + let url, options; + + switch (operation) { + case 'create': + url = `${EXPRESS_SERVER_URL}/api/elements`; + options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }; + break; + + case 'update': + url = `${EXPRESS_SERVER_URL}/api/elements/${data.id}`; + options = { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }; + break; + + case 'delete': + url = `${EXPRESS_SERVER_URL}/api/elements/${data.id}`; + options = { method: 'DELETE' }; + break; + + case 'batch_create': + url = `${EXPRESS_SERVER_URL}/api/elements/batch`; + options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ elements: data }) + }; + break; + + default: + logger.warn(`Unknown sync operation: ${operation}`); + return null; + } + + logger.debug(`Syncing to canvas: ${operation}`, { url, data }); + const response = await fetch(url, options); + + if (!response.ok) { + throw new Error(`Canvas sync failed: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + logger.debug(`Canvas sync successful: ${operation}`, result); + return result; + + } catch (error) { + logger.warn(`Canvas sync failed for ${operation}:`, error.message); + // Don't throw - we want MCP operations to work even if canvas is unavailable + return null; + } +} + +// Helper to sync element creation to canvas +async function createElementOnCanvas(elementData) { + const result = await syncToCanvas('create', elementData); + return result?.element || elementData; +} + +// Helper to sync element update to canvas +async function updateElementOnCanvas(elementData) { + const result = await syncToCanvas('update', elementData); + return result?.element || elementData; +} + +// Helper to sync element deletion to canvas +async function deleteElementOnCanvas(elementId) { + const result = await syncToCanvas('delete', { id: elementId }); + return result; +} + +// Helper to sync batch creation to canvas +async function batchCreateElementsOnCanvas(elementsData) { + const result = await syncToCanvas('batch_create', elementsData); + return result?.elements || elementsData; +} + // In-memory storage for scene state const sceneState = { theme: 'light', @@ -261,6 +356,40 @@ const server = new Server( required: ['elementIds'] } }, + batch_create_elements: { + description: 'Create multiple Excalidraw elements at once - ideal for complex diagrams', + inputSchema: { + type: 'object', + properties: { + elements: { + type: 'array', + items: { + type: 'object', + properties: { + type: { + type: 'string', + enum: Object.values(EXCALIDRAW_ELEMENT_TYPES) + }, + x: { type: 'number' }, + y: { type: 'number' }, + width: { type: 'number' }, + height: { type: 'number' }, + backgroundColor: { type: 'string' }, + strokeColor: { type: 'string' }, + strokeWidth: { type: 'number' }, + roughness: { type: 'number' }, + opacity: { type: 'number' }, + text: { type: 'string' }, + fontSize: { type: 'number' }, + fontFamily: { type: 'string' } + }, + required: ['type', 'x', 'y'] + } + } + }, + required: ['elements'] + } + }, } } } @@ -275,7 +404,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (name) { case 'create_element': { const params = ElementSchema.parse(args); - logger.info('Creating element', { type: params.type }); + logger.info('Creating element via MCP', { type: params.type }); const id = generateId(); const element = { @@ -286,10 +415,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { version: 1 }; + // Store locally (MCP server storage) elements.set(id, element); + // Sync to canvas (Express server + WebSocket broadcast) + const canvasElement = await createElementOnCanvas(element); + + const result = canvasElement || element; + logger.info('Element created via MCP and synced to canvas', { + id: result.id, + type: result.type, + synced: !!canvasElement + }); + return { - content: [{ type: 'text', text: JSON.stringify(element, null, 2) }] + content: [{ + type: 'text', + text: `Element created successfully!\n\n${JSON.stringify(result, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still created locally)'}` + }] }; } @@ -312,10 +455,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { version: existingElement.version + 1 }; + // Store locally (MCP server storage) elements.set(id, updatedElement); + // Sync to canvas (Express server + WebSocket broadcast) + const canvasElement = await updateElementOnCanvas(updatedElement); + + const result = canvasElement || updatedElement; + logger.info('Element updated via MCP and synced to canvas', { + id: result.id, + synced: !!canvasElement + }); + return { - content: [{ type: 'text', text: JSON.stringify(updatedElement, null, 2) }] + content: [{ + type: 'text', + text: `Element updated successfully!\n\n${JSON.stringify(result, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still updated locally)'}` + }] }; } @@ -325,10 +481,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!elements.has(id)) throw new Error(`Element with ID ${id} not found`); + // Delete locally (MCP server storage) elements.delete(id); + // Sync to canvas (Express server + WebSocket broadcast) + const canvasResult = await deleteElementOnCanvas(id); + + const result = { id, deleted: true, syncedToCanvas: !!canvasResult }; + logger.info('Element deleted via MCP and synced to canvas', result); + return { - content: [{ type: 'text', text: JSON.stringify({ id, deleted: true }, null, 2) }] + content: [{ + type: 'text', + text: `Element deleted successfully!\n\n${JSON.stringify(result, null, 2)}\n\n${canvasResult ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still deleted locally)'}` + }] }; } @@ -483,6 +649,51 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case 'batch_create_elements': { + const params = z.object({ elements: z.array(ElementSchema) }).parse(args); + logger.info('Batch creating elements via MCP', { count: params.elements.length }); + + const createdElements = []; + + // Create each element with unique ID + for (const elementData of params.elements) { + const id = generateId(); + const element = { + id, + ...elementData, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + version: 1 + }; + + // Store locally (MCP server storage) + elements.set(id, element); + createdElements.push(element); + } + + // Sync all elements to canvas at once (Express server + WebSocket broadcast) + const canvasElements = await batchCreateElementsOnCanvas(createdElements); + + const result = { + success: true, + elements: canvasElements || createdElements, + count: createdElements.length, + syncedToCanvas: !!canvasElements + }; + + logger.info('Batch elements created via MCP and synced to canvas', { + count: result.count, + synced: result.syncedToCanvas + }); + + return { + content: [{ + type: 'text', + text: `${result.count} elements created successfully!\n\n${JSON.stringify(result, null, 2)}\n\n${result.syncedToCanvas ? '✅ All elements synced to canvas' : '⚠️ Canvas sync failed (elements still created locally)'}` + }] + }; + } + default: throw new Error(`Unknown tool: ${name}`); } @@ -683,6 +894,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { }, required: ['elementIds'] } + }, + { + name: 'batch_create_elements', + description: 'Create multiple Excalidraw elements at once - ideal for complex diagrams', + inputSchema: { + type: 'object', + properties: { + elements: { + type: 'array', + items: { + type: 'object', + properties: { + type: { + type: 'string', + enum: Object.values(EXCALIDRAW_ELEMENT_TYPES) + }, + x: { type: 'number' }, + y: { type: 'number' }, + width: { type: 'number' }, + height: { type: 'number' }, + backgroundColor: { type: 'string' }, + strokeColor: { type: 'string' }, + strokeWidth: { type: 'number' }, + roughness: { type: 'number' }, + opacity: { type: 'number' }, + text: { type: 'string' }, + fontSize: { type: 'number' }, + fontFamily: { type: 'string' } + }, + required: ['type', 'x', 'y'] + } + } + }, + required: ['elements'] + } } ]; @@ -743,4 +989,12 @@ if (process.env.DEBUG === 'true') { logger.debug('Debug mode enabled'); } +// Start the server if this file is run directly +if (import.meta.url === `file://${process.argv[1]}`) { + runServer().catch(error => { + logger.error('Failed to start server:', error); + process.exit(1); + }); +} + export default runServer; \ No newline at end of file From 11309c731b584ee70fcf94a444a5faa5f3c71045 Mon Sep 17 00:00:00 2001 From: ycsahara Date: Fri, 11 Jul 2025 19:06:08 +0000 Subject: [PATCH 03/13] Refactor project structure and update configurations - Update .gitignore to include additional build artifacts, logs, and editor files - Modify package.json scripts for improved development workflow and remove unused scripts - Change Vite output directory to 'dist' for consistency - Simplify App.jsx by removing unused state and functions, enhancing readability - Adjust server.js to serve static files from the new 'dist' directory --- .gitignore | 24 ++- frontend/src/App.jsx | 359 +------------------------------------------ package.json | 23 +-- src/server.js | 11 +- vite.config.js | 2 +- 5 files changed, 30 insertions(+), 389 deletions(-) diff --git a/.gitignore b/.gitignore index be5020e..19b7d18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,20 @@ -node_modules -.env +# Dependencies +node_modules/ package-lock.json -.cursor -*.excalidraw -public/dist/assets/ \ No newline at end of file + +# Build artifacts +dist/ +public/dist/ + +# Environment files +.env + +# Logs +*.log + +# Editor artifacts +.cursor/ +.claude/ + +# Development artifacts +*.excalidraw \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index beb8e2d..3f1f822 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,24 +5,8 @@ import '@excalidraw/excalidraw/index.css' function App() { const [excalidrawAPI, setExcalidrawAPI] = useState(null) const [isConnected, setIsConnected] = useState(false) - const [elementCount, setElementCount] = useState(0) - const [apiPanelVisible, setApiPanelVisible] = useState(true) - const [notifications, setNotifications] = useState([]) const websocketRef = useRef(null) - // Form state - const [formData, setFormData] = useState({ - type: 'rectangle', - x: 100, - y: 100, - width: 100, - height: 100, - text: '', - backgroundColor: '#ffffff', - strokeColor: '#000000', - strokeWidth: 2 - }) - // WebSocket connection useEffect(() => { connectWebSocket() @@ -36,17 +20,11 @@ function App() { // Load existing elements when Excalidraw API becomes available useEffect(() => { if (excalidrawAPI) { - console.log('ExcalidrawAPI ready, setting up real-time sync') - - // Load existing elements immediately loadExistingElements() // Ensure WebSocket is connected for real-time updates if (!isConnected) { - console.log('ExcalidrawAPI ready but WebSocket not connected, connecting...') connectWebSocket() - } else { - console.log('Both ExcalidrawAPI and WebSocket are ready for real-time sync') } } }, [excalidrawAPI, isConnected]) @@ -57,9 +35,7 @@ function App() { const result = await response.json() if (result.success && result.elements && result.elements.length > 0) { - console.log('Loading existing elements from API:', result.elements.length) const convertedElements = convertToExcalidrawElements(result.elements) - console.log('Converted existing elements:', convertedElements) excalidrawAPI.updateScene({ elements: convertedElements }) } } catch (error) { @@ -69,24 +45,19 @@ function App() { const connectWebSocket = () => { if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) { - console.log('WebSocket already connected') return } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const wsUrl = `${protocol}//${window.location.host}` - console.log('Connecting to WebSocket:', wsUrl) websocketRef.current = new WebSocket(wsUrl) websocketRef.current.onopen = () => { - console.log('WebSocket connected successfully') setIsConnected(true) - // Request existing elements when WebSocket connects and API is ready if (excalidrawAPI) { - console.log('WebSocket connected and ExcalidrawAPI ready - requesting refresh') - setTimeout(loadExistingElements, 100) // Small delay to ensure connection is stable + setTimeout(loadExistingElements, 100) } } @@ -100,12 +71,10 @@ function App() { } websocketRef.current.onclose = (event) => { - console.log('WebSocket disconnected:', event.code, event.reason) setIsConnected(false) // Reconnect after 3 seconds if not a clean close if (event.code !== 1000) { - console.log('Attempting to reconnect in 3 seconds...') setTimeout(connectWebSocket, 3000) } } @@ -117,62 +86,44 @@ function App() { } const handleWebSocketMessage = (data) => { - console.log('WebSocket message received:', data.type, data) - if (!excalidrawAPI) { - console.log('ExcalidrawAPI not ready, skipping message:', data.type) return } try { const currentElements = excalidrawAPI.getSceneElements() - console.log('Current elements count:', currentElements.length) switch (data.type) { case 'initial_elements': if (data.elements && data.elements.length > 0) { - console.log('Loading initial elements:', data.elements.length) const convertedElements = convertToExcalidrawElements(data.elements) - console.log('Converted initial elements:', convertedElements.length) excalidrawAPI.updateScene({ elements: convertedElements }) } break case 'element_created': - console.log('Processing element_created:', data.element) const newElement = convertToExcalidrawElements([data.element]) - console.log('Converted new element:', newElement) const updatedElementsAfterCreate = [...currentElements, ...newElement] - console.log('Total elements after create:', updatedElementsAfterCreate.length) excalidrawAPI.updateScene({ elements: updatedElementsAfterCreate }) - showNotification('Element created successfully!', 'success') break case 'element_updated': - console.log('Processing element_updated:', data.element.id) const convertedUpdatedElement = convertToExcalidrawElements([data.element])[0] const updatedElements = currentElements.map(el => el.id === data.element.id ? convertedUpdatedElement : el ) excalidrawAPI.updateScene({ elements: updatedElements }) - showNotification('Element updated successfully!', 'success') break case 'element_deleted': - console.log('Processing element_deleted:', data.elementId) const filteredElements = currentElements.filter(el => el.id !== data.elementId) excalidrawAPI.updateScene({ elements: filteredElements }) - showNotification('Element deleted successfully!', 'success') break case 'elements_batch_created': - console.log('Processing elements_batch_created:', data.elements.length) const batchElements = convertToExcalidrawElements(data.elements) - console.log('Converted batch elements:', batchElements.length) const updatedElementsAfterBatch = [...currentElements, ...batchElements] - console.log('Total elements after batch:', updatedElementsAfterBatch.length) excalidrawAPI.updateScene({ elements: updatedElementsAfterBatch }) - showNotification(`${data.elements.length} elements created!`, 'success') break default: @@ -180,344 +131,51 @@ function App() { } } catch (error) { console.error('Error processing WebSocket message:', error, data) - showNotification('Error processing real-time update', 'error') - } - } - - const showNotification = (message, type = 'success') => { - const id = Date.now() - const notification = { id, message, type } - setNotifications(prev => [...prev, notification]) - - setTimeout(() => { - setNotifications(prev => prev.filter(n => n.id !== id)) - }, 3000) - } - - const handleExcalidrawChange = (elements, appState, files) => { - setElementCount(elements.length) - } - - const handleFormSubmit = async (e) => { - e.preventDefault() - - const elementData = { - type: formData.type, - x: parseInt(formData.x), - y: parseInt(formData.y), - width: parseInt(formData.width) || undefined, - height: parseInt(formData.height) || undefined, - backgroundColor: formData.backgroundColor, - strokeColor: formData.strokeColor, - strokeWidth: parseInt(formData.strokeWidth) - } - - if (formData.text) { - elementData.text = formData.text - } - - // Remove undefined values - Object.keys(elementData).forEach(key => { - if (elementData[key] === undefined) { - delete elementData[key] - } - }) - - try { - const response = await fetch('/api/elements', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(elementData) - }) - - const result = await response.json() - - if (result.success) { - showNotification('Element created via API!', 'success') - // Reset form positions - setFormData(prev => ({ - ...prev, - x: Math.floor(Math.random() * 400) + 50, - y: Math.floor(Math.random() * 300) + 50 - })) - } else { - showNotification(`Error: ${result.error}`, 'error') - } - } catch (error) { - console.error('Error creating element:', error) - showNotification('Failed to create element', 'error') - } - } - - const createSampleElements = async () => { - const sampleElements = [ - { - type: 'rectangle', - x: 50, - y: 50, - width: 150, - height: 100, - backgroundColor: '#ffeaa7', - strokeColor: '#2d3436' - }, - { - type: 'ellipse', - x: 250, - y: 50, - width: 120, - height: 120, - backgroundColor: '#74b9ff', - strokeColor: '#0984e3' - }, - { - type: 'diamond', - x: 50, - y: 200, - width: 100, - height: 100, - backgroundColor: '#fd79a8', - strokeColor: '#e84393' - }, - { - type: 'text', - x: 250, - y: 220, - text: 'Hello from API!', - fontSize: 20, - strokeColor: '#2d3436' - } - ] - - try { - const response = await fetch('/api/elements/batch', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ elements: sampleElements }) - }) - - const result = await response.json() - - if (result.success) { - showNotification(`${result.count} sample elements created!`, 'success') - } else { - showNotification(`Error: ${result.error}`, 'error') - } - } catch (error) { - console.error('Error creating sample elements:', error) - showNotification('Failed to create sample elements', 'error') } } const clearCanvas = async () => { if (excalidrawAPI) { try { - // First, get all current elements + // Get all current elements and delete them from backend const response = await fetch('/api/elements') const result = await response.json() if (result.success && result.elements) { - // Delete all elements from backend const deletePromises = result.elements.map(element => fetch(`/api/elements/${element.id}`, { method: 'DELETE' }) ) - await Promise.all(deletePromises) - console.log('All elements deleted from backend') } // Clear the frontend canvas excalidrawAPI.updateScene({ elements: [] }) - showNotification('Canvas cleared completely!', 'success') } catch (error) { console.error('Error clearing canvas:', error) // Still clear frontend even if backend fails excalidrawAPI.updateScene({ elements: [] }) - showNotification('Canvas cleared (frontend only)', 'warning') } } } - const refreshElements = async () => { - console.log('Manual refresh requested') - if (excalidrawAPI) { - await loadExistingElements() - showNotification('Elements refreshed!', 'success') - } else { - showNotification('Canvas not ready yet', 'error') - } - } - - const forceReconnectWebSocket = () => { - console.log('Force reconnecting WebSocket') - if (websocketRef.current) { - websocketRef.current.close() - } - setTimeout(connectWebSocket, 100) - } - - const toggleApiPanel = () => { - setApiPanelVisible(!apiPanelVisible) - } - - const handleInputChange = (e) => { - const { name, value } = e.target - setFormData(prev => ({ - ...prev, - [name]: value - })) - } - return (
{/* Header */}
-

Excalidraw POC - Backend API Integration

+

Excalidraw Canvas

{isConnected ? 'Connected' : 'Disconnected'}
-
- Elements: {elementCount} -
- - - {!isConnected && ( - - )}
- {/* API Panel Toggle */} - - - {/* API Panel */} - {apiPanelVisible && ( -
-

Create Element via API

-
-
- - -
- -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
- - -
- -
-
- - -
-
- - -
-
- -
- - -
- - -
-
- )} - {/* Canvas Container */}
setExcalidrawAPI(api)} - onChange={handleExcalidrawChange} initialData={{ elements: [], appState: { @@ -527,17 +185,6 @@ function App() { }} />
- - {/* Notifications */} - {notifications.map(notification => ( -
- {notification.message} -
- ))}
) } diff --git a/package.json b/package.json index a19326f..e5f7658 100644 --- a/package.json +++ b/package.json @@ -4,24 +4,12 @@ "description": "MCP server for Excalidraw", "main": "src/index.js", "type": "module", - "bin": { - "excalidraw-mcp": "./src/cli.js" - }, "scripts": { "start": "node src/index.js", - "dev": "nodemon src/index.js", - "server": "node src/server.js", - "server:dev": "nodemon src/server.js", - "frontend:dev": "vite", - "frontend:build": "vite build", - "frontend:preview": "vite preview", - "build": "npm run frontend:build", - "poc": "concurrently \"npm run server\" \"npm run frontend:dev\"", - "test": "jest", - "lint": "eslint src/**/*.js", - "docker:build": "docker build -t mcp/excalidraw .", - "docker:run": "docker run -p 3000:3000 mcp/excalidraw", - "prepare": "node -e \"try { require('fs').chmodSync('./src/cli.js', '755') } catch(e) { console.log(e) }\"" + "canvas": "node src/server.js", + "build": "vite build", + "dev": "concurrently \"npm run canvas\" \"vite\"", + "production": "npm run build && npm run canvas" }, "dependencies": { "@excalidraw/excalidraw": "^0.18.0", @@ -40,9 +28,6 @@ "devDependencies": { "@vitejs/plugin-react": "^4.6.0", "concurrently": "^9.2.0", - "eslint": "^8.56.0", - "jest": "^29.7.0", - "nodemon": "^3.0.2", "vite": "^6.3.5" }, "keywords": [ diff --git a/src/server.js b/src/server.js index a3d379b..706fc53 100644 --- a/src/server.js +++ b/src/server.js @@ -27,11 +27,8 @@ const wss = new WebSocketServer({ server }); app.use(cors()); app.use(express.json()); -// Serve static files from the build directory for production -// or from the old public directory for development -const staticDir = process.env.NODE_ENV === 'production' - ? path.join(__dirname, '../public/dist') - : path.join(__dirname, '../public'); +// Serve static files from the build directory +const staticDir = path.join(__dirname, '../dist'); app.use(express.static(staticDir)); // WebSocket connections @@ -345,9 +342,7 @@ app.post('/api/elements/batch', (req, res) => { // Serve the frontend app.get('/', (req, res) => { - const htmlFile = process.env.NODE_ENV === 'production' - ? path.join(__dirname, '../public/dist/index.html') - : path.join(__dirname, '../public/index.html'); + const htmlFile = path.join(__dirname, '../dist/index.html'); res.sendFile(htmlFile); }); diff --git a/vite.config.js b/vite.config.js index 14eeede..39f7d38 100644 --- a/vite.config.js +++ b/vite.config.js @@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], build: { - outDir: 'public/dist', + outDir: 'dist', rollupOptions: { input: { main: './frontend/index.html', From cee3b775bf9d0a87e87e791ce5486cd903056ebf Mon Sep 17 00:00:00 2001 From: ycsahara Date: Fri, 11 Jul 2025 19:22:54 +0000 Subject: [PATCH 04/13] Update README.md and remove unused files for Excalidraw MCP Canvas - Revise README.md to reflect the new project name and features, emphasizing real-time diagramming and AI integration. - Remove outdated public HTML files and CLI script as they are no longer needed. - Streamline installation and setup instructions for better clarity. - Enhance architecture overview and key features sections to provide a comprehensive understanding of the system. --- README.md | 515 ++++++++++++++------------ public/dist/frontend/index.html | 241 ------------- public/dist/index.html | 241 ------------- public/index.html | 621 -------------------------------- src/cli.js | 103 ------ src/test.js | 9 - 6 files changed, 282 insertions(+), 1448 deletions(-) delete mode 100644 public/dist/frontend/index.html delete mode 100644 public/dist/index.html delete mode 100644 public/index.html delete mode 100755 src/cli.js delete mode 100644 src/test.js diff --git a/README.md b/README.md index ae91f8f..54cf33e 100644 --- a/README.md +++ b/README.md @@ -1,280 +1,329 @@ -# Excalidraw MCP Server: Powerful Drawing API for LLM Integration +# Excalidraw MCP Canvas: Live Visual Diagramming with AI Integration -> **📣 NEWS: Version 1.0.0 is now published to npm!** You can run Excalidraw MCP directly using `npx excalidraw-mcp` without installation. No setup required - just run and enjoy! +A comprehensive system that combines **Excalidraw's powerful drawing capabilities** with **Model Context Protocol (MCP)** integration, enabling AI agents to create and manipulate diagrams in real-time on a live canvas. -A comprehensive Model Context Protocol (MCP) server that enables seamless interaction with Excalidraw diagrams and drawings. This server provides LLMs (Large Language Models) with the ability to create, modify, query, and manipulate Excalidraw drawings through a structured, developer-friendly API. +## 🚀 What This System Does -## Quick Start +- **🎨 Live Canvas**: Real-time Excalidraw canvas accessible via web browser +- **🤖 AI Integration**: MCP server allows AI agents (like Claude) to create visual diagrams +- **⚡ Real-time Sync**: Elements created via MCP API appear instantly on the canvas +- **🔄 WebSocket Updates**: Live synchronization across multiple connected clients +- **🏗️ Production Ready**: Clean, minimal UI suitable for end users -You can run the Excalidraw MCP server directly using npx without installing anything: +## 🏛️ Architecture Overview +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ AI Agent │───▶│ MCP Server │───▶│ Canvas Server │ +│ (Claude) │ │ (src/index.js) │ │ (src/server.js) │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ Frontend │ + │ (React + WS) │ + └─────────────────┘ +``` + +## 🌟 Key Features + +### **Real-time Canvas Integration** +- Elements created via MCP appear instantly on the live canvas +- WebSocket-based real-time synchronization +- Multi-client support with live updates + +### **Production-Ready Interface** +- Clean, minimal UI with connection status +- Simple "Clear Canvas" functionality +- No development clutter or debug information + +### **Comprehensive MCP API** +- **Element Creation**: rectangles, ellipses, diamonds, arrows, text, lines +- **Element Management**: update, delete, query with filters +- **Batch Operations**: create multiple elements in one call +- **Advanced Features**: grouping, alignment, distribution, locking + +### **Robust Architecture** +- Express.js backend with REST API + WebSocket +- React frontend with official Excalidraw package +- Dual-path element loading for reliability +- Auto-reconnection and error handling + +## 📦 Installation & Setup + +### **Prerequisites** +- Node.js 16+ +- npm or yarn + +### **1. Clone and Install** ```bash -npx excalidraw-mcp -``` - -If you prefer to install it globally: - -```bash -npm install -g excalidraw-mcp -excalidraw-mcp -``` - -### Options - -The following command-line options are available: - -``` --d, --debug Enable debug logging --?, --help Show this help message -``` - -> **Note:** The following options are currently only fully functional in the Docker version: -> ``` -> -p, --port Port to run the server on (default: 3000) -> -h, --host Host to bind the server to (default: localhost) -> -m, --mode Transport mode: 'stdio' or 'http' (default: stdio) -> ``` - -### Examples - -Run with default options: -```bash -npx excalidraw-mcp -``` - -Enable debug logging: -```bash -npx excalidraw-mcp --debug -``` - -## Features - -- **Full Excalidraw Element Control**: Create, update, delete, and query any Excalidraw element -- **Advanced Element Manipulation**: Group, align, distribute, lock, and unlock elements -- **Resource Management**: Access and modify scene information, libraries, themes, and elements -- **Easy Integration**: Works with Claude Desktop and other LLM platforms -- **Docker Support**: Simple deployment with containerization options - -## API Tools Reference - -### Element Creation and Modification - -* **create_element** - * Create a new Excalidraw element (rectangle, ellipse, diamond, etc.) - * Required inputs: `type`, `x`, `y` coordinates - * Optional inputs: dimensions, colors, styling properties - -* **update_element** - * Update an existing Excalidraw element by ID - * Required input: `id` of the element to update - * Optional inputs: any element property to modify - -* **delete_element** - * Delete an Excalidraw element - * Required input: `id` of the element to delete - -* **query_elements** - * Query elements with optional filtering - * Optional inputs: `type` to filter by element type, `filter` object with key-value pairs - -### Resource Management - -* **get_resource** - * Get a specific resource like scene information or all elements - * Required input: `resource` type (scene, library, theme, elements) - -### Element Organization - -* **group_elements** - * Group multiple elements together - * Required input: `elementIds` array of element IDs to group - -* **ungroup_elements** - * Ungroup a group of elements - * Required input: `groupId` of the group to ungroup - -* **align_elements** - * Align multiple elements based on specified alignment - * Required inputs: `elementIds` array and `alignment` (left, center, right, top, middle, bottom) - -* **distribute_elements** - * Distribute elements evenly across space - * Required inputs: `elementIds` array and `direction` (horizontal or vertical) - -* **lock_elements** - * Lock elements to prevent modification - * Required input: `elementIds` array of elements to lock - -* **unlock_elements** - * Unlock elements to allow modification - * Required input: `elementIds` array of elements to unlock - -## Integration with Claude Desktop - -To use this server with the Claude Desktop application, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`: - -```json -{ - "mcpServers": { - "mcp_excalidraw": { - "command": "npx", - "args": ["-y", "excalidraw-mcp"] - } - } -} -``` - -## Integration with Cursor - -To use this server with the Cursor application, add the following configuration to the "mcpServers" section of your `.cursor/mcp.json`: - -```json -{ - "mcpServers": { - "mcp_excalidraw": { - "command": "npx", - "args": ["-y", "excalidraw-mcp"] - } - } -} -``` - -## Integration with Cursor - -To use this server with Cursor, create a `.cursor/mcp.json` file in your workspace with the following configuration: - -```json -{ - "mcpServers": { - "mcp_excalidraw": { - "command": "npx", - "args": ["-y", "excalidraw-mcp"] - } - } -} -``` - -Make sure to: -1. Replace `/path/to/your/directory` with the actual absolute path to your mcp_excalidraw installation -2. Create the `.cursor` directory if it doesn't exist -3. Ensure the path to `index.js` is correct and the file exists - -### Docker Integration - -```json -{ - "mcpServers": { - "excalidraw": { - "command": "docker", - "args": ["run", "-i", "--rm", "mcp/excalidraw"], - "env": { - "LOG_LEVEL": "info", - "DEBUG": "false" - } - } - } -} -``` - -## Installation Guide - -### NPM Installation - -```bash -# Install globally -npm install -g excalidraw-mcp - -# Run the server -excalidraw-mcp -``` - -### Local Development Setup - -```bash -# Clone the repository git clone -cd excalidraw-mcp - -# Install dependencies +cd mcp_excalidraw npm install - -# Start the server -npm start ``` -### Docker Installation - +### **2. Build the Frontend** ```bash -# Build the Docker image -docker build -t mcp/excalidraw . - -# Run the container -docker run -i --rm mcp/excalidraw +npm run build ``` -## Configuration Options +### **3. Start the System** -The server can be configured using the following environment variables: +#### **Option A: Production Mode** +```bash +# Start canvas server (serves frontend + API) +npm run canvas +``` -- `LOG_LEVEL` - Set the logging level (default: "info") -- `DEBUG` - Enable debug mode (default: "false") -- `DEFAULT_THEME` - Set the default theme (default: "light") +#### **Option B: Development Mode** +```bash +# Start both canvas server and Vite dev server +npm run dev +``` -## Usage Examples +### **4. Access the Canvas** +Open your browser and navigate to: +``` +http://localhost:3000 +``` -Here are some practical examples of how to use the Excalidraw MCP server: +## 🔧 Available Scripts -### Creating a Rectangle Element +| Script | Description | +|--------|-------------| +| `npm start` | Start MCP server (`src/index.js`) | +| `npm run canvas` | Start canvas server (`src/server.js`) | +| `npm run build` | Build frontend for production | +| `npm run dev` | Start canvas + Vite dev server | +| `npm run production` | Build + start in production mode | -```json +## 🎯 Usage Guide + +### **For End Users** +1. Open the canvas at `http://localhost:3000` +2. Check connection status (should show "Connected") +3. AI agents can now create diagrams that appear in real-time +4. Use "Clear Canvas" to remove all elements + +### **For AI Agents (via MCP)** +The MCP server provides these tools for creating visual diagrams: + +#### **Basic Element Creation** +```javascript +// Create a rectangle { "type": "rectangle", "x": 100, - "y": 100, + "y": 100, "width": 200, "height": 100, - "backgroundColor": "#ffffff", - "strokeColor": "#000000", - "strokeWidth": 2, - "roughness": 1 + "backgroundColor": "#e3f2fd", + "strokeColor": "#1976d2", + "strokeWidth": 2 } ``` -### Querying Specific Elements +#### **Create Text Elements** +```javascript +{ + "type": "text", + "x": 150, + "y": 125, + "text": "Process Step", + "fontSize": 16, + "strokeColor": "#333333" +} +``` + +#### **Create Arrows & Lines** +```javascript +{ + "type": "arrow", + "x": 300, + "y": 130, + "width": 100, + "height": 0, + "strokeColor": "#666666", + "strokeWidth": 2 +} +``` + +#### **Batch Creation for Complex Diagrams** +```javascript +{ + "elements": [ + { + "type": "rectangle", + "x": 100, + "y": 100, + "width": 120, + "height": 60, + "backgroundColor": "#fff3e0", + "strokeColor": "#ff9800" + }, + { + "type": "text", + "x": 130, + "y": 125, + "text": "Start", + "fontSize": 16 + } + ] +} +``` + +## 🔌 Integration with Claude Desktop + +Add this configuration to your `claude_desktop_config.json`: ```json { - "type": "rectangle", - "filter": { - "strokeColor": "#000000" + "mcpServers": { + "excalidraw_canvas": { + "command": "node", + "args": ["/path/to/mcp_excalidraw/src/index.js"], + } } } ``` -### Grouping Multiple Elements +## Integration with Cursor + +Add this configuration to your `claude_desktop_config.json`: ```json { - "elementIds": ["elem1", "elem2", "elem3"] + "mcpServers": { + "mcp_excalidraw": { + "command": "node", + "args": ["/path/to/mcp_excalidraw/src/index.js"] + } + } } ``` -## License +**Important**: Replace `/path/to/mcp_excalidraw` with the actual absolute path to your installation. -This Excalidraw MCP server is licensed under the MIT License. You are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository. +## 🛠️ Environment Variables -## Development +| Variable | Default | Description | +|----------|---------|-------------| +| `EXPRESS_SERVER_URL` | `http://localhost:3000` | Canvas server URL for MCP sync | +| `ENABLE_CANVAS_SYNC` | `true` | Enable/disable canvas synchronization | +| `DEBUG` | `false` | Enable debug logging | +| `PORT` | `3000` | Canvas server port | +| `HOST` | `localhost` | Canvas server host | -Clone the repository and install dependencies: +## 📊 API Endpoints -```bash -git clone -cd excalidraw-mcp -npm install +The canvas server provides these REST endpoints: + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/elements` | Get all elements | +| `POST` | `/api/elements` | Create new element | +| `PUT` | `/api/elements/:id` | Update element | +| `DELETE` | `/api/elements/:id` | Delete element | +| `POST` | `/api/elements/batch` | Create multiple elements | +| `GET` | `/health` | Server health check | + +## 🎨 MCP Tools Available + +### **Element Management** +- `create_element` - Create any type of Excalidraw element +- `update_element` - Modify existing elements +- `delete_element` - Remove elements +- `query_elements` - Search elements with filters + +### **Batch Operations** +- `batch_create_elements` - Create complex diagrams in one call + +### **Element Organization** +- `group_elements` - Group multiple elements +- `ungroup_elements` - Ungroup element groups +- `align_elements` - Align elements (left, center, right, top, middle, bottom) +- `distribute_elements` - Distribute elements evenly +- `lock_elements` / `unlock_elements` - Lock/unlock elements + +### **Resource Access** +- `get_resource` - Access scene, library, theme, or elements data + +## 🏗️ Development Architecture + +### **Frontend** (`frontend/src/`) +- **React + Vite**: Modern build system +- **Official Excalidraw**: `@excalidraw/excalidraw` package +- **WebSocket Client**: Real-time element sync +- **Clean UI**: Production-ready interface + +### **Canvas Server** (`src/server.js`) +- **Express.js**: REST API + static file serving +- **WebSocket**: Real-time client communication +- **Element Storage**: In-memory with persistence options +- **CORS**: Cross-origin support + +### **MCP Server** (`src/index.js`) +- **MCP Protocol**: Standard Model Context Protocol +- **Canvas Sync**: HTTP requests to canvas server +- **Element Management**: Full CRUD operations +- **Batch Support**: Complex diagram creation + +## 🐛 Troubleshooting + +### **Canvas Not Loading** +- Ensure `npm run build` completed successfully +- Check that `dist/index.html` exists +- Verify canvas server is running on port 3000 + +### **Elements Not Syncing** +- Confirm MCP server is running (`npm start`) +- Check `ENABLE_CANVAS_SYNC=true` in environment +- Verify canvas server is accessible at `EXPRESS_SERVER_URL` + +### **WebSocket Connection Issues** +- Check browser console for WebSocket errors +- Ensure no firewall blocking WebSocket connections +- Try refreshing the browser page + +### **Build Errors** +- Delete `node_modules` and run `npm install` +- Check Node.js version (requires 16+) +- Ensure all dependencies are installed + +## 📋 Project Structure + +``` +mcp_excalidraw/ +├── frontend/ +│ ├── src/ +│ │ ├── App.jsx # Main React component +│ │ └── main.jsx # React entry point +│ └── index.html # HTML template +├── src/ +│ ├── index.js # MCP server +│ ├── server.js # Canvas server (Express + WebSocket) +│ ├── types.js # Shared types and utilities +│ └── utils/ +│ └── logger.js # Logging utility +├── dist/ # Built frontend (generated) +├── vite.config.js # Vite build configuration +├── package.json # Dependencies and scripts +└── README.md # This file ``` -Start the development server: +## 🤝 Contributing -```bash -npm run dev -``` \ No newline at end of file +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## 📝 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 🙏 Acknowledgments + +- **Excalidraw Team** - For the amazing drawing library +- **MCP Community** - For the Model Context Protocol specification \ No newline at end of file diff --git a/public/dist/frontend/index.html b/public/dist/frontend/index.html deleted file mode 100644 index 709f2ae..0000000 --- a/public/dist/frontend/index.html +++ /dev/null @@ -1,241 +0,0 @@ - - - - - - Excalidraw POC - Backend API Integration - - - - - -
- - \ No newline at end of file diff --git a/public/dist/index.html b/public/dist/index.html deleted file mode 100644 index 709f2ae..0000000 --- a/public/dist/index.html +++ /dev/null @@ -1,241 +0,0 @@ - - - - - - Excalidraw POC - Backend API Integration - - - - - -
- - \ No newline at end of file diff --git a/public/index.html b/public/index.html deleted file mode 100644 index b99c9c9..0000000 --- a/public/index.html +++ /dev/null @@ -1,621 +0,0 @@ - - - - - - Excalidraw POC - Backend API Integration - - - -
-

Excalidraw POC - Backend API Integration

-
-
-
- Connecting... -
-
- Elements: 0 -
- - -
-
- - - -
-

Create Element via API

-
-
- - -
- -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
- - -
- -
-
- - -
-
- - -
-
- -
- - -
- - -
-
- -
-
-
-
-
-
Loading Excalidraw...
-
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/src/cli.js b/src/cli.js deleted file mode 100755 index b1647be..0000000 --- a/src/cli.js +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env node - -import { parseArgs } from 'node:util'; -import { resolve } from 'path'; -import dotenv from 'dotenv'; -import logger from './utils/logger.js'; - -// Load environment variables -dotenv.config(); - -async function main() { - try { - // Parse command line arguments - const options = { - port: { - type: 'string', - short: 'p', - default: process.env.PORT || '3000' - }, - host: { - type: 'string', - short: 'h', - default: process.env.HOST || 'localhost' - }, - mode: { - type: 'string', - short: 'm', - default: 'stdio' - }, - debug: { - type: 'boolean', - short: 'd', - default: false - }, - help: { - type: 'boolean', - short: '?', - default: false - } - }; - - const { values, positionals } = parseArgs({ - options, - allowPositionals: true, - strict: false - }); - - // Show help if requested - if (values.help) { - showHelp(); - process.exit(0); - } - - // Set debug mode if requested - if (values.debug) { - process.env.DEBUG = 'true'; - logger.level = 'debug'; - logger.debug('Debug mode enabled'); - } - - // Set the mode for server transport - process.env.MCP_TRANSPORT_MODE = values.mode; - - // Set port and host - process.env.PORT = values.port; - process.env.HOST = values.host; - - // Import and run server - const { default: runServer } = await import('./index.js'); - await runServer(); - - } catch (error) { - process.stderr.write(`Error starting MCP server: ${error}\n`); - process.exit(1); - } -} - -function showHelp() { - process.stderr.write(` - Excalidraw MCP Server - - Usage: - npx excalidraw-mcp [options] - - Options: - -p, --port Port to run the server on (default: 3000) - -h, --host Host to bind the server to (default: localhost) - -m, --mode Transport mode: 'stdio' or 'http' (default: stdio) - -d, --debug Enable debug logging - -?, --help Show this help message - - Examples: - npx excalidraw-mcp - npx excalidraw-mcp --port 4000 - npx excalidraw-mcp --mode http - npx excalidraw-mcp --debug - \n`); -} - -main().catch(error => { - process.stderr.write(`Fatal error: ${error}\n`); - process.exit(1); -}); \ No newline at end of file diff --git a/src/test.js b/src/test.js deleted file mode 100644 index af8dadb..0000000 --- a/src/test.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; - -process.stderr.write('MCP SDK imports successful\n'); -process.stderr.write(`Server: ${Server}\n`); -process.stderr.write(`StdioServerTransport: ${StdioServerTransport}\n`); - -// Exit gracefully -process.exit(0); \ No newline at end of file From cb743c6a1ebc8242ef0dd52417bd0321748b46a1 Mon Sep 17 00:00:00 2001 From: yctimlin Date: Fri, 11 Jul 2025 19:52:18 +0000 Subject: [PATCH 05/13] Add MIT License, update project name and description in package.json, enhance README.md with installation instructions, and modify index.js for CLI support - Introduce LICENSE file with MIT License details. - Change project name to 'mcp-excalidraw-server' and update description in package.json for clarity on features. - Revise README.md to include new installation options and usage instructions for the npm package. - Add shebang to index.js for direct CLI execution. --- LICENSE | 21 +++++++++ README.md | 119 +++++++++++++++++++++++++++++++++++++++++++++------ package.json | 37 +++++++++++++--- src/index.js | 2 + 4 files changed, 161 insertions(+), 18 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a006783 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 MCP Excalidraw Server + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 54cf33e..eda7ba5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -# Excalidraw MCP Canvas: Live Visual Diagramming with AI Integration +# MCP Excalidraw Server: Advanced Live Visual Diagramming with AI Integration A comprehensive system that combines **Excalidraw's powerful drawing capabilities** with **Model Context Protocol (MCP)** integration, enabling AI agents to create and manipulate diagrams in real-time on a live canvas. +> **🚀 NEW: Now available as `mcp-excalidraw-server` on npm!** Install with `npm install -g mcp-excalidraw-server` or run directly with `npx mcp-excalidraw-server`. + ## 🚀 What This System Does - **🎨 Live Canvas**: Real-time Excalidraw canvas accessible via web browser @@ -55,9 +57,35 @@ A comprehensive system that combines **Excalidraw's powerful drawing capabilitie - Node.js 16+ - npm or yarn -### **1. Clone and Install** +### **Option 1: NPM Installation (Recommended)** + +#### **Global Installation** ```bash -git clone +# Install globally +npm install -g mcp-excalidraw-server + +# Run the server +mcp-excalidraw-server +``` + +#### **Run Without Installation** +```bash +# Run directly with npx (no installation needed) +npx mcp-excalidraw-server +``` + +#### **Local Project Installation** +```bash +# Install in your project +npm install mcp-excalidraw-server + +# Run from node_modules +npx mcp-excalidraw-server +``` + +### **Option 2: Clone from Source** +```bash +git clone https://github.com/yctimlin/mcp_excalidraw.git cd mcp_excalidraw npm install ``` @@ -174,29 +202,46 @@ The MCP server provides these tools for creating visual diagrams: ## 🔌 Integration with Claude Desktop +### **Using NPM Package (Recommended)** + Add this configuration to your `claude_desktop_config.json`: ```json { "mcpServers": { - "excalidraw_canvas": { - "command": "node", - "args": ["/path/to/mcp_excalidraw/src/index.js"], + "excalidraw": { + "command": "npx", + "args": ["-y", "mcp-excalidraw-server"] } } } ``` -## Integration with Cursor +### **Using Global Installation** -Add this configuration to your `claude_desktop_config.json`: +If you installed globally with `npm install -g mcp-excalidraw-server`: ```json { - "mcpServers": { - "mcp_excalidraw": { - "command": "node", - "args": ["/path/to/mcp_excalidraw/src/index.js"] + "mcpServers": { + "excalidraw": { + "command": "mcp-excalidraw-server", + "args": [] + } + } +} +``` + +### **Using Source Installation** + +If you cloned from source: + +```json +{ + "mcpServers": { + "excalidraw": { + "command": "node", + "args": ["/path/to/mcp_excalidraw/src/index.js"] } } } @@ -204,6 +249,56 @@ Add this configuration to your `claude_desktop_config.json`: **Important**: Replace `/path/to/mcp_excalidraw` with the actual absolute path to your installation. +## 🔧 Integration with Other Tools + +### **Cursor IDE** + +Add to your `claude_desktop_config.json` or MCP settings: + +```json +{ + "mcpServers": { + "excalidraw": { + "command": "npx", + "args": ["-y", "mcp-excalidraw-server"] + } + } +} +``` + +### **VS Code MCP Extension** + +For VS Code MCP extension, add to your settings: + +```json +{ + "mcp": { + "servers": { + "excalidraw": { + "command": "npx", + "args": ["-y", "mcp-excalidraw-server"] + } + } + } +} +``` + +### **Command Line Usage** + +```bash +# Run directly with npx (no installation needed) +npx mcp-excalidraw-server + +# Run with global installation +mcp-excalidraw-server + +# Run with custom canvas server URL +EXPRESS_SERVER_URL=http://localhost:8080 npx mcp-excalidraw-server + +# Run with debug logging +DEBUG=true npx mcp-excalidraw-server +``` + ## 🛠️ Environment Variables | Variable | Default | Description | diff --git a/package.json b/package.json index e5f7658..a7cf20a 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,19 @@ { - "name": "excalidraw-mcp", + "name": "mcp-excalidraw-server", "version": "1.0.0", - "description": "MCP server for Excalidraw", + "description": "Advanced MCP server for Excalidraw with real-time canvas, WebSocket sync, and comprehensive diagram management", "main": "src/index.js", "type": "module", + "bin": { + "mcp-excalidraw-server": "src/index.js" + }, "scripts": { "start": "node src/index.js", "canvas": "node src/server.js", "build": "vite build", "dev": "concurrently \"npm run canvas\" \"vite\"", - "production": "npm run build && npm run canvas" + "production": "npm run build && npm run canvas", + "prepublishOnly": "npm run build" }, "dependencies": { "@excalidraw/excalidraw": "^0.18.0", @@ -32,21 +36,42 @@ }, "keywords": [ "mcp", + "mcp-server", "excalidraw", "model-context-protocol", "ai", - "drawing" + "drawing", + "diagrams", + "canvas", + "real-time", + "websocket", + "visualization", + "claude", + "ai-tools" ], - "author": "", + "author": { + "name": "yctimlin", + "email": "c22647809@gmail.com" + }, "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/yctimlin/mcp_excalidraw.git" + }, + "homepage": "https://github.com/yctimlin/mcp_excalidraw#readme", + "bugs": { + "url": "https://github.com/yctimlin/mcp_excalidraw/issues" + }, "engines": { "node": ">=16.0.0" }, "publishConfig": { - "access": "public" + "access": "public", + "registry": "https://registry.npmjs.org/" }, "files": [ "src/**/*", + "dist/**/*", "README.md", "LICENSE" ] diff --git a/src/index.js b/src/index.js index a95c248..165aa26 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + // Disable colors to prevent ANSI color codes from breaking JSON parsing process.env.NODE_DISABLE_COLORS = '1'; process.env.NO_COLOR = '1'; From 4acb2e67f81e66af66fe9c613016f6d62423a7c6 Mon Sep 17 00:00:00 2001 From: yctimlin Date: Sat, 12 Jul 2025 05:18:01 +0000 Subject: [PATCH 06/13] Enhance build process and add TypeScript configuration - Update package.json to include separate build scripts for frontend and types, and add a build server script. - Introduce tsconfig.json for TypeScript configuration, enabling declaration files and source maps. - Add TypeScript and Node types as development dependencies. - Include TypeScript declaration files in the package distribution. --- package.json | 8 +++++++- tsconfig.json | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tsconfig.json diff --git a/package.json b/package.json index a7cf20a..2e10f8e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "scripts": { "start": "node src/index.js", "canvas": "node src/server.js", - "build": "vite build", + "build": "npm run build:frontend && npm run build:types", + "build:frontend": "vite build", + "build:types": "npx tsc --emitDeclarationOnly", + "build:server": "npx tsc", "dev": "concurrently \"npm run canvas\" \"vite\"", "production": "npm run build && npm run canvas", "prepublishOnly": "npm run build" @@ -30,8 +33,10 @@ "zod-to-json-schema": "^3.22.3" }, "devDependencies": { + "@types/node": "^20.19.7", "@vitejs/plugin-react": "^4.6.0", "concurrently": "^9.2.0", + "typescript": "^5.8.3", "vite": "^6.3.5" }, "keywords": [ @@ -72,6 +77,7 @@ "files": [ "src/**/*", "dist/**/*", + "*.d.ts", "README.md", "LICENSE" ] diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7bcf949 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "allowJs": true, + "checkJs": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmitOnError": false, + "preserveConstEnums": true, + "removeComments": false, + "types": ["node"] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "frontend", + "**/*.test.js", + "**/*.spec.js" + ], + "ts-node": { + "esm": true + } +} \ No newline at end of file From 4f9546de76e66f6e3e3a71b9b6066e929aa3ae6d Mon Sep 17 00:00:00 2001 From: yctimlin Date: Sat, 12 Jul 2025 05:32:12 +0000 Subject: [PATCH 07/13] Improve frontend error handling in server.js --- src/server.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/server.js b/src/server.js index 706fc53..06a1ff8 100644 --- a/src/server.js +++ b/src/server.js @@ -342,8 +342,13 @@ app.post('/api/elements/batch', (req, res) => { // Serve the frontend app.get('/', (req, res) => { - const htmlFile = path.join(__dirname, '../dist/index.html'); - res.sendFile(htmlFile); + 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 From 898952e359b05955e3540f66e35c71e59a438b05 Mon Sep 17 00:00:00 2001 From: yctimlin Date: Sat, 12 Jul 2025 06:20:34 +0000 Subject: [PATCH 08/13] update version --- package.json | 2 +- src/index.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) mode change 100644 => 100755 src/index.js diff --git a/package.json b/package.json index 2e10f8e..14207ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mcp-excalidraw-server", - "version": "1.0.0", + "version": "1.0.2", "description": "Advanced MCP server for Excalidraw with real-time canvas, WebSocket sync, and comprehensive diagram management", "main": "src/index.js", "type": "module", diff --git a/src/index.js b/src/index.js old mode 100644 new mode 100755 index 165aa26..9ca75c1 --- a/src/index.js +++ b/src/index.js @@ -178,9 +178,9 @@ const ResourceSchema = z.object({ // Initialize MCP server const server = new Server( { - name: "excalidraw-mcp-server", - version: "1.0.0", - description: "MCP server for Excalidraw" + name: "mcp-excalidraw-server", + version: "1.0.2", + description: "Advanced MCP server for Excalidraw with real-time canvas" }, { capabilities: { From 542e3f149f13d8257d540988d7921ac41d703232 Mon Sep 17 00:00:00 2001 From: yctimlin Date: Sat, 12 Jul 2025 07:38:35 +0000 Subject: [PATCH 09/13] update dockerfile --- Dockerfile | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index fe83e6e..2a994d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,28 @@ +# Production stage - MCP Backend Only FROM node:18-slim +# Create non-root user for security +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 --gid 1001 nodejs + WORKDIR /app +# Copy package files COPY package*.json ./ -RUN npm install -COPY . . +# Install only production dependencies +RUN npm ci --only=production && npm cache clean --force -EXPOSE 3000 +# Copy source code (only backend files needed) +COPY src ./src +# Set environment variables +ENV NODE_ENV=production +ENV EXPRESS_SERVER_URL=http://localhost:3000 +ENV ENABLE_CANVAS_SYNC=true + +# Switch to non-root user +USER nodejs + +# Run MCP server only CMD ["npm", "start"] \ No newline at end of file From afa145bd734459f226234e580fd401add424e335 Mon Sep 17 00:00:00 2001 From: yctimlin Date: Sat, 12 Jul 2025 07:56:06 +0000 Subject: [PATCH 10/13] update readme --- README.md | 193 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 100 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index eda7ba5..324a1cf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,24 @@ A comprehensive system that combines **Excalidraw's powerful drawing capabilities** with **Model Context Protocol (MCP)** integration, enabling AI agents to create and manipulate diagrams in real-time on a live canvas. -> **🚀 NEW: Now available as `mcp-excalidraw-server` on npm!** Install with `npm install -g mcp-excalidraw-server` or run directly with `npx mcp-excalidraw-server`. +## 🚦 Current Status & Version Information + +> **📋 Choose Your Installation Method** + +| Version | Status | Recommended For | +|---------|--------|----------------| +| **Local Development** | ✅ **FULLY TESTED** | **🎯 RECOMMENDED** | +| **NPM Published** | 🔧 **DEBUGGING IN PROGRESS** | Development testing | +| **Docker Version** | 🔧 **UNDER DEVELOPMENT** | Future deployment | + +### **Current Recommendation: Local Development** + +For the most stable experience, we recommend using the local development setup. We're actively working on improving the NPM package and Docker deployment options. + +### **Development Notes** +- **NPM Package**: Currently debugging MCP tool registration issues +- **Docker Version**: Improving canvas synchronization reliability +- **Local Version**: ✅ All features fully functional ## 🚀 What This System Does @@ -53,68 +70,57 @@ A comprehensive system that combines **Excalidraw's powerful drawing capabilitie ## 📦 Installation & Setup -### **Prerequisites** -- Node.js 16+ -- npm or yarn +### **✅ Recommended: Local Development Setup** -### **Option 1: NPM Installation (Recommended)** +> **Most stable and feature-complete option** -#### **Global Installation** -```bash -# Install globally -npm install -g mcp-excalidraw-server - -# Run the server -mcp-excalidraw-server -``` - -#### **Run Without Installation** -```bash -# Run directly with npx (no installation needed) -npx mcp-excalidraw-server -``` - -#### **Local Project Installation** -```bash -# Install in your project -npm install mcp-excalidraw-server - -# Run from node_modules -npx mcp-excalidraw-server -``` - -### **Option 2: Clone from Source** +#### **1. Clone the Repository** ```bash git clone https://github.com/yctimlin/mcp_excalidraw.git cd mcp_excalidraw npm install ``` -### **2. Build the Frontend** +#### **2. Build the Frontend** ```bash npm run build ``` -### **3. Start the System** +#### **3. Start the System** -#### **Option A: Production Mode** +##### **Option A: Production Mode (Recommended)** ```bash # Start canvas server (serves frontend + API) npm run canvas ``` -#### **Option B: Development Mode** +##### **Option B: Development Mode** ```bash # Start both canvas server and Vite dev server npm run dev ``` -### **4. Access the Canvas** +#### **4. Access the Canvas** Open your browser and navigate to: ``` http://localhost:3000 ``` +### **🔧 Alternative Installation Methods (In Development)** + +#### **NPM Package (Beta)** +```bash +# Currently debugging tool registration - feedback welcome! +npm install -g mcp-excalidraw-server +npx mcp-excalidraw-server +``` + +#### **Docker Version (Coming Soon)** +```bash +# Canvas sync improvements in progress +docker run -p 3000:3000 mcp-excalidraw-server +``` + ## 🔧 Available Scripts | Script | Description | @@ -202,59 +208,26 @@ The MCP server provides these tools for creating visual diagrams: ## 🔌 Integration with Claude Desktop -### **Using NPM Package (Recommended)** +### **✅ Recommended: Using Local Installation** -Add this configuration to your `claude_desktop_config.json`: - -```json -{ - "mcpServers": { - "excalidraw": { - "command": "npx", - "args": ["-y", "mcp-excalidraw-server"] - } - } -} -``` - -### **Using Global Installation** - -If you installed globally with `npm install -g mcp-excalidraw-server`: - -```json -{ - "mcpServers": { - "excalidraw": { - "command": "mcp-excalidraw-server", - "args": [] - } - } -} -``` - -### **Using Source Installation** - -If you cloned from source: +For the **local development version** (most stable), add this configuration to your `claude_desktop_config.json`: ```json { "mcpServers": { "excalidraw": { "command": "node", - "args": ["/path/to/mcp_excalidraw/src/index.js"] + "args": ["/absolute/path/to/mcp_excalidraw/src/index.js"] } } } ``` -**Important**: Replace `/path/to/mcp_excalidraw` with the actual absolute path to your installation. +**Important**: Replace `/absolute/path/to/mcp_excalidraw` with the actual absolute path to your cloned repository. -## 🔧 Integration with Other Tools - -### **Cursor IDE** - -Add to your `claude_desktop_config.json` or MCP settings: +### **🔧 Alternative Configurations (Beta)** +#### **NPM Package (Beta Testing)** ```json { "mcpServers": { @@ -265,6 +238,37 @@ Add to your `claude_desktop_config.json` or MCP settings: } } ``` +*Currently debugging tool registration - let us know if you encounter issues!* + +#### **Docker Version (Coming Soon)** +```json +{ + "mcpServers": { + "excalidraw": { + "command": "docker", + "args": ["run", "-i", "--rm", "mcp-excalidraw-server"] + } + } +} +``` +*Canvas sync improvements in progress.* + +## 🔧 Integration with Other Tools + +### **Cursor IDE** + +Add to your `.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "excalidraw": { + "command": "node", + "args": ["/absolute/path/to/mcp_excalidraw/src/index.js"] + } + } +} +``` ### **VS Code MCP Extension** @@ -275,30 +279,14 @@ For VS Code MCP extension, add to your settings: "mcp": { "servers": { "excalidraw": { - "command": "npx", - "args": ["-y", "mcp-excalidraw-server"] + "command": "node", + "args": ["/absolute/path/to/mcp_excalidraw/src/index.js"] } } } } ``` -### **Command Line Usage** - -```bash -# Run directly with npx (no installation needed) -npx mcp-excalidraw-server - -# Run with global installation -mcp-excalidraw-server - -# Run with custom canvas server URL -EXPRESS_SERVER_URL=http://localhost:8080 npx mcp-excalidraw-server - -# Run with debug logging -DEBUG=true npx mcp-excalidraw-server -``` - ## 🛠️ Environment Variables | Variable | Default | Description | @@ -365,6 +353,16 @@ The canvas server provides these REST endpoints: ## 🐛 Troubleshooting +### **NPM Package Issues** +- **Symptoms**: MCP tools not registering properly +- **Temporary Solution**: Use local development setup +- **Status**: Actively debugging - updates coming soon + +### **Docker Version Notes** +- **Symptoms**: Elements may not sync to canvas immediately +- **Temporary Solution**: Use local development setup +- **Status**: Improving synchronization reliability + ### **Canvas Not Loading** - Ensure `npm run build` completed successfully - Check that `dist/index.html` exists @@ -406,8 +404,17 @@ mcp_excalidraw/ └── README.md # This file ``` +## 🔮 Development Roadmap + +- **NPM Package**: Resolving MCP tool registration issues +- **Docker Deployment**: Improving canvas synchronization +- **Enhanced Features**: Additional MCP tools and capabilities +- **Performance Optimization**: Real-time sync improvements + ## 🤝 Contributing +We welcome contributions! If you're experiencing issues with the NPM package or Docker version, please: + 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add amazing feature'`) @@ -421,4 +428,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments - **Excalidraw Team** - For the amazing drawing library -- **MCP Community** - For the Model Context Protocol specification \ No newline at end of file +- **MCP Community** - For the Model Context Protocol specification From 86232d9c943acbf9504e7b18bd7220347e2c4bf6 Mon Sep 17 00:00:00 2001 From: yctimlin Date: Sat, 12 Jul 2025 09:01:51 +0000 Subject: [PATCH 11/13] fix text label bugs --- README.md | 8 +++++++ frontend/src/App.jsx | 26 +++++++++++++++++----- src/index.js | 53 ++++++++++++++++++++++++++++++-------------- src/server.js | 8 +++++++ src/types.js | 3 +-- 5 files changed, 74 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 324a1cf..eb29b4f 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,14 @@ For the most stable experience, we recommend using the local development setup. - **🔄 WebSocket Updates**: Live synchronization across multiple connected clients - **🏗️ Production Ready**: Clean, minimal UI suitable for end users +## 🎥 Demo Video + +> **See MCP Excalidraw in Action!** + +[![MCP Excalidraw Demo](https://img.youtube.com/vi/YOUR_VIDEO_ID/maxresdefault.jpg)](https://www.youtube.com/watch?v=YOUR_VIDEO_ID) + +*Watch how AI agents create and manipulate diagrams in real-time on the live canvas* + ## 🏛️ Architecture Overview ``` diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3f1f822..648d0d4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,6 +2,17 @@ import React, { useState, useEffect, useRef } from 'react' import { Excalidraw, convertToExcalidrawElements } from '@excalidraw/excalidraw' import '@excalidraw/excalidraw/index.css' +// Helper function to clean elements for Excalidraw +const cleanElementForExcalidraw = (element) => { + const { + createdAt, + updatedAt, + version, + ...cleanElement + } = element; + return cleanElement; +} + function App() { const [excalidrawAPI, setExcalidrawAPI] = useState(null) const [isConnected, setIsConnected] = useState(false) @@ -35,7 +46,8 @@ function App() { const result = await response.json() if (result.success && result.elements && result.elements.length > 0) { - const convertedElements = convertToExcalidrawElements(result.elements) + const cleanedElements = result.elements.map(cleanElementForExcalidraw) + const convertedElements = convertToExcalidrawElements(cleanedElements) excalidrawAPI.updateScene({ elements: convertedElements }) } } catch (error) { @@ -96,19 +108,22 @@ function App() { switch (data.type) { case 'initial_elements': if (data.elements && data.elements.length > 0) { - const convertedElements = convertToExcalidrawElements(data.elements) + const cleanedElements = data.elements.map(cleanElementForExcalidraw) + const convertedElements = convertToExcalidrawElements(cleanedElements) excalidrawAPI.updateScene({ elements: convertedElements }) } break case 'element_created': - const newElement = convertToExcalidrawElements([data.element]) + const cleanedNewElement = cleanElementForExcalidraw(data.element) + const newElement = convertToExcalidrawElements([cleanedNewElement]) const updatedElementsAfterCreate = [...currentElements, ...newElement] excalidrawAPI.updateScene({ elements: updatedElementsAfterCreate }) break case 'element_updated': - const convertedUpdatedElement = convertToExcalidrawElements([data.element])[0] + const cleanedUpdatedElement = cleanElementForExcalidraw(data.element) + const convertedUpdatedElement = convertToExcalidrawElements([cleanedUpdatedElement])[0] const updatedElements = currentElements.map(el => el.id === data.element.id ? convertedUpdatedElement : el ) @@ -121,7 +136,8 @@ function App() { break case 'elements_batch_created': - const batchElements = convertToExcalidrawElements(data.elements) + const cleanedBatchElements = data.elements.map(cleanElementForExcalidraw) + const batchElements = convertToExcalidrawElements(cleanedBatchElements) const updatedElementsAfterBatch = [...currentElements, ...batchElements] excalidrawAPI.updateScene({ elements: updatedElementsAfterBatch }) break diff --git a/src/index.js b/src/index.js index 9ca75c1..b51c3f7 100755 --- a/src/index.js +++ b/src/index.js @@ -397,6 +397,18 @@ const server = new Server( } ); +// Helper function to convert text property to label format for Excalidraw +function convertTextToLabel(element) { + const { text, ...rest } = element; + if (text) { + return { + ...rest, + label: { text } + }; + } + return element; +} + // Set up request handler for tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { try { @@ -417,23 +429,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { version: 1 }; - // Store locally (MCP server storage) - elements.set(id, element); + // Convert text to label format for Excalidraw + const excalidrawElement = convertTextToLabel(element); + + // Store the converted element locally (MCP server storage) + elements.set(id, excalidrawElement); // Sync to canvas (Express server + WebSocket broadcast) - const canvasElement = await createElementOnCanvas(element); + const canvasElement = await createElementOnCanvas(excalidrawElement); - const result = canvasElement || element; logger.info('Element created via MCP and synced to canvas', { - id: result.id, - type: result.type, + id: excalidrawElement.id, + type: excalidrawElement.type, synced: !!canvasElement }); return { content: [{ type: 'text', - text: `Element created successfully!\n\n${JSON.stringify(result, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still created locally)'}` + text: `Element created successfully!\n\n${JSON.stringify(excalidrawElement, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still created locally)'}` }] }; } @@ -457,22 +471,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { version: existingElement.version + 1 }; - // Store locally (MCP server storage) - elements.set(id, updatedElement); + // Convert text to label format for Excalidraw + const excalidrawElement = convertTextToLabel(updatedElement); + + // Store the converted element locally (MCP server storage) + elements.set(id, excalidrawElement); // Sync to canvas (Express server + WebSocket broadcast) - const canvasElement = await updateElementOnCanvas(updatedElement); + const canvasElement = await updateElementOnCanvas(excalidrawElement); - const result = canvasElement || updatedElement; logger.info('Element updated via MCP and synced to canvas', { - id: result.id, + id: excalidrawElement.id, synced: !!canvasElement }); return { content: [{ type: 'text', - text: `Element updated successfully!\n\n${JSON.stringify(result, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still updated locally)'}` + text: `Element updated successfully!\n\n${JSON.stringify(excalidrawElement, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still updated locally)'}` }] }; } @@ -668,9 +684,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { version: 1 }; - // Store locally (MCP server storage) - elements.set(id, element); - createdElements.push(element); + // Convert text to label format for Excalidraw + const excalidrawElement = convertTextToLabel(element); + + // Store the converted element locally (MCP server storage) + elements.set(id, excalidrawElement); + createdElements.push(excalidrawElement); } // Sync all elements to canvas at once (Express server + WebSocket broadcast) @@ -678,7 +697,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const result = { success: true, - elements: canvasElements || createdElements, + elements: createdElements, count: createdElements.length, syncedToCanvas: !!canvasElements }; diff --git a/src/server.js b/src/server.js index 06a1ff8..abd188e 100644 --- a/src/server.js +++ b/src/server.js @@ -30,6 +30,8 @@ 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(); @@ -79,6 +81,9 @@ const CreateElementSchema = z.object({ 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() }); @@ -96,6 +101,9 @@ const UpdateElementSchema = z.object({ 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() }); diff --git a/src/types.js b/src/types.js index 9ea807c..65b1f59 100644 --- a/src/types.js +++ b/src/types.js @@ -7,8 +7,7 @@ export const EXCALIDRAW_ELEMENT_TYPES = { TEXT: 'text', LABEL: 'label', FREEDRAW: 'freedraw', - LINE: 'line', - ARROW_LABEL: 'arrowLabel' + LINE: 'line' }; // In-memory storage for Excalidraw elements From aa605b3bed8679964bf78ea49bb4d87a9eb7f68c Mon Sep 17 00:00:00 2001 From: yctimlin Date: Sat, 12 Jul 2025 09:54:18 +0000 Subject: [PATCH 12/13] fix bug of text element --- src/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.js b/src/index.js index b51c3f7..1b2f835 100755 --- a/src/index.js +++ b/src/index.js @@ -401,6 +401,11 @@ const server = new Server( function convertTextToLabel(element) { const { text, ...rest } = element; if (text) { + // For standalone text elements, keep text as direct property + if (element.type === 'text') { + return element; // Keep text as direct property + } + // For other elements (rectangle, ellipse, diamond), convert to label format return { ...rest, label: { text } From 661d343b18bc74e11f0fe7227e85f55beecf2e89 Mon Sep 17 00:00:00 2001 From: yctimlin Date: Sat, 12 Jul 2025 10:36:24 +0000 Subject: [PATCH 13/13] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb29b4f..d502c52 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ For the most stable experience, we recommend using the local development setup. > **See MCP Excalidraw in Action!** -[![MCP Excalidraw Demo](https://img.youtube.com/vi/YOUR_VIDEO_ID/maxresdefault.jpg)](https://www.youtube.com/watch?v=YOUR_VIDEO_ID) +[![MCP Excalidraw Demo](https://img.youtube.com/vi/RRN7AF7QIew/maxresdefault.jpg)](https://youtu.be/RRN7AF7QIew) *Watch how AI agents create and manipulate diagrams in real-time on the live canvas*