mcp-excalidraw-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +424 -0
- package/dist/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
- package/dist/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
- package/dist/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
- package/dist/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
- package/dist/assets/Tableau10-B-NsZVaP.js +1 -0
- package/dist/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
- package/dist/assets/ar-SA-G6X2FPQ2-BS3fMnev.js +10 -0
- package/dist/assets/arc-0Fwkaye8.js +1 -0
- package/dist/assets/array-BKyUJesY.js +1 -0
- package/dist/assets/az-AZ-76LH7QW2-DNgzWA8S.js +1 -0
- package/dist/assets/bg-BG-XCXSNQG7-CULoJKdI.js +5 -0
- package/dist/assets/blockDiagram-38ab4fdb-CVOkr1Ma.js +118 -0
- package/dist/assets/bn-BD-2XOGV67Q-9DmWphrb.js +5 -0
- package/dist/assets/c4Diagram-3d4e48cf-BwpfSRgq.js +10 -0
- package/dist/assets/ca-ES-6MX7JW3Y-gYoEYP36.js +8 -0
- package/dist/assets/channel-BgX0NHoH.js +1 -0
- package/dist/assets/classDiagram-70f12bd4-ftK2tRy5.js +2 -0
- package/dist/assets/classDiagram-v2-f2320105--Nu78BDB.js +2 -0
- package/dist/assets/clone-sj1RjrIX.js +1 -0
- package/dist/assets/createText-2e5e7dd3-DPYwmS_z.js +7 -0
- package/dist/assets/cs-CZ-2BRQDIVT-DgwhrQZi.js +11 -0
- package/dist/assets/da-DK-5WZEPLOC-BL1Ng5As.js +5 -0
- package/dist/assets/de-DE-XR44H4JA-Cw-ySDmq.js +8 -0
- package/dist/assets/directory-open-01563666-DWU9wJ6I.js +1 -0
- package/dist/assets/directory-open-4ed118d0-BzWybGaI.js +1 -0
- package/dist/assets/edges-e0da2a9e-CB2w3jS2.js +4 -0
- package/dist/assets/el-GR-BZB4AONW-BPJAfWZm.js +10 -0
- package/dist/assets/erDiagram-9861fffd-VwqM-D3G.js +51 -0
- package/dist/assets/es-ES-U4NZUMDT-oMrzPWSn.js +9 -0
- package/dist/assets/eu-ES-A7QVB2H4-DFshkDl1.js +11 -0
- package/dist/assets/fa-IR-HGAKTJCU-DVU2rysM.js +8 -0
- package/dist/assets/fi-FI-Z5N7JZ37-BFKGqZcw.js +6 -0
- package/dist/assets/file-open-002ab408-DIuFHtCF.js +1 -0
- package/dist/assets/file-open-7c801643-684qeFg4.js +1 -0
- package/dist/assets/file-save-3189631c-x92wctJd.js +1 -0
- package/dist/assets/file-save-745eba88-Bb9F9Kg7.js +1 -0
- package/dist/assets/flowDb-956e92f1-CZj2LmRK.js +10 -0
- package/dist/assets/flowDiagram-66a62f08-D_vhln5h.js +4 -0
- package/dist/assets/flowDiagram-v2-96b9c2cf-CShVLez9.js +1 -0
- package/dist/assets/flowchart-elk-definition-4a651766-BJYKnLZA.js +139 -0
- package/dist/assets/fr-FR-RHASNOE6-M3Leevu1.js +9 -0
- package/dist/assets/ganttDiagram-c361ad54-Dl0RXJiR.js +257 -0
- package/dist/assets/gitGraphDiagram-72cf32ee-Cckr93z5.js +70 -0
- package/dist/assets/gl-ES-HMX3MZ6V-CM77q-gv.js +10 -0
- package/dist/assets/graph-CsvWB58Y.js +1 -0
- package/dist/assets/he-IL-6SHJWFNN-B3UI_ebx.js +10 -0
- package/dist/assets/hi-IN-IWLTKZ5I-DUAO_Nd9.js +4 -0
- package/dist/assets/hu-HU-A5ZG7DT2-Dm7uZbxs.js +7 -0
- package/dist/assets/id-ID-SAP4L64H-sn247bNe.js +10 -0
- package/dist/assets/image-blob-reduce.esm-B6b2_-a4.js +7 -0
- package/dist/assets/index-3862675e-pSTifqE7.js +1 -0
- package/dist/assets/index-CMTjDsOp.js +97 -0
- package/dist/assets/infoDiagram-f8f76790-Cy0QfHmF.js +7 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/it-IT-JPQ66NNP-C3cx3eLC.js +11 -0
- package/dist/assets/ja-JP-DBVTYXUO-P2fPONfc.js +8 -0
- package/dist/assets/journeyDiagram-49397b02-AWYgjzgS.js +139 -0
- package/dist/assets/kaa-6HZHGXH3-wmoL7do0.js +1 -0
- package/dist/assets/kab-KAB-ZGHBKWFO-DNNu0Xkz.js +8 -0
- package/dist/assets/katex-ChWnQ-fc.js +261 -0
- package/dist/assets/kk-KZ-P5N5QNE5-BZEEoSCw.js +1 -0
- package/dist/assets/km-KH-HSX4SM5Z-4Sf3SjAI.js +11 -0
- package/dist/assets/ko-KR-MTYHY66A-DTzTxHW5.js +9 -0
- package/dist/assets/ku-TR-6OUDTVRD-eWqCXim6.js +9 -0
- package/dist/assets/layout-ErVDAsYA.js +1 -0
- package/dist/assets/line-Cvp4skCK.js +1 -0
- package/dist/assets/linear-DlBkj_hR.js +1 -0
- package/dist/assets/lt-LT-XHIRWOB4-BzqmXLnL.js +3 -0
- package/dist/assets/lv-LV-5QDEKY6T-alH_HCjW.js +7 -0
- package/dist/assets/main-B9Rh8YyQ.css +1 -0
- package/dist/assets/main-C1TkgC3p.js +254 -0
- package/dist/assets/mindmap-definition-fc14e90a-DQB0p-81.js +425 -0
- package/dist/assets/mr-IN-CRQNXWMA-Cq2VIpVa.js +13 -0
- package/dist/assets/my-MM-5M5IBNSE-CBPHDdNv.js +1 -0
- package/dist/assets/nb-NO-T6EIAALU-BUNp_A07.js +10 -0
- package/dist/assets/nl-NL-IS3SIHDZ-CNz8gWhx.js +8 -0
- package/dist/assets/nn-NO-6E72VCQL-BFz6U-AT.js +8 -0
- package/dist/assets/oc-FR-POXYY2M6-BjY1Oz8X.js +8 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pa-IN-N4M65BXN-bLY9e9o3.js +4 -0
- package/dist/assets/path-CbwjOpE9.js +1 -0
- package/dist/assets/pica-JUO0Loj6.js +7 -0
- package/dist/assets/pieDiagram-8a3498a8-BD6Rsa6P.js +35 -0
- package/dist/assets/pl-PL-T2D74RX3-CpLUuJZ4.js +9 -0
- package/dist/assets/pt-BR-5N22H2LF-DXSmPIb5.js +9 -0
- package/dist/assets/pt-PT-UZXXM6DQ-CtilzVM5.js +9 -0
- package/dist/assets/quadrantDiagram-120e2f19-CvG-FK5I.js +7 -0
- package/dist/assets/requirementDiagram-deff3bca-CVh-bBIY.js +52 -0
- package/dist/assets/ro-RO-JPDTUUEW-4SIE29UH.js +11 -0
- package/dist/assets/roundRect-0PYZxl1G.js +1 -0
- package/dist/assets/ru-RU-B4JR7IUQ-yWKAM4lo.js +9 -0
- package/dist/assets/sankeyDiagram-04a897e0-Bb-ywaQF.js +8 -0
- package/dist/assets/sequenceDiagram-704730f1-BZu5e-R7.js +122 -0
- package/dist/assets/si-LK-N5RQ5JYF-D-XE0eOh.js +1 -0
- package/dist/assets/sk-SK-C5VTKIMK-DTdiYCrX.js +6 -0
- package/dist/assets/sl-SI-NN7IZMDC-qcTJE3hs.js +6 -0
- package/dist/assets/stateDiagram-587899a1-CoklS1cy.js +1 -0
- package/dist/assets/stateDiagram-v2-d93cdb3a-JBfFG9Hu.js +1 -0
- package/dist/assets/styles-6aaf32cf-Cg7CSuxw.js +207 -0
- package/dist/assets/styles-9a916d00-Ny9GZRfC.js +160 -0
- package/dist/assets/styles-c10674c1-BMpWBEge.js +116 -0
- package/dist/assets/subset-shared.chunk-tHzjs2ro.js +84 -0
- package/dist/assets/subset-worker.chunk-CuKy9sB1.js +1 -0
- package/dist/assets/sv-SE-XGPEYMSR-Bi6tm-oG.js +10 -0
- package/dist/assets/svgDrawCommon-08f97a94-CEEsVKXL.js +1 -0
- package/dist/assets/ta-IN-2NMHFXQM-C7Igaqv2.js +9 -0
- package/dist/assets/th-TH-HPSO5L25-BdAfwagC.js +2 -0
- package/dist/assets/timeline-definition-85554ec2-DsgJKruU.js +61 -0
- package/dist/assets/tr-TR-DEFEU3FU-BPBRoOZh.js +7 -0
- package/dist/assets/uk-UA-QMV73CPH-CeJGl_H3.js +6 -0
- package/dist/assets/vi-VN-M7AON7JQ-D537-quZ.js +5 -0
- package/dist/assets/xychartDiagram-e933f94c-DqfSpZR2.js +7 -0
- package/dist/assets/zh-CN-LNUGB5OW-Bd8rr_cr.js +10 -0
- package/dist/assets/zh-HK-E62DVLB3-Y1MdNiXR.js +1 -0
- package/dist/assets/zh-TW-RAJ6MFWO-BAzsCe6B.js +9 -0
- package/dist/frontend/index.html +241 -0
- package/package.json +78 -0
- package/src/index.js +1002 -0
- package/src/server.js +377 -0
- package/src/types.js +36 -0
- package/src/utils/logger.js +27 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,1002 @@
|
|
|
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 { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import {
|
|
10
|
+
CallToolRequestSchema,
|
|
11
|
+
ListToolsRequestSchema
|
|
12
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import dotenv from 'dotenv';
|
|
15
|
+
import logger from './utils/logger.js';
|
|
16
|
+
import {
|
|
17
|
+
elements,
|
|
18
|
+
validateElement,
|
|
19
|
+
generateId,
|
|
20
|
+
EXCALIDRAW_ELEMENT_TYPES
|
|
21
|
+
} from './types.js';
|
|
22
|
+
import fetch from 'node-fetch';
|
|
23
|
+
|
|
24
|
+
// Load environment variables
|
|
25
|
+
dotenv.config();
|
|
26
|
+
|
|
27
|
+
// Express server configuration
|
|
28
|
+
const EXPRESS_SERVER_URL = process.env.EXPRESS_SERVER_URL || 'http://localhost:3000';
|
|
29
|
+
const ENABLE_CANVAS_SYNC = process.env.ENABLE_CANVAS_SYNC !== 'false'; // Default to true
|
|
30
|
+
|
|
31
|
+
// Helper functions to sync with Express server (canvas)
|
|
32
|
+
async function syncToCanvas(operation, data) {
|
|
33
|
+
if (!ENABLE_CANVAS_SYNC) {
|
|
34
|
+
logger.debug('Canvas sync disabled, skipping');
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
let url, options;
|
|
40
|
+
|
|
41
|
+
switch (operation) {
|
|
42
|
+
case 'create':
|
|
43
|
+
url = `${EXPRESS_SERVER_URL}/api/elements`;
|
|
44
|
+
options = {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify(data)
|
|
48
|
+
};
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case 'update':
|
|
52
|
+
url = `${EXPRESS_SERVER_URL}/api/elements/${data.id}`;
|
|
53
|
+
options = {
|
|
54
|
+
method: 'PUT',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify(data)
|
|
57
|
+
};
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
case 'delete':
|
|
61
|
+
url = `${EXPRESS_SERVER_URL}/api/elements/${data.id}`;
|
|
62
|
+
options = { method: 'DELETE' };
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'batch_create':
|
|
66
|
+
url = `${EXPRESS_SERVER_URL}/api/elements/batch`;
|
|
67
|
+
options = {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ elements: data })
|
|
71
|
+
};
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
logger.warn(`Unknown sync operation: ${operation}`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
logger.debug(`Syncing to canvas: ${operation}`, { url, data });
|
|
80
|
+
const response = await fetch(url, options);
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`Canvas sync failed: ${response.status} ${response.statusText}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await response.json();
|
|
87
|
+
logger.debug(`Canvas sync successful: ${operation}`, result);
|
|
88
|
+
return result;
|
|
89
|
+
|
|
90
|
+
} catch (error) {
|
|
91
|
+
logger.warn(`Canvas sync failed for ${operation}:`, error.message);
|
|
92
|
+
// Don't throw - we want MCP operations to work even if canvas is unavailable
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Helper to sync element creation to canvas
|
|
98
|
+
async function createElementOnCanvas(elementData) {
|
|
99
|
+
const result = await syncToCanvas('create', elementData);
|
|
100
|
+
return result?.element || elementData;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Helper to sync element update to canvas
|
|
104
|
+
async function updateElementOnCanvas(elementData) {
|
|
105
|
+
const result = await syncToCanvas('update', elementData);
|
|
106
|
+
return result?.element || elementData;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Helper to sync element deletion to canvas
|
|
110
|
+
async function deleteElementOnCanvas(elementId) {
|
|
111
|
+
const result = await syncToCanvas('delete', { id: elementId });
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Helper to sync batch creation to canvas
|
|
116
|
+
async function batchCreateElementsOnCanvas(elementsData) {
|
|
117
|
+
const result = await syncToCanvas('batch_create', elementsData);
|
|
118
|
+
return result?.elements || elementsData;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// In-memory storage for scene state
|
|
122
|
+
const sceneState = {
|
|
123
|
+
theme: 'light',
|
|
124
|
+
viewport: { x: 0, y: 0, zoom: 1 },
|
|
125
|
+
selectedElements: new Set(),
|
|
126
|
+
groups: new Map()
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Schema definitions using zod
|
|
130
|
+
const ElementSchema = z.object({
|
|
131
|
+
type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES)),
|
|
132
|
+
x: z.number(),
|
|
133
|
+
y: z.number(),
|
|
134
|
+
width: z.number().optional(),
|
|
135
|
+
height: z.number().optional(),
|
|
136
|
+
points: z.array(z.object({ x: z.number(), y: z.number() })).optional(),
|
|
137
|
+
backgroundColor: z.string().optional(),
|
|
138
|
+
strokeColor: z.string().optional(),
|
|
139
|
+
strokeWidth: z.number().optional(),
|
|
140
|
+
roughness: z.number().optional(),
|
|
141
|
+
opacity: z.number().optional(),
|
|
142
|
+
text: z.string().optional(),
|
|
143
|
+
fontSize: z.number().optional(),
|
|
144
|
+
fontFamily: z.string().optional()
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const ElementIdSchema = z.object({
|
|
148
|
+
id: z.string()
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const ElementIdsSchema = z.object({
|
|
152
|
+
elementIds: z.array(z.string())
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const GroupIdSchema = z.object({
|
|
156
|
+
groupId: z.string()
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const AlignElementsSchema = z.object({
|
|
160
|
+
elementIds: z.array(z.string()),
|
|
161
|
+
alignment: z.enum(['left', 'center', 'right', 'top', 'middle', 'bottom'])
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const DistributeElementsSchema = z.object({
|
|
165
|
+
elementIds: z.array(z.string()),
|
|
166
|
+
direction: z.enum(['horizontal', 'vertical'])
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const QuerySchema = z.object({
|
|
170
|
+
type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES)).optional(),
|
|
171
|
+
filter: z.record(z.any()).optional()
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const ResourceSchema = z.object({
|
|
175
|
+
resource: z.enum(['scene', 'library', 'theme', 'elements'])
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Initialize MCP server
|
|
179
|
+
const server = new Server(
|
|
180
|
+
{
|
|
181
|
+
name: "excalidraw-mcp-server",
|
|
182
|
+
version: "1.0.0",
|
|
183
|
+
description: "MCP server for Excalidraw"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
capabilities: {
|
|
187
|
+
tools: {
|
|
188
|
+
create_element: {
|
|
189
|
+
description: 'Create a new Excalidraw element',
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties: {
|
|
193
|
+
type: {
|
|
194
|
+
type: 'string',
|
|
195
|
+
enum: Object.values(EXCALIDRAW_ELEMENT_TYPES)
|
|
196
|
+
},
|
|
197
|
+
x: { type: 'number' },
|
|
198
|
+
y: { type: 'number' },
|
|
199
|
+
width: { type: 'number' },
|
|
200
|
+
height: { type: 'number' },
|
|
201
|
+
backgroundColor: { type: 'string' },
|
|
202
|
+
strokeColor: { type: 'string' },
|
|
203
|
+
strokeWidth: { type: 'number' },
|
|
204
|
+
roughness: { type: 'number' },
|
|
205
|
+
opacity: { type: 'number' },
|
|
206
|
+
text: { type: 'string' },
|
|
207
|
+
fontSize: { type: 'number' },
|
|
208
|
+
fontFamily: { type: 'string' }
|
|
209
|
+
},
|
|
210
|
+
required: ['type', 'x', 'y']
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
update_element: {
|
|
214
|
+
description: 'Update an existing Excalidraw element',
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
properties: {
|
|
218
|
+
id: { type: 'string' },
|
|
219
|
+
type: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
enum: Object.values(EXCALIDRAW_ELEMENT_TYPES)
|
|
222
|
+
},
|
|
223
|
+
x: { type: 'number' },
|
|
224
|
+
y: { type: 'number' },
|
|
225
|
+
width: { type: 'number' },
|
|
226
|
+
height: { type: 'number' },
|
|
227
|
+
backgroundColor: { type: 'string' },
|
|
228
|
+
strokeColor: { type: 'string' },
|
|
229
|
+
strokeWidth: { type: 'number' },
|
|
230
|
+
roughness: { type: 'number' },
|
|
231
|
+
opacity: { type: 'number' },
|
|
232
|
+
text: { type: 'string' },
|
|
233
|
+
fontSize: { type: 'number' },
|
|
234
|
+
fontFamily: { type: 'string' }
|
|
235
|
+
},
|
|
236
|
+
required: ['id']
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
delete_element: {
|
|
240
|
+
description: 'Delete an Excalidraw element',
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
id: { type: 'string' }
|
|
245
|
+
},
|
|
246
|
+
required: ['id']
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
query_elements: {
|
|
250
|
+
description: 'Query Excalidraw elements with optional filters',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
type: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
enum: Object.values(EXCALIDRAW_ELEMENT_TYPES)
|
|
257
|
+
},
|
|
258
|
+
filter: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
additionalProperties: true
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
get_resource: {
|
|
266
|
+
description: 'Get an Excalidraw resource',
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: {
|
|
270
|
+
resource: {
|
|
271
|
+
type: 'string',
|
|
272
|
+
enum: ['scene', 'library', 'theme', 'elements']
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
required: ['resource']
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
group_elements: {
|
|
279
|
+
description: 'Group multiple elements together',
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: {
|
|
283
|
+
elementIds: {
|
|
284
|
+
type: 'array',
|
|
285
|
+
items: { type: 'string' }
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
required: ['elementIds']
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
ungroup_elements: {
|
|
292
|
+
description: 'Ungroup a group of elements',
|
|
293
|
+
inputSchema: {
|
|
294
|
+
type: 'object',
|
|
295
|
+
properties: {
|
|
296
|
+
groupId: { type: 'string' }
|
|
297
|
+
},
|
|
298
|
+
required: ['groupId']
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
align_elements: {
|
|
302
|
+
description: 'Align elements to a specific position',
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: 'object',
|
|
305
|
+
properties: {
|
|
306
|
+
elementIds: {
|
|
307
|
+
type: 'array',
|
|
308
|
+
items: { type: 'string' }
|
|
309
|
+
},
|
|
310
|
+
alignment: {
|
|
311
|
+
type: 'string',
|
|
312
|
+
enum: ['left', 'center', 'right', 'top', 'middle', 'bottom']
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
required: ['elementIds', 'alignment']
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
distribute_elements: {
|
|
319
|
+
description: 'Distribute elements evenly',
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
properties: {
|
|
323
|
+
elementIds: {
|
|
324
|
+
type: 'array',
|
|
325
|
+
items: { type: 'string' }
|
|
326
|
+
},
|
|
327
|
+
direction: {
|
|
328
|
+
type: 'string',
|
|
329
|
+
enum: ['horizontal', 'vertical']
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
required: ['elementIds', 'direction']
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
lock_elements: {
|
|
336
|
+
description: 'Lock elements to prevent modification',
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: 'object',
|
|
339
|
+
properties: {
|
|
340
|
+
elementIds: {
|
|
341
|
+
type: 'array',
|
|
342
|
+
items: { type: 'string' }
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
required: ['elementIds']
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
unlock_elements: {
|
|
349
|
+
description: 'Unlock elements to allow modification',
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: 'object',
|
|
352
|
+
properties: {
|
|
353
|
+
elementIds: {
|
|
354
|
+
type: 'array',
|
|
355
|
+
items: { type: 'string' }
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
required: ['elementIds']
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
batch_create_elements: {
|
|
362
|
+
description: 'Create multiple Excalidraw elements at once - ideal for complex diagrams',
|
|
363
|
+
inputSchema: {
|
|
364
|
+
type: 'object',
|
|
365
|
+
properties: {
|
|
366
|
+
elements: {
|
|
367
|
+
type: 'array',
|
|
368
|
+
items: {
|
|
369
|
+
type: 'object',
|
|
370
|
+
properties: {
|
|
371
|
+
type: {
|
|
372
|
+
type: 'string',
|
|
373
|
+
enum: Object.values(EXCALIDRAW_ELEMENT_TYPES)
|
|
374
|
+
},
|
|
375
|
+
x: { type: 'number' },
|
|
376
|
+
y: { type: 'number' },
|
|
377
|
+
width: { type: 'number' },
|
|
378
|
+
height: { type: 'number' },
|
|
379
|
+
backgroundColor: { type: 'string' },
|
|
380
|
+
strokeColor: { type: 'string' },
|
|
381
|
+
strokeWidth: { type: 'number' },
|
|
382
|
+
roughness: { type: 'number' },
|
|
383
|
+
opacity: { type: 'number' },
|
|
384
|
+
text: { type: 'string' },
|
|
385
|
+
fontSize: { type: 'number' },
|
|
386
|
+
fontFamily: { type: 'string' }
|
|
387
|
+
},
|
|
388
|
+
required: ['type', 'x', 'y']
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
required: ['elements']
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Set up request handler for tool calls
|
|
401
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
402
|
+
try {
|
|
403
|
+
const { name, arguments: args } = request.params;
|
|
404
|
+
logger.info(`Handling tool call: ${name}`);
|
|
405
|
+
|
|
406
|
+
switch (name) {
|
|
407
|
+
case 'create_element': {
|
|
408
|
+
const params = ElementSchema.parse(args);
|
|
409
|
+
logger.info('Creating element via MCP', { type: params.type });
|
|
410
|
+
|
|
411
|
+
const id = generateId();
|
|
412
|
+
const element = {
|
|
413
|
+
id,
|
|
414
|
+
...params,
|
|
415
|
+
createdAt: new Date().toISOString(),
|
|
416
|
+
updatedAt: new Date().toISOString(),
|
|
417
|
+
version: 1
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Store locally (MCP server storage)
|
|
421
|
+
elements.set(id, element);
|
|
422
|
+
|
|
423
|
+
// Sync to canvas (Express server + WebSocket broadcast)
|
|
424
|
+
const canvasElement = await createElementOnCanvas(element);
|
|
425
|
+
|
|
426
|
+
const result = canvasElement || element;
|
|
427
|
+
logger.info('Element created via MCP and synced to canvas', {
|
|
428
|
+
id: result.id,
|
|
429
|
+
type: result.type,
|
|
430
|
+
synced: !!canvasElement
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
content: [{
|
|
435
|
+
type: 'text',
|
|
436
|
+
text: `Element created successfully!\n\n${JSON.stringify(result, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still created locally)'}`
|
|
437
|
+
}]
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
case 'update_element': {
|
|
442
|
+
const params = ElementSchema.partial().extend(ElementIdSchema).parse(args);
|
|
443
|
+
const { id, ...updates } = params;
|
|
444
|
+
|
|
445
|
+
if (!id) throw new Error('Element ID is required');
|
|
446
|
+
|
|
447
|
+
const existingElement = elements.get(id);
|
|
448
|
+
if (!existingElement) throw new Error(`Element with ID ${id} not found`);
|
|
449
|
+
|
|
450
|
+
// Validate the updated element
|
|
451
|
+
ElementSchema.parse({ ...existingElement, ...updates });
|
|
452
|
+
|
|
453
|
+
const updatedElement = {
|
|
454
|
+
...existingElement,
|
|
455
|
+
...updates,
|
|
456
|
+
updatedAt: new Date().toISOString(),
|
|
457
|
+
version: existingElement.version + 1
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Store locally (MCP server storage)
|
|
461
|
+
elements.set(id, updatedElement);
|
|
462
|
+
|
|
463
|
+
// Sync to canvas (Express server + WebSocket broadcast)
|
|
464
|
+
const canvasElement = await updateElementOnCanvas(updatedElement);
|
|
465
|
+
|
|
466
|
+
const result = canvasElement || updatedElement;
|
|
467
|
+
logger.info('Element updated via MCP and synced to canvas', {
|
|
468
|
+
id: result.id,
|
|
469
|
+
synced: !!canvasElement
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
content: [{
|
|
474
|
+
type: 'text',
|
|
475
|
+
text: `Element updated successfully!\n\n${JSON.stringify(result, null, 2)}\n\n${canvasElement ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still updated locally)'}`
|
|
476
|
+
}]
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
case 'delete_element': {
|
|
481
|
+
const params = ElementIdSchema.parse(args);
|
|
482
|
+
const { id } = params;
|
|
483
|
+
|
|
484
|
+
if (!elements.has(id)) throw new Error(`Element with ID ${id} not found`);
|
|
485
|
+
|
|
486
|
+
// Delete locally (MCP server storage)
|
|
487
|
+
elements.delete(id);
|
|
488
|
+
|
|
489
|
+
// Sync to canvas (Express server + WebSocket broadcast)
|
|
490
|
+
const canvasResult = await deleteElementOnCanvas(id);
|
|
491
|
+
|
|
492
|
+
const result = { id, deleted: true, syncedToCanvas: !!canvasResult };
|
|
493
|
+
logger.info('Element deleted via MCP and synced to canvas', result);
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
content: [{
|
|
497
|
+
type: 'text',
|
|
498
|
+
text: `Element deleted successfully!\n\n${JSON.stringify(result, null, 2)}\n\n${canvasResult ? '✅ Synced to canvas' : '⚠️ Canvas sync failed (element still deleted locally)'}`
|
|
499
|
+
}]
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
case 'query_elements': {
|
|
504
|
+
const params = QuerySchema.parse(args || {});
|
|
505
|
+
const { type, filter } = params;
|
|
506
|
+
|
|
507
|
+
let results = Array.from(elements.values());
|
|
508
|
+
|
|
509
|
+
if (type) {
|
|
510
|
+
results = results.filter(element => element.type === type);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (filter) {
|
|
514
|
+
results = results.filter(element => {
|
|
515
|
+
return Object.entries(filter).every(([key, value]) => {
|
|
516
|
+
return element[key] === value;
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }]
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
case 'get_resource': {
|
|
527
|
+
const params = ResourceSchema.parse(args);
|
|
528
|
+
const { resource } = params;
|
|
529
|
+
logger.info('Getting resource', { resource });
|
|
530
|
+
|
|
531
|
+
let result;
|
|
532
|
+
switch (resource) {
|
|
533
|
+
case 'scene':
|
|
534
|
+
result = {
|
|
535
|
+
theme: sceneState.theme,
|
|
536
|
+
viewport: sceneState.viewport,
|
|
537
|
+
selectedElements: Array.from(sceneState.selectedElements)
|
|
538
|
+
};
|
|
539
|
+
break;
|
|
540
|
+
case 'library':
|
|
541
|
+
result = {
|
|
542
|
+
elements: Array.from(elements.values())
|
|
543
|
+
};
|
|
544
|
+
break;
|
|
545
|
+
case 'theme':
|
|
546
|
+
result = {
|
|
547
|
+
theme: sceneState.theme
|
|
548
|
+
};
|
|
549
|
+
break;
|
|
550
|
+
case 'elements':
|
|
551
|
+
result = {
|
|
552
|
+
elements: Array.from(elements.values())
|
|
553
|
+
};
|
|
554
|
+
break;
|
|
555
|
+
default:
|
|
556
|
+
throw new Error(`Unknown resource: ${resource}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
case 'group_elements': {
|
|
565
|
+
const params = ElementIdsSchema.parse(args);
|
|
566
|
+
const { elementIds } = params;
|
|
567
|
+
|
|
568
|
+
const groupId = generateId();
|
|
569
|
+
sceneState.groups.set(groupId, elementIds);
|
|
570
|
+
|
|
571
|
+
const result = { groupId, elementIds };
|
|
572
|
+
return {
|
|
573
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
case 'ungroup_elements': {
|
|
578
|
+
const params = GroupIdSchema.parse(args);
|
|
579
|
+
const { groupId } = params;
|
|
580
|
+
|
|
581
|
+
if (!sceneState.groups.has(groupId)) {
|
|
582
|
+
throw new Error(`Group ${groupId} not found`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const elementIds = sceneState.groups.get(groupId);
|
|
586
|
+
sceneState.groups.delete(groupId);
|
|
587
|
+
|
|
588
|
+
const result = { groupId, ungrouped: true, elementIds };
|
|
589
|
+
return {
|
|
590
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
case 'align_elements': {
|
|
595
|
+
const params = AlignElementsSchema.parse(args);
|
|
596
|
+
const { elementIds, alignment } = params;
|
|
597
|
+
|
|
598
|
+
// Implementation would align elements based on the specified alignment
|
|
599
|
+
logger.info('Aligning elements', { elementIds, alignment });
|
|
600
|
+
|
|
601
|
+
const result = { aligned: true, elementIds, alignment };
|
|
602
|
+
return {
|
|
603
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
case 'distribute_elements': {
|
|
608
|
+
const params = DistributeElementsSchema.parse(args);
|
|
609
|
+
const { elementIds, direction } = params;
|
|
610
|
+
|
|
611
|
+
// Implementation would distribute elements based on the specified direction
|
|
612
|
+
logger.info('Distributing elements', { elementIds, direction });
|
|
613
|
+
|
|
614
|
+
const result = { distributed: true, elementIds, direction };
|
|
615
|
+
return {
|
|
616
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
case 'lock_elements': {
|
|
621
|
+
const params = ElementIdsSchema.parse(args);
|
|
622
|
+
const { elementIds } = params;
|
|
623
|
+
|
|
624
|
+
elementIds.forEach(id => {
|
|
625
|
+
const element = elements.get(id);
|
|
626
|
+
if (element) {
|
|
627
|
+
element.locked = true;
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const result = { locked: true, elementIds };
|
|
632
|
+
return {
|
|
633
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
case 'unlock_elements': {
|
|
638
|
+
const params = ElementIdsSchema.parse(args);
|
|
639
|
+
const { elementIds } = params;
|
|
640
|
+
|
|
641
|
+
elementIds.forEach(id => {
|
|
642
|
+
const element = elements.get(id);
|
|
643
|
+
if (element) {
|
|
644
|
+
element.locked = false;
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const result = { unlocked: true, elementIds };
|
|
649
|
+
return {
|
|
650
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
case 'batch_create_elements': {
|
|
655
|
+
const params = z.object({ elements: z.array(ElementSchema) }).parse(args);
|
|
656
|
+
logger.info('Batch creating elements via MCP', { count: params.elements.length });
|
|
657
|
+
|
|
658
|
+
const createdElements = [];
|
|
659
|
+
|
|
660
|
+
// Create each element with unique ID
|
|
661
|
+
for (const elementData of params.elements) {
|
|
662
|
+
const id = generateId();
|
|
663
|
+
const element = {
|
|
664
|
+
id,
|
|
665
|
+
...elementData,
|
|
666
|
+
createdAt: new Date().toISOString(),
|
|
667
|
+
updatedAt: new Date().toISOString(),
|
|
668
|
+
version: 1
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// Store locally (MCP server storage)
|
|
672
|
+
elements.set(id, element);
|
|
673
|
+
createdElements.push(element);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Sync all elements to canvas at once (Express server + WebSocket broadcast)
|
|
677
|
+
const canvasElements = await batchCreateElementsOnCanvas(createdElements);
|
|
678
|
+
|
|
679
|
+
const result = {
|
|
680
|
+
success: true,
|
|
681
|
+
elements: canvasElements || createdElements,
|
|
682
|
+
count: createdElements.length,
|
|
683
|
+
syncedToCanvas: !!canvasElements
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
logger.info('Batch elements created via MCP and synced to canvas', {
|
|
687
|
+
count: result.count,
|
|
688
|
+
synced: result.syncedToCanvas
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
content: [{
|
|
693
|
+
type: 'text',
|
|
694
|
+
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)'}`
|
|
695
|
+
}]
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
default:
|
|
700
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
701
|
+
}
|
|
702
|
+
} catch (error) {
|
|
703
|
+
logger.error(`Error handling tool call: ${error.message}`, { error });
|
|
704
|
+
return {
|
|
705
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
706
|
+
isError: true
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Set up request handler for listing available tools
|
|
712
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
713
|
+
logger.info('Listing available tools');
|
|
714
|
+
|
|
715
|
+
const tools = [
|
|
716
|
+
{
|
|
717
|
+
name: 'create_element',
|
|
718
|
+
description: 'Create a new Excalidraw element',
|
|
719
|
+
inputSchema: {
|
|
720
|
+
type: 'object',
|
|
721
|
+
properties: {
|
|
722
|
+
type: {
|
|
723
|
+
type: 'string',
|
|
724
|
+
enum: Object.values(EXCALIDRAW_ELEMENT_TYPES)
|
|
725
|
+
},
|
|
726
|
+
x: { type: 'number' },
|
|
727
|
+
y: { type: 'number' },
|
|
728
|
+
width: { type: 'number' },
|
|
729
|
+
height: { type: 'number' },
|
|
730
|
+
backgroundColor: { type: 'string' },
|
|
731
|
+
strokeColor: { type: 'string' },
|
|
732
|
+
strokeWidth: { type: 'number' },
|
|
733
|
+
roughness: { type: 'number' },
|
|
734
|
+
opacity: { type: 'number' },
|
|
735
|
+
text: { type: 'string' },
|
|
736
|
+
fontSize: { type: 'number' },
|
|
737
|
+
fontFamily: { type: 'string' }
|
|
738
|
+
},
|
|
739
|
+
required: ['type', 'x', 'y']
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: 'update_element',
|
|
744
|
+
description: 'Update an existing Excalidraw element',
|
|
745
|
+
inputSchema: {
|
|
746
|
+
type: 'object',
|
|
747
|
+
properties: {
|
|
748
|
+
id: { type: 'string' },
|
|
749
|
+
type: {
|
|
750
|
+
type: 'string',
|
|
751
|
+
enum: Object.values(EXCALIDRAW_ELEMENT_TYPES)
|
|
752
|
+
},
|
|
753
|
+
x: { type: 'number' },
|
|
754
|
+
y: { type: 'number' },
|
|
755
|
+
width: { type: 'number' },
|
|
756
|
+
height: { type: 'number' },
|
|
757
|
+
backgroundColor: { type: 'string' },
|
|
758
|
+
strokeColor: { type: 'string' },
|
|
759
|
+
strokeWidth: { type: 'number' },
|
|
760
|
+
roughness: { type: 'number' },
|
|
761
|
+
opacity: { type: 'number' },
|
|
762
|
+
text: { type: 'string' },
|
|
763
|
+
fontSize: { type: 'number' },
|
|
764
|
+
fontFamily: { type: 'string' }
|
|
765
|
+
},
|
|
766
|
+
required: ['id']
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: 'delete_element',
|
|
771
|
+
description: 'Delete an Excalidraw element',
|
|
772
|
+
inputSchema: {
|
|
773
|
+
type: 'object',
|
|
774
|
+
properties: {
|
|
775
|
+
id: { type: 'string' }
|
|
776
|
+
},
|
|
777
|
+
required: ['id']
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: 'query_elements',
|
|
782
|
+
description: 'Query Excalidraw elements with optional filters',
|
|
783
|
+
inputSchema: {
|
|
784
|
+
type: 'object',
|
|
785
|
+
properties: {
|
|
786
|
+
type: {
|
|
787
|
+
type: 'string',
|
|
788
|
+
enum: Object.values(EXCALIDRAW_ELEMENT_TYPES)
|
|
789
|
+
},
|
|
790
|
+
filter: {
|
|
791
|
+
type: 'object',
|
|
792
|
+
additionalProperties: true
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
name: 'get_resource',
|
|
799
|
+
description: 'Get an Excalidraw resource',
|
|
800
|
+
inputSchema: {
|
|
801
|
+
type: 'object',
|
|
802
|
+
properties: {
|
|
803
|
+
resource: {
|
|
804
|
+
type: 'string',
|
|
805
|
+
enum: ['scene', 'library', 'theme', 'elements']
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
required: ['resource']
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
name: 'group_elements',
|
|
813
|
+
description: 'Group multiple elements together',
|
|
814
|
+
inputSchema: {
|
|
815
|
+
type: 'object',
|
|
816
|
+
properties: {
|
|
817
|
+
elementIds: {
|
|
818
|
+
type: 'array',
|
|
819
|
+
items: { type: 'string' }
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
required: ['elementIds']
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
name: 'ungroup_elements',
|
|
827
|
+
description: 'Ungroup a group of elements',
|
|
828
|
+
inputSchema: {
|
|
829
|
+
type: 'object',
|
|
830
|
+
properties: {
|
|
831
|
+
groupId: { type: 'string' }
|
|
832
|
+
},
|
|
833
|
+
required: ['groupId']
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: 'align_elements',
|
|
838
|
+
description: 'Align elements to a specific position',
|
|
839
|
+
inputSchema: {
|
|
840
|
+
type: 'object',
|
|
841
|
+
properties: {
|
|
842
|
+
elementIds: {
|
|
843
|
+
type: 'array',
|
|
844
|
+
items: { type: 'string' }
|
|
845
|
+
},
|
|
846
|
+
alignment: {
|
|
847
|
+
type: 'string',
|
|
848
|
+
enum: ['left', 'center', 'right', 'top', 'middle', 'bottom']
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
required: ['elementIds', 'alignment']
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
name: 'distribute_elements',
|
|
856
|
+
description: 'Distribute elements evenly',
|
|
857
|
+
inputSchema: {
|
|
858
|
+
type: 'object',
|
|
859
|
+
properties: {
|
|
860
|
+
elementIds: {
|
|
861
|
+
type: 'array',
|
|
862
|
+
items: { type: 'string' }
|
|
863
|
+
},
|
|
864
|
+
direction: {
|
|
865
|
+
type: 'string',
|
|
866
|
+
enum: ['horizontal', 'vertical']
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
required: ['elementIds', 'direction']
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
name: 'lock_elements',
|
|
874
|
+
description: 'Lock elements to prevent modification',
|
|
875
|
+
inputSchema: {
|
|
876
|
+
type: 'object',
|
|
877
|
+
properties: {
|
|
878
|
+
elementIds: {
|
|
879
|
+
type: 'array',
|
|
880
|
+
items: { type: 'string' }
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
required: ['elementIds']
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
name: 'unlock_elements',
|
|
888
|
+
description: 'Unlock elements to allow modification',
|
|
889
|
+
inputSchema: {
|
|
890
|
+
type: 'object',
|
|
891
|
+
properties: {
|
|
892
|
+
elementIds: {
|
|
893
|
+
type: 'array',
|
|
894
|
+
items: { type: 'string' }
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
required: ['elementIds']
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: 'batch_create_elements',
|
|
902
|
+
description: 'Create multiple Excalidraw elements at once - ideal for complex diagrams',
|
|
903
|
+
inputSchema: {
|
|
904
|
+
type: 'object',
|
|
905
|
+
properties: {
|
|
906
|
+
elements: {
|
|
907
|
+
type: 'array',
|
|
908
|
+
items: {
|
|
909
|
+
type: 'object',
|
|
910
|
+
properties: {
|
|
911
|
+
type: {
|
|
912
|
+
type: 'string',
|
|
913
|
+
enum: Object.values(EXCALIDRAW_ELEMENT_TYPES)
|
|
914
|
+
},
|
|
915
|
+
x: { type: 'number' },
|
|
916
|
+
y: { type: 'number' },
|
|
917
|
+
width: { type: 'number' },
|
|
918
|
+
height: { type: 'number' },
|
|
919
|
+
backgroundColor: { type: 'string' },
|
|
920
|
+
strokeColor: { type: 'string' },
|
|
921
|
+
strokeWidth: { type: 'number' },
|
|
922
|
+
roughness: { type: 'number' },
|
|
923
|
+
opacity: { type: 'number' },
|
|
924
|
+
text: { type: 'string' },
|
|
925
|
+
fontSize: { type: 'number' },
|
|
926
|
+
fontFamily: { type: 'string' }
|
|
927
|
+
},
|
|
928
|
+
required: ['type', 'x', 'y']
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
required: ['elements']
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
];
|
|
936
|
+
|
|
937
|
+
return { tools };
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// Start server with transport based on mode
|
|
941
|
+
async function runServer() {
|
|
942
|
+
try {
|
|
943
|
+
logger.info('Starting Excalidraw MCP server...');
|
|
944
|
+
|
|
945
|
+
const transportMode = process.env.MCP_TRANSPORT_MODE || 'stdio';
|
|
946
|
+
let transport;
|
|
947
|
+
|
|
948
|
+
if (transportMode === 'http') {
|
|
949
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
950
|
+
const host = process.env.HOST || 'localhost';
|
|
951
|
+
|
|
952
|
+
logger.info(`Starting HTTP server on ${host}:${port}`);
|
|
953
|
+
// Here you would create an HTTP transport
|
|
954
|
+
// This is a placeholder - actual HTTP transport implementation would need to be added
|
|
955
|
+
transport = new StdioServerTransport(); // Fallback to stdio for now
|
|
956
|
+
} else {
|
|
957
|
+
// Default to stdio transport
|
|
958
|
+
transport = new StdioServerTransport();
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Add a debug message before connecting
|
|
962
|
+
logger.debug('Connecting to transport...');
|
|
963
|
+
|
|
964
|
+
await server.connect(transport);
|
|
965
|
+
logger.info(`Excalidraw MCP server running on ${transportMode}`);
|
|
966
|
+
|
|
967
|
+
// Keep the process running
|
|
968
|
+
process.stdin.resume();
|
|
969
|
+
} catch (error) {
|
|
970
|
+
logger.error('Error starting server:', error);
|
|
971
|
+
process.stderr.write(`Failed to start MCP server: ${error.message}\n${error.stack}\n`);
|
|
972
|
+
process.exit(1);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Add global error handlers
|
|
977
|
+
process.on('uncaughtException', (error) => {
|
|
978
|
+
logger.error('Uncaught exception:', error);
|
|
979
|
+
process.stderr.write(`UNCAUGHT EXCEPTION: ${error.message}\n${error.stack}\n`);
|
|
980
|
+
setTimeout(() => process.exit(1), 1000);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
984
|
+
logger.error('Unhandled promise rejection:', reason);
|
|
985
|
+
process.stderr.write(`UNHANDLED REJECTION: ${reason}\n`);
|
|
986
|
+
setTimeout(() => process.exit(1), 1000);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// For testing and debugging purposes
|
|
990
|
+
if (process.env.DEBUG === 'true') {
|
|
991
|
+
logger.debug('Debug mode enabled');
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Start the server if this file is run directly
|
|
995
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
996
|
+
runServer().catch(error => {
|
|
997
|
+
logger.error('Failed to start server:', error);
|
|
998
|
+
process.exit(1);
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
export default runServer;
|