@asiones/mcp-excalidraw 1.0.1 → 1.2.1
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/dist/index.js +288 -90
- package/dist/types.js +44 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,8 +31,15 @@ function sanitizeFilePath(filePath) {
|
|
|
31
31
|
// Express server configuration
|
|
32
32
|
const EXPRESS_SERVER_URL = process.env.EXPRESS_SERVER_URL || 'http://localhost:3000';
|
|
33
33
|
const ENABLE_CANVAS_SYNC = process.env.ENABLE_CANVAS_SYNC !== 'false'; // Default to true
|
|
34
|
+
// Auth headers for canvas server requests
|
|
35
|
+
function getAuthHeaders() {
|
|
36
|
+
const creds = process.env.AUTH_CREDENTIALS;
|
|
37
|
+
if (!creds)
|
|
38
|
+
return {};
|
|
39
|
+
return { 'Authorization': `Basic ${Buffer.from(creds).toString('base64')}` };
|
|
40
|
+
}
|
|
34
41
|
// Helper functions to sync with Express server (canvas)
|
|
35
|
-
async function syncToCanvas(operation, data) {
|
|
42
|
+
async function syncToCanvas(sessionId, operation, data) {
|
|
36
43
|
if (!ENABLE_CANVAS_SYNC) {
|
|
37
44
|
logger.debug('Canvas sync disabled, skipping');
|
|
38
45
|
return null;
|
|
@@ -42,30 +49,30 @@ async function syncToCanvas(operation, data) {
|
|
|
42
49
|
let options;
|
|
43
50
|
switch (operation) {
|
|
44
51
|
case 'create':
|
|
45
|
-
url = `${EXPRESS_SERVER_URL}/api/elements`;
|
|
52
|
+
url = `${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements`;
|
|
46
53
|
options = {
|
|
47
54
|
method: 'POST',
|
|
48
|
-
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
49
56
|
body: JSON.stringify(data)
|
|
50
57
|
};
|
|
51
58
|
break;
|
|
52
59
|
case 'update':
|
|
53
|
-
url = `${EXPRESS_SERVER_URL}/api/elements/${data.id}`;
|
|
60
|
+
url = `${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/${data.id}`;
|
|
54
61
|
options = {
|
|
55
62
|
method: 'PUT',
|
|
56
|
-
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
57
64
|
body: JSON.stringify(data)
|
|
58
65
|
};
|
|
59
66
|
break;
|
|
60
67
|
case 'delete':
|
|
61
|
-
url = `${EXPRESS_SERVER_URL}/api/elements/${data.id}`;
|
|
62
|
-
options = { method: 'DELETE' };
|
|
68
|
+
url = `${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/${data.id}`;
|
|
69
|
+
options = { method: 'DELETE', headers: { ...getAuthHeaders() } };
|
|
63
70
|
break;
|
|
64
71
|
case 'batch_create':
|
|
65
|
-
url = `${EXPRESS_SERVER_URL}/api/elements/batch`;
|
|
72
|
+
url = `${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/batch`;
|
|
66
73
|
options = {
|
|
67
74
|
method: 'POST',
|
|
68
|
-
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
69
76
|
body: JSON.stringify({ elements: data })
|
|
70
77
|
};
|
|
71
78
|
break;
|
|
@@ -91,33 +98,35 @@ async function syncToCanvas(operation, data) {
|
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
// Helper to sync element creation to canvas
|
|
94
|
-
async function createElementOnCanvas(elementData) {
|
|
95
|
-
const result = await syncToCanvas('create', elementData);
|
|
101
|
+
async function createElementOnCanvas(sessionId, elementData) {
|
|
102
|
+
const result = await syncToCanvas(sessionId, 'create', elementData);
|
|
96
103
|
return result?.element || elementData;
|
|
97
104
|
}
|
|
98
|
-
// Helper to sync element update to canvas
|
|
99
|
-
async function updateElementOnCanvas(elementData) {
|
|
100
|
-
const result = await syncToCanvas('update', elementData);
|
|
105
|
+
// Helper to sync element update to canvas
|
|
106
|
+
async function updateElementOnCanvas(sessionId, elementData) {
|
|
107
|
+
const result = await syncToCanvas(sessionId, 'update', elementData);
|
|
101
108
|
return result?.element || null;
|
|
102
109
|
}
|
|
103
110
|
// Helper to sync element deletion to canvas
|
|
104
|
-
async function deleteElementOnCanvas(elementId) {
|
|
105
|
-
const result = await syncToCanvas('delete', { id: elementId });
|
|
111
|
+
async function deleteElementOnCanvas(sessionId, elementId) {
|
|
112
|
+
const result = await syncToCanvas(sessionId, 'delete', { id: elementId });
|
|
106
113
|
return result;
|
|
107
114
|
}
|
|
108
115
|
// Helper to sync batch creation to canvas
|
|
109
|
-
async function batchCreateElementsOnCanvas(elementsData) {
|
|
110
|
-
const result = await syncToCanvas('batch_create', elementsData);
|
|
116
|
+
async function batchCreateElementsOnCanvas(sessionId, elementsData) {
|
|
117
|
+
const result = await syncToCanvas(sessionId, 'batch_create', elementsData);
|
|
111
118
|
return result?.elements || elementsData;
|
|
112
119
|
}
|
|
113
120
|
// Helper to fetch element from canvas
|
|
114
|
-
async function getElementFromCanvas(elementId) {
|
|
121
|
+
async function getElementFromCanvas(sessionId, elementId) {
|
|
115
122
|
if (!ENABLE_CANVAS_SYNC) {
|
|
116
123
|
logger.debug('Canvas sync disabled, skipping fetch');
|
|
117
124
|
return null;
|
|
118
125
|
}
|
|
119
126
|
try {
|
|
120
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements/${elementId}
|
|
127
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/${elementId}`, {
|
|
128
|
+
headers: { ...getAuthHeaders() }
|
|
129
|
+
});
|
|
121
130
|
if (!response.ok) {
|
|
122
131
|
logger.warn(`Failed to fetch element ${elementId}: ${response.status}`);
|
|
123
132
|
return null;
|
|
@@ -300,6 +309,7 @@ const tools = [
|
|
|
300
309
|
inputSchema: {
|
|
301
310
|
type: 'object',
|
|
302
311
|
properties: {
|
|
312
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
303
313
|
id: { type: 'string', description: 'Custom element ID (optional, auto-generated if omitted). Use with startElementId/endElementId in batch_create_elements.' },
|
|
304
314
|
type: {
|
|
305
315
|
type: 'string',
|
|
@@ -323,7 +333,7 @@ const tools = [
|
|
|
323
333
|
endArrowhead: { type: 'string', description: 'Arrowhead style at end: arrow, bar, dot, triangle, or null' },
|
|
324
334
|
startArrowhead: { type: 'string', description: 'Arrowhead style at start: arrow, bar, dot, triangle, or null' }
|
|
325
335
|
},
|
|
326
|
-
required: ['type', 'x', 'y']
|
|
336
|
+
required: ['sessionId', 'type', 'x', 'y']
|
|
327
337
|
}
|
|
328
338
|
},
|
|
329
339
|
{
|
|
@@ -332,6 +342,7 @@ const tools = [
|
|
|
332
342
|
inputSchema: {
|
|
333
343
|
type: 'object',
|
|
334
344
|
properties: {
|
|
345
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
335
346
|
id: { type: 'string' },
|
|
336
347
|
type: {
|
|
337
348
|
type: 'string',
|
|
@@ -351,7 +362,7 @@ const tools = [
|
|
|
351
362
|
fontSize: { type: 'number' },
|
|
352
363
|
fontFamily: { type: ['string', 'number'], description: 'Font family: virgil/hand/handwritten (1), helvetica/sans/sans-serif (2), cascadia/mono/monospace (3), excalifont (5), nunito (6), lilita/lilita one (7), comic shanns/comic (8), or numeric ID' }
|
|
353
364
|
},
|
|
354
|
-
required: ['id']
|
|
365
|
+
required: ['sessionId', 'id']
|
|
355
366
|
}
|
|
356
367
|
},
|
|
357
368
|
{
|
|
@@ -360,9 +371,10 @@ const tools = [
|
|
|
360
371
|
inputSchema: {
|
|
361
372
|
type: 'object',
|
|
362
373
|
properties: {
|
|
374
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
363
375
|
id: { type: 'string' }
|
|
364
376
|
},
|
|
365
|
-
required: ['id']
|
|
377
|
+
required: ['sessionId', 'id']
|
|
366
378
|
}
|
|
367
379
|
},
|
|
368
380
|
{
|
|
@@ -371,6 +383,7 @@ const tools = [
|
|
|
371
383
|
inputSchema: {
|
|
372
384
|
type: 'object',
|
|
373
385
|
properties: {
|
|
386
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
374
387
|
type: {
|
|
375
388
|
type: 'string',
|
|
376
389
|
enum: Object.values(EXCALIDRAW_ELEMENT_TYPES)
|
|
@@ -379,7 +392,8 @@ const tools = [
|
|
|
379
392
|
type: 'object',
|
|
380
393
|
additionalProperties: true
|
|
381
394
|
}
|
|
382
|
-
}
|
|
395
|
+
},
|
|
396
|
+
required: ['sessionId']
|
|
383
397
|
}
|
|
384
398
|
},
|
|
385
399
|
{
|
|
@@ -388,12 +402,13 @@ const tools = [
|
|
|
388
402
|
inputSchema: {
|
|
389
403
|
type: 'object',
|
|
390
404
|
properties: {
|
|
405
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
391
406
|
resource: {
|
|
392
407
|
type: 'string',
|
|
393
408
|
enum: ['scene', 'library', 'theme', 'elements']
|
|
394
409
|
}
|
|
395
410
|
},
|
|
396
|
-
required: ['resource']
|
|
411
|
+
required: ['sessionId', 'resource']
|
|
397
412
|
}
|
|
398
413
|
},
|
|
399
414
|
{
|
|
@@ -402,12 +417,13 @@ const tools = [
|
|
|
402
417
|
inputSchema: {
|
|
403
418
|
type: 'object',
|
|
404
419
|
properties: {
|
|
420
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
405
421
|
elementIds: {
|
|
406
422
|
type: 'array',
|
|
407
423
|
items: { type: 'string' }
|
|
408
424
|
}
|
|
409
425
|
},
|
|
410
|
-
required: ['elementIds']
|
|
426
|
+
required: ['sessionId', 'elementIds']
|
|
411
427
|
}
|
|
412
428
|
},
|
|
413
429
|
{
|
|
@@ -416,9 +432,10 @@ const tools = [
|
|
|
416
432
|
inputSchema: {
|
|
417
433
|
type: 'object',
|
|
418
434
|
properties: {
|
|
435
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
419
436
|
groupId: { type: 'string' }
|
|
420
437
|
},
|
|
421
|
-
required: ['groupId']
|
|
438
|
+
required: ['sessionId', 'groupId']
|
|
422
439
|
}
|
|
423
440
|
},
|
|
424
441
|
{
|
|
@@ -427,6 +444,7 @@ const tools = [
|
|
|
427
444
|
inputSchema: {
|
|
428
445
|
type: 'object',
|
|
429
446
|
properties: {
|
|
447
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
430
448
|
elementIds: {
|
|
431
449
|
type: 'array',
|
|
432
450
|
items: { type: 'string' }
|
|
@@ -436,7 +454,7 @@ const tools = [
|
|
|
436
454
|
enum: ['left', 'center', 'right', 'top', 'middle', 'bottom']
|
|
437
455
|
}
|
|
438
456
|
},
|
|
439
|
-
required: ['elementIds', 'alignment']
|
|
457
|
+
required: ['sessionId', 'elementIds', 'alignment']
|
|
440
458
|
}
|
|
441
459
|
},
|
|
442
460
|
{
|
|
@@ -445,6 +463,7 @@ const tools = [
|
|
|
445
463
|
inputSchema: {
|
|
446
464
|
type: 'object',
|
|
447
465
|
properties: {
|
|
466
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
448
467
|
elementIds: {
|
|
449
468
|
type: 'array',
|
|
450
469
|
items: { type: 'string' }
|
|
@@ -454,7 +473,7 @@ const tools = [
|
|
|
454
473
|
enum: ['horizontal', 'vertical']
|
|
455
474
|
}
|
|
456
475
|
},
|
|
457
|
-
required: ['elementIds', 'direction']
|
|
476
|
+
required: ['sessionId', 'elementIds', 'direction']
|
|
458
477
|
}
|
|
459
478
|
},
|
|
460
479
|
{
|
|
@@ -463,12 +482,13 @@ const tools = [
|
|
|
463
482
|
inputSchema: {
|
|
464
483
|
type: 'object',
|
|
465
484
|
properties: {
|
|
485
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
466
486
|
elementIds: {
|
|
467
487
|
type: 'array',
|
|
468
488
|
items: { type: 'string' }
|
|
469
489
|
}
|
|
470
490
|
},
|
|
471
|
-
required: ['elementIds']
|
|
491
|
+
required: ['sessionId', 'elementIds']
|
|
472
492
|
}
|
|
473
493
|
},
|
|
474
494
|
{
|
|
@@ -477,12 +497,13 @@ const tools = [
|
|
|
477
497
|
inputSchema: {
|
|
478
498
|
type: 'object',
|
|
479
499
|
properties: {
|
|
500
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
480
501
|
elementIds: {
|
|
481
502
|
type: 'array',
|
|
482
503
|
items: { type: 'string' }
|
|
483
504
|
}
|
|
484
505
|
},
|
|
485
|
-
required: ['elementIds']
|
|
506
|
+
required: ['sessionId', 'elementIds']
|
|
486
507
|
}
|
|
487
508
|
},
|
|
488
509
|
{
|
|
@@ -491,6 +512,7 @@ const tools = [
|
|
|
491
512
|
inputSchema: {
|
|
492
513
|
type: 'object',
|
|
493
514
|
properties: {
|
|
515
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
494
516
|
mermaidDiagram: {
|
|
495
517
|
type: 'string',
|
|
496
518
|
description: 'The Mermaid diagram definition (e.g., "graph TD; A-->B; B-->C;")'
|
|
@@ -517,7 +539,7 @@ const tools = [
|
|
|
517
539
|
}
|
|
518
540
|
}
|
|
519
541
|
},
|
|
520
|
-
required: ['mermaidDiagram']
|
|
542
|
+
required: ['sessionId', 'mermaidDiagram']
|
|
521
543
|
}
|
|
522
544
|
},
|
|
523
545
|
{
|
|
@@ -526,6 +548,7 @@ const tools = [
|
|
|
526
548
|
inputSchema: {
|
|
527
549
|
type: 'object',
|
|
528
550
|
properties: {
|
|
551
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
529
552
|
elements: {
|
|
530
553
|
type: 'array',
|
|
531
554
|
items: {
|
|
@@ -558,7 +581,7 @@ const tools = [
|
|
|
558
581
|
}
|
|
559
582
|
}
|
|
560
583
|
},
|
|
561
|
-
required: ['elements']
|
|
584
|
+
required: ['sessionId', 'elements']
|
|
562
585
|
}
|
|
563
586
|
},
|
|
564
587
|
{
|
|
@@ -567,9 +590,10 @@ const tools = [
|
|
|
567
590
|
inputSchema: {
|
|
568
591
|
type: 'object',
|
|
569
592
|
properties: {
|
|
593
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
570
594
|
id: { type: 'string', description: 'The element ID' }
|
|
571
595
|
},
|
|
572
|
-
required: ['id']
|
|
596
|
+
required: ['sessionId', 'id']
|
|
573
597
|
}
|
|
574
598
|
},
|
|
575
599
|
{
|
|
@@ -577,7 +601,10 @@ const tools = [
|
|
|
577
601
|
description: 'Clear all elements from the canvas',
|
|
578
602
|
inputSchema: {
|
|
579
603
|
type: 'object',
|
|
580
|
-
properties: {
|
|
604
|
+
properties: {
|
|
605
|
+
sessionId: { type: 'string', description: 'Session ID for canvas isolation' }
|
|
606
|
+
},
|
|
607
|
+
required: ['sessionId']
|
|
581
608
|
}
|
|
582
609
|
},
|
|
583
610
|
{
|
|
@@ -586,11 +613,13 @@ const tools = [
|
|
|
586
613
|
inputSchema: {
|
|
587
614
|
type: 'object',
|
|
588
615
|
properties: {
|
|
616
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
589
617
|
filePath: {
|
|
590
618
|
type: 'string',
|
|
591
619
|
description: 'Optional file path to write the .excalidraw JSON file'
|
|
592
620
|
}
|
|
593
|
-
}
|
|
621
|
+
},
|
|
622
|
+
required: ['sessionId']
|
|
594
623
|
}
|
|
595
624
|
},
|
|
596
625
|
{
|
|
@@ -599,6 +628,7 @@ const tools = [
|
|
|
599
628
|
inputSchema: {
|
|
600
629
|
type: 'object',
|
|
601
630
|
properties: {
|
|
631
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
602
632
|
filePath: {
|
|
603
633
|
type: 'string',
|
|
604
634
|
description: 'Path to a .excalidraw JSON file'
|
|
@@ -613,7 +643,7 @@ const tools = [
|
|
|
613
643
|
description: '"replace" clears canvas first, "merge" appends to existing elements'
|
|
614
644
|
}
|
|
615
645
|
},
|
|
616
|
-
required: ['mode']
|
|
646
|
+
required: ['sessionId', 'mode']
|
|
617
647
|
}
|
|
618
648
|
},
|
|
619
649
|
{
|
|
@@ -622,6 +652,7 @@ const tools = [
|
|
|
622
652
|
inputSchema: {
|
|
623
653
|
type: 'object',
|
|
624
654
|
properties: {
|
|
655
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
625
656
|
format: {
|
|
626
657
|
type: 'string',
|
|
627
658
|
enum: ['png', 'svg'],
|
|
@@ -636,7 +667,7 @@ const tools = [
|
|
|
636
667
|
description: 'Include background in export (default: true)'
|
|
637
668
|
}
|
|
638
669
|
},
|
|
639
|
-
required: ['format']
|
|
670
|
+
required: ['sessionId', 'format']
|
|
640
671
|
}
|
|
641
672
|
},
|
|
642
673
|
{
|
|
@@ -645,6 +676,7 @@ const tools = [
|
|
|
645
676
|
inputSchema: {
|
|
646
677
|
type: 'object',
|
|
647
678
|
properties: {
|
|
679
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
648
680
|
elementIds: {
|
|
649
681
|
type: 'array',
|
|
650
682
|
items: { type: 'string' },
|
|
@@ -653,7 +685,7 @@ const tools = [
|
|
|
653
685
|
offsetX: { type: 'number', description: 'Horizontal offset (default: 20)' },
|
|
654
686
|
offsetY: { type: 'number', description: 'Vertical offset (default: 20)' }
|
|
655
687
|
},
|
|
656
|
-
required: ['elementIds']
|
|
688
|
+
required: ['sessionId', 'elementIds']
|
|
657
689
|
}
|
|
658
690
|
},
|
|
659
691
|
{
|
|
@@ -662,12 +694,13 @@ const tools = [
|
|
|
662
694
|
inputSchema: {
|
|
663
695
|
type: 'object',
|
|
664
696
|
properties: {
|
|
697
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
665
698
|
name: {
|
|
666
699
|
type: 'string',
|
|
667
700
|
description: 'Name for this snapshot'
|
|
668
701
|
}
|
|
669
702
|
},
|
|
670
|
-
required: ['name']
|
|
703
|
+
required: ['sessionId', 'name']
|
|
671
704
|
}
|
|
672
705
|
},
|
|
673
706
|
{
|
|
@@ -676,12 +709,13 @@ const tools = [
|
|
|
676
709
|
inputSchema: {
|
|
677
710
|
type: 'object',
|
|
678
711
|
properties: {
|
|
712
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
679
713
|
name: {
|
|
680
714
|
type: 'string',
|
|
681
715
|
description: 'Name of the snapshot to restore'
|
|
682
716
|
}
|
|
683
717
|
},
|
|
684
|
-
required: ['name']
|
|
718
|
+
required: ['sessionId', 'name']
|
|
685
719
|
}
|
|
686
720
|
},
|
|
687
721
|
{
|
|
@@ -689,7 +723,10 @@ const tools = [
|
|
|
689
723
|
description: 'Get an AI-readable description of the current canvas: element types, positions, connections, labels, spatial layout, and bounding box. Use this to understand what is on the canvas before making changes.',
|
|
690
724
|
inputSchema: {
|
|
691
725
|
type: 'object',
|
|
692
|
-
properties: {
|
|
726
|
+
properties: {
|
|
727
|
+
sessionId: { type: 'string', description: 'Session ID for canvas isolation' }
|
|
728
|
+
},
|
|
729
|
+
required: ['sessionId']
|
|
693
730
|
}
|
|
694
731
|
},
|
|
695
732
|
{
|
|
@@ -698,11 +735,13 @@ const tools = [
|
|
|
698
735
|
inputSchema: {
|
|
699
736
|
type: 'object',
|
|
700
737
|
properties: {
|
|
738
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
701
739
|
background: {
|
|
702
740
|
type: 'boolean',
|
|
703
741
|
description: 'Include background in screenshot (default: true)'
|
|
704
742
|
}
|
|
705
|
-
}
|
|
743
|
+
},
|
|
744
|
+
required: ['sessionId']
|
|
706
745
|
}
|
|
707
746
|
},
|
|
708
747
|
{
|
|
@@ -718,7 +757,10 @@ const tools = [
|
|
|
718
757
|
description: 'Export the current canvas to a shareable excalidraw.com URL. The diagram is encrypted and uploaded; anyone with the URL can view it. Returns the shareable link.',
|
|
719
758
|
inputSchema: {
|
|
720
759
|
type: 'object',
|
|
721
|
-
properties: {
|
|
760
|
+
properties: {
|
|
761
|
+
sessionId: { type: 'string', description: 'Session ID for canvas isolation' }
|
|
762
|
+
},
|
|
763
|
+
required: ['sessionId']
|
|
722
764
|
}
|
|
723
765
|
},
|
|
724
766
|
{
|
|
@@ -727,6 +769,7 @@ const tools = [
|
|
|
727
769
|
inputSchema: {
|
|
728
770
|
type: 'object',
|
|
729
771
|
properties: {
|
|
772
|
+
sessionId: { type: 'string', description: 'Session ID (use list_sessions to find existing, or create_session to make a new one)' },
|
|
730
773
|
scrollToContent: {
|
|
731
774
|
type: 'boolean',
|
|
732
775
|
description: 'Auto-fit all elements in view (zoom-to-fit)'
|
|
@@ -737,7 +780,7 @@ const tools = [
|
|
|
737
780
|
},
|
|
738
781
|
zoom: {
|
|
739
782
|
type: 'number',
|
|
740
|
-
description: 'Zoom level (0.1
|
|
783
|
+
description: 'Zoom level (0.1-10, where 1 = 100%)'
|
|
741
784
|
},
|
|
742
785
|
offsetX: {
|
|
743
786
|
type: 'number',
|
|
@@ -747,8 +790,30 @@ const tools = [
|
|
|
747
790
|
type: 'number',
|
|
748
791
|
description: 'Vertical scroll offset'
|
|
749
792
|
}
|
|
793
|
+
},
|
|
794
|
+
required: ['sessionId']
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
name: 'create_session',
|
|
799
|
+
description: 'Create a new drawing session. Call this FIRST to get a sessionId, then pass it to all other tools. Returns the session ID and canvas URL.',
|
|
800
|
+
inputSchema: {
|
|
801
|
+
type: 'object',
|
|
802
|
+
properties: {
|
|
803
|
+
sessionId: {
|
|
804
|
+
type: 'string',
|
|
805
|
+
description: 'Optional custom session ID (any length). If omitted, a 6-character random ID is generated.'
|
|
806
|
+
}
|
|
750
807
|
}
|
|
751
808
|
}
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: 'list_sessions',
|
|
812
|
+
description: 'List all active drawing sessions. Call this FIRST to find an existing sessionId, or use create_session to make a new one. The sessionId is required for all other tools.',
|
|
813
|
+
inputSchema: {
|
|
814
|
+
type: 'object',
|
|
815
|
+
properties: {}
|
|
816
|
+
}
|
|
752
817
|
}
|
|
753
818
|
];
|
|
754
819
|
// Initialize MCP server
|
|
@@ -762,7 +827,24 @@ const server = new Server({
|
|
|
762
827
|
description: tool.description,
|
|
763
828
|
inputSchema: tool.inputSchema
|
|
764
829
|
}]))
|
|
765
|
-
}
|
|
830
|
+
},
|
|
831
|
+
instructions: `Excalidraw MCP Server — Programmatic canvas toolkit for creating and editing diagrams.
|
|
832
|
+
|
|
833
|
+
## Workflow
|
|
834
|
+
|
|
835
|
+
1. **Get a session**: Call \`list_sessions\` to see existing sessions, or \`create_session\` to create a new one. Every other tool requires a \`sessionId\`.
|
|
836
|
+
2. **Draw**: Use \`batch_create_elements\` to create shapes and arrows in one call. Assign custom \`id\` to shapes so arrows can bind to them via \`startElementId\`/\`endElementId\`.
|
|
837
|
+
3. **Verify**: Call \`get_canvas_screenshot\` to visually check the result. Fix issues with \`update_element\` or \`delete_element\`.
|
|
838
|
+
4. **Iterate**: Repeat draw → verify until the diagram looks correct.
|
|
839
|
+
|
|
840
|
+
## Key Rules
|
|
841
|
+
|
|
842
|
+
- **sessionId is required** for all element/scene tools. Use \`list_sessions\` or \`create_session\` to obtain one.
|
|
843
|
+
- Use \`text\` field to label shapes (auto-converts to Excalidraw label format).
|
|
844
|
+
- Use \`startElementId\`/\`endElementId\` on arrows to bind them to shapes — arrows auto-route to element edges.
|
|
845
|
+
- Size shapes for their text: \`width = max(160, textLength * 9)\`.
|
|
846
|
+
- Call \`read_diagram_guide\` before creating diagrams for color palette and layout best practices.
|
|
847
|
+
- Browser must be open at the canvas URL for \`get_canvas_screenshot\`, \`export_to_image\`, and \`set_viewport\` to work.`
|
|
766
848
|
});
|
|
767
849
|
// Helper function to convert text property to label format for Excalidraw
|
|
768
850
|
function convertTextToLabel(element) {
|
|
@@ -787,6 +869,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
787
869
|
logger.info(`Handling tool call: ${name}`);
|
|
788
870
|
switch (name) {
|
|
789
871
|
case 'create_element': {
|
|
872
|
+
const sessionId = args.sessionId;
|
|
873
|
+
if (!sessionId)
|
|
874
|
+
throw new Error('sessionId is required');
|
|
790
875
|
const params = ElementSchema.parse(args);
|
|
791
876
|
logger.info('Creating element via MCP', { type: params.type });
|
|
792
877
|
const { startElementId, endElementId, id: customId, ...elementProps } = params;
|
|
@@ -813,7 +898,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
813
898
|
// Convert text to label format for Excalidraw
|
|
814
899
|
const excalidrawElement = convertTextToLabel(element);
|
|
815
900
|
// Create element directly on HTTP server (no local storage)
|
|
816
|
-
const canvasElement = await createElementOnCanvas(excalidrawElement);
|
|
901
|
+
const canvasElement = await createElementOnCanvas(sessionId, excalidrawElement);
|
|
817
902
|
if (!canvasElement) {
|
|
818
903
|
throw new Error('Failed to create element: HTTP server unavailable');
|
|
819
904
|
}
|
|
@@ -830,6 +915,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
830
915
|
};
|
|
831
916
|
}
|
|
832
917
|
case 'update_element': {
|
|
918
|
+
const sessionId = args.sessionId;
|
|
919
|
+
if (!sessionId)
|
|
920
|
+
throw new Error('sessionId is required');
|
|
833
921
|
const params = ElementIdSchema.merge(ElementSchema.partial()).parse(args);
|
|
834
922
|
const { id, points: rawPoints, ...updates } = params;
|
|
835
923
|
if (!id)
|
|
@@ -848,7 +936,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
848
936
|
// Convert text to label format for Excalidraw
|
|
849
937
|
const excalidrawElement = convertTextToLabel(updatePayload);
|
|
850
938
|
// Update element directly on HTTP server (no local storage)
|
|
851
|
-
const canvasElement = await updateElementOnCanvas(excalidrawElement);
|
|
939
|
+
const canvasElement = await updateElementOnCanvas(sessionId, excalidrawElement);
|
|
852
940
|
if (!canvasElement) {
|
|
853
941
|
throw new Error('Failed to update element: HTTP server unavailable or element not found');
|
|
854
942
|
}
|
|
@@ -864,10 +952,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
864
952
|
};
|
|
865
953
|
}
|
|
866
954
|
case 'delete_element': {
|
|
955
|
+
const sessionId = args.sessionId;
|
|
956
|
+
if (!sessionId)
|
|
957
|
+
throw new Error('sessionId is required');
|
|
867
958
|
const params = ElementIdSchema.parse(args);
|
|
868
959
|
const { id } = params;
|
|
869
960
|
// Delete element directly on HTTP server (no local storage)
|
|
870
|
-
const canvasResult = await deleteElementOnCanvas(id);
|
|
961
|
+
const canvasResult = await deleteElementOnCanvas(sessionId, id);
|
|
871
962
|
if (!canvasResult || !canvasResult.success) {
|
|
872
963
|
throw new Error('Failed to delete element: HTTP server unavailable or element not found');
|
|
873
964
|
}
|
|
@@ -881,6 +972,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
881
972
|
};
|
|
882
973
|
}
|
|
883
974
|
case 'query_elements': {
|
|
975
|
+
const sessionId = args.sessionId;
|
|
976
|
+
if (!sessionId)
|
|
977
|
+
throw new Error('sessionId is required');
|
|
884
978
|
const params = QuerySchema.parse(args || {});
|
|
885
979
|
const { type, filter } = params;
|
|
886
980
|
try {
|
|
@@ -894,8 +988,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
894
988
|
});
|
|
895
989
|
}
|
|
896
990
|
// Query elements from HTTP server
|
|
897
|
-
const url = `${EXPRESS_SERVER_URL}/api/elements/search?${queryParams}`;
|
|
898
|
-
const response = await fetch(url);
|
|
991
|
+
const url = `${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/search?${queryParams}`;
|
|
992
|
+
const response = await fetch(url, { headers: { ...getAuthHeaders() } });
|
|
899
993
|
if (!response.ok) {
|
|
900
994
|
throw new Error(`HTTP server error: ${response.status} ${response.statusText}`);
|
|
901
995
|
}
|
|
@@ -910,6 +1004,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
910
1004
|
}
|
|
911
1005
|
}
|
|
912
1006
|
case 'get_resource': {
|
|
1007
|
+
const sessionId = args.sessionId;
|
|
1008
|
+
if (!sessionId)
|
|
1009
|
+
throw new Error('sessionId is required');
|
|
913
1010
|
const params = ResourceSchema.parse(args);
|
|
914
1011
|
const { resource } = params;
|
|
915
1012
|
logger.info('Getting resource', { resource });
|
|
@@ -926,7 +1023,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
926
1023
|
case 'elements':
|
|
927
1024
|
try {
|
|
928
1025
|
// Get elements from HTTP server
|
|
929
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements
|
|
1026
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements`, {
|
|
1027
|
+
headers: { ...getAuthHeaders() }
|
|
1028
|
+
});
|
|
930
1029
|
if (!response.ok) {
|
|
931
1030
|
throw new Error(`HTTP server error: ${response.status} ${response.statusText}`);
|
|
932
1031
|
}
|
|
@@ -952,6 +1051,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
952
1051
|
};
|
|
953
1052
|
}
|
|
954
1053
|
case 'group_elements': {
|
|
1054
|
+
const sessionId = args.sessionId;
|
|
1055
|
+
if (!sessionId)
|
|
1056
|
+
throw new Error('sessionId is required');
|
|
955
1057
|
const params = ElementIdsSchema.parse(args);
|
|
956
1058
|
const { elementIds } = params;
|
|
957
1059
|
try {
|
|
@@ -960,10 +1062,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
960
1062
|
// Update elements on canvas with proper error handling
|
|
961
1063
|
// Fetch existing groups and append new groupId to preserve multi-group membership
|
|
962
1064
|
const updatePromises = elementIds.map(async (id) => {
|
|
963
|
-
const element = await getElementFromCanvas(id);
|
|
1065
|
+
const element = await getElementFromCanvas(sessionId, id);
|
|
964
1066
|
const existingGroups = element?.groupIds || [];
|
|
965
1067
|
const updatedGroupIds = [...existingGroups, groupId];
|
|
966
|
-
return await updateElementOnCanvas({ id, groupIds: updatedGroupIds });
|
|
1068
|
+
return await updateElementOnCanvas(sessionId, { id, groupIds: updatedGroupIds });
|
|
967
1069
|
});
|
|
968
1070
|
const results = await Promise.all(updatePromises);
|
|
969
1071
|
const successCount = results.filter(result => result).length;
|
|
@@ -982,6 +1084,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
982
1084
|
}
|
|
983
1085
|
}
|
|
984
1086
|
case 'ungroup_elements': {
|
|
1087
|
+
const sessionId = args.sessionId;
|
|
1088
|
+
if (!sessionId)
|
|
1089
|
+
throw new Error('sessionId is required');
|
|
985
1090
|
const params = GroupIdSchema.parse(args);
|
|
986
1091
|
const { groupId } = params;
|
|
987
1092
|
if (!sceneState.groups.has(groupId)) {
|
|
@@ -993,14 +1098,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
993
1098
|
// Update elements on canvas, removing only this specific groupId
|
|
994
1099
|
const updatePromises = (elementIds ?? []).map(async (id) => {
|
|
995
1100
|
// Fetch current element to get existing groupIds
|
|
996
|
-
const element = await getElementFromCanvas(id);
|
|
1101
|
+
const element = await getElementFromCanvas(sessionId, id);
|
|
997
1102
|
if (!element) {
|
|
998
1103
|
logger.warn(`Element ${id} not found on canvas, skipping ungroup`);
|
|
999
1104
|
return null;
|
|
1000
1105
|
}
|
|
1001
1106
|
// Remove only the specific groupId, preserve others
|
|
1002
1107
|
const updatedGroupIds = (element.groupIds || []).filter(gid => gid !== groupId);
|
|
1003
|
-
return await updateElementOnCanvas({ id, groupIds: updatedGroupIds });
|
|
1108
|
+
return await updateElementOnCanvas(sessionId, { id, groupIds: updatedGroupIds });
|
|
1004
1109
|
});
|
|
1005
1110
|
const results = await Promise.all(updatePromises);
|
|
1006
1111
|
const successCount = results.filter(result => result !== null).length;
|
|
@@ -1018,13 +1123,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1018
1123
|
}
|
|
1019
1124
|
}
|
|
1020
1125
|
case 'align_elements': {
|
|
1126
|
+
const sessionId = args.sessionId;
|
|
1127
|
+
if (!sessionId)
|
|
1128
|
+
throw new Error('sessionId is required');
|
|
1021
1129
|
const params = AlignElementsSchema.parse(args);
|
|
1022
1130
|
const { elementIds, alignment } = params;
|
|
1023
1131
|
logger.info('Aligning elements', { elementIds, alignment });
|
|
1024
1132
|
// Fetch all elements
|
|
1025
1133
|
const elementsToAlign = [];
|
|
1026
1134
|
for (const id of elementIds) {
|
|
1027
|
-
const el = await getElementFromCanvas(id);
|
|
1135
|
+
const el = await getElementFromCanvas(sessionId, id);
|
|
1028
1136
|
if (el)
|
|
1029
1137
|
elementsToAlign.push(el);
|
|
1030
1138
|
}
|
|
@@ -1070,7 +1178,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1070
1178
|
// Apply updates
|
|
1071
1179
|
const updatePromises = elementsToAlign.map(async (el) => {
|
|
1072
1180
|
const coords = updateFn(el);
|
|
1073
|
-
return await updateElementOnCanvas({ id: el.id, ...coords });
|
|
1181
|
+
return await updateElementOnCanvas(sessionId, { id: el.id, ...coords });
|
|
1074
1182
|
});
|
|
1075
1183
|
const results = await Promise.all(updatePromises);
|
|
1076
1184
|
const successCount = results.filter(r => r).length;
|
|
@@ -1083,13 +1191,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1083
1191
|
};
|
|
1084
1192
|
}
|
|
1085
1193
|
case 'distribute_elements': {
|
|
1194
|
+
const sessionId = args.sessionId;
|
|
1195
|
+
if (!sessionId)
|
|
1196
|
+
throw new Error('sessionId is required');
|
|
1086
1197
|
const params = DistributeElementsSchema.parse(args);
|
|
1087
1198
|
const { elementIds, direction } = params;
|
|
1088
1199
|
logger.info('Distributing elements', { elementIds, direction });
|
|
1089
1200
|
// Fetch all elements
|
|
1090
1201
|
const elementsToDist = [];
|
|
1091
1202
|
for (const id of elementIds) {
|
|
1092
|
-
const el = await getElementFromCanvas(id);
|
|
1203
|
+
const el = await getElementFromCanvas(sessionId, id);
|
|
1093
1204
|
if (el)
|
|
1094
1205
|
elementsToDist.push(el);
|
|
1095
1206
|
}
|
|
@@ -1106,7 +1217,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1106
1217
|
const gap = (totalSpan - totalElementWidth) / (elementsToDist.length - 1);
|
|
1107
1218
|
let currentX = first.x;
|
|
1108
1219
|
for (const el of elementsToDist) {
|
|
1109
|
-
await updateElementOnCanvas({ id: el.id, x: currentX });
|
|
1220
|
+
await updateElementOnCanvas(sessionId, { id: el.id, x: currentX });
|
|
1110
1221
|
currentX += (el.width || 0) + gap;
|
|
1111
1222
|
}
|
|
1112
1223
|
}
|
|
@@ -1120,7 +1231,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1120
1231
|
const gap = (totalSpan - totalElementHeight) / (elementsToDist.length - 1);
|
|
1121
1232
|
let currentY = first.y;
|
|
1122
1233
|
for (const el of elementsToDist) {
|
|
1123
|
-
await updateElementOnCanvas({ id: el.id, y: currentY });
|
|
1234
|
+
await updateElementOnCanvas(sessionId, { id: el.id, y: currentY });
|
|
1124
1235
|
currentY += (el.height || 0) + gap;
|
|
1125
1236
|
}
|
|
1126
1237
|
}
|
|
@@ -1130,12 +1241,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1130
1241
|
};
|
|
1131
1242
|
}
|
|
1132
1243
|
case 'lock_elements': {
|
|
1244
|
+
const sessionId = args.sessionId;
|
|
1245
|
+
if (!sessionId)
|
|
1246
|
+
throw new Error('sessionId is required');
|
|
1133
1247
|
const params = ElementIdsSchema.parse(args);
|
|
1134
1248
|
const { elementIds } = params;
|
|
1135
1249
|
try {
|
|
1136
1250
|
// Lock elements through HTTP API updates
|
|
1137
1251
|
const updatePromises = elementIds.map(async (id) => {
|
|
1138
|
-
return await updateElementOnCanvas({ id, locked: true });
|
|
1252
|
+
return await updateElementOnCanvas(sessionId, { id, locked: true });
|
|
1139
1253
|
});
|
|
1140
1254
|
const results = await Promise.all(updatePromises);
|
|
1141
1255
|
const successCount = results.filter(result => result).length;
|
|
@@ -1152,12 +1266,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1152
1266
|
}
|
|
1153
1267
|
}
|
|
1154
1268
|
case 'unlock_elements': {
|
|
1269
|
+
const sessionId = args.sessionId;
|
|
1270
|
+
if (!sessionId)
|
|
1271
|
+
throw new Error('sessionId is required');
|
|
1155
1272
|
const params = ElementIdsSchema.parse(args);
|
|
1156
1273
|
const { elementIds } = params;
|
|
1157
1274
|
try {
|
|
1158
1275
|
// Unlock elements through HTTP API updates
|
|
1159
1276
|
const updatePromises = elementIds.map(async (id) => {
|
|
1160
|
-
return await updateElementOnCanvas({ id, locked: false });
|
|
1277
|
+
return await updateElementOnCanvas(sessionId, { id, locked: false });
|
|
1161
1278
|
});
|
|
1162
1279
|
const results = await Promise.all(updatePromises);
|
|
1163
1280
|
const successCount = results.filter(result => result).length;
|
|
@@ -1174,6 +1291,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1174
1291
|
}
|
|
1175
1292
|
}
|
|
1176
1293
|
case 'create_from_mermaid': {
|
|
1294
|
+
const sessionId = args.sessionId;
|
|
1295
|
+
if (!sessionId)
|
|
1296
|
+
throw new Error('sessionId is required');
|
|
1177
1297
|
const params = z.object({
|
|
1178
1298
|
mermaidDiagram: z.string(),
|
|
1179
1299
|
config: z.object({
|
|
@@ -1195,9 +1315,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1195
1315
|
try {
|
|
1196
1316
|
// Send the Mermaid diagram to the frontend via the API
|
|
1197
1317
|
// The frontend will use mermaid-to-excalidraw to convert it
|
|
1198
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements/from-mermaid`, {
|
|
1318
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/from-mermaid`, {
|
|
1199
1319
|
method: 'POST',
|
|
1200
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1320
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
1201
1321
|
body: JSON.stringify({
|
|
1202
1322
|
mermaidDiagram: params.mermaidDiagram,
|
|
1203
1323
|
config: params.config
|
|
@@ -1222,6 +1342,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1222
1342
|
}
|
|
1223
1343
|
}
|
|
1224
1344
|
case 'batch_create_elements': {
|
|
1345
|
+
const sessionId = args.sessionId;
|
|
1346
|
+
if (!sessionId)
|
|
1347
|
+
throw new Error('sessionId is required');
|
|
1225
1348
|
const params = z.object({ elements: z.array(ElementSchema) }).parse(args);
|
|
1226
1349
|
logger.info('Batch creating elements via MCP', { count: params.elements.length });
|
|
1227
1350
|
const createdElements = [];
|
|
@@ -1250,7 +1373,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1250
1373
|
const excalidrawElement = convertTextToLabel(element);
|
|
1251
1374
|
createdElements.push(excalidrawElement);
|
|
1252
1375
|
}
|
|
1253
|
-
const canvasElements = await batchCreateElementsOnCanvas(createdElements);
|
|
1376
|
+
const canvasElements = await batchCreateElementsOnCanvas(sessionId, createdElements);
|
|
1254
1377
|
if (!canvasElements) {
|
|
1255
1378
|
throw new Error('Failed to batch create elements: HTTP server unavailable');
|
|
1256
1379
|
}
|
|
@@ -1272,9 +1395,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1272
1395
|
};
|
|
1273
1396
|
}
|
|
1274
1397
|
case 'get_element': {
|
|
1398
|
+
const sessionId = args.sessionId;
|
|
1399
|
+
if (!sessionId)
|
|
1400
|
+
throw new Error('sessionId is required');
|
|
1275
1401
|
const params = ElementIdSchema.parse(args);
|
|
1276
1402
|
const { id } = params;
|
|
1277
|
-
const element = await getElementFromCanvas(id);
|
|
1403
|
+
const element = await getElementFromCanvas(sessionId, id);
|
|
1278
1404
|
if (!element) {
|
|
1279
1405
|
throw new Error(`Element ${id} not found`);
|
|
1280
1406
|
}
|
|
@@ -1283,9 +1409,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1283
1409
|
};
|
|
1284
1410
|
}
|
|
1285
1411
|
case 'clear_canvas': {
|
|
1412
|
+
const sessionId = args.sessionId;
|
|
1413
|
+
if (!sessionId)
|
|
1414
|
+
throw new Error('sessionId is required');
|
|
1286
1415
|
logger.info('Clearing canvas via MCP');
|
|
1287
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements/clear`, {
|
|
1288
|
-
method: 'DELETE'
|
|
1416
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/clear`, {
|
|
1417
|
+
method: 'DELETE',
|
|
1418
|
+
headers: { ...getAuthHeaders() }
|
|
1289
1419
|
});
|
|
1290
1420
|
if (!response.ok) {
|
|
1291
1421
|
throw new Error(`Failed to clear canvas: ${response.status} ${response.statusText}`);
|
|
@@ -1299,11 +1429,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1299
1429
|
};
|
|
1300
1430
|
}
|
|
1301
1431
|
case 'export_scene': {
|
|
1432
|
+
const sessionId = args.sessionId;
|
|
1433
|
+
if (!sessionId)
|
|
1434
|
+
throw new Error('sessionId is required');
|
|
1302
1435
|
const params = z.object({
|
|
1303
1436
|
filePath: z.string().optional()
|
|
1304
1437
|
}).parse(args || {});
|
|
1305
1438
|
logger.info('Exporting scene via MCP');
|
|
1306
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements
|
|
1439
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements`, {
|
|
1440
|
+
headers: { ...getAuthHeaders() }
|
|
1441
|
+
});
|
|
1307
1442
|
if (!response.ok) {
|
|
1308
1443
|
throw new Error(`Failed to fetch elements: ${response.status} ${response.statusText}`);
|
|
1309
1444
|
}
|
|
@@ -1338,6 +1473,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1338
1473
|
};
|
|
1339
1474
|
}
|
|
1340
1475
|
case 'import_scene': {
|
|
1476
|
+
const sessionId = args.sessionId;
|
|
1477
|
+
if (!sessionId)
|
|
1478
|
+
throw new Error('sessionId is required');
|
|
1341
1479
|
const params = z.object({
|
|
1342
1480
|
filePath: z.string().optional(),
|
|
1343
1481
|
data: z.string().optional(),
|
|
@@ -1365,7 +1503,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1365
1503
|
}
|
|
1366
1504
|
// If replace mode, clear first
|
|
1367
1505
|
if (params.mode === 'replace') {
|
|
1368
|
-
await fetch(`${EXPRESS_SERVER_URL}/api/elements/clear`, { method: 'DELETE' });
|
|
1506
|
+
await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/clear`, { method: 'DELETE', headers: { ...getAuthHeaders() } });
|
|
1369
1507
|
}
|
|
1370
1508
|
// Batch create the imported elements
|
|
1371
1509
|
const elementsToCreate = importElements.map(el => ({
|
|
@@ -1375,7 +1513,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1375
1513
|
updatedAt: new Date().toISOString(),
|
|
1376
1514
|
version: 1
|
|
1377
1515
|
}));
|
|
1378
|
-
const canvasElements = await batchCreateElementsOnCanvas(elementsToCreate);
|
|
1516
|
+
const canvasElements = await batchCreateElementsOnCanvas(sessionId, elementsToCreate);
|
|
1379
1517
|
return {
|
|
1380
1518
|
content: [{
|
|
1381
1519
|
type: 'text',
|
|
@@ -1384,15 +1522,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1384
1522
|
};
|
|
1385
1523
|
}
|
|
1386
1524
|
case 'export_to_image': {
|
|
1525
|
+
const sessionId = args.sessionId;
|
|
1526
|
+
if (!sessionId)
|
|
1527
|
+
throw new Error('sessionId is required');
|
|
1387
1528
|
const params = z.object({
|
|
1388
1529
|
format: z.enum(['png', 'svg']),
|
|
1389
1530
|
filePath: z.string().optional(),
|
|
1390
1531
|
background: z.boolean().optional()
|
|
1391
1532
|
}).parse(args);
|
|
1392
1533
|
logger.info('Exporting to image via MCP', { format: params.format });
|
|
1393
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/export/image`, {
|
|
1534
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/export/image`, {
|
|
1394
1535
|
method: 'POST',
|
|
1395
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1536
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
1396
1537
|
body: JSON.stringify({
|
|
1397
1538
|
format: params.format,
|
|
1398
1539
|
background: params.background ?? true
|
|
@@ -1428,6 +1569,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1428
1569
|
};
|
|
1429
1570
|
}
|
|
1430
1571
|
case 'duplicate_elements': {
|
|
1572
|
+
const sessionId = args.sessionId;
|
|
1573
|
+
if (!sessionId)
|
|
1574
|
+
throw new Error('sessionId is required');
|
|
1431
1575
|
const params = z.object({
|
|
1432
1576
|
elementIds: z.array(z.string()),
|
|
1433
1577
|
offsetX: z.number().optional(),
|
|
@@ -1438,7 +1582,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1438
1582
|
logger.info('Duplicating elements via MCP', { count: params.elementIds.length });
|
|
1439
1583
|
const duplicates = [];
|
|
1440
1584
|
for (const id of params.elementIds) {
|
|
1441
|
-
const original = await getElementFromCanvas(id);
|
|
1585
|
+
const original = await getElementFromCanvas(sessionId, id);
|
|
1442
1586
|
if (!original) {
|
|
1443
1587
|
logger.warn(`Element ${id} not found, skipping duplicate`);
|
|
1444
1588
|
continue;
|
|
@@ -1458,7 +1602,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1458
1602
|
if (duplicates.length === 0) {
|
|
1459
1603
|
throw new Error('No elements could be duplicated (none found)');
|
|
1460
1604
|
}
|
|
1461
|
-
const canvasElements = await batchCreateElementsOnCanvas(duplicates);
|
|
1605
|
+
const canvasElements = await batchCreateElementsOnCanvas(sessionId, duplicates);
|
|
1462
1606
|
return {
|
|
1463
1607
|
content: [{
|
|
1464
1608
|
type: 'text',
|
|
@@ -1467,11 +1611,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1467
1611
|
};
|
|
1468
1612
|
}
|
|
1469
1613
|
case 'snapshot_scene': {
|
|
1614
|
+
const sessionId = args.sessionId;
|
|
1615
|
+
if (!sessionId)
|
|
1616
|
+
throw new Error('sessionId is required');
|
|
1470
1617
|
const params = z.object({ name: z.string() }).parse(args);
|
|
1471
1618
|
logger.info('Saving snapshot via MCP', { name: params.name });
|
|
1472
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/snapshots`, {
|
|
1619
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/snapshots`, {
|
|
1473
1620
|
method: 'POST',
|
|
1474
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1621
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
1475
1622
|
body: JSON.stringify({ name: params.name })
|
|
1476
1623
|
});
|
|
1477
1624
|
if (!response.ok) {
|
|
@@ -1486,18 +1633,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1486
1633
|
};
|
|
1487
1634
|
}
|
|
1488
1635
|
case 'restore_snapshot': {
|
|
1636
|
+
const sessionId = args.sessionId;
|
|
1637
|
+
if (!sessionId)
|
|
1638
|
+
throw new Error('sessionId is required');
|
|
1489
1639
|
const params = z.object({ name: z.string() }).parse(args);
|
|
1490
1640
|
logger.info('Restoring snapshot via MCP', { name: params.name });
|
|
1491
1641
|
// Fetch the snapshot
|
|
1492
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/snapshots/${encodeURIComponent(params.name)}
|
|
1642
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/snapshots/${encodeURIComponent(params.name)}`, {
|
|
1643
|
+
headers: { ...getAuthHeaders() }
|
|
1644
|
+
});
|
|
1493
1645
|
if (!response.ok) {
|
|
1494
1646
|
throw new Error(`Snapshot "${params.name}" not found`);
|
|
1495
1647
|
}
|
|
1496
1648
|
const data = await response.json();
|
|
1497
1649
|
// Clear current canvas
|
|
1498
|
-
await fetch(`${EXPRESS_SERVER_URL}/api/elements/clear`, { method: 'DELETE' });
|
|
1650
|
+
await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/clear`, { method: 'DELETE', headers: { ...getAuthHeaders() } });
|
|
1499
1651
|
// Restore elements
|
|
1500
|
-
const canvasElements = await batchCreateElementsOnCanvas(data.snapshot.elements);
|
|
1652
|
+
const canvasElements = await batchCreateElementsOnCanvas(sessionId, data.snapshot.elements);
|
|
1501
1653
|
return {
|
|
1502
1654
|
content: [{
|
|
1503
1655
|
type: 'text',
|
|
@@ -1506,8 +1658,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1506
1658
|
};
|
|
1507
1659
|
}
|
|
1508
1660
|
case 'describe_scene': {
|
|
1661
|
+
const sessionId = args.sessionId;
|
|
1662
|
+
if (!sessionId)
|
|
1663
|
+
throw new Error('sessionId is required');
|
|
1509
1664
|
logger.info('Describing scene via MCP');
|
|
1510
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements
|
|
1665
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements`, {
|
|
1666
|
+
headers: { ...getAuthHeaders() }
|
|
1667
|
+
});
|
|
1511
1668
|
if (!response.ok) {
|
|
1512
1669
|
throw new Error(`Failed to fetch elements: ${response.status}`);
|
|
1513
1670
|
}
|
|
@@ -1608,13 +1765,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1608
1765
|
};
|
|
1609
1766
|
}
|
|
1610
1767
|
case 'get_canvas_screenshot': {
|
|
1768
|
+
const sessionId = args.sessionId;
|
|
1769
|
+
if (!sessionId)
|
|
1770
|
+
throw new Error('sessionId is required');
|
|
1611
1771
|
const params = z.object({
|
|
1612
1772
|
background: z.boolean().optional()
|
|
1613
1773
|
}).parse(args || {});
|
|
1614
1774
|
logger.info('Taking canvas screenshot via MCP');
|
|
1615
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/export/image`, {
|
|
1775
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/export/image`, {
|
|
1616
1776
|
method: 'POST',
|
|
1617
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1777
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
1618
1778
|
body: JSON.stringify({
|
|
1619
1779
|
format: 'png',
|
|
1620
1780
|
background: params.background ?? true
|
|
@@ -1645,9 +1805,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1645
1805
|
};
|
|
1646
1806
|
}
|
|
1647
1807
|
case 'export_to_excalidraw_url': {
|
|
1808
|
+
const sessionId = args.sessionId;
|
|
1809
|
+
if (!sessionId)
|
|
1810
|
+
throw new Error('sessionId is required');
|
|
1648
1811
|
logger.info('Exporting to excalidraw.com URL');
|
|
1649
1812
|
// 1. Fetch current scene elements
|
|
1650
|
-
const urlExportResponse = await fetch(`${EXPRESS_SERVER_URL}/api/elements
|
|
1813
|
+
const urlExportResponse = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements`, {
|
|
1814
|
+
headers: { ...getAuthHeaders() }
|
|
1815
|
+
});
|
|
1651
1816
|
if (!urlExportResponse.ok) {
|
|
1652
1817
|
throw new Error(`Failed to fetch elements: ${urlExportResponse.status}`);
|
|
1653
1818
|
}
|
|
@@ -1894,6 +2059,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1894
2059
|
};
|
|
1895
2060
|
}
|
|
1896
2061
|
case 'set_viewport': {
|
|
2062
|
+
const sessionId = args.sessionId;
|
|
2063
|
+
if (!sessionId)
|
|
2064
|
+
throw new Error('sessionId is required');
|
|
1897
2065
|
const viewportParams = z.object({
|
|
1898
2066
|
scrollToContent: z.boolean().optional(),
|
|
1899
2067
|
scrollToElementId: z.string().optional(),
|
|
@@ -1902,9 +2070,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1902
2070
|
offsetY: z.number().optional()
|
|
1903
2071
|
}).parse(args || {});
|
|
1904
2072
|
logger.info('Setting viewport via MCP', viewportParams);
|
|
1905
|
-
const viewportResponse = await fetch(`${EXPRESS_SERVER_URL}/api/viewport`, {
|
|
2073
|
+
const viewportResponse = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/viewport`, {
|
|
1906
2074
|
method: 'POST',
|
|
1907
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2075
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
1908
2076
|
body: JSON.stringify(viewportParams)
|
|
1909
2077
|
});
|
|
1910
2078
|
if (!viewportResponse.ok) {
|
|
@@ -1919,6 +2087,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1919
2087
|
}]
|
|
1920
2088
|
};
|
|
1921
2089
|
}
|
|
2090
|
+
case 'create_session': {
|
|
2091
|
+
const customId = args?.sessionId;
|
|
2092
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/sessions`, {
|
|
2093
|
+
method: 'POST',
|
|
2094
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
2095
|
+
body: JSON.stringify({ sessionId: customId })
|
|
2096
|
+
});
|
|
2097
|
+
if (!response.ok)
|
|
2098
|
+
throw new Error(`Failed to create session: ${response.status}`);
|
|
2099
|
+
const result = await response.json();
|
|
2100
|
+
const canvasUrl = `${EXPRESS_SERVER_URL}/s/${result.sessionId}`;
|
|
2101
|
+
return {
|
|
2102
|
+
content: [{
|
|
2103
|
+
type: 'text',
|
|
2104
|
+
text: JSON.stringify({ sessionId: result.sessionId, canvasUrl, created: result.created }, null, 2)
|
|
2105
|
+
}]
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
case 'list_sessions': {
|
|
2109
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/sessions`, {
|
|
2110
|
+
headers: { ...getAuthHeaders() }
|
|
2111
|
+
});
|
|
2112
|
+
if (!response.ok)
|
|
2113
|
+
throw new Error(`Failed to list sessions: ${response.status}`);
|
|
2114
|
+
const result = await response.json();
|
|
2115
|
+
return {
|
|
2116
|
+
content: [{
|
|
2117
|
+
type: 'text',
|
|
2118
|
+
text: JSON.stringify(result, null, 2)
|
|
2119
|
+
}]
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
1922
2122
|
default:
|
|
1923
2123
|
throw new Error(`Unknown tool: ${name}`);
|
|
1924
2124
|
}
|
|
@@ -1968,9 +2168,7 @@ if (process.env.DEBUG === 'true') {
|
|
|
1968
2168
|
logger.debug('Debug mode enabled');
|
|
1969
2169
|
}
|
|
1970
2170
|
// Start the server if this file is run directly
|
|
1971
|
-
|
|
1972
|
-
const __argv1 = process.argv[1] ? fs.realpathSync(process.argv[1]) : '';
|
|
1973
|
-
if (__thisFile === __argv1) {
|
|
2171
|
+
if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
1974
2172
|
runServer().catch(error => {
|
|
1975
2173
|
logger.error('Failed to start server:', error);
|
|
1976
2174
|
process.exit(1);
|
package/dist/types.js
CHANGED
|
@@ -8,10 +8,50 @@ export const EXCALIDRAW_ELEMENT_TYPES = {
|
|
|
8
8
|
FREEDRAW: 'freedraw',
|
|
9
9
|
LINE: 'line'
|
|
10
10
|
};
|
|
11
|
-
//
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Session store: manages all sessions with isolated storage
|
|
12
|
+
export class SessionStore {
|
|
13
|
+
sessions = new Map();
|
|
14
|
+
generateSessionId() {
|
|
15
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
16
|
+
let result = '';
|
|
17
|
+
for (let i = 0; i < 6; i++) {
|
|
18
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
getSession(sessionId) {
|
|
23
|
+
let session = this.sessions.get(sessionId);
|
|
24
|
+
if (!session) {
|
|
25
|
+
session = {
|
|
26
|
+
elements: new Map(),
|
|
27
|
+
snapshots: new Map(),
|
|
28
|
+
createdAt: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
this.sessions.set(sessionId, session);
|
|
31
|
+
}
|
|
32
|
+
return session;
|
|
33
|
+
}
|
|
34
|
+
hasSession(sessionId) {
|
|
35
|
+
return this.sessions.has(sessionId);
|
|
36
|
+
}
|
|
37
|
+
createSession(customId) {
|
|
38
|
+
const sessionId = customId || this.generateSessionId();
|
|
39
|
+
if (this.sessions.has(sessionId)) {
|
|
40
|
+
return { sessionId, created: false };
|
|
41
|
+
}
|
|
42
|
+
this.getSession(sessionId);
|
|
43
|
+
return { sessionId, created: true };
|
|
44
|
+
}
|
|
45
|
+
listSessions() {
|
|
46
|
+
return Array.from(this.sessions.entries()).map(([id, data]) => ({
|
|
47
|
+
sessionId: id,
|
|
48
|
+
elementCount: data.elements.size,
|
|
49
|
+
snapshotCount: data.snapshots.size,
|
|
50
|
+
createdAt: data.createdAt,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export const sessionStore = new SessionStore();
|
|
15
55
|
// Validation function for Excalidraw elements
|
|
16
56
|
export function validateElement(element) {
|
|
17
57
|
const requiredFields = ['type', 'x', 'y'];
|
package/package.json
CHANGED