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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +424 -0
  3. package/dist/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
  4. package/dist/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
  5. package/dist/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
  6. package/dist/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
  7. package/dist/assets/Tableau10-B-NsZVaP.js +1 -0
  8. package/dist/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
  9. package/dist/assets/ar-SA-G6X2FPQ2-BS3fMnev.js +10 -0
  10. package/dist/assets/arc-0Fwkaye8.js +1 -0
  11. package/dist/assets/array-BKyUJesY.js +1 -0
  12. package/dist/assets/az-AZ-76LH7QW2-DNgzWA8S.js +1 -0
  13. package/dist/assets/bg-BG-XCXSNQG7-CULoJKdI.js +5 -0
  14. package/dist/assets/blockDiagram-38ab4fdb-CVOkr1Ma.js +118 -0
  15. package/dist/assets/bn-BD-2XOGV67Q-9DmWphrb.js +5 -0
  16. package/dist/assets/c4Diagram-3d4e48cf-BwpfSRgq.js +10 -0
  17. package/dist/assets/ca-ES-6MX7JW3Y-gYoEYP36.js +8 -0
  18. package/dist/assets/channel-BgX0NHoH.js +1 -0
  19. package/dist/assets/classDiagram-70f12bd4-ftK2tRy5.js +2 -0
  20. package/dist/assets/classDiagram-v2-f2320105--Nu78BDB.js +2 -0
  21. package/dist/assets/clone-sj1RjrIX.js +1 -0
  22. package/dist/assets/createText-2e5e7dd3-DPYwmS_z.js +7 -0
  23. package/dist/assets/cs-CZ-2BRQDIVT-DgwhrQZi.js +11 -0
  24. package/dist/assets/da-DK-5WZEPLOC-BL1Ng5As.js +5 -0
  25. package/dist/assets/de-DE-XR44H4JA-Cw-ySDmq.js +8 -0
  26. package/dist/assets/directory-open-01563666-DWU9wJ6I.js +1 -0
  27. package/dist/assets/directory-open-4ed118d0-BzWybGaI.js +1 -0
  28. package/dist/assets/edges-e0da2a9e-CB2w3jS2.js +4 -0
  29. package/dist/assets/el-GR-BZB4AONW-BPJAfWZm.js +10 -0
  30. package/dist/assets/erDiagram-9861fffd-VwqM-D3G.js +51 -0
  31. package/dist/assets/es-ES-U4NZUMDT-oMrzPWSn.js +9 -0
  32. package/dist/assets/eu-ES-A7QVB2H4-DFshkDl1.js +11 -0
  33. package/dist/assets/fa-IR-HGAKTJCU-DVU2rysM.js +8 -0
  34. package/dist/assets/fi-FI-Z5N7JZ37-BFKGqZcw.js +6 -0
  35. package/dist/assets/file-open-002ab408-DIuFHtCF.js +1 -0
  36. package/dist/assets/file-open-7c801643-684qeFg4.js +1 -0
  37. package/dist/assets/file-save-3189631c-x92wctJd.js +1 -0
  38. package/dist/assets/file-save-745eba88-Bb9F9Kg7.js +1 -0
  39. package/dist/assets/flowDb-956e92f1-CZj2LmRK.js +10 -0
  40. package/dist/assets/flowDiagram-66a62f08-D_vhln5h.js +4 -0
  41. package/dist/assets/flowDiagram-v2-96b9c2cf-CShVLez9.js +1 -0
  42. package/dist/assets/flowchart-elk-definition-4a651766-BJYKnLZA.js +139 -0
  43. package/dist/assets/fr-FR-RHASNOE6-M3Leevu1.js +9 -0
  44. package/dist/assets/ganttDiagram-c361ad54-Dl0RXJiR.js +257 -0
  45. package/dist/assets/gitGraphDiagram-72cf32ee-Cckr93z5.js +70 -0
  46. package/dist/assets/gl-ES-HMX3MZ6V-CM77q-gv.js +10 -0
  47. package/dist/assets/graph-CsvWB58Y.js +1 -0
  48. package/dist/assets/he-IL-6SHJWFNN-B3UI_ebx.js +10 -0
  49. package/dist/assets/hi-IN-IWLTKZ5I-DUAO_Nd9.js +4 -0
  50. package/dist/assets/hu-HU-A5ZG7DT2-Dm7uZbxs.js +7 -0
  51. package/dist/assets/id-ID-SAP4L64H-sn247bNe.js +10 -0
  52. package/dist/assets/image-blob-reduce.esm-B6b2_-a4.js +7 -0
  53. package/dist/assets/index-3862675e-pSTifqE7.js +1 -0
  54. package/dist/assets/index-CMTjDsOp.js +97 -0
  55. package/dist/assets/infoDiagram-f8f76790-Cy0QfHmF.js +7 -0
  56. package/dist/assets/init-Gi6I4Gst.js +1 -0
  57. package/dist/assets/it-IT-JPQ66NNP-C3cx3eLC.js +11 -0
  58. package/dist/assets/ja-JP-DBVTYXUO-P2fPONfc.js +8 -0
  59. package/dist/assets/journeyDiagram-49397b02-AWYgjzgS.js +139 -0
  60. package/dist/assets/kaa-6HZHGXH3-wmoL7do0.js +1 -0
  61. package/dist/assets/kab-KAB-ZGHBKWFO-DNNu0Xkz.js +8 -0
  62. package/dist/assets/katex-ChWnQ-fc.js +261 -0
  63. package/dist/assets/kk-KZ-P5N5QNE5-BZEEoSCw.js +1 -0
  64. package/dist/assets/km-KH-HSX4SM5Z-4Sf3SjAI.js +11 -0
  65. package/dist/assets/ko-KR-MTYHY66A-DTzTxHW5.js +9 -0
  66. package/dist/assets/ku-TR-6OUDTVRD-eWqCXim6.js +9 -0
  67. package/dist/assets/layout-ErVDAsYA.js +1 -0
  68. package/dist/assets/line-Cvp4skCK.js +1 -0
  69. package/dist/assets/linear-DlBkj_hR.js +1 -0
  70. package/dist/assets/lt-LT-XHIRWOB4-BzqmXLnL.js +3 -0
  71. package/dist/assets/lv-LV-5QDEKY6T-alH_HCjW.js +7 -0
  72. package/dist/assets/main-B9Rh8YyQ.css +1 -0
  73. package/dist/assets/main-C1TkgC3p.js +254 -0
  74. package/dist/assets/mindmap-definition-fc14e90a-DQB0p-81.js +425 -0
  75. package/dist/assets/mr-IN-CRQNXWMA-Cq2VIpVa.js +13 -0
  76. package/dist/assets/my-MM-5M5IBNSE-CBPHDdNv.js +1 -0
  77. package/dist/assets/nb-NO-T6EIAALU-BUNp_A07.js +10 -0
  78. package/dist/assets/nl-NL-IS3SIHDZ-CNz8gWhx.js +8 -0
  79. package/dist/assets/nn-NO-6E72VCQL-BFz6U-AT.js +8 -0
  80. package/dist/assets/oc-FR-POXYY2M6-BjY1Oz8X.js +8 -0
  81. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  82. package/dist/assets/pa-IN-N4M65BXN-bLY9e9o3.js +4 -0
  83. package/dist/assets/path-CbwjOpE9.js +1 -0
  84. package/dist/assets/pica-JUO0Loj6.js +7 -0
  85. package/dist/assets/pieDiagram-8a3498a8-BD6Rsa6P.js +35 -0
  86. package/dist/assets/pl-PL-T2D74RX3-CpLUuJZ4.js +9 -0
  87. package/dist/assets/pt-BR-5N22H2LF-DXSmPIb5.js +9 -0
  88. package/dist/assets/pt-PT-UZXXM6DQ-CtilzVM5.js +9 -0
  89. package/dist/assets/quadrantDiagram-120e2f19-CvG-FK5I.js +7 -0
  90. package/dist/assets/requirementDiagram-deff3bca-CVh-bBIY.js +52 -0
  91. package/dist/assets/ro-RO-JPDTUUEW-4SIE29UH.js +11 -0
  92. package/dist/assets/roundRect-0PYZxl1G.js +1 -0
  93. package/dist/assets/ru-RU-B4JR7IUQ-yWKAM4lo.js +9 -0
  94. package/dist/assets/sankeyDiagram-04a897e0-Bb-ywaQF.js +8 -0
  95. package/dist/assets/sequenceDiagram-704730f1-BZu5e-R7.js +122 -0
  96. package/dist/assets/si-LK-N5RQ5JYF-D-XE0eOh.js +1 -0
  97. package/dist/assets/sk-SK-C5VTKIMK-DTdiYCrX.js +6 -0
  98. package/dist/assets/sl-SI-NN7IZMDC-qcTJE3hs.js +6 -0
  99. package/dist/assets/stateDiagram-587899a1-CoklS1cy.js +1 -0
  100. package/dist/assets/stateDiagram-v2-d93cdb3a-JBfFG9Hu.js +1 -0
  101. package/dist/assets/styles-6aaf32cf-Cg7CSuxw.js +207 -0
  102. package/dist/assets/styles-9a916d00-Ny9GZRfC.js +160 -0
  103. package/dist/assets/styles-c10674c1-BMpWBEge.js +116 -0
  104. package/dist/assets/subset-shared.chunk-tHzjs2ro.js +84 -0
  105. package/dist/assets/subset-worker.chunk-CuKy9sB1.js +1 -0
  106. package/dist/assets/sv-SE-XGPEYMSR-Bi6tm-oG.js +10 -0
  107. package/dist/assets/svgDrawCommon-08f97a94-CEEsVKXL.js +1 -0
  108. package/dist/assets/ta-IN-2NMHFXQM-C7Igaqv2.js +9 -0
  109. package/dist/assets/th-TH-HPSO5L25-BdAfwagC.js +2 -0
  110. package/dist/assets/timeline-definition-85554ec2-DsgJKruU.js +61 -0
  111. package/dist/assets/tr-TR-DEFEU3FU-BPBRoOZh.js +7 -0
  112. package/dist/assets/uk-UA-QMV73CPH-CeJGl_H3.js +6 -0
  113. package/dist/assets/vi-VN-M7AON7JQ-D537-quZ.js +5 -0
  114. package/dist/assets/xychartDiagram-e933f94c-DqfSpZR2.js +7 -0
  115. package/dist/assets/zh-CN-LNUGB5OW-Bd8rr_cr.js +10 -0
  116. package/dist/assets/zh-HK-E62DVLB3-Y1MdNiXR.js +1 -0
  117. package/dist/assets/zh-TW-RAJ6MFWO-BAzsCe6B.js +9 -0
  118. package/dist/frontend/index.html +241 -0
  119. package/package.json +78 -0
  120. package/src/index.js +1002 -0
  121. package/src/server.js +377 -0
  122. package/src/types.js +36 -0
  123. 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;