@asiones/mcp-excalidraw 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +269 -86
- 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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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 for canvas isolation' },
|
|
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. Returns the session ID and canvas URL. Each session has isolated elements and snapshots.',
|
|
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 with their element and snapshot counts.',
|
|
813
|
+
inputSchema: {
|
|
814
|
+
type: 'object',
|
|
815
|
+
properties: {}
|
|
816
|
+
}
|
|
752
817
|
}
|
|
753
818
|
];
|
|
754
819
|
// Initialize MCP server
|
|
@@ -787,6 +852,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
787
852
|
logger.info(`Handling tool call: ${name}`);
|
|
788
853
|
switch (name) {
|
|
789
854
|
case 'create_element': {
|
|
855
|
+
const sessionId = args.sessionId;
|
|
856
|
+
if (!sessionId)
|
|
857
|
+
throw new Error('sessionId is required');
|
|
790
858
|
const params = ElementSchema.parse(args);
|
|
791
859
|
logger.info('Creating element via MCP', { type: params.type });
|
|
792
860
|
const { startElementId, endElementId, id: customId, ...elementProps } = params;
|
|
@@ -813,7 +881,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
813
881
|
// Convert text to label format for Excalidraw
|
|
814
882
|
const excalidrawElement = convertTextToLabel(element);
|
|
815
883
|
// Create element directly on HTTP server (no local storage)
|
|
816
|
-
const canvasElement = await createElementOnCanvas(excalidrawElement);
|
|
884
|
+
const canvasElement = await createElementOnCanvas(sessionId, excalidrawElement);
|
|
817
885
|
if (!canvasElement) {
|
|
818
886
|
throw new Error('Failed to create element: HTTP server unavailable');
|
|
819
887
|
}
|
|
@@ -830,6 +898,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
830
898
|
};
|
|
831
899
|
}
|
|
832
900
|
case 'update_element': {
|
|
901
|
+
const sessionId = args.sessionId;
|
|
902
|
+
if (!sessionId)
|
|
903
|
+
throw new Error('sessionId is required');
|
|
833
904
|
const params = ElementIdSchema.merge(ElementSchema.partial()).parse(args);
|
|
834
905
|
const { id, points: rawPoints, ...updates } = params;
|
|
835
906
|
if (!id)
|
|
@@ -848,7 +919,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
848
919
|
// Convert text to label format for Excalidraw
|
|
849
920
|
const excalidrawElement = convertTextToLabel(updatePayload);
|
|
850
921
|
// Update element directly on HTTP server (no local storage)
|
|
851
|
-
const canvasElement = await updateElementOnCanvas(excalidrawElement);
|
|
922
|
+
const canvasElement = await updateElementOnCanvas(sessionId, excalidrawElement);
|
|
852
923
|
if (!canvasElement) {
|
|
853
924
|
throw new Error('Failed to update element: HTTP server unavailable or element not found');
|
|
854
925
|
}
|
|
@@ -864,10 +935,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
864
935
|
};
|
|
865
936
|
}
|
|
866
937
|
case 'delete_element': {
|
|
938
|
+
const sessionId = args.sessionId;
|
|
939
|
+
if (!sessionId)
|
|
940
|
+
throw new Error('sessionId is required');
|
|
867
941
|
const params = ElementIdSchema.parse(args);
|
|
868
942
|
const { id } = params;
|
|
869
943
|
// Delete element directly on HTTP server (no local storage)
|
|
870
|
-
const canvasResult = await deleteElementOnCanvas(id);
|
|
944
|
+
const canvasResult = await deleteElementOnCanvas(sessionId, id);
|
|
871
945
|
if (!canvasResult || !canvasResult.success) {
|
|
872
946
|
throw new Error('Failed to delete element: HTTP server unavailable or element not found');
|
|
873
947
|
}
|
|
@@ -881,6 +955,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
881
955
|
};
|
|
882
956
|
}
|
|
883
957
|
case 'query_elements': {
|
|
958
|
+
const sessionId = args.sessionId;
|
|
959
|
+
if (!sessionId)
|
|
960
|
+
throw new Error('sessionId is required');
|
|
884
961
|
const params = QuerySchema.parse(args || {});
|
|
885
962
|
const { type, filter } = params;
|
|
886
963
|
try {
|
|
@@ -894,8 +971,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
894
971
|
});
|
|
895
972
|
}
|
|
896
973
|
// Query elements from HTTP server
|
|
897
|
-
const url = `${EXPRESS_SERVER_URL}/api/elements/search?${queryParams}`;
|
|
898
|
-
const response = await fetch(url);
|
|
974
|
+
const url = `${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/search?${queryParams}`;
|
|
975
|
+
const response = await fetch(url, { headers: { ...getAuthHeaders() } });
|
|
899
976
|
if (!response.ok) {
|
|
900
977
|
throw new Error(`HTTP server error: ${response.status} ${response.statusText}`);
|
|
901
978
|
}
|
|
@@ -910,6 +987,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
910
987
|
}
|
|
911
988
|
}
|
|
912
989
|
case 'get_resource': {
|
|
990
|
+
const sessionId = args.sessionId;
|
|
991
|
+
if (!sessionId)
|
|
992
|
+
throw new Error('sessionId is required');
|
|
913
993
|
const params = ResourceSchema.parse(args);
|
|
914
994
|
const { resource } = params;
|
|
915
995
|
logger.info('Getting resource', { resource });
|
|
@@ -926,7 +1006,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
926
1006
|
case 'elements':
|
|
927
1007
|
try {
|
|
928
1008
|
// Get elements from HTTP server
|
|
929
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements
|
|
1009
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements`, {
|
|
1010
|
+
headers: { ...getAuthHeaders() }
|
|
1011
|
+
});
|
|
930
1012
|
if (!response.ok) {
|
|
931
1013
|
throw new Error(`HTTP server error: ${response.status} ${response.statusText}`);
|
|
932
1014
|
}
|
|
@@ -952,6 +1034,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
952
1034
|
};
|
|
953
1035
|
}
|
|
954
1036
|
case 'group_elements': {
|
|
1037
|
+
const sessionId = args.sessionId;
|
|
1038
|
+
if (!sessionId)
|
|
1039
|
+
throw new Error('sessionId is required');
|
|
955
1040
|
const params = ElementIdsSchema.parse(args);
|
|
956
1041
|
const { elementIds } = params;
|
|
957
1042
|
try {
|
|
@@ -960,10 +1045,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
960
1045
|
// Update elements on canvas with proper error handling
|
|
961
1046
|
// Fetch existing groups and append new groupId to preserve multi-group membership
|
|
962
1047
|
const updatePromises = elementIds.map(async (id) => {
|
|
963
|
-
const element = await getElementFromCanvas(id);
|
|
1048
|
+
const element = await getElementFromCanvas(sessionId, id);
|
|
964
1049
|
const existingGroups = element?.groupIds || [];
|
|
965
1050
|
const updatedGroupIds = [...existingGroups, groupId];
|
|
966
|
-
return await updateElementOnCanvas({ id, groupIds: updatedGroupIds });
|
|
1051
|
+
return await updateElementOnCanvas(sessionId, { id, groupIds: updatedGroupIds });
|
|
967
1052
|
});
|
|
968
1053
|
const results = await Promise.all(updatePromises);
|
|
969
1054
|
const successCount = results.filter(result => result).length;
|
|
@@ -982,6 +1067,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
982
1067
|
}
|
|
983
1068
|
}
|
|
984
1069
|
case 'ungroup_elements': {
|
|
1070
|
+
const sessionId = args.sessionId;
|
|
1071
|
+
if (!sessionId)
|
|
1072
|
+
throw new Error('sessionId is required');
|
|
985
1073
|
const params = GroupIdSchema.parse(args);
|
|
986
1074
|
const { groupId } = params;
|
|
987
1075
|
if (!sceneState.groups.has(groupId)) {
|
|
@@ -993,14 +1081,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
993
1081
|
// Update elements on canvas, removing only this specific groupId
|
|
994
1082
|
const updatePromises = (elementIds ?? []).map(async (id) => {
|
|
995
1083
|
// Fetch current element to get existing groupIds
|
|
996
|
-
const element = await getElementFromCanvas(id);
|
|
1084
|
+
const element = await getElementFromCanvas(sessionId, id);
|
|
997
1085
|
if (!element) {
|
|
998
1086
|
logger.warn(`Element ${id} not found on canvas, skipping ungroup`);
|
|
999
1087
|
return null;
|
|
1000
1088
|
}
|
|
1001
1089
|
// Remove only the specific groupId, preserve others
|
|
1002
1090
|
const updatedGroupIds = (element.groupIds || []).filter(gid => gid !== groupId);
|
|
1003
|
-
return await updateElementOnCanvas({ id, groupIds: updatedGroupIds });
|
|
1091
|
+
return await updateElementOnCanvas(sessionId, { id, groupIds: updatedGroupIds });
|
|
1004
1092
|
});
|
|
1005
1093
|
const results = await Promise.all(updatePromises);
|
|
1006
1094
|
const successCount = results.filter(result => result !== null).length;
|
|
@@ -1018,13 +1106,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1018
1106
|
}
|
|
1019
1107
|
}
|
|
1020
1108
|
case 'align_elements': {
|
|
1109
|
+
const sessionId = args.sessionId;
|
|
1110
|
+
if (!sessionId)
|
|
1111
|
+
throw new Error('sessionId is required');
|
|
1021
1112
|
const params = AlignElementsSchema.parse(args);
|
|
1022
1113
|
const { elementIds, alignment } = params;
|
|
1023
1114
|
logger.info('Aligning elements', { elementIds, alignment });
|
|
1024
1115
|
// Fetch all elements
|
|
1025
1116
|
const elementsToAlign = [];
|
|
1026
1117
|
for (const id of elementIds) {
|
|
1027
|
-
const el = await getElementFromCanvas(id);
|
|
1118
|
+
const el = await getElementFromCanvas(sessionId, id);
|
|
1028
1119
|
if (el)
|
|
1029
1120
|
elementsToAlign.push(el);
|
|
1030
1121
|
}
|
|
@@ -1070,7 +1161,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1070
1161
|
// Apply updates
|
|
1071
1162
|
const updatePromises = elementsToAlign.map(async (el) => {
|
|
1072
1163
|
const coords = updateFn(el);
|
|
1073
|
-
return await updateElementOnCanvas({ id: el.id, ...coords });
|
|
1164
|
+
return await updateElementOnCanvas(sessionId, { id: el.id, ...coords });
|
|
1074
1165
|
});
|
|
1075
1166
|
const results = await Promise.all(updatePromises);
|
|
1076
1167
|
const successCount = results.filter(r => r).length;
|
|
@@ -1083,13 +1174,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1083
1174
|
};
|
|
1084
1175
|
}
|
|
1085
1176
|
case 'distribute_elements': {
|
|
1177
|
+
const sessionId = args.sessionId;
|
|
1178
|
+
if (!sessionId)
|
|
1179
|
+
throw new Error('sessionId is required');
|
|
1086
1180
|
const params = DistributeElementsSchema.parse(args);
|
|
1087
1181
|
const { elementIds, direction } = params;
|
|
1088
1182
|
logger.info('Distributing elements', { elementIds, direction });
|
|
1089
1183
|
// Fetch all elements
|
|
1090
1184
|
const elementsToDist = [];
|
|
1091
1185
|
for (const id of elementIds) {
|
|
1092
|
-
const el = await getElementFromCanvas(id);
|
|
1186
|
+
const el = await getElementFromCanvas(sessionId, id);
|
|
1093
1187
|
if (el)
|
|
1094
1188
|
elementsToDist.push(el);
|
|
1095
1189
|
}
|
|
@@ -1106,7 +1200,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1106
1200
|
const gap = (totalSpan - totalElementWidth) / (elementsToDist.length - 1);
|
|
1107
1201
|
let currentX = first.x;
|
|
1108
1202
|
for (const el of elementsToDist) {
|
|
1109
|
-
await updateElementOnCanvas({ id: el.id, x: currentX });
|
|
1203
|
+
await updateElementOnCanvas(sessionId, { id: el.id, x: currentX });
|
|
1110
1204
|
currentX += (el.width || 0) + gap;
|
|
1111
1205
|
}
|
|
1112
1206
|
}
|
|
@@ -1120,7 +1214,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1120
1214
|
const gap = (totalSpan - totalElementHeight) / (elementsToDist.length - 1);
|
|
1121
1215
|
let currentY = first.y;
|
|
1122
1216
|
for (const el of elementsToDist) {
|
|
1123
|
-
await updateElementOnCanvas({ id: el.id, y: currentY });
|
|
1217
|
+
await updateElementOnCanvas(sessionId, { id: el.id, y: currentY });
|
|
1124
1218
|
currentY += (el.height || 0) + gap;
|
|
1125
1219
|
}
|
|
1126
1220
|
}
|
|
@@ -1130,12 +1224,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1130
1224
|
};
|
|
1131
1225
|
}
|
|
1132
1226
|
case 'lock_elements': {
|
|
1227
|
+
const sessionId = args.sessionId;
|
|
1228
|
+
if (!sessionId)
|
|
1229
|
+
throw new Error('sessionId is required');
|
|
1133
1230
|
const params = ElementIdsSchema.parse(args);
|
|
1134
1231
|
const { elementIds } = params;
|
|
1135
1232
|
try {
|
|
1136
1233
|
// Lock elements through HTTP API updates
|
|
1137
1234
|
const updatePromises = elementIds.map(async (id) => {
|
|
1138
|
-
return await updateElementOnCanvas({ id, locked: true });
|
|
1235
|
+
return await updateElementOnCanvas(sessionId, { id, locked: true });
|
|
1139
1236
|
});
|
|
1140
1237
|
const results = await Promise.all(updatePromises);
|
|
1141
1238
|
const successCount = results.filter(result => result).length;
|
|
@@ -1152,12 +1249,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1152
1249
|
}
|
|
1153
1250
|
}
|
|
1154
1251
|
case 'unlock_elements': {
|
|
1252
|
+
const sessionId = args.sessionId;
|
|
1253
|
+
if (!sessionId)
|
|
1254
|
+
throw new Error('sessionId is required');
|
|
1155
1255
|
const params = ElementIdsSchema.parse(args);
|
|
1156
1256
|
const { elementIds } = params;
|
|
1157
1257
|
try {
|
|
1158
1258
|
// Unlock elements through HTTP API updates
|
|
1159
1259
|
const updatePromises = elementIds.map(async (id) => {
|
|
1160
|
-
return await updateElementOnCanvas({ id, locked: false });
|
|
1260
|
+
return await updateElementOnCanvas(sessionId, { id, locked: false });
|
|
1161
1261
|
});
|
|
1162
1262
|
const results = await Promise.all(updatePromises);
|
|
1163
1263
|
const successCount = results.filter(result => result).length;
|
|
@@ -1174,6 +1274,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1174
1274
|
}
|
|
1175
1275
|
}
|
|
1176
1276
|
case 'create_from_mermaid': {
|
|
1277
|
+
const sessionId = args.sessionId;
|
|
1278
|
+
if (!sessionId)
|
|
1279
|
+
throw new Error('sessionId is required');
|
|
1177
1280
|
const params = z.object({
|
|
1178
1281
|
mermaidDiagram: z.string(),
|
|
1179
1282
|
config: z.object({
|
|
@@ -1195,9 +1298,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1195
1298
|
try {
|
|
1196
1299
|
// Send the Mermaid diagram to the frontend via the API
|
|
1197
1300
|
// The frontend will use mermaid-to-excalidraw to convert it
|
|
1198
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements/from-mermaid`, {
|
|
1301
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/from-mermaid`, {
|
|
1199
1302
|
method: 'POST',
|
|
1200
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1303
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
1201
1304
|
body: JSON.stringify({
|
|
1202
1305
|
mermaidDiagram: params.mermaidDiagram,
|
|
1203
1306
|
config: params.config
|
|
@@ -1222,6 +1325,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1222
1325
|
}
|
|
1223
1326
|
}
|
|
1224
1327
|
case 'batch_create_elements': {
|
|
1328
|
+
const sessionId = args.sessionId;
|
|
1329
|
+
if (!sessionId)
|
|
1330
|
+
throw new Error('sessionId is required');
|
|
1225
1331
|
const params = z.object({ elements: z.array(ElementSchema) }).parse(args);
|
|
1226
1332
|
logger.info('Batch creating elements via MCP', { count: params.elements.length });
|
|
1227
1333
|
const createdElements = [];
|
|
@@ -1250,7 +1356,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1250
1356
|
const excalidrawElement = convertTextToLabel(element);
|
|
1251
1357
|
createdElements.push(excalidrawElement);
|
|
1252
1358
|
}
|
|
1253
|
-
const canvasElements = await batchCreateElementsOnCanvas(createdElements);
|
|
1359
|
+
const canvasElements = await batchCreateElementsOnCanvas(sessionId, createdElements);
|
|
1254
1360
|
if (!canvasElements) {
|
|
1255
1361
|
throw new Error('Failed to batch create elements: HTTP server unavailable');
|
|
1256
1362
|
}
|
|
@@ -1272,9 +1378,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1272
1378
|
};
|
|
1273
1379
|
}
|
|
1274
1380
|
case 'get_element': {
|
|
1381
|
+
const sessionId = args.sessionId;
|
|
1382
|
+
if (!sessionId)
|
|
1383
|
+
throw new Error('sessionId is required');
|
|
1275
1384
|
const params = ElementIdSchema.parse(args);
|
|
1276
1385
|
const { id } = params;
|
|
1277
|
-
const element = await getElementFromCanvas(id);
|
|
1386
|
+
const element = await getElementFromCanvas(sessionId, id);
|
|
1278
1387
|
if (!element) {
|
|
1279
1388
|
throw new Error(`Element ${id} not found`);
|
|
1280
1389
|
}
|
|
@@ -1283,9 +1392,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1283
1392
|
};
|
|
1284
1393
|
}
|
|
1285
1394
|
case 'clear_canvas': {
|
|
1395
|
+
const sessionId = args.sessionId;
|
|
1396
|
+
if (!sessionId)
|
|
1397
|
+
throw new Error('sessionId is required');
|
|
1286
1398
|
logger.info('Clearing canvas via MCP');
|
|
1287
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements/clear`, {
|
|
1288
|
-
method: 'DELETE'
|
|
1399
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/clear`, {
|
|
1400
|
+
method: 'DELETE',
|
|
1401
|
+
headers: { ...getAuthHeaders() }
|
|
1289
1402
|
});
|
|
1290
1403
|
if (!response.ok) {
|
|
1291
1404
|
throw new Error(`Failed to clear canvas: ${response.status} ${response.statusText}`);
|
|
@@ -1299,11 +1412,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1299
1412
|
};
|
|
1300
1413
|
}
|
|
1301
1414
|
case 'export_scene': {
|
|
1415
|
+
const sessionId = args.sessionId;
|
|
1416
|
+
if (!sessionId)
|
|
1417
|
+
throw new Error('sessionId is required');
|
|
1302
1418
|
const params = z.object({
|
|
1303
1419
|
filePath: z.string().optional()
|
|
1304
1420
|
}).parse(args || {});
|
|
1305
1421
|
logger.info('Exporting scene via MCP');
|
|
1306
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements
|
|
1422
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements`, {
|
|
1423
|
+
headers: { ...getAuthHeaders() }
|
|
1424
|
+
});
|
|
1307
1425
|
if (!response.ok) {
|
|
1308
1426
|
throw new Error(`Failed to fetch elements: ${response.status} ${response.statusText}`);
|
|
1309
1427
|
}
|
|
@@ -1338,6 +1456,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1338
1456
|
};
|
|
1339
1457
|
}
|
|
1340
1458
|
case 'import_scene': {
|
|
1459
|
+
const sessionId = args.sessionId;
|
|
1460
|
+
if (!sessionId)
|
|
1461
|
+
throw new Error('sessionId is required');
|
|
1341
1462
|
const params = z.object({
|
|
1342
1463
|
filePath: z.string().optional(),
|
|
1343
1464
|
data: z.string().optional(),
|
|
@@ -1365,7 +1486,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1365
1486
|
}
|
|
1366
1487
|
// If replace mode, clear first
|
|
1367
1488
|
if (params.mode === 'replace') {
|
|
1368
|
-
await fetch(`${EXPRESS_SERVER_URL}/api/elements/clear`, { method: 'DELETE' });
|
|
1489
|
+
await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/clear`, { method: 'DELETE', headers: { ...getAuthHeaders() } });
|
|
1369
1490
|
}
|
|
1370
1491
|
// Batch create the imported elements
|
|
1371
1492
|
const elementsToCreate = importElements.map(el => ({
|
|
@@ -1375,7 +1496,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1375
1496
|
updatedAt: new Date().toISOString(),
|
|
1376
1497
|
version: 1
|
|
1377
1498
|
}));
|
|
1378
|
-
const canvasElements = await batchCreateElementsOnCanvas(elementsToCreate);
|
|
1499
|
+
const canvasElements = await batchCreateElementsOnCanvas(sessionId, elementsToCreate);
|
|
1379
1500
|
return {
|
|
1380
1501
|
content: [{
|
|
1381
1502
|
type: 'text',
|
|
@@ -1384,15 +1505,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1384
1505
|
};
|
|
1385
1506
|
}
|
|
1386
1507
|
case 'export_to_image': {
|
|
1508
|
+
const sessionId = args.sessionId;
|
|
1509
|
+
if (!sessionId)
|
|
1510
|
+
throw new Error('sessionId is required');
|
|
1387
1511
|
const params = z.object({
|
|
1388
1512
|
format: z.enum(['png', 'svg']),
|
|
1389
1513
|
filePath: z.string().optional(),
|
|
1390
1514
|
background: z.boolean().optional()
|
|
1391
1515
|
}).parse(args);
|
|
1392
1516
|
logger.info('Exporting to image via MCP', { format: params.format });
|
|
1393
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/export/image`, {
|
|
1517
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/export/image`, {
|
|
1394
1518
|
method: 'POST',
|
|
1395
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1519
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
1396
1520
|
body: JSON.stringify({
|
|
1397
1521
|
format: params.format,
|
|
1398
1522
|
background: params.background ?? true
|
|
@@ -1428,6 +1552,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1428
1552
|
};
|
|
1429
1553
|
}
|
|
1430
1554
|
case 'duplicate_elements': {
|
|
1555
|
+
const sessionId = args.sessionId;
|
|
1556
|
+
if (!sessionId)
|
|
1557
|
+
throw new Error('sessionId is required');
|
|
1431
1558
|
const params = z.object({
|
|
1432
1559
|
elementIds: z.array(z.string()),
|
|
1433
1560
|
offsetX: z.number().optional(),
|
|
@@ -1438,7 +1565,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1438
1565
|
logger.info('Duplicating elements via MCP', { count: params.elementIds.length });
|
|
1439
1566
|
const duplicates = [];
|
|
1440
1567
|
for (const id of params.elementIds) {
|
|
1441
|
-
const original = await getElementFromCanvas(id);
|
|
1568
|
+
const original = await getElementFromCanvas(sessionId, id);
|
|
1442
1569
|
if (!original) {
|
|
1443
1570
|
logger.warn(`Element ${id} not found, skipping duplicate`);
|
|
1444
1571
|
continue;
|
|
@@ -1458,7 +1585,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1458
1585
|
if (duplicates.length === 0) {
|
|
1459
1586
|
throw new Error('No elements could be duplicated (none found)');
|
|
1460
1587
|
}
|
|
1461
|
-
const canvasElements = await batchCreateElementsOnCanvas(duplicates);
|
|
1588
|
+
const canvasElements = await batchCreateElementsOnCanvas(sessionId, duplicates);
|
|
1462
1589
|
return {
|
|
1463
1590
|
content: [{
|
|
1464
1591
|
type: 'text',
|
|
@@ -1467,11 +1594,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1467
1594
|
};
|
|
1468
1595
|
}
|
|
1469
1596
|
case 'snapshot_scene': {
|
|
1597
|
+
const sessionId = args.sessionId;
|
|
1598
|
+
if (!sessionId)
|
|
1599
|
+
throw new Error('sessionId is required');
|
|
1470
1600
|
const params = z.object({ name: z.string() }).parse(args);
|
|
1471
1601
|
logger.info('Saving snapshot via MCP', { name: params.name });
|
|
1472
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/snapshots`, {
|
|
1602
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/snapshots`, {
|
|
1473
1603
|
method: 'POST',
|
|
1474
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1604
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
1475
1605
|
body: JSON.stringify({ name: params.name })
|
|
1476
1606
|
});
|
|
1477
1607
|
if (!response.ok) {
|
|
@@ -1486,18 +1616,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1486
1616
|
};
|
|
1487
1617
|
}
|
|
1488
1618
|
case 'restore_snapshot': {
|
|
1619
|
+
const sessionId = args.sessionId;
|
|
1620
|
+
if (!sessionId)
|
|
1621
|
+
throw new Error('sessionId is required');
|
|
1489
1622
|
const params = z.object({ name: z.string() }).parse(args);
|
|
1490
1623
|
logger.info('Restoring snapshot via MCP', { name: params.name });
|
|
1491
1624
|
// Fetch the snapshot
|
|
1492
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/snapshots/${encodeURIComponent(params.name)}
|
|
1625
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/snapshots/${encodeURIComponent(params.name)}`, {
|
|
1626
|
+
headers: { ...getAuthHeaders() }
|
|
1627
|
+
});
|
|
1493
1628
|
if (!response.ok) {
|
|
1494
1629
|
throw new Error(`Snapshot "${params.name}" not found`);
|
|
1495
1630
|
}
|
|
1496
1631
|
const data = await response.json();
|
|
1497
1632
|
// Clear current canvas
|
|
1498
|
-
await fetch(`${EXPRESS_SERVER_URL}/api/elements/clear`, { method: 'DELETE' });
|
|
1633
|
+
await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements/clear`, { method: 'DELETE', headers: { ...getAuthHeaders() } });
|
|
1499
1634
|
// Restore elements
|
|
1500
|
-
const canvasElements = await batchCreateElementsOnCanvas(data.snapshot.elements);
|
|
1635
|
+
const canvasElements = await batchCreateElementsOnCanvas(sessionId, data.snapshot.elements);
|
|
1501
1636
|
return {
|
|
1502
1637
|
content: [{
|
|
1503
1638
|
type: 'text',
|
|
@@ -1506,8 +1641,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1506
1641
|
};
|
|
1507
1642
|
}
|
|
1508
1643
|
case 'describe_scene': {
|
|
1644
|
+
const sessionId = args.sessionId;
|
|
1645
|
+
if (!sessionId)
|
|
1646
|
+
throw new Error('sessionId is required');
|
|
1509
1647
|
logger.info('Describing scene via MCP');
|
|
1510
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements
|
|
1648
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements`, {
|
|
1649
|
+
headers: { ...getAuthHeaders() }
|
|
1650
|
+
});
|
|
1511
1651
|
if (!response.ok) {
|
|
1512
1652
|
throw new Error(`Failed to fetch elements: ${response.status}`);
|
|
1513
1653
|
}
|
|
@@ -1608,13 +1748,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1608
1748
|
};
|
|
1609
1749
|
}
|
|
1610
1750
|
case 'get_canvas_screenshot': {
|
|
1751
|
+
const sessionId = args.sessionId;
|
|
1752
|
+
if (!sessionId)
|
|
1753
|
+
throw new Error('sessionId is required');
|
|
1611
1754
|
const params = z.object({
|
|
1612
1755
|
background: z.boolean().optional()
|
|
1613
1756
|
}).parse(args || {});
|
|
1614
1757
|
logger.info('Taking canvas screenshot via MCP');
|
|
1615
|
-
const response = await fetch(`${EXPRESS_SERVER_URL}/api/export/image`, {
|
|
1758
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/export/image`, {
|
|
1616
1759
|
method: 'POST',
|
|
1617
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1760
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
1618
1761
|
body: JSON.stringify({
|
|
1619
1762
|
format: 'png',
|
|
1620
1763
|
background: params.background ?? true
|
|
@@ -1645,9 +1788,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1645
1788
|
};
|
|
1646
1789
|
}
|
|
1647
1790
|
case 'export_to_excalidraw_url': {
|
|
1791
|
+
const sessionId = args.sessionId;
|
|
1792
|
+
if (!sessionId)
|
|
1793
|
+
throw new Error('sessionId is required');
|
|
1648
1794
|
logger.info('Exporting to excalidraw.com URL');
|
|
1649
1795
|
// 1. Fetch current scene elements
|
|
1650
|
-
const urlExportResponse = await fetch(`${EXPRESS_SERVER_URL}/api/elements
|
|
1796
|
+
const urlExportResponse = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/elements`, {
|
|
1797
|
+
headers: { ...getAuthHeaders() }
|
|
1798
|
+
});
|
|
1651
1799
|
if (!urlExportResponse.ok) {
|
|
1652
1800
|
throw new Error(`Failed to fetch elements: ${urlExportResponse.status}`);
|
|
1653
1801
|
}
|
|
@@ -1894,6 +2042,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1894
2042
|
};
|
|
1895
2043
|
}
|
|
1896
2044
|
case 'set_viewport': {
|
|
2045
|
+
const sessionId = args.sessionId;
|
|
2046
|
+
if (!sessionId)
|
|
2047
|
+
throw new Error('sessionId is required');
|
|
1897
2048
|
const viewportParams = z.object({
|
|
1898
2049
|
scrollToContent: z.boolean().optional(),
|
|
1899
2050
|
scrollToElementId: z.string().optional(),
|
|
@@ -1902,9 +2053,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1902
2053
|
offsetY: z.number().optional()
|
|
1903
2054
|
}).parse(args || {});
|
|
1904
2055
|
logger.info('Setting viewport via MCP', viewportParams);
|
|
1905
|
-
const viewportResponse = await fetch(`${EXPRESS_SERVER_URL}/api/viewport`, {
|
|
2056
|
+
const viewportResponse = await fetch(`${EXPRESS_SERVER_URL}/api/s/${sessionId}/viewport`, {
|
|
1906
2057
|
method: 'POST',
|
|
1907
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2058
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
1908
2059
|
body: JSON.stringify(viewportParams)
|
|
1909
2060
|
});
|
|
1910
2061
|
if (!viewportResponse.ok) {
|
|
@@ -1919,6 +2070,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1919
2070
|
}]
|
|
1920
2071
|
};
|
|
1921
2072
|
}
|
|
2073
|
+
case 'create_session': {
|
|
2074
|
+
const customId = args?.sessionId;
|
|
2075
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/sessions`, {
|
|
2076
|
+
method: 'POST',
|
|
2077
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
2078
|
+
body: JSON.stringify({ sessionId: customId })
|
|
2079
|
+
});
|
|
2080
|
+
if (!response.ok)
|
|
2081
|
+
throw new Error(`Failed to create session: ${response.status}`);
|
|
2082
|
+
const result = await response.json();
|
|
2083
|
+
const canvasUrl = `${EXPRESS_SERVER_URL}/s/${result.sessionId}`;
|
|
2084
|
+
return {
|
|
2085
|
+
content: [{
|
|
2086
|
+
type: 'text',
|
|
2087
|
+
text: JSON.stringify({ sessionId: result.sessionId, canvasUrl, created: result.created }, null, 2)
|
|
2088
|
+
}]
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
case 'list_sessions': {
|
|
2092
|
+
const response = await fetch(`${EXPRESS_SERVER_URL}/api/sessions`, {
|
|
2093
|
+
headers: { ...getAuthHeaders() }
|
|
2094
|
+
});
|
|
2095
|
+
if (!response.ok)
|
|
2096
|
+
throw new Error(`Failed to list sessions: ${response.status}`);
|
|
2097
|
+
const result = await response.json();
|
|
2098
|
+
return {
|
|
2099
|
+
content: [{
|
|
2100
|
+
type: 'text',
|
|
2101
|
+
text: JSON.stringify(result, null, 2)
|
|
2102
|
+
}]
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
1922
2105
|
default:
|
|
1923
2106
|
throw new Error(`Unknown tool: ${name}`);
|
|
1924
2107
|
}
|
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