@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 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.110, where 1 = 100%)'
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
- // In-memory storage for Excalidraw elements
12
- export const elements = new Map();
13
- // In-memory storage for snapshots
14
- export const snapshots = new Map();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asiones/mcp-excalidraw",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for Excalidraw canvas control — programmatic element CRUD, layout, snapshots, and real-time sync",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",