Merge pull request #11 from yctimlin/dev

Dev
This commit is contained in:
yctimlin
2025-07-12 18:43:16 +08:00
committed by GitHub
15 changed files with 1684 additions and 363 deletions
+18 -3
View File
@@ -1,5 +1,20 @@
node_modules
.env
# Dependencies
node_modules/
package-lock.json
.cursor
# Build artifacts
dist/
public/dist/
# Environment files
.env
# Logs
*.log
# Editor artifacts
.cursor/
.claude/
# Development artifacts
*.excalidraw
+19 -3
View File
@@ -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"]
+21
View File
@@ -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.
+393 -234
View File
@@ -1,280 +1,439 @@
# Excalidraw MCP Server: Powerful Drawing API for LLM Integration
# MCP Excalidraw Server: Advanced 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.
## 🚦 Current Status & Version Information
## Quick Start
> **📋 Choose Your Installation Method**
You can run the Excalidraw MCP server directly using npx without installing anything:
| 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
- **🎨 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
## 🎥 Demo Video
> **See MCP Excalidraw in Action!**
[![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*
## 🏛️ 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
### **✅ Recommended: Local Development Setup**
> **Most stable and feature-complete option**
#### **1. Clone the Repository**
```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> Port to run the server on (default: 3000)
> -h, --host <host> Host to bind the server to (default: localhost)
> -m, --mode <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 <repository-url>
cd excalidraw-mcp
# Install dependencies
git clone https://github.com/yctimlin/mcp_excalidraw.git
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 (Recommended)**
```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:
### **🔧 Alternative Installation Methods (In Development)**
### Creating a Rectangle Element
#### **NPM Package (Beta)**
```bash
# Currently debugging tool registration - feedback welcome!
npm install -g mcp-excalidraw-server
npx mcp-excalidraw-server
```
```json
#### **Docker Version (Coming Soon)**
```bash
# Canvas sync improvements in progress
docker run -p 3000:3000 mcp-excalidraw-server
```
## 🔧 Available Scripts
| 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 |
## 🎯 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,
"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
### **✅ Recommended: Using Local Installation**
For the **local development version** (most stable), add this configuration to your `claude_desktop_config.json`:
```json
{
"type": "rectangle",
"filter": {
"strokeColor": "#000000"
"mcpServers": {
"excalidraw": {
"command": "node",
"args": ["/absolute/path/to/mcp_excalidraw/src/index.js"]
}
}
}
```
### Grouping Multiple Elements
**Important**: Replace `/absolute/path/to/mcp_excalidraw` with the actual absolute path to your cloned repository.
### **🔧 Alternative Configurations (Beta)**
#### **NPM Package (Beta Testing)**
```json
{
"mcpServers": {
"excalidraw": {
"command": "npx",
"args": ["-y", "mcp-excalidraw-server"]
}
}
}
```
*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
{
"elementIds": ["elem1", "elem2", "elem3"]
"mcpServers": {
"excalidraw": {
"command": "node",
"args": ["/absolute/path/to/mcp_excalidraw/src/index.js"]
}
}
}
```
## License
### **VS Code MCP Extension**
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.
For VS Code MCP extension, add to your settings:
## Development
Clone the repository and install dependencies:
```bash
git clone <repository-url>
cd excalidraw-mcp
npm install
```json
{
"mcp": {
"servers": {
"excalidraw": {
"command": "node",
"args": ["/absolute/path/to/mcp_excalidraw/src/index.js"]
}
}
}
}
```
Start the development server:
## 🛠️ Environment Variables
| 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 |
## 📊 API Endpoints
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
### **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
- 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
```bash
npm run dev
```
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
```
## 🔮 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'`)
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
+240
View File
@@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excalidraw POC - Backend API Integration</title>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background-color: #f5f5f5;
}
.header {
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.header h1 {
margin: 0;
color: #333;
font-size: 24px;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.status {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-connected {
background-color: #28a745;
}
.status-disconnected {
background-color: #dc3545;
}
.canvas-container {
height: calc(100vh - 80px);
width: 100%;
position: relative;
}
.api-panel {
position: fixed;
right: 20px;
top: 100px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 20px;
width: 300px;
max-height: 400px;
overflow-y: auto;
z-index: 1000;
}
.api-panel h3 {
margin: 0 0 15px 0;
color: #333;
font-size: 18px;
}
.api-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
}
label {
font-weight: 500;
color: #555;
font-size: 14px;
}
input, select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-row {
display: flex;
gap: 10px;
}
.form-row .form-group {
flex: 1;
}
.toggle-panel {
position: fixed;
right: 20px;
top: 60px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
padding: 10px;
cursor: pointer;
font-size: 14px;
z-index: 1001;
}
.api-panel.hidden {
display: none;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
border-radius: 4px;
color: white;
font-size: 14px;
z-index: 1002;
animation: slideIn 0.3s ease;
}
.notification.success {
background-color: #28a745;
}
.notification.error {
background-color: #dc3545;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.element-count {
font-size: 14px;
color: #666;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 16px;
color: #666;
}
.loading-content {
text-align: center;
}
.loading-content div:first-child {
margin-bottom: 10px;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.jsx"></script>
</body>
</html>
+208
View File
@@ -0,0 +1,208 @@
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)
const websocketRef = useRef(null)
// WebSocket connection
useEffect(() => {
connectWebSocket()
return () => {
if (websocketRef.current) {
websocketRef.current.close()
}
}
}, [])
// Load existing elements when Excalidraw API becomes available
useEffect(() => {
if (excalidrawAPI) {
loadExistingElements()
// Ensure WebSocket is connected for real-time updates
if (!isConnected) {
connectWebSocket()
}
}
}, [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) {
const cleanedElements = result.elements.map(cleanElementForExcalidraw)
const convertedElements = convertToExcalidrawElements(cleanedElements)
excalidrawAPI.updateScene({ elements: convertedElements })
}
} catch (error) {
console.error('Error loading existing elements:', error)
}
}
const connectWebSocket = () => {
if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) {
return
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${protocol}//${window.location.host}`
websocketRef.current = new WebSocket(wsUrl)
websocketRef.current.onopen = () => {
setIsConnected(true)
if (excalidrawAPI) {
setTimeout(loadExistingElements, 100)
}
}
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) => {
setIsConnected(false)
// Reconnect after 3 seconds if not a clean close
if (event.code !== 1000) {
setTimeout(connectWebSocket, 3000)
}
}
websocketRef.current.onerror = (error) => {
console.error('WebSocket error:', error)
setIsConnected(false)
}
}
const handleWebSocketMessage = (data) => {
if (!excalidrawAPI) {
return
}
try {
const currentElements = excalidrawAPI.getSceneElements()
switch (data.type) {
case 'initial_elements':
if (data.elements && data.elements.length > 0) {
const cleanedElements = data.elements.map(cleanElementForExcalidraw)
const convertedElements = convertToExcalidrawElements(cleanedElements)
excalidrawAPI.updateScene({ elements: convertedElements })
}
break
case 'element_created':
const cleanedNewElement = cleanElementForExcalidraw(data.element)
const newElement = convertToExcalidrawElements([cleanedNewElement])
const updatedElementsAfterCreate = [...currentElements, ...newElement]
excalidrawAPI.updateScene({ elements: updatedElementsAfterCreate })
break
case 'element_updated':
const cleanedUpdatedElement = cleanElementForExcalidraw(data.element)
const convertedUpdatedElement = convertToExcalidrawElements([cleanedUpdatedElement])[0]
const updatedElements = currentElements.map(el =>
el.id === data.element.id ? convertedUpdatedElement : el
)
excalidrawAPI.updateScene({ elements: updatedElements })
break
case 'element_deleted':
const filteredElements = currentElements.filter(el => el.id !== data.elementId)
excalidrawAPI.updateScene({ elements: filteredElements })
break
case 'elements_batch_created':
const cleanedBatchElements = data.elements.map(cleanElementForExcalidraw)
const batchElements = convertToExcalidrawElements(cleanedBatchElements)
const updatedElementsAfterBatch = [...currentElements, ...batchElements]
excalidrawAPI.updateScene({ elements: updatedElementsAfterBatch })
break
default:
console.log('Unknown WebSocket message type:', data.type)
}
} catch (error) {
console.error('Error processing WebSocket message:', error, data)
}
}
const clearCanvas = async () => {
if (excalidrawAPI) {
try {
// 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) {
const deletePromises = result.elements.map(element =>
fetch(`/api/elements/${element.id}`, { method: 'DELETE' })
)
await Promise.all(deletePromises)
}
// Clear the frontend canvas
excalidrawAPI.updateScene({ elements: [] })
} catch (error) {
console.error('Error clearing canvas:', error)
// Still clear frontend even if backend fails
excalidrawAPI.updateScene({ elements: [] })
}
}
}
return (
<div className="app">
{/* Header */}
<div className="header">
<h1>Excalidraw Canvas</h1>
<div className="controls">
<div className="status">
<div className={`status-dot ${isConnected ? 'status-connected' : 'status-disconnected'}`}></div>
<span>{isConnected ? 'Connected' : 'Disconnected'}</span>
</div>
<button className="btn-secondary" onClick={clearCanvas}>Clear Canvas</button>
</div>
</div>
{/* Canvas Container */}
<div className="canvas-container">
<Excalidraw
excalidrawAPI={(api) => setExcalidrawAPI(api)}
initialData={{
elements: [],
appState: {
theme: 'light',
viewBackgroundColor: '#ffffff'
}
}}
/>
</div>
</div>
)
}
export default App
+9
View File
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>,
)
+48 -17
View File
@@ -1,52 +1,83 @@
{
"name": "excalidraw-mcp",
"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, WebSocket sync, and comprehensive diagram management",
"main": "src/index.js",
"type": "module",
"bin": {
"excalidraw-mcp": "./src/cli.js"
"mcp-excalidraw-server": "src/index.js"
},
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"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) }\""
"canvas": "node src/server.js",
"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"
},
"dependencies": {
"@excalidraw/excalidraw": "^0.18.0",
"@modelcontextprotocol/sdk": "latest",
"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",
"ws": "^8.14.2",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.3"
},
"devDependencies": {
"nodemon": "^3.0.2",
"jest": "^29.7.0",
"eslint": "^8.56.0"
"@types/node": "^20.19.7",
"@vitejs/plugin-react": "^4.6.0",
"concurrently": "^9.2.0",
"typescript": "^5.8.3",
"vite": "^6.3.5"
},
"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/**/*",
"*.d.ts",
"README.md",
"LICENSE"
]
-103
View File
@@ -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> Port to run the server on (default: 3000)
-h, --host <host> Host to bind the server to (default: localhost)
-m, --mode <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);
});
Regular → Executable
+289 -9
View File
@@ -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';
@@ -17,10 +19,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',
@@ -81,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: {
@@ -261,11 +358,62 @@ 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']
}
},
}
}
}
);
// Helper function to convert text property to label format for Excalidraw
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 }
};
}
return element;
}
// Set up request handler for tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
@@ -275,7 +423,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 +434,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
version: 1
};
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(excalidrawElement);
logger.info('Element created via MCP and synced to canvas', {
id: excalidrawElement.id,
type: excalidrawElement.type,
synced: !!canvasElement
});
return {
content: [{ type: 'text', text: JSON.stringify(element, null, 2) }]
content: [{
type: 'text',
text: `Element created successfully!\n\n${JSON.stringify(excalidrawElement, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still created locally)'}`
}]
};
}
@@ -312,10 +476,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
version: existingElement.version + 1
};
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(excalidrawElement);
logger.info('Element updated via MCP and synced to canvas', {
id: excalidrawElement.id,
synced: !!canvasElement
});
return {
content: [{ type: 'text', text: JSON.stringify(updatedElement, null, 2) }]
content: [{
type: 'text',
text: `Element updated successfully!\n\n${JSON.stringify(excalidrawElement, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still updated locally)'}`
}]
};
}
@@ -325,10 +504,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 +672,54 @@ 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
};
// 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)
const canvasElements = await batchCreateElementsOnCanvas(createdElements);
const result = {
success: true,
elements: 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 +920,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 +1015,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;
+390
View File
@@ -0,0 +1,390 @@
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
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();
// 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(),
label: z.object({
text: z.string()
}).optional(),
fontSize: z.number().optional(),
fontFamily: z.string().optional()
});
const UpdateElementSchema = z.object({
id: z.string(),
type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES)).optional(),
x: z.number().optional(),
y: z.number().optional(),
width: z.number().optional(),
height: z.number().optional(),
backgroundColor: z.string().optional(),
strokeColor: z.string().optional(),
strokeWidth: z.number().optional(),
roughness: z.number().optional(),
opacity: z.number().optional(),
text: z.string().optional(),
label: z.object({
text: z.string()
}).optional(),
fontSize: z.number().optional(),
fontFamily: z.string().optional()
});
// API Routes
// Get all elements
app.get('/api/elements', (req, 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 = path.join(__dirname, '../dist/frontend/index.html');
res.sendFile(htmlFile, (err) => {
if (err) {
logger.error('Error serving frontend:', err);
res.status(404).send('Frontend not found. Please run "npm run build" first.');
}
});
});
// Health check endpoint
app.get('/health', (req, 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;
-9
View File
@@ -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);
+1 -2
View File
@@ -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
+38
View File
@@ -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
}
}
+27
View File
@@ -0,0 +1,27 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
outDir: '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,
},
},
},
})