#
tokens: 32467/50000 19/19 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .dockerignore
├── .github
│   └── workflows
│       ├── ci.yml
│       ├── docker.yml
│       └── npm-publish.yml
├── .gitignore
├── claude_desktop_config.json
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.canvas
├── frontend
│   ├── index.html
│   └── src
│       ├── App.tsx
│       ├── main.tsx
│       └── utils
│           └── mermaidConverter.ts
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── index.ts
│   ├── server.ts
│   ├── types.ts
│   └── utils
│       └── logger.ts
├── tsconfig.json
└── vite.config.js
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | 
 4 | # Build artifacts
 5 | dist/
 6 | public/dist/
 7 | 
 8 | # Environment files
 9 | .env
10 | 
11 | # Logs
12 | *.log
13 | 
14 | # Editor artifacts
15 | .cursor/
16 | .claude/
17 | 
18 | # Development artifacts
19 | *.excalidraw
20 | 
21 | docs/
```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules
 3 | npm-debug.log*
 4 | yarn-debug.log*
 5 | yarn-error.log*
 6 | 
 7 | # Build outputs
 8 | dist
 9 | build
10 | *.log
11 | 
12 | # Git
13 | .git
14 | .gitignore
15 | .gitattributes
16 | 
17 | # IDE
18 | .vscode
19 | .idea
20 | *.swp
21 | *.swo
22 | *~
23 | 
24 | # OS
25 | .DS_Store
26 | Thumbs.db
27 | 
28 | # Testing
29 | coverage
30 | .nyc_output
31 | *.test.ts
32 | *.spec.ts
33 | 
34 | # CI/CD
35 | .github
36 | 
37 | # Documentation
38 | *.md
39 | !README.md
40 | docs
41 | 
42 | # Docker
43 | Dockerfile*
44 | docker-compose*.yml
45 | .dockerignore
46 | 
47 | # Environment
48 | .env
49 | .env.local
50 | .env.*.local
51 | 
52 | # Misc
53 | tmp
54 | temp
55 | *.tmp
56 | 
```

--------------------------------------------------------------------------------
/claude_desktop_config.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "mcpServers": {
3 |     "mcp_excalidraw": {
4 |       "command": "npx",
5 |       "args": ["-y", "excalidraw-mcp"]
6 |     }
7 |   }
8 | }
```

--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | import React from 'react'
 2 | import ReactDOM from 'react-dom/client'
 3 | import App from './App.tsx'
 4 | import '@excalidraw/excalidraw/index.css'
 5 | 
 6 | const rootElement = document.getElementById('root');
 7 | if (!rootElement) {
 8 |   throw new Error('Root element not found');
 9 | }
10 | 
11 | ReactDOM.createRoot(rootElement).render(
12 |   <React.StrictMode>
13 |     <App />
14 |   </React.StrictMode>,
15 | )
```

--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { defineConfig } from 'vite'
 2 | import react from '@vitejs/plugin-react'
 3 | 
 4 | export default defineConfig({
 5 |   root: 'frontend',
 6 |   plugins: [react()],
 7 |   build: {
 8 |     outDir: '../dist/frontend',
 9 |     emptyOutDir: true,
10 |   },
11 |   server: {
12 |     port: 5173,
13 |     proxy: {
14 |       '/api': {
15 |         target: 'http://localhost:3000',
16 |         changeOrigin: true,
17 |       },
18 |       '/health': {
19 |         target: 'http://localhost:3000',
20 |         changeOrigin: true,
21 |       },
22 |     },
23 |   },
24 | }) 
```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import winston from 'winston';
 2 | 
 3 | const LOG_FILE_PATH = process.env.LOG_FILE_PATH || 'excalidraw.log';
 4 | 
 5 | const logger: winston.Logger = winston.createLogger({
 6 |   level: process.env.LOG_LEVEL || 'info',
 7 | 
 8 |   format: winston.format.combine(
 9 |     winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
10 |     winston.format.uncolorize(),
11 |     winston.format.metadata({ fillExcept: ['message', 'level', 'timestamp'] }),
12 |     winston.format.printf(info => {
13 |       const extra = info.metadata && Object.keys(info.metadata).length
14 |         ? ` ${JSON.stringify(info.metadata)}`
15 |         : '';
16 |       return `${info.timestamp} [${info.level}] ${info.message}${extra}`
17 |     })
18 |   ),
19 | 
20 |   transports: [
21 |     new winston.transports.Console({
22 |       level: 'warn',                 // only warn+error to stderr
23 |       stderrLevels: ['warn','error']
24 |     }),
25 | 
26 |     new winston.transports.File({
27 |       filename: LOG_FILE_PATH,    // all levels to file
28 |       level: 'debug'
29 |     })
30 |   ]
31 | });
32 | 
33 | export default logger;
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "ESNext",
 5 |     "moduleResolution": "node",
 6 |     "allowJs": false,
 7 |     "checkJs": false,
 8 |     "declaration": true,
 9 |     "declarationMap": true,
10 |     "sourceMap": true,
11 |     "outDir": "./dist",
12 |     "rootDir": "./src",
13 |     "strict": true,
14 |     "noImplicitAny": true,
15 |     "strictNullChecks": true,
16 |     "strictFunctionTypes": true,
17 |     "strictBindCallApply": true,
18 |     "strictPropertyInitialization": true,
19 |     "noImplicitThis": true,
20 |     "noImplicitReturns": false,
21 |     "noFallthroughCasesInSwitch": true,
22 |     "noUncheckedIndexedAccess": true,
23 |     "noImplicitOverride": true,
24 |     "esModuleInterop": true,
25 |     "allowSyntheticDefaultImports": true,
26 |     "forceConsistentCasingInFileNames": true,
27 |     "skipLibCheck": true,
28 |     "resolveJsonModule": true,
29 |     "isolatedModules": true,
30 |     "noEmitOnError": false,
31 |     "preserveConstEnums": true,
32 |     "removeComments": false,
33 |     "types": ["node"]
34 |   },
35 |   "include": [
36 |     "src/**/*.ts"
37 |   ],
38 |   "exclude": [
39 |     "node_modules",
40 |     "dist",
41 |     "frontend",
42 |     "**/*.test.ts",
43 |     "**/*.spec.ts",
44 |     "**/*.js",
45 |     "**/*.jsx"
46 |   ],
47 |   "ts-node": {
48 |     "esm": true
49 |   }
50 | } 
```

