+19
-4
@@ -1,5 +1,20 @@
|
||||
node_modules
|
||||
.env
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
.cursor
|
||||
*.excalidraw
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
public/dist/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Editor artifacts
|
||||
.cursor/
|
||||
.claude/
|
||||
|
||||
# Development artifacts
|
||||
*.excalidraw
|
||||
+19
-3
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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!**
|
||||
|
||||
[](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
|
||||
git clone https://github.com/yctimlin/mcp_excalidraw.git
|
||||
cd mcp_excalidraw
|
||||
npm install
|
||||
```
|
||||
|
||||
If you prefer to install it globally:
|
||||
|
||||
#### **2. Build the Frontend**
|
||||
```bash
|
||||
npm install -g excalidraw-mcp
|
||||
excalidraw-mcp
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Options
|
||||
#### **3. Start the System**
|
||||
|
||||
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:
|
||||
##### **Option A: Production Mode (Recommended)**
|
||||
```bash
|
||||
npx excalidraw-mcp
|
||||
# Start canvas server (serves frontend + API)
|
||||
npm run canvas
|
||||
```
|
||||
|
||||
Enable debug logging:
|
||||
##### **Option B: Development Mode**
|
||||
```bash
|
||||
npx excalidraw-mcp --debug
|
||||
# Start both canvas server and Vite dev server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Features
|
||||
#### **4. Access the Canvas**
|
||||
Open your browser and navigate to:
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
- **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
|
||||
### **🔧 Alternative Installation Methods (In Development)**
|
||||
|
||||
## API Tools Reference
|
||||
#### **NPM Package (Beta)**
|
||||
```bash
|
||||
# Currently debugging tool registration - feedback welcome!
|
||||
npm install -g mcp-excalidraw-server
|
||||
npx mcp-excalidraw-server
|
||||
```
|
||||
|
||||
### Element Creation and Modification
|
||||
#### **Docker Version (Coming Soon)**
|
||||
```bash
|
||||
# Canvas sync improvements in progress
|
||||
docker run -p 3000:3000 mcp-excalidraw-server
|
||||
```
|
||||
|
||||
* **create_element**
|
||||
* Create a new Excalidraw element (rectangle, ellipse, diamond, etc.)
|
||||
* Required inputs: `type`, `x`, `y` coordinates
|
||||
* Optional inputs: dimensions, colors, styling properties
|
||||
## 🔧 Available Scripts
|
||||
|
||||
* **update_element**
|
||||
* Update an existing Excalidraw element by ID
|
||||
* Required input: `id` of the element to update
|
||||
* Optional inputs: any element property to modify
|
||||
| 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 |
|
||||
|
||||
* **delete_element**
|
||||
* Delete an Excalidraw element
|
||||
* Required input: `id` of the element to delete
|
||||
## 🎯 Usage Guide
|
||||
|
||||
* **query_elements**
|
||||
* Query elements with optional filtering
|
||||
* Optional inputs: `type` to filter by element type, `filter` object with key-value pairs
|
||||
### **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
|
||||
|
||||
### Resource Management
|
||||
### **For AI Agents (via MCP)**
|
||||
The MCP server provides these tools for creating visual diagrams:
|
||||
|
||||
* **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
|
||||
#### **Basic Element Creation**
|
||||
```javascript
|
||||
// Create a rectangle
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp_excalidraw": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "excalidraw-mcp"]
|
||||
}
|
||||
}
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 100,
|
||||
"backgroundColor": "#e3f2fd",
|
||||
"strokeColor": "#1976d2",
|
||||
"strokeWidth": 2
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
#### **Create Text Elements**
|
||||
```javascript
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp_excalidraw": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "excalidraw-mcp"]
|
||||
}
|
||||
}
|
||||
"type": "text",
|
||||
"x": 150,
|
||||
"y": 125,
|
||||
"text": "Process Step",
|
||||
"fontSize": 16,
|
||||
"strokeColor": "#333333"
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Cursor
|
||||
|
||||
To use this server with Cursor, create a `.cursor/mcp.json` file in your workspace with the following configuration:
|
||||
|
||||
```json
|
||||
#### **Create Arrows & Lines**
|
||||
```javascript
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp_excalidraw": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "excalidraw-mcp"]
|
||||
}
|
||||
}
|
||||
"type": "arrow",
|
||||
"x": 300,
|
||||
"y": 130,
|
||||
"width": 100,
|
||||
"height": 0,
|
||||
"strokeColor": "#666666",
|
||||
"strokeWidth": 2
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
#### **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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Docker Integration
|
||||
## 🔌 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
|
||||
{
|
||||
"mcpServers": {
|
||||
"excalidraw": {
|
||||
"command": "node",
|
||||
"args": ["/absolute/path/to/mcp_excalidraw/src/index.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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"],
|
||||
"env": {
|
||||
"LOG_LEVEL": "info",
|
||||
"DEBUG": "false"
|
||||
"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**
|
||||
|
||||
For VS Code MCP extension, add to your settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"excalidraw": {
|
||||
"command": "node",
|
||||
"args": ["/absolute/path/to/mcp_excalidraw/src/index.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Installation Guide
|
||||
## 🛠️ Environment Variables
|
||||
|
||||
### NPM Installation
|
||||
| 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 |
|
||||
|
||||
```bash
|
||||
# Install globally
|
||||
npm install -g excalidraw-mcp
|
||||
## 📊 API Endpoints
|
||||
|
||||
# Run the server
|
||||
excalidraw-mcp
|
||||
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
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
### Local Development Setup
|
||||
## 🔮 Development Roadmap
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
cd excalidraw-mcp
|
||||
- **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
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
## 🤝 Contributing
|
||||
|
||||
# Start the server
|
||||
npm start
|
||||
```
|
||||
We welcome contributions! If you're experiencing issues with the NPM package or Docker version, please:
|
||||
|
||||
### Docker Installation
|
||||
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
|
||||
|
||||
```bash
|
||||
# Build the Docker image
|
||||
docker build -t mcp/excalidraw .
|
||||
## 📝 License
|
||||
|
||||
# Run the container
|
||||
docker run -i --rm mcp/excalidraw
|
||||
```
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Configuration Options
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
The server can be configured using the following environment variables:
|
||||
|
||||
- `LOG_LEVEL` - Set the logging level (default: "info")
|
||||
- `DEBUG` - Enable debug mode (default: "false")
|
||||
- `DEFAULT_THEME` - Set the default theme (default: "light")
|
||||
|
||||
## Usage Examples
|
||||
|
||||
Here are some practical examples of how to use the Excalidraw MCP server:
|
||||
|
||||
### Creating a Rectangle Element
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 100,
|
||||
"backgroundColor": "#ffffff",
|
||||
"strokeColor": "#000000",
|
||||
"strokeWidth": 2,
|
||||
"roughness": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Querying Specific Elements
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "rectangle",
|
||||
"filter": {
|
||||
"strokeColor": "#000000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Grouping Multiple Elements
|
||||
|
||||
```json
|
||||
{
|
||||
"elementIds": ["elem1", "elem2", "elem3"]
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
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.
|
||||
|
||||
## Development
|
||||
|
||||
Clone the repository and install dependencies:
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd excalidraw-mcp
|
||||
npm install
|
||||
```
|
||||
|
||||
Start the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
- **Excalidraw Team** - For the amazing drawing library
|
||||
- **MCP Community** - For the Model Context Protocol specification
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>,
|
||||
)
|
||||
+49
-18
@@ -1,53 +1,84 @@
|
||||
{
|
||||
"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
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user