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