--------------------------------------------------------------------------------
/frontend/src/utils/mermaidConverter.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { parseMermaidToExcalidraw, MermaidConfig } from '@excalidraw/mermaid-to-excalidraw';
 2 | import type { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types';
 3 | import type { BinaryFiles } from '@excalidraw/excalidraw/types/types';
 4 | 
 5 | export interface MermaidConversionResult {
 6 |   elements: readonly ExcalidrawElement[];
 7 |   files?: BinaryFiles;
 8 |   error?: string;
 9 | }
10 | 
11 | /**
12 |  * Converts a Mermaid diagram definition to Excalidraw elements
13 |  * This function needs to run in the browser context as it requires DOM access
14 |  */
15 | export const convertMermaidToExcalidraw = async (
16 |   mermaidDefinition: string,
17 |   config?: MermaidConfig
18 | ): Promise<MermaidConversionResult> => {
19 |   try {
20 |     // Parse the Mermaid diagram to Excalidraw elements
21 |     const result = await parseMermaidToExcalidraw(mermaidDefinition, config);
22 |     
23 |     return {
24 |       elements: result.elements,
25 |       files: result.files,
26 |     };
27 |   } catch (error) {
28 |     console.error('Error converting Mermaid to Excalidraw:', error);
29 |     return {
30 |       elements: [],
31 |       error: error instanceof Error ? error.message : String(error),
32 |     };
33 |   }
34 | };
35 | 
36 | /**
37 |  * Default Mermaid configuration for Excalidraw conversion
38 |  */
39 | export const DEFAULT_MERMAID_CONFIG: MermaidConfig = {
40 |   startOnLoad: false,
41 |   flowchart: {
42 |     curve: 'linear',
43 |   },
44 |   themeVariables: {
45 |     fontSize: '20px',
46 |   },
47 |   maxEdges: 500,
48 |   maxTextSize: 50000,
49 | };
50 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Dockerfile for MCP Excalidraw Server
 2 | # This builds the MCP server only (core product for CI/CD and GHCR)
 3 | # The canvas server is optional and runs separately
 4 | 
 5 | # Stage 1: Build backend (TypeScript compilation)
 6 | FROM node:18-slim AS builder
 7 | 
 8 | WORKDIR /app
 9 | 
10 | # Copy package files
11 | COPY package*.json ./
12 | 
13 | # Install all dependencies (including TypeScript compiler)
14 | RUN npm ci && npm cache clean --force
15 | 
16 | # Copy backend source
17 | COPY src ./src
18 | COPY tsconfig.json ./
19 | 
20 | # Compile TypeScript
21 | RUN npm run build:server
22 | 
23 | # Stage 2: Production MCP Server
24 | FROM node:18-slim AS production
25 | 
26 | # Create non-root user for security
27 | RUN addgroup --system --gid 1001 nodejs && \
28 |     adduser --system --uid 1001 --gid 1001 nodejs
29 | 
30 | WORKDIR /app
31 | 
32 | # Copy package files
33 | COPY package*.json ./
34 | 
35 | # Install only production dependencies
36 | RUN npm ci --only=production && npm cache clean --force
37 | 
38 | # Copy compiled backend (MCP server only)
39 | COPY --from=builder /app/dist ./dist
40 | 
41 | # Set ownership to nodejs user
42 | RUN chown -R nodejs:nodejs /app
43 | 
44 | # Switch to non-root user
45 | USER nodejs
46 | 
47 | # Set environment variables with defaults
48 | ENV NODE_ENV=production
49 | ENV EXPRESS_SERVER_URL=http://localhost:3000
50 | ENV ENABLE_CANVAS_SYNC=true
51 | 
52 | # Run MCP server (stdin/stdout protocol)
53 | CMD ["node", "dist/index.js"]
54 | 
55 | # Labels for metadata
56 | LABEL org.opencontainers.image.source="https://github.com/yctimlin/mcp_excalidraw"
57 | LABEL org.opencontainers.image.description="MCP Excalidraw Server - Model Context Protocol for AI agents"
58 | LABEL org.opencontainers.image.licenses="MIT"
59 | 
```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
 1 | version: '3.8'
 2 | 
 3 | # Docker Compose for MCP Excalidraw
 4 | #
 5 | # Usage scenarios:
 6 | # 1. Canvas only:       docker-compose up canvas
 7 | # 2. MCP only:          docker-compose up mcp (requires canvas running elsewhere)
 8 | # 3. Both:              docker-compose --profile full up
 9 | #
10 | # Most common: Run canvas locally, MCP via Claude Desktop config
11 | 
12 | services:
13 |   # Canvas server (optional) - Visual UI and REST API
14 |   canvas:
15 |     build:
16 |       context: .
17 |       dockerfile: Dockerfile.canvas
18 |     image: mcp-excalidraw-canvas:latest
19 |     container_name: mcp-excalidraw-canvas
20 |     ports:
21 |       - "3000:3000"
22 |     environment:
23 |       - NODE_ENV=production
24 |       - PORT=3000
25 |       - HOST=0.0.0.0
26 |       - DEBUG=false
27 |     restart: unless-stopped
28 |     healthcheck:
29 |       test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
30 |       interval: 30s
31 |       timeout: 10s
32 |       retries: 3
33 |       start_period: 40s
34 |     networks:
35 |       - mcp-network
36 | 
37 |   # MCP server - Core product (typically run via Claude Desktop, not docker-compose)
38 |   # This is here for testing or special deployment scenarios
39 |   mcp:
40 |     build:
41 |       context: .
42 |       dockerfile: Dockerfile
43 |     image: mcp-excalidraw:latest
44 |     container_name: mcp-excalidraw-mcp
45 |     stdin_open: true
46 |     tty: true
47 |     environment:
48 |       - NODE_ENV=production
49 |       - EXPRESS_SERVER_URL=http://canvas:3000
50 |       - ENABLE_CANVAS_SYNC=true
51 |       - DEBUG=false
52 |     depends_on:
53 |       canvas:
54 |         condition: service_healthy
55 |     networks:
56 |       - mcp-network
57 |     profiles:
58 |       - full
59 | 
60 | networks:
61 |   mcp-network:
62 |     driver: bridge
63 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main, develop ]
 6 |   pull_request:
 7 |     branches: [ main, develop ]
 8 | 
 9 | jobs:
10 |   build-and-test:
11 |     name: Build and Type Check
12 |     runs-on: ubuntu-latest
13 | 
14 |     strategy:
15 |       matrix:
16 |         node-version: [18.x, 20.x, 22.x]
17 | 
18 |     steps:
19 |       - name: Checkout code
20 |         uses: actions/checkout@v4
21 | 
22 |       - name: Setup Node.js ${{ matrix.node-version }}
23 |         uses: actions/setup-node@v4
24 |         with:
25 |           node-version: ${{ matrix.node-version }}
26 |           cache: 'npm'
27 | 
28 |       - name: Install dependencies
29 |         run: npm ci
30 | 
31 |       - name: Run TypeScript type check
32 |         run: npm run type-check
33 | 
34 |       - name: Build project
35 |         run: npm run build
36 | 
37 |       - name: Check build artifacts
38 |         run: |
39 |           echo "Checking if build artifacts exist..."
40 |           test -f dist/index.js || (echo "dist/index.js not found" && exit 1)
41 |           test -f dist/server.js || (echo "dist/server.js not found" && exit 1)
42 |           test -d dist/frontend || (echo "dist/frontend not found" && exit 1)
43 |           echo "All build artifacts present!"
44 | 
45 |       - name: Upload build artifacts
46 |         if: matrix.node-version == '20.x'
47 |         uses: actions/upload-artifact@v4
48 |         with:
49 |           name: build-artifacts
50 |           path: |
51 |             dist/
52 |           retention-days: 7
53 | 
54 |   lint-check:
55 |     name: Lint Check
56 |     runs-on: ubuntu-latest
57 | 
58 |     steps:
59 |       - name: Checkout code
60 |         uses: actions/checkout@v4
61 | 
62 |       - name: Setup Node.js
63 |         uses: actions/setup-node@v4
64 |         with:
65 |           node-version: '20.x'
66 |           cache: 'npm'
67 | 
68 |       - name: Install dependencies
69 |         run: npm ci
70 | 
71 |       - name: Check for TypeScript errors
72 |         run: npm run type-check
73 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "mcp-excalidraw-server",
 3 |   "version": "1.0.2",
 4 |   "description": "Advanced MCP server for Excalidraw with real-time canvas, WebSocket sync, and comprehensive diagram management",
 5 |   "main": "dist/index.js",
 6 |   "type": "module",
 7 |   "bin": {
 8 |     "mcp-excalidraw-server": "dist/index.js"
 9 |   },
10 |   "scripts": {
11 |     "start": "npm run build:server && node dist/index.js",
12 |     "canvas": "npm run build:server && node dist/server.js",
13 |     "build": "npm run build:frontend && npm run build:server",
14 |     "build:frontend": "vite build",
15 |     "build:server": "npx tsc",
16 |     "build:types": "npx tsc --emitDeclarationOnly",
17 |     "dev": "concurrently \"npm run dev:server\" \"vite\"",
18 |     "dev:server": "npx tsc --watch",
19 |     "production": "npm run build && npm run canvas",
20 |     "prepublishOnly": "npm run build",
21 |     "type-check": "npx tsc --noEmit"
22 |   },
23 |   "dependencies": {
24 |     "@excalidraw/excalidraw": "^0.18.0",
25 |     "@excalidraw/mermaid-to-excalidraw": "^1.1.3",
26 |     "@modelcontextprotocol/sdk": "latest",
27 |     "cors": "^2.8.5",
28 |     "dotenv": "^16.3.1",
29 |     "express": "^4.18.2",
30 |     "mermaid": "^11.12.1",
31 |     "node-fetch": "^3.3.2",
32 |     "react": "^18.3.1",
33 |     "react-dom": "^18.3.1",
34 |     "winston": "^3.11.0",
35 |     "ws": "^8.14.2",
36 |     "zod": "^3.22.4",
37 |     "zod-to-json-schema": "^3.22.3"
38 |   },
39 |   "devDependencies": {
40 |     "@types/cors": "^2.8.17",
41 |     "@types/express": "^4.17.21",
42 |     "@types/node": "^20.19.7",
43 |     "@types/react": "^18.3.3",
44 |     "@types/react-dom": "^18.3.0",
45 |     "@types/ws": "^8.5.10",
46 |     "@vitejs/plugin-react": "^4.6.0",
47 |     "concurrently": "^9.2.0",
48 |     "typescript": "^5.8.3",
49 |     "vite": "^6.3.5"
50 |   },
51 |   "keywords": [
52 |     "mcp",
53 |     "mcp-server",
54 |     "excalidraw",
55 |     "model-context-protocol",
56 |     "ai",
57 |     "drawing",
58 |     "diagrams",
59 |     "canvas",
60 |     "real-time",
61 |     "websocket",
62 |     "visualization",
63 |     "claude",
64 |     "ai-tools"
65 |   ],
66 |   "author": {
67 |     "name": "yctimlin",
68 |     "email": "[email protected]"
69 |   },
70 |   "license": "MIT",
71 |   "repository": {
72 |     "type": "git",
73 |     "url": "https://github.com/yctimlin/mcp_excalidraw.git"
74 |   },
75 |   "homepage": "https://github.com/yctimlin/mcp_excalidraw#readme",
76 |   "bugs": {
77 |     "url": "https://github.com/yctimlin/mcp_excalidraw/issues"
78 |   },
79 |   "engines": {
80 |     "node": ">=18.0.0"
81 |   },
82 |   "publishConfig": {
83 |     "access": "public",
84 |     "registry": "https://registry.npmjs.org/"
85 |   },
86 |   "files": [
87 |     "src/**/*",
88 |     "dist/**/*",
89 |     "*.d.ts",
90 |     "README.md",
91 |     "LICENSE"
92 |   ]
93 | }
94 | 
```

--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------

```yaml
  1 | name: Publish to NPM
  2 | 
  3 | on:
  4 |   release:
  5 |     types: [published]
  6 |   workflow_dispatch:
  7 |     inputs:
  8 |       tag:
  9 |         description: 'Tag to publish (e.g., latest, beta, next)'
 10 |         required: true
 11 |         default: 'latest'
 12 | 
 13 | jobs:
 14 |   publish:
 15 |     name: Publish to NPM Registry
 16 |     runs-on: ubuntu-latest
 17 |     permissions:
 18 |       contents: read
 19 |       id-token: write
 20 | 
 21 |     steps:
 22 |       - name: Checkout code
 23 |         uses: actions/checkout@v4
 24 | 
 25 |       - name: Setup Node.js
 26 |         uses: actions/setup-node@v4
 27 |         with:
 28 |           node-version: '20.x'
 29 |           registry-url: 'https://registry.npmjs.org'
 30 |           cache: 'npm'
 31 | 
 32 |       - name: Install dependencies
 33 |         run: npm ci
 34 | 
 35 |       - name: Run type check
 36 |         run: npm run type-check
 37 | 
 38 |       - name: Build project
 39 |         run: npm run build
 40 | 
 41 |       - name: Verify build artifacts
 42 |         run: |
 43 |           echo "Verifying build artifacts..."
 44 |           test -f dist/index.js || (echo "ERROR: dist/index.js not found" && exit 1)
 45 |           test -f dist/server.js || (echo "ERROR: dist/server.js not found" && exit 1)
 46 |           test -d dist/frontend || (echo "ERROR: dist/frontend not found" && exit 1)
 47 |           echo "All required artifacts present!"
 48 | 
 49 |       - name: Get package version
 50 |         id: package-version
 51 |         run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
 52 | 
 53 |       - name: Check if version exists on NPM
 54 |         id: check-version
 55 |         run: |
 56 |           if npm view mcp-excalidraw-server@${{ steps.package-version.outputs.version }} version 2>/dev/null; then
 57 |             echo "exists=true" >> $GITHUB_OUTPUT
 58 |             echo "Version ${{ steps.package-version.outputs.version }} already exists on NPM"
 59 |           else
 60 |             echo "exists=false" >> $GITHUB_OUTPUT
 61 |             echo "Version ${{ steps.package-version.outputs.version }} does not exist on NPM"
 62 |           fi
 63 | 
 64 |       - name: Publish to NPM (Release)
 65 |         if: github.event_name == 'release' && steps.check-version.outputs.exists == 'false'
 66 |         run: npm publish --provenance --access public
 67 |         env:
 68 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
 69 | 
 70 |       - name: Publish to NPM (Manual)
 71 |         if: github.event_name == 'workflow_dispatch' && steps.check-version.outputs.exists == 'false'
 72 |         run: npm publish --tag ${{ github.event.inputs.tag }} --provenance --access public
 73 |         env:
 74 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
 75 | 
 76 |       - name: Skip publishing (version exists)
 77 |         if: steps.check-version.outputs.exists == 'true'
 78 |         run: |
 79 |           echo "⚠️  Skipping publish - version ${{ steps.package-version.outputs.version }} already exists on NPM"
 80 |           echo "Please bump the version in package.json before publishing"
 81 | 
 82 |       - name: Create GitHub Release Assets
 83 |         if: github.event_name == 'release'
 84 |         run: |
 85 |           tar -czf mcp-excalidraw-server-${{ steps.package-version.outputs.version }}.tar.gz dist/
 86 | 
 87 |       - name: Upload Release Assets
 88 |         if: github.event_name == 'release'
 89 |         uses: softprops/action-gh-release@v1
 90 |         with:
 91 |           files: |
 92 |             mcp-excalidraw-server-${{ steps.package-version.outputs.version }}.tar.gz
 93 |         env:
 94 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 95 | 
 96 |   notify:
 97 |     name: Publish Notification
 98 |     needs: publish
 99 |     runs-on: ubuntu-latest
100 |     if: success()
101 | 
102 |     steps:
103 |       - name: Success notification
104 |         run: |
105 |           echo "✅ Package successfully published to NPM!"
106 |           echo "View at: https://www.npmjs.com/package/mcp-excalidraw-server"
107 | 
```

--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------

```yaml
  1 | name: Docker Build & Push
  2 | 
  3 | on:
  4 |   push:
  5 |     branches: [ main ]
  6 |     tags:
  7 |       - 'v*.*.*'
  8 |   pull_request:
  9 |     branches: [ main ]
 10 |   workflow_dispatch:
 11 | 
 12 | env:
 13 |   REGISTRY: ghcr.io
 14 |   IMAGE_NAME_MCP: ${{ github.repository }}
 15 |   IMAGE_NAME_CANVAS: ${{ github.repository }}-canvas
 16 | 
 17 | jobs:
 18 |   build-and-push-mcp:
 19 |     name: Build and Push MCP Server Image
 20 |     runs-on: ubuntu-latest
 21 |     permissions:
 22 |       contents: read
 23 |       packages: write
 24 | 
 25 |     steps:
 26 |       - name: Checkout code
 27 |         uses: actions/checkout@v4
 28 | 
 29 |       - name: Set up Docker Buildx
 30 |         uses: docker/setup-buildx-action@v3
 31 | 
 32 |       - name: Log in to GitHub Container Registry
 33 |         uses: docker/login-action@v3
 34 |         with:
 35 |           registry: ${{ env.REGISTRY }}
 36 |           username: ${{ github.actor }}
 37 |           password: ${{ secrets.GITHUB_TOKEN }}
 38 | 
 39 |       - name: Extract metadata for MCP Server
 40 |         id: meta
 41 |         uses: docker/metadata-action@v5
 42 |         with:
 43 |           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_MCP }}
 44 |           tags: |
 45 |             type=ref,event=branch
 46 |             type=ref,event=pr
 47 |             type=semver,pattern={{version}}
 48 |             type=semver,pattern={{major}}.{{minor}}
 49 |             type=semver,pattern={{major}}
 50 |             type=sha,prefix=sha-
 51 |             type=raw,value=latest,enable={{is_default_branch}}
 52 | 
 53 |       - name: Build and push MCP Server image
 54 |         uses: docker/build-push-action@v5
 55 |         with:
 56 |           context: .
 57 |           file: ./Dockerfile
 58 |           push: true
 59 |           tags: ${{ steps.meta.outputs.tags }}
 60 |           labels: ${{ steps.meta.outputs.labels }}
 61 |           cache-from: type=gha
 62 |           cache-to: type=gha,mode=max
 63 |           platforms: linux/amd64,linux/arm64
 64 | 
 65 |   build-and-push-canvas:
 66 |     name: Build and Push Canvas Server Image
 67 |     runs-on: ubuntu-latest
 68 |     permissions:
 69 |       contents: read
 70 |       packages: write
 71 | 
 72 |     steps:
 73 |       - name: Checkout code
 74 |         uses: actions/checkout@v4
 75 | 
 76 |       - name: Set up Docker Buildx
 77 |         uses: docker/setup-buildx-action@v3
 78 | 
 79 |       - name: Log in to GitHub Container Registry
 80 |         uses: docker/login-action@v3
 81 |         with:
 82 |           registry: ${{ env.REGISTRY }}
 83 |           username: ${{ github.actor }}
 84 |           password: ${{ secrets.GITHUB_TOKEN }}
 85 | 
 86 |       - name: Extract metadata for Canvas Server
 87 |         id: meta
 88 |         uses: docker/metadata-action@v5
 89 |         with:
 90 |           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_CANVAS }}
 91 |           tags: |
 92 |             type=ref,event=branch
 93 |             type=ref,event=pr
 94 |             type=semver,pattern={{version}}
 95 |             type=semver,pattern={{major}}.{{minor}}
 96 |             type=semver,pattern={{major}}
 97 |             type=sha,prefix=sha-
 98 |             type=raw,value=latest,enable={{is_default_branch}}
 99 | 
100 |       - name: Build and push Canvas Server image
101 |         uses: docker/build-push-action@v5
102 |         with:
103 |           context: .
104 |           file: ./Dockerfile.canvas
105 |           push: true
106 |           tags: ${{ steps.meta.outputs.tags }}
107 |           labels: ${{ steps.meta.outputs.labels }}
108 |           cache-from: type=gha
109 |           cache-to: type=gha,mode=max
110 |           platforms: linux/amd64,linux/arm64
111 | 
112 |   test-docker-images:
113 |     name: Test Docker Images
114 |     needs: [build-and-push-mcp, build-and-push-canvas]
115 |     runs-on: ubuntu-latest
116 | 
117 |     steps:
118 |       - name: Log in to GitHub Container Registry
119 |         uses: docker/login-action@v3
120 |         with:
121 |           registry: ${{ env.REGISTRY }}
122 |           username: ${{ github.actor }}
123 |           password: ${{ secrets.GITHUB_TOKEN }}
124 | 
125 |       - name: Determine image tag
126 |         id: tag
127 |         run: |
128 |           if [[ "${{ github.event_name }}" == "pull_request" ]]; then
129 |             echo "value=pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
130 |           else
131 |             echo "value=${{ github.ref_name }}" >> $GITHUB_OUTPUT
132 |           fi
133 | 
134 |       - name: Test Canvas Server image
135 |         run: |
136 |           docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_CANVAS }}:${{ steps.tag.outputs.value }}
137 |           docker run -d -p 3000:3000 --name test-canvas ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_CANVAS }}:${{ steps.tag.outputs.value }}
138 |           sleep 10
139 |           curl -f http://localhost:3000/health || exit 1
140 |           docker logs test-canvas
141 |           docker stop test-canvas
142 | 
143 |       - name: Test MCP Server image
144 |         run: |
145 |           docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_MCP }}:${{ steps.tag.outputs.value }}
146 |           echo "MCP Server image pulled successfully"
147 | 
```

--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------

```html
  1 | <!DOCTYPE html>
  2 | <html lang="en">
  3 | <head>
  4 |     <meta charset="UTF-8">
  5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6 |     <title>Excalidraw POC - Backend API Integration</title>
  7 |     <style>
  8 |         body {
  9 |             margin: 0;
 10 |             padding: 0;
 11 |             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
 12 |             background-color: #f5f5f5;
 13 |         }
 14 |         
 15 |         .header {
 16 |             background: #fff;
 17 |             box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 18 |             padding: 10px 20px;
 19 |             display: flex;
 20 |             align-items: center;
 21 |             justify-content: space-between;
 22 |             flex-wrap: wrap;
 23 |             gap: 10px;
 24 |         }
 25 |         
 26 |         .header h1 {
 27 |             margin: 0;
 28 |             color: #333;
 29 |             font-size: 24px;
 30 |         }
 31 |         
 32 |         .controls {
 33 |             display: flex;
 34 |             gap: 10px;
 35 |             flex-wrap: wrap;
 36 |         }
 37 |         
 38 |         button {
 39 |             padding: 8px 16px;
 40 |             border: none;
 41 |             border-radius: 4px;
 42 |             cursor: pointer;
 43 |             font-size: 14px;
 44 |             transition: background-color 0.2s;
 45 |         }
 46 |         
 47 |         .btn-primary {
 48 |             background-color: #007bff;
 49 |             color: white;
 50 |         }
 51 |         
 52 |         .btn-primary:hover {
 53 |             background-color: #0056b3;
 54 |         }
 55 |         
 56 |         .btn-secondary {
 57 |             background-color: #6c757d;
 58 |             color: white;
 59 |         }
 60 |         
 61 |         .btn-secondary:hover {
 62 |             background-color: #545b62;
 63 |         }
 64 |         
 65 |         .btn-success {
 66 |             background-color: #28a745;
 67 |             color: white;
 68 |         }
 69 |         
 70 |         .btn-success:hover {
 71 |             background-color: #218838;
 72 |         }
 73 |         
 74 |         .btn-danger {
 75 |             background-color: #dc3545;
 76 |             color: white;
 77 |         }
 78 |         
 79 |         .btn-danger:hover {
 80 |             background-color: #c82333;
 81 |         }
 82 |         
 83 |         .status {
 84 |             display: flex;
 85 |             align-items: center;
 86 |             gap: 8px;
 87 |             font-size: 14px;
 88 |         }
 89 |         
 90 |         .status-dot {
 91 |             width: 8px;
 92 |             height: 8px;
 93 |             border-radius: 50%;
 94 |         }
 95 |         
 96 |         .status-connected {
 97 |             background-color: #28a745;
 98 |         }
 99 |         
100 |         .status-disconnected {
101 |             background-color: #dc3545;
102 |         }
103 |         
104 |         .canvas-container {
105 |             height: calc(100vh - 80px);
106 |             width: 100%;
107 |             position: relative;
108 |         }
109 |         
110 |         .api-panel {
111 |             position: fixed;
112 |             right: 20px;
113 |             top: 100px;
114 |             background: white;
115 |             border-radius: 8px;
116 |             box-shadow: 0 4px 12px rgba(0,0,0,0.15);
117 |             padding: 20px;
118 |             width: 300px;
119 |             max-height: 400px;
120 |             overflow-y: auto;
121 |             z-index: 1000;
122 |         }
123 |         
124 |         .api-panel h3 {
125 |             margin: 0 0 15px 0;
126 |             color: #333;
127 |             font-size: 18px;
128 |         }
129 |         
130 |         .api-form {
131 |             display: flex;
132 |             flex-direction: column;
133 |             gap: 10px;
134 |         }
135 |         
136 |         .form-group {
137 |             display: flex;
138 |             flex-direction: column;
139 |             gap: 5px;
140 |         }
141 |         
142 |         label {
143 |             font-weight: 500;
144 |             color: #555;
145 |             font-size: 14px;
146 |         }
147 |         
148 |         input, select {
149 |             padding: 8px;
150 |             border: 1px solid #ddd;
151 |             border-radius: 4px;
152 |             font-size: 14px;
153 |         }
154 |         
155 |         .form-row {
156 |             display: flex;
157 |             gap: 10px;
158 |         }
159 |         
160 |         .form-row .form-group {
161 |             flex: 1;
162 |         }
163 |         
164 |         .toggle-panel {
165 |             position: fixed;
166 |             right: 20px;
167 |             top: 60px;
168 |             background: #007bff;
169 |             color: white;
170 |             border: none;
171 |             border-radius: 4px;
172 |             padding: 10px;
173 |             cursor: pointer;
174 |             font-size: 14px;
175 |             z-index: 1001;
176 |         }
177 |         
178 |         .api-panel.hidden {
179 |             display: none;
180 |         }
181 |         
182 |         .notification {
183 |             position: fixed;
184 |             top: 20px;
185 |             right: 20px;
186 |             padding: 12px 16px;
187 |             border-radius: 4px;
188 |             color: white;
189 |             font-size: 14px;
190 |             z-index: 1002;
191 |             animation: slideIn 0.3s ease;
192 |         }
193 |         
194 |         .notification.success {
195 |             background-color: #28a745;
196 |         }
197 |         
198 |         .notification.error {
199 |             background-color: #dc3545;
200 |         }
201 |         
202 |         @keyframes slideIn {
203 |             from {
204 |                 transform: translateX(100%);
205 |                 opacity: 0;
206 |             }
207 |             to {
208 |                 transform: translateX(0);
209 |                 opacity: 1;
210 |             }
211 |         }
212 |         
213 |         .element-count {
214 |             font-size: 14px;
215 |             color: #666;
216 |         }
217 |         
218 |         .loading {
219 |             display: flex;
220 |             justify-content: center;
221 |             align-items: center;
222 |             height: 100%;
223 |             font-size: 16px;
224 |             color: #666;
225 |         }
226 |         
227 |         .loading-content {
228 |             text-align: center;
229 |         }
230 |         
231 |         .loading-content div:first-child {
232 |             margin-bottom: 10px;
233 |         }
234 |         
235 |         /* Sync Controls Styles */
236 |         .sync-controls {
237 |             display: flex;
238 |             align-items: center;
239 |             gap: 10px;
240 |         }
241 | 
242 |         .btn-loading {
243 |             position: relative;
244 |         }
245 | 
246 |         .spinner {
247 |             display: inline-block;
248 |             width: 12px;
249 |             height: 12px;
250 |             border: 2px solid #ffffff40;
251 |             border-top: 2px solid #ffffff;
252 |             border-radius: 50%;
253 |             animation: spin 1s linear infinite;
254 |             margin-right: 5px;
255 |         }
256 | 
257 |         @keyframes spin {
258 |             0% { transform: rotate(0deg); }
259 |             100% { transform: rotate(360deg); }
260 |         }
261 | 
262 |         .sync-status {
263 |             font-size: 12px;
264 |             min-width: 100px;
265 |         }
266 | 
267 |         .sync-success {
268 |             color: #4caf50;
269 |         }
270 | 
271 |         .sync-error {
272 |             color: #f44336;
273 |         }
274 | 
275 |         .sync-time {
276 |             color: #666;
277 |         }
278 |     </style>
279 | </head>
280 | <body>
281 |     <div id="root"></div>
282 |     <script type="module" src="./src/main.jsx"></script>
283 | </body>
284 | </html> 
```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | export interface ExcalidrawElementBase {
  2 |   id: string;
  3 |   type: ExcalidrawElementType;
  4 |   x: number;
  5 |   y: number;
  6 |   width?: number;
  7 |   height?: number;
  8 |   angle?: number;
  9 |   strokeColor?: string;
 10 |   backgroundColor?: string;
 11 |   fillStyle?: string;
 12 |   strokeWidth?: number;
 13 |   strokeStyle?: string;
 14 |   roughness?: number;
 15 |   opacity?: number;
 16 |   groupIds?: string[];
 17 |   frameId?: string | null;
 18 |   roundness?: {
 19 |     type: number;
 20 |     value?: number;
 21 |   } | null;
 22 |   seed?: number;
 23 |   versionNonce?: number;
 24 |   isDeleted?: boolean;
 25 |   locked?: boolean;
 26 |   link?: string | null;
 27 |   customData?: Record<string, any> | null;
 28 |   boundElements?: readonly ExcalidrawBoundElement[] | null;
 29 |   updated?: number;
 30 |   containerId?: string | null;
 31 | }
 32 | 
 33 | export interface ExcalidrawTextElement extends ExcalidrawElementBase {
 34 |   type: 'text';
 35 |   text: string;
 36 |   fontSize?: number;
 37 |   fontFamily?: number;
 38 |   textAlign?: string;
 39 |   verticalAlign?: string;
 40 |   baseline?: number;
 41 |   lineHeight?: number;
 42 | }
 43 | 
 44 | export interface ExcalidrawRectangleElement extends ExcalidrawElementBase {
 45 |   type: 'rectangle';
 46 |   width: number;
 47 |   height: number;
 48 | }
 49 | 
 50 | export interface ExcalidrawEllipseElement extends ExcalidrawElementBase {
 51 |   type: 'ellipse';
 52 |   width: number;
 53 |   height: number;
 54 | }
 55 | 
 56 | export interface ExcalidrawDiamondElement extends ExcalidrawElementBase {
 57 |   type: 'diamond';
 58 |   width: number;
 59 |   height: number;
 60 | }
 61 | 
 62 | export interface ExcalidrawArrowElement extends ExcalidrawElementBase {
 63 |   type: 'arrow';
 64 |   points: readonly [number, number][];
 65 |   lastCommittedPoint?: readonly [number, number] | null;
 66 |   startBinding?: ExcalidrawBinding | null;
 67 |   endBinding?: ExcalidrawBinding | null;
 68 |   startArrowhead?: string | null;
 69 |   endArrowhead?: string | null;
 70 | }
 71 | 
 72 | export interface ExcalidrawLineElement extends ExcalidrawElementBase {
 73 |   type: 'line';
 74 |   points: readonly [number, number][];
 75 |   lastCommittedPoint?: readonly [number, number] | null;
 76 |   startBinding?: ExcalidrawBinding | null;
 77 |   endBinding?: ExcalidrawBinding | null;
 78 | }
 79 | 
 80 | export interface ExcalidrawFreedrawElement extends ExcalidrawElementBase {
 81 |   type: 'freedraw';
 82 |   points: readonly [number, number][];
 83 |   pressures?: readonly number[];
 84 |   simulatePressure?: boolean;
 85 |   lastCommittedPoint?: readonly [number, number] | null;
 86 | }
 87 | 
 88 | export type ExcalidrawElement = 
 89 |   | ExcalidrawTextElement
 90 |   | ExcalidrawRectangleElement
 91 |   | ExcalidrawEllipseElement
 92 |   | ExcalidrawDiamondElement
 93 |   | ExcalidrawArrowElement
 94 |   | ExcalidrawLineElement
 95 |   | ExcalidrawFreedrawElement;
 96 | 
 97 | export interface ExcalidrawBoundElement {
 98 |   id: string;
 99 |   type: 'text' | 'arrow';
100 | }
101 | 
102 | export interface ExcalidrawBinding {
103 |   elementId: string;
104 |   focus: number;
105 |   gap: number;
106 |   fixedPoint?: readonly [number, number] | null;
107 | }
108 | 
109 | export type ExcalidrawElementType = 'rectangle' | 'ellipse' | 'diamond' | 'arrow' | 'text' | 'line' | 'freedraw' | 'label';
110 | 
111 | // Excalidraw element types
112 | export const EXCALIDRAW_ELEMENT_TYPES: Record<string, ExcalidrawElementType> = {
113 |   RECTANGLE: 'rectangle',
114 |   ELLIPSE: 'ellipse',
115 |   DIAMOND: 'diamond',
116 |   ARROW: 'arrow',
117 |   TEXT: 'text',
118 |   LABEL: 'label',
119 |   FREEDRAW: 'freedraw',
120 |   LINE: 'line'
121 | } as const;
122 | 
123 | // Server-side element with metadata
124 | export interface ServerElement extends Omit<ExcalidrawElementBase, 'id'> {
125 |   id: string;
126 |   type: ExcalidrawElementType;
127 |   createdAt?: string;
128 |   updatedAt?: string;
129 |   version?: number;
130 |   syncedAt?: string;
131 |   source?: string;
132 |   syncTimestamp?: string;
133 |   text?: string;
134 |   fontSize?: number;
135 |   fontFamily?: string | number;
136 |   label?: {
137 |     text: string;
138 |   };
139 | }
140 | 
141 | // API Response types
142 | export interface ApiResponse<T = any> {
143 |   success: boolean;
144 |   data?: T;
145 |   error?: string;
146 |   message?: string;
147 | }
148 | 
149 | export interface ElementsResponse extends ApiResponse {
150 |   elements: ServerElement[];
151 |   count: number;
152 | }
153 | 
154 | export interface ElementResponse extends ApiResponse {
155 |   element: ServerElement;
156 | }
157 | 
158 | export interface SyncResponse extends ApiResponse {
159 |   count: number;
160 |   syncedAt: string;
161 |   beforeCount: number;
162 |   afterCount: number;
163 | }
164 | 
165 | // WebSocket message types
166 | export interface WebSocketMessage {
167 |   type: WebSocketMessageType;
168 |   [key: string]: any;
169 | }
170 | 
171 | export type WebSocketMessageType = 
172 |   | 'initial_elements'
173 |   | 'element_created'
174 |   | 'element_updated'
175 |   | 'element_deleted'
176 |   | 'elements_batch_created'
177 |   | 'elements_synced'
178 |   | 'sync_status'
179 |   | 'mermaid_convert';
180 | 
181 | export interface InitialElementsMessage extends WebSocketMessage {
182 |   type: 'initial_elements';
183 |   elements: ServerElement[];
184 | }
185 | 
186 | export interface ElementCreatedMessage extends WebSocketMessage {
187 |   type: 'element_created';
188 |   element: ServerElement;
189 | }
190 | 
191 | export interface ElementUpdatedMessage extends WebSocketMessage {
192 |   type: 'element_updated';
193 |   element: ServerElement;
194 | }
195 | 
196 | export interface ElementDeletedMessage extends WebSocketMessage {
197 |   type: 'element_deleted';
198 |   elementId: string;
199 | }
200 | 
201 | export interface BatchCreatedMessage extends WebSocketMessage {
202 |   type: 'elements_batch_created';
203 |   elements: ServerElement[];
204 | }
205 | 
206 | export interface SyncStatusMessage extends WebSocketMessage {
207 |   type: 'sync_status';
208 |   elementCount: number;
209 |   timestamp: string;
210 | }
211 | 
212 | export interface MermaidConvertMessage extends WebSocketMessage {
213 |   type: 'mermaid_convert';
214 |   mermaidDiagram: string;
215 |   config?: MermaidConfig;
216 |   timestamp: string;
217 | }
218 | 
219 | // Mermaid conversion types
220 | export interface MermaidConfig {
221 |   startOnLoad?: boolean;
222 |   flowchart?: {
223 |     curve?: 'linear' | 'basis';
224 |   };
225 |   themeVariables?: {
226 |     fontSize?: string;
227 |   };
228 |   maxEdges?: number;
229 |   maxTextSize?: number;
230 | }
231 | 
232 | export interface MermaidConversionRequest {
233 |   mermaidDiagram: string;
234 |   config?: MermaidConfig;
235 | }
236 | 
237 | export interface MermaidConversionResponse extends ApiResponse {
238 |   elements: ServerElement[];
239 |   files?: any;
240 |   count: number;
241 | }
242 | 
243 | // In-memory storage for Excalidraw elements
244 | export const elements = new Map<string, ServerElement>();
245 | 
246 | // Validation function for Excalidraw elements
247 | export function validateElement(element: Partial<ServerElement>): element is ServerElement {
248 |   const requiredFields: (keyof ServerElement)[] = ['type', 'x', 'y'];
249 |   const hasRequiredFields = requiredFields.every(field => field in element);
250 |   
251 |   if (!hasRequiredFields) {
252 |     throw new Error(`Missing required fields: ${requiredFields.join(', ')}`);
253 |   }
254 | 
255 |   if (!Object.values(EXCALIDRAW_ELEMENT_TYPES).includes(element.type as ExcalidrawElementType)) {
256 |     throw new Error(`Invalid element type: ${element.type}`);
257 |   }
258 | 
259 |   return true;
260 | }
261 | 
262 | // Helper function to generate unique IDs
263 | export function generateId(): string {
264 |   return Date.now().toString(36) + Math.random().toString(36).substring(2);
265 | }
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import express, { Request, Response, NextFunction } from 'express';
  2 | import cors from 'cors';
  3 | import { WebSocketServer } from 'ws';
  4 | import { createServer } from 'http';
  5 | import path from 'path';
  6 | import { fileURLToPath } from 'url';
  7 | import dotenv from 'dotenv';
  8 | import logger from './utils/logger.js';
  9 | import { 
 10 |   elements,
 11 |   generateId, 
 12 |   EXCALIDRAW_ELEMENT_TYPES,
 13 |   ServerElement,
 14 |   ExcalidrawElementType,
 15 |   WebSocketMessage,
 16 |   ElementCreatedMessage,
 17 |   ElementUpdatedMessage,
 18 |   ElementDeletedMessage,
 19 |   BatchCreatedMessage,
 20 |   SyncStatusMessage,
 21 |   InitialElementsMessage
 22 | } from './types.js';
 23 | import { z } from 'zod';
 24 | import WebSocket from 'ws';
 25 | 
 26 | // Load environment variables
 27 | dotenv.config();
 28 | 
 29 | const __filename = fileURLToPath(import.meta.url);
 30 | const __dirname = path.dirname(__filename);
 31 | 
 32 | const app = express();
 33 | const server = createServer(app);
 34 | const wss = new WebSocketServer({ server });
 35 | 
 36 | // Middleware
 37 | app.use(cors());
 38 | app.use(express.json());
 39 | 
 40 | // Serve static files from the build directory
 41 | const staticDir = path.join(__dirname, '../dist');
 42 | app.use(express.static(staticDir));
 43 | // Also serve frontend assets
 44 | app.use(express.static(path.join(__dirname, '../dist/frontend')));
 45 | 
 46 | // WebSocket connections
 47 | const clients = new Set<WebSocket>();
 48 | 
 49 | // Broadcast to all connected clients
 50 | function broadcast(message: WebSocketMessage): void {
 51 |   const data = JSON.stringify(message);
 52 |   clients.forEach(client => {
 53 |     if (client.readyState === WebSocket.OPEN) {
 54 |       client.send(data);
 55 |     }
 56 |   });
 57 | }
 58 | 
 59 | // WebSocket connection handling
 60 | wss.on('connection', (ws: WebSocket) => {
 61 |   clients.add(ws);
 62 |   logger.info('New WebSocket connection established');
 63 |   
 64 |   // Send current elements to new client
 65 |   const initialMessage: InitialElementsMessage = {
 66 |     type: 'initial_elements',
 67 |     elements: Array.from(elements.values())
 68 |   };
 69 |   ws.send(JSON.stringify(initialMessage));
 70 |   
 71 |   // Send sync status to new client
 72 |   const syncMessage: SyncStatusMessage = {
 73 |     type: 'sync_status',
 74 |     elementCount: elements.size,
 75 |     timestamp: new Date().toISOString()
 76 |   };
 77 |   ws.send(JSON.stringify(syncMessage));
 78 |   
 79 |   ws.on('close', () => {
 80 |     clients.delete(ws);
 81 |     logger.info('WebSocket connection closed');
 82 |   });
 83 |   
 84 |   ws.on('error', (error) => {
 85 |     logger.error('WebSocket error:', error);
 86 |     clients.delete(ws);
 87 |   });
 88 | });
 89 | 
 90 | // Schema validation
 91 | const CreateElementSchema = z.object({
 92 |   id: z.string().optional(), // Allow passing ID for MCP sync
 93 |   type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES) as [ExcalidrawElementType, ...ExcalidrawElementType[]]),
 94 |   x: z.number(),
 95 |   y: z.number(),
 96 |   width: z.number().optional(),
 97 |   height: z.number().optional(),
 98 |   backgroundColor: z.string().optional(),
 99 |   strokeColor: z.string().optional(),
100 |   strokeWidth: z.number().optional(),
101 |   roughness: z.number().optional(),
102 |   opacity: z.number().optional(),
103 |   text: z.string().optional(),
104 |   label: z.object({
105 |     text: z.string()
106 |   }).optional(),
107 |   fontSize: z.number().optional(),
108 |   fontFamily: z.string().optional(),
109 |   groupIds: z.array(z.string()).optional(),
110 |   locked: z.boolean().optional()
111 | });
112 | 
113 | const UpdateElementSchema = z.object({
114 |   id: z.string(),
115 |   type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES) as [ExcalidrawElementType, ...ExcalidrawElementType[]]).optional(),
116 |   x: z.number().optional(),
117 |   y: z.number().optional(),
118 |   width: z.number().optional(),
119 |   height: z.number().optional(),
120 |   backgroundColor: z.string().optional(),
121 |   strokeColor: z.string().optional(),
122 |   strokeWidth: z.number().optional(),
123 |   roughness: z.number().optional(),
124 |   opacity: z.number().optional(),
125 |   text: z.string().optional(),
126 |   label: z.object({
127 |     text: z.string()
128 |   }).optional(),
129 |   fontSize: z.number().optional(),
130 |   fontFamily: z.string().optional(),
131 |   groupIds: z.array(z.string()).optional(),
132 |   locked: z.boolean().optional()
133 | });
134 | 
135 | // API Routes
136 | 
137 | // Get all elements
138 | app.get('/api/elements', (req: Request, res: Response) => {
139 |   try {
140 |     const elementsArray = Array.from(elements.values());
141 |     res.json({
142 |       success: true,
143 |       elements: elementsArray,
144 |       count: elementsArray.length
145 |     });
146 |   } catch (error) {
147 |     logger.error('Error fetching elements:', error);
148 |     res.status(500).json({
149 |       success: false,
150 |       error: (error as Error).message
151 |     });
152 |   }
153 | });
154 | 
155 | // Create new element
156 | app.post('/api/elements', (req: Request, res: Response) => {
157 |   try {
158 |     const params = CreateElementSchema.parse(req.body);
159 |     logger.info('Creating element via API', { type: params.type });
160 | 
161 |     // Prioritize passed ID (for MCP sync), otherwise generate new ID
162 |     const id = params.id || generateId();
163 |     const element: ServerElement = {
164 |       id,
165 |       ...params,
166 |       createdAt: new Date().toISOString(),
167 |       updatedAt: new Date().toISOString(),
168 |       version: 1
169 |     };
170 | 
171 |     elements.set(id, element);
172 |     
173 |     // Broadcast to all connected clients
174 |     const message: ElementCreatedMessage = {
175 |       type: 'element_created',
176 |       element: element
177 |     };
178 |     broadcast(message);
179 |     
180 |     res.json({
181 |       success: true,
182 |       element: element
183 |     });
184 |   } catch (error) {
185 |     logger.error('Error creating element:', error);
186 |     res.status(400).json({
187 |       success: false,
188 |       error: (error as Error).message
189 |     });
190 |   }
191 | });
192 | 
193 | // Update element
194 | app.put('/api/elements/:id', (req: Request, res: Response) => {
195 |   try {
196 |     const { id } = req.params;
197 |     const updates = UpdateElementSchema.parse({ id, ...req.body });
198 |     
199 |     if (!id) {
200 |       return res.status(400).json({
201 |         success: false,
202 |         error: 'Element ID is required'
203 |       });
204 |     }
205 |     
206 |     const existingElement = elements.get(id);
207 |     if (!existingElement) {
208 |       return res.status(404).json({
209 |         success: false,
210 |         error: `Element with ID ${id} not found`
211 |       });
212 |     }
213 | 
214 |     const updatedElement: ServerElement = {
215 |       ...existingElement,
216 |       ...updates,
217 |       updatedAt: new Date().toISOString(),
218 |       version: (existingElement.version || 0) + 1
219 |     };
220 | 
221 |     elements.set(id, updatedElement);
222 |     
223 |     // Broadcast to all connected clients
224 |     const message: ElementUpdatedMessage = {
225 |       type: 'element_updated',
226 |       element: updatedElement
227 |     };
228 |     broadcast(message);
229 |     
230 |     res.json({
231 |       success: true,
232 |       element: updatedElement
233 |     });
234 |   } catch (error) {
235 |     logger.error('Error updating element:', error);
236 |     res.status(400).json({
237 |       success: false,
238 |       error: (error as Error).message
239 |     });
240 |   }
241 | });
242 | 
243 | // Delete element
244 | app.delete('/api/elements/:id', (req: Request, res: Response) => {
245 |   try {
246 |     const { id } = req.params;
247 |     
248 |     if (!id) {
249 |       return res.status(400).json({
250 |         success: false,
251 |         error: 'Element ID is required'
252 |       });
253 |     }
254 |     
255 |     if (!elements.has(id)) {
256 |       return res.status(404).json({
257 |         success: false,
258 |         error: `Element with ID ${id} not found`
259 |       });
260 |     }
261 |     
262 |     elements.delete(id);
263 |     
264 |     // Broadcast to all connected clients
265 |     const message: ElementDeletedMessage = {
266 |       type: 'element_deleted',
267 |       elementId: id!
268 |     };
269 |     broadcast(message);
270 |     
271 |     res.json({
272 |       success: true,
273 |       message: `Element ${id} deleted successfully`
274 |     });
275 |   } catch (error) {
276 |     logger.error('Error deleting element:', error);
277 |     res.status(500).json({
278 |       success: false,
279 |       error: (error as Error).message
280 |     });
281 |   }
282 | });
283 | 
284 | // Query elements with filters
285 | app.get('/api/elements/search', (req: Request, res: Response) => {
286 |   try {
287 |     const { type, ...filters } = req.query;
288 |     let results = Array.from(elements.values());
289 |     
290 |     // Filter by type if specified
291 |     if (type && typeof type === 'string') {
292 |       results = results.filter(element => element.type === type);
293 |     }
294 |     
295 |     // Apply additional filters
296 |     if (Object.keys(filters).length > 0) {
297 |       results = results.filter(element => {
298 |         return Object.entries(filters).every(([key, value]) => {
299 |           return (element as any)[key] === value;
300 |         });
301 |       });
302 |     }
303 |     
304 |     res.json({
305 |       success: true,
306 |       elements: results,
307 |       count: results.length
308 |     });
309 |   } catch (error) {
310 |     logger.error('Error querying elements:', error);
311 |     res.status(500).json({
312 |       success: false,
313 |       error: (error as Error).message
314 |     });
315 |   }
316 | });
317 | 
318 | // Get element by ID
319 | app.get('/api/elements/:id', (req: Request, res: Response) => {
320 |   try {
321 |     const { id } = req.params;
322 |     
323 |     if (!id) {
324 |       return res.status(400).json({
325 |         success: false,
326 |         error: 'Element ID is required'
327 |       });
328 |     }
329 |     
330 |     const element = elements.get(id);
331 |     
332 |     if (!element) {
333 |       return res.status(404).json({
334 |         success: false,
335 |         error: `Element with ID ${id} not found`
336 |       });
337 |     }
338 |     
339 |     res.json({
340 |       success: true,
341 |       element: element
342 |     });
343 |   } catch (error) {
344 |     logger.error('Error fetching element:', error);
345 |     res.status(500).json({
346 |       success: false,
347 |       error: (error as Error).message
348 |     });
349 |   }
350 | });
351 | 
352 | // Batch create elements
353 | app.post('/api/elements/batch', (req: Request, res: Response) => {
354 |   try {
355 |     const { elements: elementsToCreate } = req.body;
356 |     
357 |     if (!Array.isArray(elementsToCreate)) {
358 |       return res.status(400).json({
359 |         success: false,
360 |         error: 'Expected an array of elements'
361 |       });
362 |     }
363 |     
364 |     const createdElements: ServerElement[] = [];
365 |     
366 |     elementsToCreate.forEach(elementData => {
367 |       const params = CreateElementSchema.parse(elementData);
368 |       const id = generateId();
369 |       const element: ServerElement = {
370 |         id,
371 |         ...params,
372 |         createdAt: new Date().toISOString(),
373 |         updatedAt: new Date().toISOString(),
374 |         version: 1
375 |       };
376 |       
377 |       elements.set(id, element);
378 |       createdElements.push(element);
379 |     });
380 |     
381 |     // Broadcast to all connected clients
382 |     const message: BatchCreatedMessage = {
383 |       type: 'elements_batch_created',
384 |       elements: createdElements
385 |     };
386 |     broadcast(message);
387 |     
388 |     res.json({
389 |       success: true,
390 |       elements: createdElements,
391 |       count: createdElements.length
392 |     });
393 |   } catch (error) {
394 |     logger.error('Error batch creating elements:', error);
395 |     res.status(400).json({
396 |       success: false,
397 |       error: (error as Error).message
398 |     });
399 |   }
400 | });
401 | 
402 | // Convert Mermaid diagram to Excalidraw elements
403 | app.post('/api/elements/from-mermaid', (req: Request, res: Response) => {
404 |   try {
405 |     const { mermaidDiagram, config } = req.body;
406 |     
407 |     if (!mermaidDiagram || typeof mermaidDiagram !== 'string') {
408 |       return res.status(400).json({
409 |         success: false,
410 |         error: 'Mermaid diagram definition is required'
411 |       });
412 |     }
413 |     
414 |     logger.info('Received Mermaid conversion request', { 
415 |       diagramLength: mermaidDiagram.length,
416 |       hasConfig: !!config 
417 |     });
418 |     
419 |     // Broadcast to all WebSocket clients to process the Mermaid diagram
420 |     broadcast({
421 |       type: 'mermaid_convert',
422 |       mermaidDiagram,
423 |       config: config || {},
424 |       timestamp: new Date().toISOString()
425 |     });
426 |     
427 |     // Return the diagram for frontend processing
428 |     res.json({
429 |       success: true,
430 |       mermaidDiagram,
431 |       config: config || {},
432 |       message: 'Mermaid diagram sent to frontend for conversion.'
433 |     });
434 |   } catch (error) {
435 |     logger.error('Error processing Mermaid diagram:', error);
436 |     res.status(400).json({
437 |       success: false,
438 |       error: (error as Error).message
439 |     });
440 |   }
441 | });
442 | 
443 | // Sync elements from frontend (overwrite sync)
444 | app.post('/api/elements/sync', (req: Request, res: Response) => {
445 |   try {
446 |     const { elements: frontendElements, timestamp } = req.body;
447 |     
448 |     logger.info(`Sync request received: ${frontendElements.length} elements`, {
449 |       timestamp,
450 |       elementCount: frontendElements.length
451 |     });
452 |     
453 |     // Validate input data
454 |     if (!Array.isArray(frontendElements)) {
455 |       return res.status(400).json({
456 |         success: false,
457 |         error: 'Expected elements to be an array'
458 |       });
459 |     }
460 |     
461 |     // Record element count before sync
462 |     const beforeCount = elements.size;
463 |     
464 |     // 1. Clear existing memory storage
465 |     elements.clear();
466 |     logger.info(`Cleared existing elements: ${beforeCount} elements removed`);
467 |     
468 |     // 2. Batch write new data
469 |     let successCount = 0;
470 |     const processedElements: ServerElement[] = [];
471 |     
472 |     frontendElements.forEach((element: any, index: number) => {
473 |       try {
474 |         // Ensure element has ID, generate one if missing
475 |         const elementId = element.id || generateId();
476 |         
477 |         // Add server metadata
478 |         const processedElement: ServerElement = {
479 |           ...element,
480 |           id: elementId,
481 |           syncedAt: new Date().toISOString(),
482 |           source: 'frontend_sync',
483 |           syncTimestamp: timestamp,
484 |           version: 1
485 |         };
486 |         
487 |         // Store to memory
488 |         elements.set(elementId, processedElement);
489 |         processedElements.push(processedElement);
490 |         successCount++;
491 |         
492 |       } catch (elementError) {
493 |         logger.warn(`Failed to process element ${index}:`, elementError);
494 |       }
495 |     });
496 |     
497 |     logger.info(`Sync completed: ${successCount}/${frontendElements.length} elements synced`);
498 |     
499 |     // 3. Broadcast sync event to all WebSocket clients
500 |     broadcast({
501 |       type: 'elements_synced',
502 |       count: successCount,
503 |       timestamp: new Date().toISOString(),
504 |       source: 'manual_sync'
505 |     });
506 |     
507 |     // 4. Return sync results
508 |     res.json({
509 |       success: true,
510 |       message: `Successfully synced ${successCount} elements`,
511 |       count: successCount,
512 |       syncedAt: new Date().toISOString(),
513 |       beforeCount,
514 |       afterCount: elements.size
515 |     });
516 |     
517 |   } catch (error) {
518 |     logger.error('Sync error:', error);
519 |     res.status(500).json({
520 |       success: false,
521 |       error: (error as Error).message,
522 |       details: 'Internal server error during sync operation'
523 |     });
524 |   }
525 | });
526 | 
527 | // Serve the frontend
528 | app.get('/', (req: Request, res: Response) => {
529 |   const htmlFile = path.join(__dirname, '../dist/frontend/index.html');
530 |   res.sendFile(htmlFile, (err) => {
531 |     if (err) {
532 |       logger.error('Error serving frontend:', err);
533 |       res.status(404).send('Frontend not found. Please run "npm run build" first.');
534 |     }
535 |   });
536 | });
537 | 
538 | // Health check endpoint
539 | app.get('/health', (req: Request, res: Response) => {
540 |   res.json({
541 |     status: 'healthy',
542 |     timestamp: new Date().toISOString(),
543 |     elements_count: elements.size,
544 |     websocket_clients: clients.size
545 |   });
546 | });
547 | 
548 | // Sync status endpoint
549 | app.get('/api/sync/status', (req: Request, res: Response) => {
550 |   res.json({
551 |     success: true,
552 |     elementCount: elements.size,
553 |     timestamp: new Date().toISOString(),
554 |     memoryUsage: {
555 |       heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), // MB
556 |       heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), // MB
557 |     },
558 |     websocketClients: clients.size
559 |   });
560 | });
561 | 
562 | // Error handling middleware
563 | app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
564 |   logger.error('Unhandled error:', err);
565 |   res.status(500).json({
566 |     success: false,
567 |     error: 'Internal server error'
568 |   });
569 | });
570 | 
571 | // Start server
572 | const PORT = parseInt(process.env.PORT || '3000', 10);
573 | const HOST = process.env.HOST || 'localhost';
574 | 
575 | server.listen(PORT, HOST, () => {
576 |   logger.info(`POC server running on http://${HOST}:${PORT}`);
577 |   logger.info(`WebSocket server running on ws://${HOST}:${PORT}`);
578 | });
579 | 
580 | export default app;
```

--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React, { useState, useEffect, useRef } from 'react'
  2 | import {
  3 |   Excalidraw,
  4 |   convertToExcalidrawElements,
  5 |   CaptureUpdateAction,
  6 |   ExcalidrawImperativeAPI
  7 | } from '@excalidraw/excalidraw'
  8 | import type { ExcalidrawElement, NonDeleted, NonDeletedExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
  9 | import { convertMermaidToExcalidraw, DEFAULT_MERMAID_CONFIG } from './utils/mermaidConverter'
 10 | import type { MermaidConfig } from '@excalidraw/mermaid-to-excalidraw'
 11 | 
 12 | // Type definitions
 13 | type ExcalidrawAPIRefValue = ExcalidrawImperativeAPI;
 14 | 
 15 | interface ServerElement {
 16 |   id: string;
 17 |   type: string;
 18 |   x: number;
 19 |   y: number;
 20 |   width?: number;
 21 |   height?: number;
 22 |   backgroundColor?: string;
 23 |   strokeColor?: string;
 24 |   strokeWidth?: number;
 25 |   roughness?: number;
 26 |   opacity?: number;
 27 |   text?: string;
 28 |   fontSize?: number;
 29 |   fontFamily?: string | number;
 30 |   label?: {
 31 |     text: string;
 32 |   };
 33 |   createdAt?: string;
 34 |   updatedAt?: string;
 35 |   version?: number;
 36 |   syncedAt?: string;
 37 |   source?: string;
 38 |   syncTimestamp?: string;
 39 |   boundElements?: any[] | null;
 40 |   containerId?: string | null;
 41 |   locked?: boolean;
 42 | }
 43 | 
 44 | interface WebSocketMessage {
 45 |   type: string;
 46 |   element?: ServerElement;
 47 |   elements?: ServerElement[];
 48 |   elementId?: string;
 49 |   count?: number;
 50 |   timestamp?: string;
 51 |   source?: string;
 52 |   mermaidDiagram?: string;
 53 |   config?: MermaidConfig;
 54 | }
 55 | 
 56 | interface ApiResponse {
 57 |   success: boolean;
 58 |   elements?: ServerElement[];
 59 |   element?: ServerElement;
 60 |   count?: number;
 61 |   error?: string;
 62 |   message?: string;
 63 | }
 64 | 
 65 | type SyncStatus = 'idle' | 'syncing' | 'success' | 'error';
 66 | 
 67 | // Helper function to clean elements for Excalidraw
 68 | const cleanElementForExcalidraw = (element: ServerElement): Partial<ExcalidrawElement> => {
 69 |   const {
 70 |     createdAt,
 71 |     updatedAt,
 72 |     version,
 73 |     syncedAt,
 74 |     source,
 75 |     syncTimestamp,
 76 |     ...cleanElement
 77 |   } = element;
 78 |   return cleanElement;
 79 | }
 80 | 
 81 | // Helper function to validate and fix element binding data
 82 | const validateAndFixBindings = (elements: Partial<ExcalidrawElement>[]): Partial<ExcalidrawElement>[] => {
 83 |   const elementMap = new Map(elements.map(el => [el.id!, el]));
 84 |   
 85 |   return elements.map(element => {
 86 |     const fixedElement = { ...element };
 87 |     
 88 |     // Validate and fix boundElements
 89 |     if (fixedElement.boundElements) {
 90 |       if (Array.isArray(fixedElement.boundElements)) {
 91 |         fixedElement.boundElements = fixedElement.boundElements.filter((binding: any) => {
 92 |           // Ensure binding has required properties
 93 |           if (!binding || typeof binding !== 'object') return false;
 94 |           if (!binding.id || !binding.type) return false;
 95 |           
 96 |           // Ensure the referenced element exists
 97 |           const referencedElement = elementMap.get(binding.id);
 98 |           if (!referencedElement) return false;
 99 |           
100 |           // Validate binding type
101 |           if (!['text', 'arrow'].includes(binding.type)) return false;
102 |           
103 |           return true;
104 |         });
105 |         
106 |         // Remove boundElements if empty
107 |         if (fixedElement.boundElements.length === 0) {
108 |           fixedElement.boundElements = null;
109 |         }
110 |       } else {
111 |         // Invalid boundElements format, set to null
112 |         fixedElement.boundElements = null;
113 |       }
114 |     }
115 |     
116 |     // Validate and fix containerId
117 |     if (fixedElement.containerId) {
118 |       const containerElement = elementMap.get(fixedElement.containerId);
119 |       if (!containerElement) {
120 |         // Container doesn't exist, remove containerId
121 |         fixedElement.containerId = null;
122 |       }
123 |     }
124 |     
125 |     return fixedElement;
126 |   });
127 | }
128 | 
129 | function App(): JSX.Element {
130 |   const [excalidrawAPI, setExcalidrawAPI] = useState<ExcalidrawAPIRefValue | null>(null)
131 |   const [isConnected, setIsConnected] = useState<boolean>(false)
132 |   const websocketRef = useRef<WebSocket | null>(null)
133 |   
134 |   // Sync state management
135 |   const [syncStatus, setSyncStatus] = useState<SyncStatus>('idle')
136 |   const [lastSyncTime, setLastSyncTime] = useState<Date | null>(null)
137 | 
138 |   // WebSocket connection
139 |   useEffect(() => {
140 |     connectWebSocket()
141 |     return () => {
142 |       if (websocketRef.current) {
143 |         websocketRef.current.close()
144 |       }
145 |     }
146 |   }, [])
147 | 
148 |   // Load existing elements when Excalidraw API becomes available
149 |   useEffect(() => {
150 |     if (excalidrawAPI) {
151 |       loadExistingElements()
152 |       
153 |       // Ensure WebSocket is connected for real-time updates
154 |       if (!isConnected) {
155 |         connectWebSocket()
156 |       }
157 |     }
158 |   }, [excalidrawAPI, isConnected])
159 | 
160 |   const loadExistingElements = async (): Promise<void> => {
161 |     try {
162 |       const response = await fetch('/api/elements')
163 |       const result: ApiResponse = await response.json()
164 |       
165 |       if (result.success && result.elements && result.elements.length > 0) {
166 |         const cleanedElements = result.elements.map(cleanElementForExcalidraw)
167 |         const convertedElements = convertToExcalidrawElements(cleanedElements, { regenerateIds: false })
168 |         excalidrawAPI?.updateScene({ elements: convertedElements })
169 |       }
170 |     } catch (error) {
171 |       console.error('Error loading existing elements:', error)
172 |     }
173 |   }
174 | 
175 |   const connectWebSocket = (): void => {
176 |     if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) {
177 |       return
178 |     }
179 | 
180 |     const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
181 |     const wsUrl = `${protocol}//${window.location.host}`
182 |     
183 |     websocketRef.current = new WebSocket(wsUrl)
184 |     
185 |     websocketRef.current.onopen = () => {
186 |       setIsConnected(true)
187 |       
188 |       if (excalidrawAPI) {
189 |         setTimeout(loadExistingElements, 100)
190 |       }
191 |     }
192 |     
193 |     websocketRef.current.onmessage = (event: MessageEvent) => {
194 |       try {
195 |         const data: WebSocketMessage = JSON.parse(event.data)
196 |         handleWebSocketMessage(data)
197 |       } catch (error) {
198 |         console.error('Error parsing WebSocket message:', error, event.data)
199 |       }
200 |     }
201 |     
202 |     websocketRef.current.onclose = (event: CloseEvent) => {
203 |       setIsConnected(false)
204 |       
205 |       // Reconnect after 3 seconds if not a clean close
206 |       if (event.code !== 1000) {
207 |         setTimeout(connectWebSocket, 3000)
208 |       }
209 |     }
210 |     
211 |     websocketRef.current.onerror = (error: Event) => {
212 |       console.error('WebSocket error:', error)
213 |       setIsConnected(false)
214 |     }
215 |   }
216 | 
217 |   const handleWebSocketMessage = async (data: WebSocketMessage): Promise<void> => {
218 |     if (!excalidrawAPI) {
219 |       return
220 |     }
221 | 
222 |     try {
223 |       const currentElements = excalidrawAPI.getSceneElements()
224 |       console.log('Current elements:', currentElements);
225 | 
226 |       switch (data.type) {
227 |         case 'initial_elements':
228 |           if (data.elements && data.elements.length > 0) {
229 |             const cleanedElements = data.elements.map(cleanElementForExcalidraw)
230 |             const validatedElements = validateAndFixBindings(cleanedElements)
231 |             const convertedElements = convertToExcalidrawElements(validatedElements)
232 |             excalidrawAPI.updateScene({
233 |               elements: convertedElements,
234 |               captureUpdate: CaptureUpdateAction.NEVER
235 |             })
236 |           }
237 |           break
238 | 
239 |         case 'element_created':
240 |           if (data.element) {
241 |             const cleanedNewElement = cleanElementForExcalidraw(data.element)
242 |             const newElement = convertToExcalidrawElements([cleanedNewElement])
243 |             const updatedElementsAfterCreate = [...currentElements, ...newElement]
244 |             excalidrawAPI.updateScene({ 
245 |               elements: updatedElementsAfterCreate,
246 |               captureUpdate: CaptureUpdateAction.NEVER
247 |             })
248 |           }
249 |           break
250 |           
251 |         case 'element_updated':
252 |           if (data.element) {
253 |             const cleanedUpdatedElement = cleanElementForExcalidraw(data.element)
254 |             const convertedUpdatedElement = convertToExcalidrawElements([cleanedUpdatedElement])[0]
255 |             const updatedElements = currentElements.map(el =>
256 |               el.id === data.element!.id ? convertedUpdatedElement : el
257 |             )
258 |             excalidrawAPI.updateScene({
259 |               elements: updatedElements,
260 |               captureUpdate: CaptureUpdateAction.NEVER
261 |             })
262 |           }
263 |           break
264 | 
265 |         case 'element_deleted':
266 |           if (data.elementId) {
267 |             const filteredElements = currentElements.filter(el => el.id !== data.elementId)
268 |             excalidrawAPI.updateScene({
269 |               elements: filteredElements,
270 |               captureUpdate: CaptureUpdateAction.NEVER
271 |             })
272 |           }
273 |           break
274 | 
275 |         case 'elements_batch_created':
276 |           if (data.elements) {
277 |             const cleanedBatchElements = data.elements.map(cleanElementForExcalidraw)
278 |             const batchElements = convertToExcalidrawElements(cleanedBatchElements)
279 |             const updatedElementsAfterBatch = [...currentElements, ...batchElements]
280 |             excalidrawAPI.updateScene({ 
281 |               elements: updatedElementsAfterBatch,
282 |               captureUpdate: CaptureUpdateAction.NEVER
283 |             })
284 |           }
285 |           break
286 |           
287 |         case 'elements_synced':
288 |           console.log(`Sync confirmed by server: ${data.count} elements`)
289 |           // Sync confirmation already handled by HTTP response
290 |           break
291 |           
292 |         case 'sync_status':
293 |           console.log(`Server sync status: ${data.count} elements`)
294 |           break
295 |           
296 |         case 'mermaid_convert':
297 |           console.log('Received Mermaid conversion request from MCP')
298 |           if (data.mermaidDiagram) {
299 |             try {
300 |               const result = await convertMermaidToExcalidraw(data.mermaidDiagram, data.config || DEFAULT_MERMAID_CONFIG)
301 | 
302 |               if (result.error) {
303 |                 console.error('Mermaid conversion error:', result.error)
304 |                 return
305 |               }
306 | 
307 |               if (result.elements && result.elements.length > 0) {
308 |                 const convertedElements = convertToExcalidrawElements(result.elements, { regenerateIds: false })
309 |                 excalidrawAPI.updateScene({
310 |                   elements: convertedElements,
311 |                   captureUpdate: CaptureUpdateAction.IMMEDIATELY
312 |                 })
313 | 
314 |                 if (result.files) {
315 |                   excalidrawAPI.addFiles(Object.values(result.files))
316 |                 }
317 | 
318 |                 console.log('Mermaid diagram converted successfully:', result.elements.length, 'elements')
319 | 
320 |                 // Sync to backend automatically after creating elements
321 |                 await syncToBackend()
322 |               }
323 |             } catch (error) {
324 |               console.error('Error converting Mermaid diagram from WebSocket:', error)
325 |             }
326 |           }
327 |           break
328 |           
329 |         default:
330 |           console.log('Unknown WebSocket message type:', data.type)
331 |       }
332 |     } catch (error) {
333 |       console.error('Error processing WebSocket message:', error, data)
334 |     }
335 |   }
336 | 
337 |   // Data format conversion for backend
338 |   const convertToBackendFormat = (element: ExcalidrawElement): ServerElement => {
339 |     return {
340 |       ...element
341 |     } as ServerElement
342 |   }
343 | 
344 |   // Format sync time display
345 |   const formatSyncTime = (time: Date | null): string => {
346 |     if (!time) return ''
347 |     return time.toLocaleTimeString('zh-CN', {
348 |       hour: '2-digit',
349 |       minute: '2-digit',
350 |       second: '2-digit'
351 |     })
352 |   }
353 | 
354 |   // Main sync function
355 |   const syncToBackend = async (): Promise<void> => {
356 |     if (!excalidrawAPI) {
357 |       console.warn('Excalidraw API not available')
358 |       return
359 |     }
360 |     
361 |     setSyncStatus('syncing')
362 |     
363 |     try {
364 |       // 1. Get current elements
365 |       const currentElements = excalidrawAPI.getSceneElements()
366 |       console.log(`Syncing ${currentElements.length} elements to backend`)
367 |       
368 |       // Filter out deleted elements
369 |       const activeElements = currentElements.filter(el => !el.isDeleted)
370 |       
371 |       // 3. Convert to backend format
372 |       const backendElements = activeElements.map(convertToBackendFormat)
373 |       
374 |       // 4. Send to backend
375 |       const response = await fetch('/api/elements/sync', {
376 |         method: 'POST',
377 |         headers: {
378 |           'Content-Type': 'application/json',
379 |         },
380 |         body: JSON.stringify({
381 |           elements: backendElements,
382 |           timestamp: new Date().toISOString()
383 |         })
384 |       })
385 |       
386 |       if (response.ok) {
387 |         const result: ApiResponse = await response.json()
388 |         setSyncStatus('success')
389 |         setLastSyncTime(new Date())
390 |         console.log(`Sync successful: ${result.count} elements synced`)
391 |         
392 |         // Reset status after 2 seconds
393 |         setTimeout(() => setSyncStatus('idle'), 2000)
394 |       } else {
395 |         const error: ApiResponse = await response.json()
396 |         setSyncStatus('error')
397 |         console.error('Sync failed:', error.error)
398 |       }
399 |     } catch (error) {
400 |       setSyncStatus('error')
401 |       console.error('Sync error:', error)
402 |     }
403 |   }
404 | 
405 |   const clearCanvas = async (): Promise<void> => {
406 |     if (excalidrawAPI) {
407 |       try {
408 |         // Get all current elements and delete them from backend
409 |         const response = await fetch('/api/elements')
410 |         const result: ApiResponse = await response.json()
411 |         
412 |         if (result.success && result.elements) {
413 |           const deletePromises = result.elements.map(element => 
414 |             fetch(`/api/elements/${element.id}`, { method: 'DELETE' })
415 |           )
416 |           await Promise.all(deletePromises)
417 |         }
418 |         
419 |         // Clear the frontend canvas
420 |         excalidrawAPI.updateScene({ 
421 |           elements: [],
422 |           captureUpdate: CaptureUpdateAction.IMMEDIATELY
423 |         })
424 |       } catch (error) {
425 |         console.error('Error clearing canvas:', error)
426 |         // Still clear frontend even if backend fails
427 |         excalidrawAPI.updateScene({ 
428 |           elements: [],
429 |           captureUpdate: CaptureUpdateAction.IMMEDIATELY
430 |         })
431 |       }
432 |     }
433 |   }
434 | 
435 |   return (
436 |     <div className="app">
437 |       {/* Header */}
438 |       <div className="header">
439 |         <h1>Excalidraw Canvas</h1>
440 |         <div className="controls">
441 |           <div className="status">
442 |             <div className={`status-dot ${isConnected ? 'status-connected' : 'status-disconnected'}`}></div>
443 |             <span>{isConnected ? 'Connected' : 'Disconnected'}</span>
444 |           </div>
445 |           
446 |           {/* Sync Controls */}
447 |           <div className="sync-controls">
448 |             <button 
449 |               className={`btn-primary ${syncStatus === 'syncing' ? 'btn-loading' : ''}`}
450 |               onClick={syncToBackend}
451 |               disabled={syncStatus === 'syncing' || !excalidrawAPI}
452 |             >
453 |               {syncStatus === 'syncing' && <span className="spinner"></span>}
454 |               {syncStatus === 'syncing' ? 'Syncing...' : 'Sync to Backend'}
455 |             </button>
456 |             
457 |             {/* Sync Status */}
458 |             <div className="sync-status">
459 |               {syncStatus === 'success' && (
460 |                 <span className="sync-success">✅ Synced</span>
461 |               )}
462 |               {syncStatus === 'error' && (
463 |                 <span className="sync-error">❌ Sync Failed</span>
464 |               )}
465 |               {lastSyncTime && syncStatus === 'idle' && (
466 |                 <span className="sync-time">
467 |                   Last sync: {formatSyncTime(lastSyncTime)}
468 |                 </span>
469 |               )}
470 |             </div>
471 |           </div>
472 |           
473 |           <button className="btn-secondary" onClick={clearCanvas}>Clear Canvas</button>
474 |         </div>
475 |       </div>
476 | 
477 |       {/* Canvas Container */}
478 |       <div className="canvas-container">
479 |         <Excalidraw
480 |           excalidrawAPI={(api: ExcalidrawAPIRefValue) => setExcalidrawAPI(api)}
481 |           initialData={{
482 |             elements: [],
483 |             appState: {
484 |               theme: 'light',
485 |               viewBackgroundColor: '#ffffff'
486 |             }
487 |           }}
488 |         />
489 |       </div>
490 |     </div>
491 |   )
492 | }
493 | 
494 | export default App
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
   1 | #!/usr/bin/env node
   2 | 
   3 | // Disable colors to prevent ANSI color codes from breaking JSON parsing
   4 | process.env.NODE_DISABLE_COLORS = '1';
   5 | process.env.NO_COLOR = '1';
   6 | 
   7 | import { fileURLToPath } from "url";
   8 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
   9 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  10 | import { 
  11 |   CallToolRequestSchema, 
  12 |   ListToolsRequestSchema,
  13 |   CallToolRequest,
  14 |   Tool
  15 | } from '@modelcontextprotocol/sdk/types.js';
  16 | import { z } from 'zod';
  17 | import dotenv from 'dotenv';
  18 | import logger from './utils/logger.js';
  19 | import { 
  20 |   generateId, 
  21 |   EXCALIDRAW_ELEMENT_TYPES,
  22 |   ServerElement,
  23 |   ExcalidrawElementType,
  24 |   validateElement
  25 | } from './types.js';
  26 | import fetch from 'node-fetch';
  27 | 
  28 | // Load environment variables
  29 | dotenv.config();
  30 | 
  31 | // Express server configuration
  32 | const EXPRESS_SERVER_URL = process.env.EXPRESS_SERVER_URL || 'http://localhost:3000';
  33 | const ENABLE_CANVAS_SYNC = process.env.ENABLE_CANVAS_SYNC !== 'false'; // Default to true
  34 | 
  35 | // API Response types
  36 | interface ApiResponse {
  37 |   success: boolean;
  38 |   element?: ServerElement;
  39 |   elements?: ServerElement[];
  40 |   message?: string;
  41 |   error?: string;
  42 |   count?: number;
  43 | }
  44 | 
  45 | interface SyncResponse {
  46 |   element?: ServerElement;
  47 |   elements?: ServerElement[];
  48 | }
  49 | 
  50 | // Helper functions to sync with Express server (canvas)
  51 | async function syncToCanvas(operation: string, data: any): Promise<SyncResponse | null> {
  52 |   if (!ENABLE_CANVAS_SYNC) {
  53 |     logger.debug('Canvas sync disabled, skipping');
  54 |     return null;
  55 |   }
  56 | 
  57 |   try {
  58 |     let url: string;
  59 |     let options: any;
  60 |     
  61 |     switch (operation) {
  62 |       case 'create':
  63 |         url = `${EXPRESS_SERVER_URL}/api/elements`;
  64 |         options = {
  65 |           method: 'POST',
  66 |           headers: { 'Content-Type': 'application/json' },
  67 |           body: JSON.stringify(data)
  68 |         };
  69 |         break;
  70 |         
  71 |       case 'update':
  72 |         url = `${EXPRESS_SERVER_URL}/api/elements/${data.id}`;
  73 |         options = {
  74 |           method: 'PUT',
  75 |           headers: { 'Content-Type': 'application/json' },
  76 |           body: JSON.stringify(data)
  77 |         };
  78 |         break;
  79 |         
  80 |       case 'delete':
  81 |         url = `${EXPRESS_SERVER_URL}/api/elements/${data.id}`;
  82 |         options = { method: 'DELETE' };
  83 |         break;
  84 |         
  85 |       case 'batch_create':
  86 |         url = `${EXPRESS_SERVER_URL}/api/elements/batch`;
  87 |         options = {
  88 |           method: 'POST',
  89 |           headers: { 'Content-Type': 'application/json' },
  90 |           body: JSON.stringify({ elements: data })
  91 |         };
  92 |         break;
  93 |         
  94 |       default:
  95 |         logger.warn(`Unknown sync operation: ${operation}`);
  96 |         return null;
  97 |     }
  98 | 
  99 |     logger.debug(`Syncing to canvas: ${operation}`, { url, data });
 100 |     const response = await fetch(url, options);
 101 | 
 102 |     // Parse JSON response regardless of HTTP status
 103 |     const result = await response.json() as ApiResponse;
 104 | 
 105 |     if (!response.ok) {
 106 |       logger.warn(`Canvas sync returned error status: ${response.status}`, result);
 107 |       throw new Error(result.error || `Canvas sync failed: ${response.status} ${response.statusText}`);
 108 |     }
 109 | 
 110 |     logger.debug(`Canvas sync successful: ${operation}`, result);
 111 |     return result as SyncResponse;
 112 |     
 113 |   } catch (error) {
 114 |     logger.warn(`Canvas sync failed for ${operation}:`, (error as Error).message);
 115 |     // Don't throw - we want MCP operations to work even if canvas is unavailable
 116 |     return null;
 117 |   }
 118 | }
 119 | 
 120 | // Helper to sync element creation to canvas
 121 | async function createElementOnCanvas(elementData: ServerElement): Promise<ServerElement | null> {
 122 |   const result = await syncToCanvas('create', elementData);
 123 |   return result?.element || elementData;
 124 | }
 125 | 
 126 | // Helper to sync element update to canvas  
 127 | async function updateElementOnCanvas(elementData: Partial<ServerElement> & { id: string }): Promise<ServerElement | null> {
 128 |   const result = await syncToCanvas('update', elementData);
 129 |   return result?.element || null;
 130 | }
 131 | 
 132 | // Helper to sync element deletion to canvas
 133 | async function deleteElementOnCanvas(elementId: string): Promise<any> {
 134 |   const result = await syncToCanvas('delete', { id: elementId });
 135 |   return result;
 136 | }
 137 | 
 138 | // Helper to sync batch creation to canvas
 139 | async function batchCreateElementsOnCanvas(elementsData: ServerElement[]): Promise<ServerElement[] | null> {
 140 |   const result = await syncToCanvas('batch_create', elementsData);
 141 |   return result?.elements || elementsData;
 142 | }
 143 | 
 144 | // Helper to fetch element from canvas
 145 | async function getElementFromCanvas(elementId: string): Promise<ServerElement | null> {
 146 |   if (!ENABLE_CANVAS_SYNC) {
 147 |     logger.debug('Canvas sync disabled, skipping fetch');
 148 |     return null;
 149 |   }
 150 | 
 151 |   try {
 152 |     const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements/${elementId}`);
 153 |     if (!response.ok) {
 154 |       logger.warn(`Failed to fetch element ${elementId}: ${response.status}`);
 155 |       return null;
 156 |     }
 157 |     const data = await response.json() as { element?: ServerElement };
 158 |     return data.element || null;
 159 |   } catch (error) {
 160 |     logger.error('Error fetching element from canvas:', error);
 161 |     return null;
 162 |   }
 163 | }
 164 | 
 165 | // In-memory storage for scene state
 166 | interface SceneState {
 167 |   theme: string;
 168 |   viewport: { x: number; y: number; zoom: number };
 169 |   selectedElements: Set<string>;
 170 |   groups: Map<string, string[]>;
 171 | }
 172 | 
 173 | const sceneState: SceneState = {
 174 |   theme: 'light',
 175 |   viewport: { x: 0, y: 0, zoom: 1 },
 176 |   selectedElements: new Set(),
 177 |   groups: new Map()
 178 | };
 179 | 
 180 | // Schema definitions using zod
 181 | const ElementSchema = z.object({
 182 |   type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES) as [ExcalidrawElementType, ...ExcalidrawElementType[]]),
 183 |   x: z.number(),
 184 |   y: z.number(),
 185 |   width: z.number().optional(),
 186 |   height: z.number().optional(),
 187 |   points: z.array(z.object({ x: z.number(), y: z.number() })).optional(),
 188 |   backgroundColor: z.string().optional(),
 189 |   strokeColor: z.string().optional(),
 190 |   strokeWidth: z.number().optional(),
 191 |   roughness: z.number().optional(),
 192 |   opacity: z.number().optional(),
 193 |   text: z.string().optional(),
 194 |   fontSize: z.number().optional(),
 195 |   fontFamily: z.string().optional(),
 196 |   groupIds: z.array(z.string()).optional(),
 197 |   locked: z.boolean().optional()
 198 | });
 199 | 
 200 | const ElementIdSchema = z.object({
 201 |   id: z.string()
 202 | });
 203 | 
 204 | const ElementIdsSchema = z.object({
 205 |   elementIds: z.array(z.string())
 206 | });
 207 | 
 208 | const GroupIdSchema = z.object({
 209 |   groupId: z.string()
 210 | });
 211 | 
 212 | const AlignElementsSchema = z.object({
 213 |   elementIds: z.array(z.string()),
 214 |   alignment: z.enum(['left', 'center', 'right', 'top', 'middle', 'bottom'])
 215 | });
 216 | 
 217 | const DistributeElementsSchema = z.object({
 218 |   elementIds: z.array(z.string()),
 219 |   direction: z.enum(['horizontal', 'vertical'])
 220 | });
 221 | 
 222 | const QuerySchema = z.object({
 223 |   type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES) as [ExcalidrawElementType, ...ExcalidrawElementType[]]).optional(),
 224 |   filter: z.record(z.any()).optional()
 225 | });
 226 | 
 227 | const ResourceSchema = z.object({
 228 |   resource: z.enum(['scene', 'library', 'theme', 'elements'])
 229 | });
 230 | 
 231 | // Tool definitions
 232 | const tools: Tool[] = [
 233 |   {
 234 |     name: 'create_element',
 235 |     description: 'Create a new Excalidraw element',
 236 |     inputSchema: {
 237 |       type: 'object',
 238 |       properties: {
 239 |         type: { 
 240 |           type: 'string', 
 241 |           enum: Object.values(EXCALIDRAW_ELEMENT_TYPES) 
 242 |         },
 243 |         x: { type: 'number' },
 244 |         y: { type: 'number' },
 245 |         width: { type: 'number' },
 246 |         height: { type: 'number' },
 247 |         backgroundColor: { type: 'string' },
 248 |         strokeColor: { type: 'string' },
 249 |         strokeWidth: { type: 'number' },
 250 |         roughness: { type: 'number' },
 251 |         opacity: { type: 'number' },
 252 |         text: { type: 'string' },
 253 |         fontSize: { type: 'number' },
 254 |         fontFamily: { type: 'string' }
 255 |       },
 256 |       required: ['type', 'x', 'y']
 257 |     }
 258 |   },
 259 |   {
 260 |     name: 'update_element',
 261 |     description: 'Update an existing Excalidraw element',
 262 |     inputSchema: {
 263 |       type: 'object',
 264 |       properties: {
 265 |         id: { type: 'string' },
 266 |         type: { 
 267 |           type: 'string', 
 268 |           enum: Object.values(EXCALIDRAW_ELEMENT_TYPES) 
 269 |         },
 270 |         x: { type: 'number' },
 271 |         y: { type: 'number' },
 272 |         width: { type: 'number' },
 273 |         height: { type: 'number' },
 274 |         backgroundColor: { type: 'string' },
 275 |         strokeColor: { type: 'string' },
 276 |         strokeWidth: { type: 'number' },
 277 |         roughness: { type: 'number' },
 278 |         opacity: { type: 'number' },
 279 |         text: { type: 'string' },
 280 |         fontSize: { type: 'number' },
 281 |         fontFamily: { type: 'string' }
 282 |       },
 283 |       required: ['id']
 284 |     }
 285 |   },
 286 |   {
 287 |     name: 'delete_element',
 288 |     description: 'Delete an Excalidraw element',
 289 |     inputSchema: {
 290 |       type: 'object',
 291 |       properties: {
 292 |         id: { type: 'string' }
 293 |       },
 294 |       required: ['id']
 295 |     }
 296 |   },
 297 |   {
 298 |     name: 'query_elements',
 299 |     description: 'Query Excalidraw elements with optional filters',
 300 |     inputSchema: {
 301 |       type: 'object',
 302 |       properties: {
 303 |         type: { 
 304 |           type: 'string', 
 305 |           enum: Object.values(EXCALIDRAW_ELEMENT_TYPES) 
 306 |         },
 307 |         filter: { 
 308 |           type: 'object',
 309 |           additionalProperties: true
 310 |         }
 311 |       }
 312 |     }
 313 |   },
 314 |   {
 315 |     name: 'get_resource',
 316 |     description: 'Get an Excalidraw resource',
 317 |     inputSchema: {
 318 |       type: 'object',
 319 |       properties: {
 320 |         resource: { 
 321 |           type: 'string', 
 322 |           enum: ['scene', 'library', 'theme', 'elements'] 
 323 |         }
 324 |       },
 325 |       required: ['resource']
 326 |     }
 327 |   },
 328 |   {
 329 |     name: 'group_elements',
 330 |     description: 'Group multiple elements together',
 331 |     inputSchema: {
 332 |       type: 'object',
 333 |       properties: {
 334 |         elementIds: { 
 335 |           type: 'array',
 336 |           items: { type: 'string' }
 337 |         }
 338 |       },
 339 |       required: ['elementIds']
 340 |     }
 341 |   },
 342 |   {
 343 |     name: 'ungroup_elements',
 344 |     description: 'Ungroup a group of elements',
 345 |     inputSchema: {
 346 |       type: 'object',
 347 |       properties: {
 348 |         groupId: { type: 'string' }
 349 |       },
 350 |       required: ['groupId']
 351 |     }
 352 |   },
 353 |   {
 354 |     name: 'align_elements',
 355 |     description: 'Align elements to a specific position',
 356 |     inputSchema: {
 357 |       type: 'object',
 358 |       properties: {
 359 |         elementIds: { 
 360 |           type: 'array',
 361 |           items: { type: 'string' }
 362 |         },
 363 |         alignment: { 
 364 |           type: 'string', 
 365 |           enum: ['left', 'center', 'right', 'top', 'middle', 'bottom'] 
 366 |         }
 367 |       },
 368 |       required: ['elementIds', 'alignment']
 369 |     }
 370 |   },
 371 |   {
 372 |     name: 'distribute_elements',
 373 |     description: 'Distribute elements evenly',
 374 |     inputSchema: {
 375 |       type: 'object',
 376 |       properties: {
 377 |         elementIds: { 
 378 |           type: 'array',
 379 |           items: { type: 'string' }
 380 |         },
 381 |         direction: { 
 382 |           type: 'string', 
 383 |           enum: ['horizontal', 'vertical'] 
 384 |         }
 385 |       },
 386 |       required: ['elementIds', 'direction']
 387 |     }
 388 |   },
 389 |   {
 390 |     name: 'lock_elements',
 391 |     description: 'Lock elements to prevent modification',
 392 |     inputSchema: {
 393 |       type: 'object',
 394 |       properties: {
 395 |         elementIds: { 
 396 |           type: 'array',
 397 |           items: { type: 'string' }
 398 |         }
 399 |       },
 400 |       required: ['elementIds']
 401 |     }
 402 |   },
 403 |   {
 404 |     name: 'unlock_elements',
 405 |     description: 'Unlock elements to allow modification',
 406 |     inputSchema: {
 407 |       type: 'object',
 408 |       properties: {
 409 |         elementIds: { 
 410 |           type: 'array',
 411 |           items: { type: 'string' }
 412 |         }
 413 |       },
 414 |       required: ['elementIds']
 415 |     }
 416 |   },
 417 |   {
 418 |     name: 'create_from_mermaid',
 419 |     description: 'Convert a Mermaid diagram to Excalidraw elements and render them on the canvas',
 420 |     inputSchema: {
 421 |       type: 'object',
 422 |       properties: {
 423 |         mermaidDiagram: {
 424 |           type: 'string',
 425 |           description: 'The Mermaid diagram definition (e.g., "graph TD; A-->B; B-->C;")'
 426 |         },
 427 |         config: {
 428 |           type: 'object',
 429 |           description: 'Optional Mermaid configuration',
 430 |           properties: {
 431 |             startOnLoad: { type: 'boolean' },
 432 |             flowchart: {
 433 |               type: 'object',
 434 |               properties: {
 435 |                 curve: { type: 'string', enum: ['linear', 'basis'] }
 436 |               }
 437 |             },
 438 |             themeVariables: {
 439 |               type: 'object',
 440 |               properties: {
 441 |                 fontSize: { type: 'string' }
 442 |               }
 443 |             },
 444 |             maxEdges: { type: 'number' },
 445 |             maxTextSize: { type: 'number' }
 446 |           }
 447 |         }
 448 |       },
 449 |       required: ['mermaidDiagram']
 450 |     }
 451 |   },
 452 |   {
 453 |     name: 'batch_create_elements',
 454 |     description: 'Create multiple Excalidraw elements at once - ideal for complex diagrams',
 455 |     inputSchema: {
 456 |       type: 'object',
 457 |       properties: {
 458 |         elements: {
 459 |           type: 'array',
 460 |           items: {
 461 |             type: 'object',
 462 |             properties: {
 463 |               type: { 
 464 |                 type: 'string', 
 465 |                 enum: Object.values(EXCALIDRAW_ELEMENT_TYPES) 
 466 |               },
 467 |               x: { type: 'number' },
 468 |               y: { type: 'number' },
 469 |               width: { type: 'number' },
 470 |               height: { type: 'number' },
 471 |               backgroundColor: { type: 'string' },
 472 |               strokeColor: { type: 'string' },
 473 |               strokeWidth: { type: 'number' },
 474 |               roughness: { type: 'number' },
 475 |               opacity: { type: 'number' },
 476 |               text: { type: 'string' },
 477 |               fontSize: { type: 'number' },
 478 |               fontFamily: { type: 'string' }
 479 |             },
 480 |             required: ['type', 'x', 'y']
 481 |           }
 482 |         }
 483 |       },
 484 |       required: ['elements']
 485 |     }
 486 |   }
 487 | ];
 488 | 
 489 | // Initialize MCP server
 490 | const server = new Server(
 491 |   {
 492 |     name: "mcp-excalidraw-server",
 493 |     version: "1.0.2",
 494 |     description: "Advanced MCP server for Excalidraw with real-time canvas"
 495 |   },
 496 |   {
 497 |     capabilities: {
 498 |       tools: Object.fromEntries(tools.map(tool => [tool.name, {
 499 |         description: tool.description,
 500 |         inputSchema: tool.inputSchema
 501 |       }]))
 502 |     }
 503 |   }
 504 | );
 505 | 
 506 | // Helper function to convert text property to label format for Excalidraw
 507 | function convertTextToLabel(element: ServerElement): ServerElement {
 508 |   const { text, ...rest } = element;
 509 |   if (text) {
 510 |     // For standalone text elements, keep text as direct property
 511 |     if (element.type === 'text') {
 512 |       return element; // Keep text as direct property
 513 |     }
 514 |     // For other elements (rectangle, ellipse, diamond), convert to label format
 515 |     return {
 516 |       ...rest,
 517 |       label: { text }
 518 |     } as ServerElement;
 519 |   }
 520 |   return element;
 521 | }
 522 | 
 523 | // Set up request handler for tool calls
 524 | server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
 525 |   try {
 526 |     const { name, arguments: args } = request.params;
 527 |     logger.info(`Handling tool call: ${name}`);
 528 |     
 529 |     switch (name) {
 530 |       case 'create_element': {
 531 |         const params = ElementSchema.parse(args);
 532 |         logger.info('Creating element via MCP', { type: params.type });
 533 | 
 534 |         const id = generateId();
 535 |         const element: ServerElement = {
 536 |           id,
 537 |           ...params,
 538 |           createdAt: new Date().toISOString(),
 539 |           updatedAt: new Date().toISOString(),
 540 |           version: 1
 541 |         };
 542 | 
 543 |         // Convert text to label format for Excalidraw
 544 |         const excalidrawElement = convertTextToLabel(element);
 545 |         
 546 |         // Create element directly on HTTP server (no local storage)
 547 |         const canvasElement = await createElementOnCanvas(excalidrawElement);
 548 |         
 549 |         if (!canvasElement) {
 550 |           throw new Error('Failed to create element: HTTP server unavailable');
 551 |         }
 552 |         
 553 |         logger.info('Element created via MCP and synced to canvas', { 
 554 |           id: excalidrawElement.id, 
 555 |           type: excalidrawElement.type,
 556 |           synced: !!canvasElement 
 557 |         });
 558 |         
 559 |         return {
 560 |           content: [{ 
 561 |             type: 'text', 
 562 |             text: `Element created successfully!\n\n${JSON.stringify(canvasElement, null, 2)}\n\n✅ Synced to canvas` 
 563 |           }]
 564 |         };
 565 |       }
 566 |       
 567 |       case 'update_element': {
 568 |         const params = ElementIdSchema.merge(ElementSchema.partial()).parse(args);
 569 |         const { id, ...updates } = params;
 570 |         
 571 |         if (!id) throw new Error('Element ID is required');
 572 | 
 573 |         // Build update payload with timestamp and version increment
 574 |         const updatePayload: Partial<ServerElement> & { id: string } = {
 575 |           id,
 576 |           ...updates,
 577 |           updatedAt: new Date().toISOString()
 578 |         };
 579 | 
 580 |         // Convert text to label format for Excalidraw
 581 |         const excalidrawElement = convertTextToLabel(updatePayload as ServerElement);
 582 |         
 583 |         // Update element directly on HTTP server (no local storage)
 584 |         const canvasElement = await updateElementOnCanvas(excalidrawElement);
 585 |         
 586 |         if (!canvasElement) {
 587 |           throw new Error('Failed to update element: HTTP server unavailable or element not found');
 588 |         }
 589 |         
 590 |         logger.info('Element updated via MCP and synced to canvas', { 
 591 |           id: excalidrawElement.id, 
 592 |           synced: !!canvasElement 
 593 |         });
 594 |         
 595 |         return {
 596 |           content: [{ 
 597 |             type: 'text', 
 598 |             text: `Element updated successfully!\n\n${JSON.stringify(canvasElement, null, 2)}\n\n✅ Synced to canvas` 
 599 |           }]
 600 |         };
 601 |       }
 602 |       
 603 |       case 'delete_element': {
 604 |         const params = ElementIdSchema.parse(args);
 605 |         const { id } = params;
 606 | 
 607 |         // Delete element directly on HTTP server (no local storage)
 608 |         const canvasResult = await deleteElementOnCanvas(id);
 609 | 
 610 |         if (!canvasResult || !(canvasResult as ApiResponse).success) {
 611 |           throw new Error('Failed to delete element: HTTP server unavailable or element not found');
 612 |         }
 613 | 
 614 |         const result = { id, deleted: true, syncedToCanvas: true };
 615 |         logger.info('Element deleted via MCP and synced to canvas', result);
 616 | 
 617 |         return {
 618 |           content: [{
 619 |             type: 'text',
 620 |             text: `Element deleted successfully!\n\n${JSON.stringify(result, null, 2)}\n\n✅ Synced to canvas`
 621 |           }]
 622 |         };
 623 |       }
 624 |       
 625 |       case 'query_elements': {
 626 |         const params = QuerySchema.parse(args || {});
 627 |         const { type, filter } = params;
 628 |         
 629 |         try {
 630 |           // Build query parameters
 631 |           const queryParams = new URLSearchParams();
 632 |           if (type) queryParams.set('type', type);
 633 |           if (filter) {
 634 |             Object.entries(filter).forEach(([key, value]) => {
 635 |               queryParams.set(key, String(value));
 636 |             });
 637 |           }
 638 |           
 639 |           // Query elements from HTTP server
 640 |           const url = `${EXPRESS_SERVER_URL}/api/elements/search?${queryParams}`;
 641 |           const response = await fetch(url);
 642 |           
 643 |           if (!response.ok) {
 644 |             throw new Error(`HTTP server error: ${response.status} ${response.statusText}`);
 645 |           }
 646 |           
 647 |           const data = await response.json() as ApiResponse;
 648 |           const results = data.elements || [];
 649 |           
 650 |           return {
 651 |             content: [{ type: 'text', text: JSON.stringify(results, null, 2) }]
 652 |           };
 653 |         } catch (error) {
 654 |           throw new Error(`Failed to query elements: ${(error as Error).message}`);
 655 |         }
 656 |       }
 657 |       
 658 |       case 'get_resource': {
 659 |         const params = ResourceSchema.parse(args);
 660 |         const { resource } = params;
 661 |         logger.info('Getting resource', { resource });
 662 |         
 663 |         let result: any;
 664 |         switch (resource) {
 665 |           case 'scene':
 666 |             result = {
 667 |               theme: sceneState.theme,
 668 |               viewport: sceneState.viewport,
 669 |               selectedElements: Array.from(sceneState.selectedElements)
 670 |             };
 671 |             break;
 672 |           case 'library':
 673 |           case 'elements':
 674 |             try {
 675 |               // Get elements from HTTP server
 676 |               const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements`);
 677 |               if (!response.ok) {
 678 |                 throw new Error(`HTTP server error: ${response.status} ${response.statusText}`);
 679 |               }
 680 |               const data = await response.json() as ApiResponse;
 681 |               result = {
 682 |                 elements: data.elements || []
 683 |               };
 684 |             } catch (error) {
 685 |               throw new Error(`Failed to get elements: ${(error as Error).message}`);
 686 |             }
 687 |             break;
 688 |           case 'theme':
 689 |             result = {
 690 |               theme: sceneState.theme
 691 |             };
 692 |             break;
 693 |           default:
 694 |             throw new Error(`Unknown resource: ${resource}`);
 695 |         }
 696 |         
 697 |         return {
 698 |           content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
 699 |         };
 700 |       }
 701 |       
 702 |       case 'group_elements': {
 703 |         const params = ElementIdsSchema.parse(args);
 704 |         const { elementIds } = params;
 705 | 
 706 |         try {
 707 |           const groupId = generateId();
 708 |           sceneState.groups.set(groupId, elementIds);
 709 | 
 710 |           // Update elements on canvas with proper error handling
 711 |           // Fetch existing groups and append new groupId to preserve multi-group membership
 712 |           const updatePromises = elementIds.map(async (id) => {
 713 |             const element = await getElementFromCanvas(id);
 714 |             const existingGroups = element?.groupIds || [];
 715 |             const updatedGroupIds = [...existingGroups, groupId];
 716 |             return await updateElementOnCanvas({ id, groupIds: updatedGroupIds });
 717 |           });
 718 | 
 719 |           const results = await Promise.all(updatePromises);
 720 |           const successCount = results.filter(result => result).length;
 721 | 
 722 |           if (successCount === 0) {
 723 |             sceneState.groups.delete(groupId); // Rollback local state
 724 |             throw new Error('Failed to group any elements: HTTP server unavailable');
 725 |           }
 726 | 
 727 |           logger.info('Grouping elements', { elementIds, groupId, successCount });
 728 | 
 729 |           const result = { groupId, elementIds, successCount };
 730 |           return {
 731 |             content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
 732 |           };
 733 |         } catch (error) {
 734 |           throw new Error(`Failed to group elements: ${(error as Error).message}`);
 735 |         }
 736 |       }
 737 |       
 738 |       case 'ungroup_elements': {
 739 |         const params = GroupIdSchema.parse(args);
 740 |         const { groupId } = params;
 741 | 
 742 |         if (!sceneState.groups.has(groupId)) {
 743 |           throw new Error(`Group ${groupId} not found`);
 744 |         }
 745 | 
 746 |         try {
 747 |           const elementIds = sceneState.groups.get(groupId);
 748 |           sceneState.groups.delete(groupId);
 749 | 
 750 |           // Update elements on canvas, removing only this specific groupId
 751 |           const updatePromises = (elementIds ?? []).map(async (id) => {
 752 |             // Fetch current element to get existing groupIds
 753 |             const element = await getElementFromCanvas(id);
 754 |             if (!element) {
 755 |               logger.warn(`Element ${id} not found on canvas, skipping ungroup`);
 756 |               return null;
 757 |             }
 758 | 
 759 |             // Remove only the specific groupId, preserve others
 760 |             const updatedGroupIds = (element.groupIds || []).filter(gid => gid !== groupId);
 761 |             return await updateElementOnCanvas({ id, groupIds: updatedGroupIds });
 762 |           });
 763 | 
 764 |           const results = await Promise.all(updatePromises);
 765 |           const successCount = results.filter(result => result !== null).length;
 766 | 
 767 |           if (successCount === 0) {
 768 |             logger.warn('Failed to ungroup any elements: HTTP server unavailable or elements not found');
 769 |           }
 770 | 
 771 |           logger.info('Ungrouping elements', { groupId, elementIds, successCount });
 772 | 
 773 |           const result = { groupId, ungrouped: true, elementIds, successCount };
 774 |           return {
 775 |             content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
 776 |           };
 777 |         } catch (error) {
 778 |           throw new Error(`Failed to ungroup elements: ${(error as Error).message}`);
 779 |         }
 780 |       }
 781 |       
 782 |       case 'align_elements': {
 783 |         const params = AlignElementsSchema.parse(args);
 784 |         const { elementIds, alignment } = params;
 785 |         
 786 |         // Implementation would align elements based on the specified alignment
 787 |         logger.info('Aligning elements', { elementIds, alignment });
 788 |         
 789 |         const result = { aligned: true, elementIds, alignment };
 790 |         return {
 791 |           content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
 792 |         };
 793 |       }
 794 |       
 795 |       case 'distribute_elements': {
 796 |         const params = DistributeElementsSchema.parse(args);
 797 |         const { elementIds, direction } = params;
 798 |         
 799 |         // Implementation would distribute elements based on the specified direction
 800 |         logger.info('Distributing elements', { elementIds, direction });
 801 |         
 802 |         const result = { distributed: true, elementIds, direction };
 803 |         return {
 804 |           content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
 805 |         };
 806 |       }
 807 |       
 808 |       case 'lock_elements': {
 809 |         const params = ElementIdsSchema.parse(args);
 810 |         const { elementIds } = params;
 811 |         
 812 |         try {
 813 |           // Lock elements through HTTP API updates
 814 |           const updatePromises = elementIds.map(async (id) => {
 815 |             return await updateElementOnCanvas({ id, locked: true });
 816 |           });
 817 |           
 818 |           const results = await Promise.all(updatePromises);
 819 |           const successCount = results.filter(result => result).length;
 820 |           
 821 |           if (successCount === 0) {
 822 |             throw new Error('Failed to lock any elements: HTTP server unavailable');
 823 |           }
 824 |           
 825 |           const result = { locked: true, elementIds, successCount };
 826 |           return {
 827 |             content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
 828 |           };
 829 |         } catch (error) {
 830 |           throw new Error(`Failed to lock elements: ${(error as Error).message}`);
 831 |         }
 832 |       }
 833 |       
 834 |       case 'unlock_elements': {
 835 |         const params = ElementIdsSchema.parse(args);
 836 |         const { elementIds } = params;
 837 |         
 838 |         try {
 839 |           // Unlock elements through HTTP API updates
 840 |           const updatePromises = elementIds.map(async (id) => {
 841 |             return await updateElementOnCanvas({ id, locked: false });
 842 |           });
 843 |           
 844 |           const results = await Promise.all(updatePromises);
 845 |           const successCount = results.filter(result => result).length;
 846 |           
 847 |           if (successCount === 0) {
 848 |             throw new Error('Failed to unlock any elements: HTTP server unavailable');
 849 |           }
 850 |           
 851 |           const result = { unlocked: true, elementIds, successCount };
 852 |           return {
 853 |             content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
 854 |           };
 855 |         } catch (error) {
 856 |           throw new Error(`Failed to unlock elements: ${(error as Error).message}`);
 857 |         }
 858 |       }
 859 |       
 860 |       case 'create_from_mermaid': {
 861 |         const params = z.object({
 862 |           mermaidDiagram: z.string(),
 863 |           config: z.object({
 864 |             startOnLoad: z.boolean().optional(),
 865 |             flowchart: z.object({
 866 |               curve: z.enum(['linear', 'basis']).optional()
 867 |             }).optional(),
 868 |             themeVariables: z.object({
 869 |               fontSize: z.string().optional()
 870 |             }).optional(),
 871 |             maxEdges: z.number().optional(),
 872 |             maxTextSize: z.number().optional()
 873 |           }).optional()
 874 |         }).parse(args);
 875 |         
 876 |         logger.info('Creating Excalidraw elements from Mermaid diagram via MCP', {
 877 |           diagramLength: params.mermaidDiagram.length,
 878 |           hasConfig: !!params.config
 879 |         });
 880 | 
 881 |         try {
 882 |           // Send the Mermaid diagram to the frontend via the API
 883 |           // The frontend will use mermaid-to-excalidraw to convert it
 884 |           const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements/from-mermaid`, {
 885 |             method: 'POST',
 886 |             headers: { 'Content-Type': 'application/json' },
 887 |             body: JSON.stringify({
 888 |               mermaidDiagram: params.mermaidDiagram,
 889 |               config: params.config
 890 |             })
 891 |           });
 892 | 
 893 |           if (!response.ok) {
 894 |             throw new Error(`HTTP server error: ${response.status} ${response.statusText}`);
 895 |           }
 896 | 
 897 |           const result = await response.json() as ApiResponse;
 898 |           
 899 |           logger.info('Mermaid diagram sent to frontend for conversion', {
 900 |             success: result.success
 901 |           });
 902 | 
 903 |           return {
 904 |             content: [{
 905 |               type: 'text',
 906 |               text: `Mermaid diagram sent for conversion!\n\n${JSON.stringify(result, null, 2)}\n\n⚠️  Note: The actual conversion happens in the frontend canvas with DOM access. Open the canvas at ${EXPRESS_SERVER_URL} to see the diagram rendered.`
 907 |             }]
 908 |           };
 909 |         } catch (error) {
 910 |           throw new Error(`Failed to process Mermaid diagram: ${(error as Error).message}`);
 911 |         }
 912 |       }
 913 |       
 914 |       case 'batch_create_elements': {
 915 |         const params = z.object({ elements: z.array(ElementSchema) }).parse(args);
 916 |         logger.info('Batch creating elements via MCP', { count: params.elements.length });
 917 | 
 918 |         const createdElements: ServerElement[] = [];
 919 |         
 920 |         // Create each element with unique ID
 921 |         for (const elementData of params.elements) {
 922 |           const id = generateId();
 923 |           const element: ServerElement = {
 924 |             id,
 925 |             ...elementData,
 926 |             createdAt: new Date().toISOString(),
 927 |             updatedAt: new Date().toISOString(),
 928 |             version: 1
 929 |           };
 930 |           
 931 |           // Convert text to label format for Excalidraw
 932 |           const excalidrawElement = convertTextToLabel(element);
 933 |           createdElements.push(excalidrawElement);
 934 |         }
 935 |         
 936 |         // Create all elements directly on HTTP server (no local storage)
 937 |         const canvasElements = await batchCreateElementsOnCanvas(createdElements);
 938 |         
 939 |         if (!canvasElements) {
 940 |           throw new Error('Failed to batch create elements: HTTP server unavailable');
 941 |         }
 942 |         
 943 |         const result = {
 944 |           success: true,
 945 |           elements: canvasElements,
 946 |           count: canvasElements.length,
 947 |           syncedToCanvas: true
 948 |         };
 949 |         
 950 |         logger.info('Batch elements created via MCP and synced to canvas', { 
 951 |           count: result.count,
 952 |           synced: result.syncedToCanvas 
 953 |         });
 954 |         
 955 |         return {
 956 |           content: [{ 
 957 |             type: 'text', 
 958 |             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)'}` 
 959 |           }]
 960 |         };
 961 |       }
 962 |       
 963 |       default:
 964 |         throw new Error(`Unknown tool: ${name}`);
 965 |     }
 966 |   } catch (error) {
 967 |     logger.error(`Error handling tool call: ${(error as Error).message}`, { error });
 968 |     return {
 969 |       content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
 970 |       isError: true
 971 |     };
 972 |   }
 973 | });
 974 | 
 975 | // Set up request handler for listing available tools
 976 | server.setRequestHandler(ListToolsRequestSchema, async () => {
 977 |   logger.info('Listing available tools');
 978 |   return { tools };
 979 | });
 980 | 
 981 | // Start server with transport based on mode
 982 | async function runServer(): Promise<void> {
 983 |   try {
 984 |     logger.info('Starting Excalidraw MCP server...');
 985 |     
 986 |     const transportMode = process.env.MCP_TRANSPORT_MODE || 'stdio';
 987 |     let transport;
 988 |     
 989 |     if (transportMode === 'http') {
 990 |       const port = parseInt(process.env.PORT || '3000', 10);
 991 |       const host = process.env.HOST || 'localhost';
 992 |       
 993 |       logger.info(`Starting HTTP server on ${host}:${port}`);
 994 |       // Here you would create an HTTP transport
 995 |       // This is a placeholder - actual HTTP transport implementation would need to be added
 996 |       transport = new StdioServerTransport(); // Fallback to stdio for now
 997 |     } else {
 998 |       // Default to stdio transport
 999 |       transport = new StdioServerTransport();
1000 |     }
1001 |     
1002 |     // Add a debug message before connecting
1003 |     logger.debug('Connecting to transport...');
1004 |     
1005 |     await server.connect(transport);
1006 |     logger.info(`Excalidraw MCP server running on ${transportMode}`);
1007 |     
1008 |     // Keep the process running
1009 |     process.stdin.resume();
1010 |   } catch (error) {
1011 |     logger.error('Error starting server:', error);
1012 |     process.stderr.write(`Failed to start MCP server: ${(error as Error).message}\n${(error as Error).stack}\n`);
1013 |     process.exit(1);
1014 |   }
1015 | }
1016 | 
1017 | // Add global error handlers
1018 | process.on('uncaughtException', (error: Error) => {
1019 |   logger.error('Uncaught exception:', error);
1020 |   process.stderr.write(`UNCAUGHT EXCEPTION: ${error.message}\n${error.stack}\n`);
1021 |   setTimeout(() => process.exit(1), 1000);
1022 | });
1023 | 
1024 | process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
1025 |   logger.error('Unhandled promise rejection:', reason);
1026 |   process.stderr.write(`UNHANDLED REJECTION: ${reason}\n`);
1027 |   setTimeout(() => process.exit(1), 1000);
1028 | });
1029 | 
1030 | // For testing and debugging purposes
1031 | if (process.env.DEBUG === 'true') {
1032 |   logger.debug('Debug mode enabled');
1033 | }
1034 | 
1035 | // Start the server if this file is run directly
1036 | if (fileURLToPath(import.meta.url) === process.argv[1]) {
1037 |   runServer().catch(error => {
1038 |     logger.error('Failed to start server:', error);
1039 |     process.exit(1);
1040 |   });
1041 | }
1042 | 
1043 | export default runServer;
```