@asiones/mcp-excalidraw 1.0.1 → 1.2.1

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