excalidrop 0.1.4 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +70 -125
  2. package/dist/cli.d.ts +7 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +936 -389
  5. package/dist/cli.js.map +1 -1
  6. package/dist/frontend/assets/{advancedFormat-tiBAHNGo.js → advancedFormat-Be4KR5mp.js} +1 -1
  7. package/dist/frontend/assets/{ar-SA-G6X2FPQ2-DVxiWV5Z.js → ar-SA-G6X2FPQ2-B6ekz3Ba.js} +1 -1
  8. package/dist/frontend/assets/arc-DlgXyGPj.js +1 -0
  9. package/dist/frontend/assets/{az-AZ-76LH7QW2-Z_2BqDo-.js → az-AZ-76LH7QW2-01MDUsF0.js} +1 -1
  10. package/dist/frontend/assets/{bg-BG-XCXSNQG7-DrxGJZOG.js → bg-BG-XCXSNQG7--fArB8Ao.js} +1 -1
  11. package/dist/frontend/assets/{blockDiagram-38ab4fdb-CDnkTFfP.js → blockDiagram-38ab4fdb-CmsVhOwi.js} +1 -1
  12. package/dist/frontend/assets/{blockDiagram-68f4deed-Bb0W6mxN.js → blockDiagram-68f4deed-DU_wNYlO.js} +6 -6
  13. package/dist/frontend/assets/{bn-BD-2XOGV67Q-BkGJ-w8k.js → bn-BD-2XOGV67Q-h8juqzfQ.js} +1 -1
  14. package/dist/frontend/assets/{c4Diagram-15b5d702-ByRV0tka.js → c4Diagram-15b5d702-ClZalz0L.js} +1 -1
  15. package/dist/frontend/assets/{c4Diagram-3d4e48cf-QSVnzYYK.js → c4Diagram-3d4e48cf-BkFVM0tP.js} +1 -1
  16. package/dist/frontend/assets/{ca-ES-6MX7JW3Y-BBJvOPE1.js → ca-ES-6MX7JW3Y-DMBR3qVX.js} +1 -1
  17. package/dist/frontend/assets/channel-BxFR7168.js +1 -0
  18. package/dist/frontend/assets/{classDiagram-70f12bd4-DDFBNGUt.js → classDiagram-70f12bd4-BXAJnakg.js} +1 -1
  19. package/dist/frontend/assets/{classDiagram-d40c83e7-cgC3s7Nz.js → classDiagram-d40c83e7-DVhipa45.js} +1 -1
  20. package/dist/frontend/assets/classDiagram-v2-d5a6b087-CiHMsbhj.js +2 -0
  21. package/dist/frontend/assets/{classDiagram-v2-f2320105-Dtywkpeg.js → classDiagram-v2-f2320105-1CqIxWOB.js} +2 -2
  22. package/dist/frontend/assets/clone-CMl85WSi.js +1 -0
  23. package/dist/frontend/assets/{createText-2e5e7dd3-CqY00v-4.js → createText-2e5e7dd3-D3kSz3YE.js} +1 -1
  24. package/dist/frontend/assets/{createText-d213de94-D59sBfqq.js → createText-d213de94-DvJeLlQX.js} +4 -4
  25. package/dist/frontend/assets/{cs-CZ-2BRQDIVT-CK09MAsg.js → cs-CZ-2BRQDIVT-BUcdPrAr.js} +1 -1
  26. package/dist/frontend/assets/{cytoscape-cose-bilkent-CKy_tuGZ.js → cytoscape-cose-bilkent-CKoSm_2z.js} +1 -1
  27. package/dist/frontend/assets/{da-DK-5WZEPLOC-V4hdLKuT.js → da-DK-5WZEPLOC-C1nkchuC.js} +1 -1
  28. package/dist/frontend/assets/{de-DE-XR44H4JA-Dw8qkP9g.js → de-DE-XR44H4JA-DO2_bO1X.js} +1 -1
  29. package/dist/frontend/assets/{edges-332bd1c7-D-u0KMZB.js → edges-332bd1c7-C5qM1sDU.js} +1 -1
  30. package/dist/frontend/assets/{edges-e0da2a9e-B3UFqLqY.js → edges-e0da2a9e-BRt7qomS.js} +1 -1
  31. package/dist/frontend/assets/{el-GR-BZB4AONW-BUdBQbYg.js → el-GR-BZB4AONW-CrjSdaU2.js} +1 -1
  32. package/dist/frontend/assets/{elk.bundled-BZA3Xcoo.js → elk.bundled-Dmec6Rq6.js} +1 -1
  33. package/dist/frontend/assets/{erDiagram-880f2ed8-CNxm7e1x.js → erDiagram-880f2ed8-BI7kP523.js} +4 -4
  34. package/dist/frontend/assets/{erDiagram-9861fffd-C2u_ziAM.js → erDiagram-9861fffd-DMyCDrgI.js} +1 -1
  35. package/dist/frontend/assets/{es-ES-U4NZUMDT-DS0FAZqX.js → es-ES-U4NZUMDT-rrdjPi-M.js} +1 -1
  36. package/dist/frontend/assets/{eu-ES-A7QVB2H4-D-D5uidc.js → eu-ES-A7QVB2H4-uYn3S3D5.js} +1 -1
  37. package/dist/frontend/assets/{fa-IR-HGAKTJCU-mAW_2-jg.js → fa-IR-HGAKTJCU-DWW_BVFR.js} +1 -1
  38. package/dist/frontend/assets/{fi-FI-Z5N7JZ37-B9mW6nxS.js → fi-FI-Z5N7JZ37-CMXxeZ4W.js} +1 -1
  39. package/dist/frontend/assets/{flowDb-7c981674-6JB2gd0I.js → flowDb-7c981674-Dsj6xMzr.js} +1 -1
  40. package/dist/frontend/assets/{flowDb-956e92f1-BbpkWRi7.js → flowDb-956e92f1-26g7kJju.js} +1 -1
  41. package/dist/frontend/assets/{flowDiagram-66a62f08-DrhrQuXc.js → flowDiagram-66a62f08-C9eQZtdf.js} +1 -1
  42. package/dist/frontend/assets/{flowDiagram-cbd28bf7-BmNY7bvJ.js → flowDiagram-cbd28bf7-D051xFrI.js} +3 -3
  43. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-DIa4RdMh.js +1 -0
  44. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CnvFT8HV.js +1 -0
  45. package/dist/frontend/assets/{flowchart-elk-definition-36e2d292-4G1YiIlk.js → flowchart-elk-definition-36e2d292-C9fsvhV-.js} +2 -2
  46. package/dist/frontend/assets/{flowchart-elk-definition-4a651766-Cm94SrrQ.js → flowchart-elk-definition-4a651766-BkVNuJOw.js} +1 -1
  47. package/dist/frontend/assets/{fr-FR-RHASNOE6-BXnmUxTA.js → fr-FR-RHASNOE6-DIfK_rjW.js} +1 -1
  48. package/dist/frontend/assets/{ganttDiagram-04f9e578-X0BweAnG.js → ganttDiagram-04f9e578-CZeF2a4Y.js} +1 -1
  49. package/dist/frontend/assets/{ganttDiagram-c361ad54-D3vxZ5MQ.js → ganttDiagram-c361ad54-DbkIZ-wU.js} +1 -1
  50. package/dist/frontend/assets/{gitGraphDiagram-21fc4d3e-wznrpRWn.js → gitGraphDiagram-21fc4d3e-CLqlSEuE.js} +6 -6
  51. package/dist/frontend/assets/{gitGraphDiagram-72cf32ee-B9zCS3Pw.js → gitGraphDiagram-72cf32ee-Cvf9uYWB.js} +1 -1
  52. package/dist/frontend/assets/{gl-ES-HMX3MZ6V-CrUx-ab6.js → gl-ES-HMX3MZ6V-CCxCH2oK.js} +1 -1
  53. package/dist/frontend/assets/{graph-BQZ6SIVr.js → graph-MptL1QoF.js} +1 -1
  54. package/dist/frontend/assets/{graph-Cda8xSTx.js → graph-Mzuu2QFH.js} +1 -1
  55. package/dist/frontend/assets/{he-IL-6SHJWFNN-D33eaw6-.js → he-IL-6SHJWFNN-CJuJutJZ.js} +1 -1
  56. package/dist/frontend/assets/{hi-IN-IWLTKZ5I-DEnStF-g.js → hi-IN-IWLTKZ5I-BORPH6vU.js} +1 -1
  57. package/dist/frontend/assets/{hu-HU-A5ZG7DT2-BjH2w8Sn.js → hu-HU-A5ZG7DT2-Dr7UDbjo.js} +1 -1
  58. package/dist/frontend/assets/{id-ID-SAP4L64H-Bc6R-JEz.js → id-ID-SAP4L64H-D0Widkq7.js} +1 -1
  59. package/dist/frontend/assets/{index-3862675e-CpYfS3Db.js → index-3862675e-DVT5-vN2.js} +1 -1
  60. package/dist/frontend/assets/{index-6079d271-Y2bgyRpa.js → index-6079d271-R4KRnW5Z.js} +1 -1
  61. package/dist/frontend/assets/{index-D7eYnAQp.js → index-B45XzZiR.js} +4 -4
  62. package/dist/frontend/assets/index-Bd-8869C.js +349 -0
  63. package/dist/frontend/assets/{infoDiagram-4a4f5b27-Bm3Oq-Rq.js → infoDiagram-4a4f5b27-BvY1qS17.js} +3 -3
  64. package/dist/frontend/assets/{infoDiagram-f8f76790-oNVitBKM.js → infoDiagram-f8f76790-D60YvG33.js} +1 -1
  65. package/dist/frontend/assets/{it-IT-JPQ66NNP-BcxQABF2.js → it-IT-JPQ66NNP-C08Vu0BG.js} +1 -1
  66. package/dist/frontend/assets/{ja-JP-DBVTYXUO-D1zIM-nt.js → ja-JP-DBVTYXUO-Ciu7LaRi.js} +1 -1
  67. package/dist/frontend/assets/{journeyDiagram-29694f62-CamPk_3P.js → journeyDiagram-29694f62-DvrnJXsd.js} +5 -5
  68. package/dist/frontend/assets/{journeyDiagram-49397b02-D6w3tLtf.js → journeyDiagram-49397b02-Dy9LU86E.js} +1 -1
  69. package/dist/frontend/assets/{kaa-6HZHGXH3-CoILysVg.js → kaa-6HZHGXH3-GhGTIjpu.js} +1 -1
  70. package/dist/frontend/assets/{kab-KAB-ZGHBKWFO-B9ZbybBt.js → kab-KAB-ZGHBKWFO-B3q4BqYw.js} +1 -1
  71. package/dist/frontend/assets/{kk-KZ-P5N5QNE5-Dxi4GIQ7.js → kk-KZ-P5N5QNE5-GWoLZvco.js} +1 -1
  72. package/dist/frontend/assets/{km-KH-HSX4SM5Z-DaJ7EtO2.js → km-KH-HSX4SM5Z-CnAQJCYr.js} +1 -1
  73. package/dist/frontend/assets/{ko-KR-MTYHY66A-DG57Ah12.js → ko-KR-MTYHY66A-B2Rh9krp.js} +1 -1
  74. package/dist/frontend/assets/{ku-TR-6OUDTVRD-DnDf9E8a.js → ku-TR-6OUDTVRD-C27elmON.js} +1 -1
  75. package/dist/frontend/assets/{layout-Drr-66M9.js → layout-D1hl6oKi.js} +1 -1
  76. package/dist/frontend/assets/{layout-BsWfhUAF.js → layout-c-hOA8zE.js} +1 -1
  77. package/dist/frontend/assets/{line-DwQObOhP.js → line-DtMz1ssZ.js} +1 -1
  78. package/dist/frontend/assets/{linear-C36MjJps.js → linear-D-Y4WtO_.js} +1 -1
  79. package/dist/frontend/assets/{lt-LT-XHIRWOB4-D_zqS0yg.js → lt-LT-XHIRWOB4-B6jd693w.js} +1 -1
  80. package/dist/frontend/assets/{lv-LV-5QDEKY6T-owIsIh1b.js → lv-LV-5QDEKY6T-sXB-8i6R.js} +1 -1
  81. package/dist/frontend/assets/{mindmap-definition-ac74a2e8-Cf9JbrBJ.js → mindmap-definition-ac74a2e8-CawO9I_f.js} +6 -6
  82. package/dist/frontend/assets/{mindmap-definition-fc14e90a-BpsNCNCp.js → mindmap-definition-fc14e90a-CXMNA68v.js} +4 -4
  83. package/dist/frontend/assets/{mr-IN-CRQNXWMA-CS9XUj74.js → mr-IN-CRQNXWMA-BJ6QSiPi.js} +1 -1
  84. package/dist/frontend/assets/{my-MM-5M5IBNSE-BKB060k9.js → my-MM-5M5IBNSE-BMdOIRP5.js} +1 -1
  85. package/dist/frontend/assets/{nb-NO-T6EIAALU-CkmLUrvm.js → nb-NO-T6EIAALU-BVG6JUMN.js} +1 -1
  86. package/dist/frontend/assets/{nl-NL-IS3SIHDZ-DdiLyJoz.js → nl-NL-IS3SIHDZ-ZprCvTYR.js} +1 -1
  87. package/dist/frontend/assets/{nn-NO-6E72VCQL-Yevp6f_Y.js → nn-NO-6E72VCQL-CcljFAGy.js} +1 -1
  88. package/dist/frontend/assets/{oc-FR-POXYY2M6-D_M6GHup.js → oc-FR-POXYY2M6-zCuu6Ns2.js} +1 -1
  89. package/dist/frontend/assets/{pa-IN-N4M65BXN-1vuACN9n.js → pa-IN-N4M65BXN-D_iftL9v.js} +1 -1
  90. package/dist/frontend/assets/{pica-DDDx0iqx.js → pica-DsW4eP6T.js} +1 -1
  91. package/dist/frontend/assets/{pie-CFIpuQMO.js → pie-U_uHwWsL.js} +1 -1
  92. package/dist/frontend/assets/{pieDiagram-421022e6-DO7e_Qgw.js → pieDiagram-421022e6-naaI7HiE.js} +5 -5
  93. package/dist/frontend/assets/{pieDiagram-8a3498a8-BEoqChXo.js → pieDiagram-8a3498a8-m6nTf1O_.js} +1 -1
  94. package/dist/frontend/assets/{pl-PL-T2D74RX3-Cc6Q5nSh.js → pl-PL-T2D74RX3-CRQQfZUe.js} +1 -1
  95. package/dist/frontend/assets/{pt-BR-5N22H2LF-3eEiAax3.js → pt-BR-5N22H2LF-DLvfG7RR.js} +1 -1
  96. package/dist/frontend/assets/{pt-PT-UZXXM6DQ-CgnF0Le7.js → pt-PT-UZXXM6DQ-Chkg6-JC.js} +1 -1
  97. package/dist/frontend/assets/{quadrantDiagram-0957ecba-C1Eo-ZUY.js → quadrantDiagram-0957ecba-BJR82kAx.js} +3 -3
  98. package/dist/frontend/assets/{quadrantDiagram-120e2f19-Bdfsl_O7.js → quadrantDiagram-120e2f19-CsWgLp5e.js} +1 -1
  99. package/dist/frontend/assets/relay-BSVGqyIy.js +2 -0
  100. package/dist/frontend/assets/{requirementDiagram-23d650b8-DiOK6QW3.js → requirementDiagram-23d650b8-CHmg-ycX.js} +1 -1
  101. package/dist/frontend/assets/{requirementDiagram-deff3bca-BDJ-7T4h.js → requirementDiagram-deff3bca-LIsyxsZZ.js} +1 -1
  102. package/dist/frontend/assets/{ro-RO-JPDTUUEW-CbJ3IB7a.js → ro-RO-JPDTUUEW-B_a5A5-x.js} +1 -1
  103. package/dist/frontend/assets/{ru-RU-B4JR7IUQ-PQGmeGG4.js → ru-RU-B4JR7IUQ-CY5sI1Ru.js} +1 -1
  104. package/dist/frontend/assets/{sankeyDiagram-04a897e0-DV25bgN2.js → sankeyDiagram-04a897e0-DN5e16d0.js} +1 -1
  105. package/dist/frontend/assets/{sankeyDiagram-23345273-szSb_Sei.js → sankeyDiagram-23345273-BBjQCtHz.js} +1 -1
  106. package/dist/frontend/assets/{selectAll-DsrPvhqV.js → selectAll-Dcq-dC_E.js} +1 -1
  107. package/dist/frontend/assets/{sequenceDiagram-17ac3bff-Dg88bSlc.js → sequenceDiagram-17ac3bff-CIXyQiN6.js} +1 -1
  108. package/dist/frontend/assets/{sequenceDiagram-704730f1-DRIjLCxx.js → sequenceDiagram-704730f1-BmDwBhsF.js} +1 -1
  109. package/dist/frontend/assets/{si-LK-N5RQ5JYF-BtIIudm3.js → si-LK-N5RQ5JYF-DEPKL_1i.js} +1 -1
  110. package/dist/frontend/assets/{sk-SK-C5VTKIMK-CIwXticU.js → sk-SK-C5VTKIMK-XqrGDM2K.js} +1 -1
  111. package/dist/frontend/assets/{sl-SI-NN7IZMDC-CXEdg57y.js → sl-SI-NN7IZMDC-Dia3d1Yy.js} +1 -1
  112. package/dist/frontend/assets/{stateDiagram-587899a1-BwRwgRo6.js → stateDiagram-587899a1-CY1BCaUa.js} +1 -1
  113. package/dist/frontend/assets/{stateDiagram-9c5f0230-C8n96rER.js → stateDiagram-9c5f0230-gyjWQtSe.js} +1 -1
  114. package/dist/frontend/assets/{stateDiagram-v2-51a3dcff-B7e5E4J7.js → stateDiagram-v2-51a3dcff-CAIiXqRE.js} +1 -1
  115. package/dist/frontend/assets/{stateDiagram-v2-d93cdb3a-B18iZQLw.js → stateDiagram-v2-d93cdb3a-DvUpzRiQ.js} +1 -1
  116. package/dist/frontend/assets/{styles-2ab5d517-CfWubwio.js → styles-2ab5d517-BE3yBf4Z.js} +3 -3
  117. package/dist/frontend/assets/{styles-5f03d8d2-C-_n19AB.js → styles-5f03d8d2-CGBEZiW_.js} +3 -3
  118. package/dist/frontend/assets/{styles-6aaf32cf-Dh-DHkVP.js → styles-6aaf32cf-JdIhb4_Y.js} +1 -1
  119. package/dist/frontend/assets/{styles-9a916d00-Boy0hUEG.js → styles-9a916d00-DRmxoyEF.js} +1 -1
  120. package/dist/frontend/assets/{styles-c10674c1-BfxiK3Ns.js → styles-c10674c1-C3TEvIjw.js} +1 -1
  121. package/dist/frontend/assets/{styles-edf9a4b0-T7mKViRF.js → styles-edf9a4b0-B_FeyXdP.js} +1 -1
  122. package/dist/frontend/assets/subset-shared.chunk.js +9 -9
  123. package/dist/frontend/assets/subset-worker.chunk.js +1 -1
  124. package/dist/frontend/assets/{sv-SE-XGPEYMSR-dVrUAqld.js → sv-SE-XGPEYMSR-CBrvnOWh.js} +1 -1
  125. package/dist/frontend/assets/{svgDrawCommon-08f97a94-fsWk6SQc.js → svgDrawCommon-08f97a94-DI4ZG6xI.js} +1 -1
  126. package/dist/frontend/assets/{svgDrawCommon-3ba9043b-BJ8X3qS-.js → svgDrawCommon-3ba9043b-Dl5HsA0X.js} +1 -1
  127. package/dist/frontend/assets/{ta-IN-2NMHFXQM-CtVVZfIT.js → ta-IN-2NMHFXQM-DGYl9sGK.js} +1 -1
  128. package/dist/frontend/assets/{th-TH-HPSO5L25-Do90CyDN.js → th-TH-HPSO5L25-Bq3K3L0F.js} +1 -1
  129. package/dist/frontend/assets/{timeline-definition-7e6b55e7-D_iHA485.js → timeline-definition-7e6b55e7-DWL-fZtL.js} +3 -3
  130. package/dist/frontend/assets/{timeline-definition-85554ec2-nhT8QMsO.js → timeline-definition-85554ec2-C14kwDFd.js} +1 -1
  131. package/dist/frontend/assets/{tr-TR-DEFEU3FU-MfR-IQke.js → tr-TR-DEFEU3FU-CSwI35xw.js} +1 -1
  132. package/dist/frontend/assets/{uk-UA-QMV73CPH-CPet8B1O.js → uk-UA-QMV73CPH-Dxy7prmb.js} +1 -1
  133. package/dist/frontend/assets/{union-BgYwhB7T.js → union-DQ7m6Pel.js} +1 -1
  134. package/dist/frontend/assets/{vi-VN-M7AON7JQ-3HD06T_8.js → vi-VN-M7AON7JQ-BNyYTgjT.js} +1 -1
  135. package/dist/frontend/assets/{xychartDiagram-b6496bcd-D5vSaFT2.js → xychartDiagram-b6496bcd-DWKYL0ZD.js} +4 -4
  136. package/dist/frontend/assets/{xychartDiagram-e933f94c-DLIHXsUi.js → xychartDiagram-e933f94c-Bw4brW9l.js} +1 -1
  137. package/dist/frontend/assets/youtubeResume-DPG81gOQ.js +1 -0
  138. package/dist/frontend/assets/{zh-CN-LNUGB5OW-t2RyWPw2.js → zh-CN-LNUGB5OW-BNhZCPEt.js} +1 -1
  139. package/dist/frontend/assets/{zh-HK-E62DVLB3-Bj22hc7e.js → zh-HK-E62DVLB3-D_rDALSq.js} +1 -1
  140. package/dist/frontend/assets/{zh-TW-RAJ6MFWO-CBRhqhU1.js → zh-TW-RAJ6MFWO-rUlbmCTA.js} +1 -1
  141. package/dist/frontend/assets/zipObject-Db5-6vIz.js +1 -0
  142. package/dist/frontend/index.html +172 -236
  143. package/dist/{index.d.ts → mcp.d.ts} +1 -1
  144. package/dist/mcp.d.ts.map +1 -0
  145. package/dist/mcp.js +1183 -0
  146. package/dist/mcp.js.map +1 -0
  147. package/package.json +19 -34
  148. package/skills/excalidraw-skill/SKILL.md +42 -55
  149. package/skills/excalidraw-skill/references/cheatsheet.md +23 -56
  150. package/skills/excalidraw-skill/scripts/clear-canvas.cjs +9 -32
  151. package/skills/excalidraw-skill/scripts/create-element.cjs +18 -61
  152. package/skills/excalidraw-skill/scripts/delete-element.cjs +12 -42
  153. package/skills/excalidraw-skill/scripts/export-elements.cjs +17 -44
  154. package/skills/excalidraw-skill/scripts/gh-helper.cjs +58 -0
  155. package/skills/excalidraw-skill/scripts/healthcheck.cjs +8 -29
  156. package/skills/excalidraw-skill/scripts/import-elements.cjs +20 -70
  157. package/skills/excalidraw-skill/scripts/update-element.cjs +19 -63
  158. package/tsconfig.json +3 -2
  159. package/vite.config.js +3 -10
  160. package/dist/frontend/assets/arc-Cc3ncJ1n.js +0 -1
  161. package/dist/frontend/assets/channel-BQSIb3i3.js +0 -1
  162. package/dist/frontend/assets/classDiagram-v2-d5a6b087-BgB8AG2e.js +0 -2
  163. package/dist/frontend/assets/clone-BoZziNL9.js +0 -1
  164. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-Z3H2d3Wj.js +0 -1
  165. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CIQOMghC.js +0 -1
  166. package/dist/frontend/assets/index-DIz4WrSB.js +0 -349
  167. package/dist/frontend/assets/zipObject-DMfHW2o8.js +0 -1
  168. package/dist/index.d.ts.map +0 -1
  169. package/dist/index.js +0 -2279
  170. package/dist/index.js.map +0 -1
  171. package/dist/remote.d.ts +0 -5
  172. package/dist/remote.d.ts.map +0 -1
  173. package/dist/remote.js +0 -129
  174. package/dist/remote.js.map +0 -1
  175. package/dist/server.d.ts +0 -4
  176. package/dist/server.d.ts.map +0 -1
  177. package/dist/server.js +0 -1208
  178. package/dist/server.js.map +0 -1
  179. package/dist/target.d.ts +0 -38
  180. package/dist/target.d.ts.map +0 -1
  181. package/dist/target.js +0 -225
  182. package/dist/target.js.map +0 -1
  183. package/dist/types.d.ts +0 -228
  184. package/dist/types.d.ts.map +0 -1
  185. package/dist/types.js +0 -45
  186. package/dist/types.js.map +0 -1
  187. package/dist/utils/auth.d.ts +0 -14
  188. package/dist/utils/auth.d.ts.map +0 -1
  189. package/dist/utils/auth.js +0 -67
  190. package/dist/utils/auth.js.map +0 -1
  191. package/dist/utils/github.d.ts +0 -24
  192. package/dist/utils/github.d.ts.map +0 -1
  193. package/dist/utils/github.js +0 -132
  194. package/dist/utils/github.js.map +0 -1
  195. package/dist/utils/logger.d.ts +0 -4
  196. package/dist/utils/logger.d.ts.map +0 -1
  197. package/dist/utils/logger.js +0 -25
  198. package/dist/utils/logger.js.map +0 -1
  199. package/dist/utils/r2store.d.ts +0 -12
  200. package/dist/utils/r2store.d.ts.map +0 -1
  201. package/dist/utils/r2store.js +0 -75
  202. package/dist/utils/r2store.js.map +0 -1
  203. package/frontend/drop.png +0 -0
  204. package/frontend/index.html +0 -339
  205. package/frontend/src/App.tsx +0 -1091
  206. package/frontend/src/main.tsx +0 -15
  207. package/frontend/src/utils/ghSync.ts +0 -234
  208. package/frontend/src/utils/mermaidConverter.ts +0 -43
  209. package/frontend/src/utils/webmcp.ts +0 -265
  210. package/scripts/check-local-bind.mjs +0 -164
  211. package/scripts/postinstall.mjs +0 -23
  212. package/scripts/publish-pages.sh +0 -39
  213. package/scripts/seed-r2.mjs +0 -9
  214. package/src/cli.ts +0 -528
  215. package/src/index.ts +0 -2538
  216. package/src/remote.ts +0 -92
  217. package/src/server.ts +0 -1349
  218. package/src/target.ts +0 -198
  219. package/src/types.ts +0 -336
  220. package/src/utils/auth.ts +0 -61
  221. package/src/utils/github.ts +0 -121
  222. package/src/utils/logger.ts +0 -35
  223. package/src/utils/r2store.ts +0 -70
package/dist/mcp.js ADDED
@@ -0,0 +1,1183 @@
1
+ #!/usr/bin/env node
2
+ // excalidrop MCP server — single file, remote-only (GitHub is the canvas).
3
+ // Usage: npx -y excalidrop@latest mcp --repo owner/repo
4
+ process.env.NODE_DISABLE_COLORS = '1';
5
+ process.env.NO_COLOR = '1';
6
+ import { execSync } from 'node:child_process';
7
+ import { deflateSync } from 'zlib';
8
+ import { webcrypto } from 'crypto';
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'url';
13
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
16
+ import { z } from 'zod';
17
+ import dotenv from 'dotenv';
18
+ dotenv.config();
19
+ // MCP must never write stdout — file + stderr only.
20
+ const LOG_FILE = process.env.LOG_FILE_PATH || path.join(os.tmpdir(), 'excalidrop-mcp.log');
21
+ function log(level, msg, extra) {
22
+ const line = `${new Date().toISOString()} [${level}] ${msg}${extra ? ' ' + JSON.stringify(extra) : ''}\n`;
23
+ try {
24
+ fs.appendFileSync(LOG_FILE, line);
25
+ }
26
+ catch { /* ignore */ }
27
+ if (level === 'warn' || level === 'error')
28
+ process.stderr.write(line);
29
+ }
30
+ const EXCALIDRAW_ELEMENT_TYPES = {
31
+ RECTANGLE: 'rectangle', ELLIPSE: 'ellipse', DIAMOND: 'diamond', ARROW: 'arrow',
32
+ TEXT: 'text', FREEDRAW: 'freedraw', LINE: 'line', IMAGE: 'image',
33
+ };
34
+ function generateId() {
35
+ return Date.now().toString(36) + Math.random().toString(36).substring(2);
36
+ }
37
+ function normalizeFontFamily(ff) {
38
+ if (ff === undefined)
39
+ return undefined;
40
+ if (typeof ff === 'number')
41
+ return ff;
42
+ const map = {
43
+ virgil: 1, hand: 1, handwritten: 1, helvetica: 2, sans: 2, 'sans-serif': 2,
44
+ cascadia: 3, mono: 3, monospace: 3, excalifont: 5, nunito: 6,
45
+ lilita: 7, 'lilita one': 7, 'comic shanns': 8, comic: 8,
46
+ '1': 1, '2': 2, '3': 3, '5': 5, '6': 6, '7': 7, '8': 8,
47
+ };
48
+ return map[ff.toLowerCase()];
49
+ }
50
+ const APP_CLIENT_ID = process.env.GITHUB_OAUTH_CLIENT_ID || 'Iv23liuS2fx3QOEIoDmx';
51
+ const RELAY_URL = process.env.EXCALIDROP_RELAY_URL || 'https://excalidrop.wtf403.workers.dev';
52
+ const CONFIG_NAME = '.excalidrop.json';
53
+ const TOKEN_FILE = path.join(os.homedir(), '.config', 'excalidrop', 'gh_token');
54
+ const SCENE_PATH = process.env.SCENE_PATH || 'canvas.excalidraw';
55
+ const CANVAS_BRANCH = process.env.CANVAS_BRANCH || 'excalidrop';
56
+ function resolveToken() {
57
+ if (process.env.GITHUB_TOKEN)
58
+ return process.env.GITHUB_TOKEN;
59
+ try {
60
+ const t = execSync('gh auth token', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
61
+ if (t)
62
+ return t;
63
+ }
64
+ catch { /* no gh */ }
65
+ try {
66
+ const t = fs.readFileSync(TOKEN_FILE, 'utf8').trim();
67
+ if (t)
68
+ return t;
69
+ }
70
+ catch { /* no stored token */ }
71
+ return '';
72
+ }
73
+ function storeToken(t) {
74
+ fs.mkdirSync(path.dirname(TOKEN_FILE), { recursive: true });
75
+ fs.writeFileSync(TOKEN_FILE, t + '\n', { mode: 0o600 });
76
+ }
77
+ function detectSlug() {
78
+ try {
79
+ const url = execSync('git config --get remote.origin.url', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().replace(/\.git$/, '');
80
+ return url.match(/github\.com[:/](.+)/)?.[1]?.toLowerCase() || '';
81
+ }
82
+ catch {
83
+ return '';
84
+ }
85
+ }
86
+ function parseRepo(input) {
87
+ const s = input.trim().replace(/\/$/, '');
88
+ const m = s.match(/^https?:\/\/([a-z0-9-]+)\.github\.io\/([a-z0-9_.-]+)/i);
89
+ if (m)
90
+ return `${m[1]}/${m[2]}`.toLowerCase();
91
+ if (/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(s))
92
+ return s.toLowerCase();
93
+ throw new Error(`Cannot parse repo from "${input}" — use owner/repo`);
94
+ }
95
+ function parseRepoFlag() {
96
+ const arg = process.argv.find((a) => a.startsWith('--repo='));
97
+ if (!arg)
98
+ return null;
99
+ try {
100
+ return parseRepo(arg.split('=').slice(1).join('='));
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ function readConfig() {
107
+ let dir = process.cwd();
108
+ const root = path.parse(dir).root;
109
+ while (true) {
110
+ try {
111
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, CONFIG_NAME), 'utf8'));
112
+ if (raw && typeof raw === 'object')
113
+ return raw;
114
+ }
115
+ catch { /* keep walking */ }
116
+ if (dir === root)
117
+ return {};
118
+ dir = path.dirname(dir);
119
+ }
120
+ }
121
+ function loadSavedRepo() {
122
+ const flag = parseRepoFlag();
123
+ if (flag)
124
+ return flag;
125
+ if (process.env.EXCALIDROP_REPO) {
126
+ try {
127
+ return parseRepo(process.env.EXCALIDROP_REPO);
128
+ }
129
+ catch { /* fall through */ }
130
+ }
131
+ return readConfig().remote || detectSlug() || null;
132
+ }
133
+ function persistRepo(repo) {
134
+ let dir = process.cwd();
135
+ const root = path.parse(dir).root;
136
+ while (true) {
137
+ try {
138
+ const file = path.join(dir, CONFIG_NAME);
139
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
140
+ if (raw && typeof raw === 'object') {
141
+ fs.writeFileSync(file, JSON.stringify({ ...raw, remote: repo }, null, 2) + '\n');
142
+ return;
143
+ }
144
+ }
145
+ catch { /* keep walking */ }
146
+ if (dir === root)
147
+ return;
148
+ dir = path.dirname(dir);
149
+ }
150
+ }
151
+ const GH_API = 'https://api.github.com';
152
+ class SceneConflictError extends Error {
153
+ freshSha;
154
+ freshElements;
155
+ freshFiles;
156
+ constructor(freshSha, freshElements, freshFiles) {
157
+ super('CONFLICT: scene changed upstream, merged retry required');
158
+ this.name = 'SceneConflictError';
159
+ this.freshSha = freshSha;
160
+ this.freshElements = freshElements;
161
+ this.freshFiles = freshFiles;
162
+ }
163
+ }
164
+ function ghHeaders() {
165
+ return { Authorization: `Bearer ${resolveToken()}`, Accept: 'application/vnd.github+json', 'User-Agent': 'excalidrop', 'Content-Type': 'application/json' };
166
+ }
167
+ async function getFile(repo, filePath, ref = CANVAS_BRANCH) {
168
+ const r = await fetch(`${GH_API}/repos/${repo}/contents/${filePath}?ref=${ref}`, { headers: ghHeaders() });
169
+ if (r.status === 404)
170
+ return null;
171
+ if (!r.ok)
172
+ throw new Error(`GitHub get ${r.status}: ${(await r.text()).slice(0, 200)}`);
173
+ const j = await r.json();
174
+ // Contents API omits `content` for files >1MB — fall back to the blob API.
175
+ let b64 = typeof j.content === 'string' && j.content.length ? j.content : null;
176
+ if (!b64) {
177
+ const blob = await fetch(`${GH_API}/repos/${repo}/git/blobs/${j.sha}`, { headers: ghHeaders() });
178
+ if (!blob.ok)
179
+ throw new Error(`GitHub blob ${blob.status}: ${(await blob.text()).slice(0, 200)}`);
180
+ b64 = (await blob.json()).content || null;
181
+ }
182
+ if (!b64)
183
+ throw new Error(`GitHub get: ${filePath} returned no content`);
184
+ return { sha: j.sha, content: JSON.parse(Buffer.from(b64, 'base64').toString('utf8')) };
185
+ }
186
+ async function ensureBranch(repo, branch) {
187
+ const has = await fetch(`${GH_API}/repos/${repo}/git/ref/heads/${branch}`, { headers: ghHeaders() });
188
+ if (has.ok)
189
+ return;
190
+ const repoInfo = await (await fetch(`${GH_API}/repos/${repo}`, { headers: ghHeaders() })).json();
191
+ const from = repoInfo.default_branch || 'main';
192
+ const ref = await (await fetch(`${GH_API}/repos/${repo}/git/ref/heads/${from}`, { headers: ghHeaders() })).json();
193
+ if (!ref.object?.sha)
194
+ throw new Error(`cannot create ${branch}: no ${from} branch`);
195
+ const mk = await fetch(`${GH_API}/repos/${repo}/git/refs`, { method: 'POST', headers: ghHeaders(), body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: ref.object.sha }) });
196
+ if (!mk.ok)
197
+ throw new Error(`cannot create ${branch}: ${(await mk.text()).slice(0, 200)}`);
198
+ }
199
+ async function putFile(repo, filePath, data, message, branch = CANVAS_BRANCH, sha) {
200
+ if (!resolveToken())
201
+ throw new Error('No GitHub token. Run `gh auth login` or `npx excalidrop login` first.');
202
+ const body = { message, content: Buffer.from(JSON.stringify(data, null, 2)).toString('base64'), branch };
203
+ if (sha)
204
+ body.sha = sha;
205
+ const attempt = () => fetch(`${GH_API}/repos/${repo}/contents/${filePath}`, { method: 'PUT', headers: ghHeaders(), body: JSON.stringify(body) });
206
+ let r = await attempt();
207
+ if (r.status === 404) {
208
+ await ensureBranch(repo, branch);
209
+ const existing = await getFile(repo, filePath, branch).catch(() => null);
210
+ if (existing?.sha)
211
+ body.sha = existing.sha;
212
+ else
213
+ delete body.sha;
214
+ r = await attempt();
215
+ }
216
+ if (r.status === 409 || (r.status === 422 && !sha)) {
217
+ if (r.status === 422) {
218
+ const existing = await getFile(repo, filePath, branch).catch(() => null);
219
+ if (!existing?.sha)
220
+ throw new Error(`GitHub put 422: ${(await r.text()).slice(0, 200)}`);
221
+ body.sha = existing.sha;
222
+ r = await attempt();
223
+ }
224
+ if (r.status === 409) {
225
+ const fresh = await getFile(repo, filePath, branch).catch(() => null);
226
+ const doc = fresh?.content;
227
+ throw new SceneConflictError(fresh?.sha || null, Array.isArray(doc?.elements) ? doc.elements : null, doc?.files);
228
+ }
229
+ }
230
+ if (!r.ok)
231
+ throw new Error(`GitHub put ${r.status}: ${(await r.text()).slice(0, 200)}`);
232
+ log('info', `Committed ${filePath}@${branch} in ${repo}`);
233
+ return (await r.json()).content.sha;
234
+ }
235
+ function unionIntoMap(map, fresh) {
236
+ if (!fresh)
237
+ return;
238
+ for (const el of fresh)
239
+ if (el?.id && !map.has(el.id))
240
+ map.set(el.id, el);
241
+ }
242
+ const syncedAssetIds = new Set();
243
+ async function syncAssets(repo, files) {
244
+ if (!resolveToken())
245
+ return;
246
+ const list = Array.isArray(files) ? files : Object.values(files || {});
247
+ const withData = list.filter((f) => f?.id && typeof f?.dataURL === 'string' && f.dataURL.startsWith('data:'));
248
+ if (!withData.length)
249
+ return;
250
+ await Promise.all(withData.map(async (f) => {
251
+ try {
252
+ if (syncedAssetIds.has(f.id))
253
+ return;
254
+ const m = /^data:([^;]+);base64,(.+)$/s.exec(f.dataURL);
255
+ if (!m)
256
+ return;
257
+ const ext = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp', 'image/svg+xml': 'svg' }[f.mimeType || m[1]] || 'png';
258
+ const assetPath = `assets/${f.id}.${ext}`;
259
+ const head = await fetch(`${GH_API}/repos/${repo}/contents/${assetPath}?ref=${CANVAS_BRANCH}`, { headers: ghHeaders() });
260
+ if (head.ok) {
261
+ syncedAssetIds.add(f.id);
262
+ return;
263
+ }
264
+ if (head.status !== 404)
265
+ throw new Error(`asset check ${head.status}`);
266
+ const r = await fetch(`${GH_API}/repos/${repo}/contents/${assetPath}`, { method: 'PUT', headers: ghHeaders(), body: JSON.stringify({ message: `excalidrop: add image asset ${f.id}`, content: m[2], branch: CANVAS_BRANCH }) });
267
+ if (!r.ok)
268
+ throw new Error(`asset put ${r.status}`);
269
+ syncedAssetIds.add(f.id);
270
+ }
271
+ catch (e) {
272
+ log('warn', 'asset sync failed: ' + e.message);
273
+ }
274
+ }));
275
+ }
276
+ async function ensureMainBranch(repo) {
277
+ if (!resolveToken())
278
+ throw new Error('GITHUB_TOKEN missing');
279
+ const main = await fetch(`${GH_API}/repos/${repo}/git/ref/heads/main`, { headers: ghHeaders() });
280
+ if (main.ok)
281
+ return;
282
+ const branches = await (await fetch(`${GH_API}/repos/${repo}/branches`, { headers: ghHeaders() })).json();
283
+ if (branches.length !== 0)
284
+ return;
285
+ const r = await fetch(`${GH_API}/repos/${repo}/contents/README.md`, {
286
+ method: 'PUT', headers: ghHeaders(),
287
+ body: JSON.stringify({ message: 'Initial commit', content: Buffer.from('').toString('base64'), branch: 'main' }),
288
+ });
289
+ if (!r.ok)
290
+ throw new Error(`Failed to create main branch: ${await r.text()}`);
291
+ }
292
+ const scenes = new Map();
293
+ let activeRepo = null;
294
+ let commitTimer = null;
295
+ async function ensureLoaded(repo) {
296
+ let st = scenes.get(repo);
297
+ if (st)
298
+ return st;
299
+ if (!resolveToken())
300
+ throw new Error('No GitHub token. Run github_login or `gh auth login` first.');
301
+ await ensureMainBranch(repo);
302
+ st = { elements: new Map(), files: [], sha: null };
303
+ const f = await getFile(repo, SCENE_PATH, CANVAS_BRANCH).catch(() => null);
304
+ if (f) {
305
+ const doc = f.content;
306
+ for (const el of doc.elements || [])
307
+ st.elements.set(el.id, el);
308
+ st.files = Array.isArray(doc.files) ? doc.files : (doc.files ? [doc.files] : []);
309
+ st.sha = f.sha;
310
+ }
311
+ scenes.set(repo, st);
312
+ activeRepo = repo;
313
+ return st;
314
+ }
315
+ async function ensureActiveRepo() {
316
+ const repo = activeRepo || loadSavedRepo();
317
+ if (!repo)
318
+ throw new Error('No repo selected. Run `npx excalidrop setup owner/repo` or pass --repo=owner/repo.');
319
+ await ensureLoaded(repo);
320
+ return repo;
321
+ }
322
+ function current() {
323
+ const repo = activeRepo || loadSavedRepo();
324
+ if (!repo)
325
+ throw new Error('call switch_remote first (or pass --repo=owner/repo)');
326
+ const st = scenes.get(repo);
327
+ if (!st)
328
+ throw new Error('call switch_remote first');
329
+ return { repo, st };
330
+ }
331
+ function scheduleCommit() {
332
+ if (commitTimer)
333
+ clearTimeout(commitTimer);
334
+ commitTimer = setTimeout(() => { void commitNow('excalidrop: autosync from MCP'); }, 10000);
335
+ }
336
+ async function commitNow(message) {
337
+ const { repo, st } = current();
338
+ const doc = () => ({
339
+ type: 'excalidraw', version: 2, source: 'excalidrop',
340
+ elements: Array.from(st.elements.values()),
341
+ ...(st.files?.length ? { files: st.files } : {}),
342
+ });
343
+ const msg = message || `excalidrop: update ${st.elements.size} elements`;
344
+ try {
345
+ st.sha = await putFile(repo, SCENE_PATH, doc(), msg, CANVAS_BRANCH, st.sha || undefined);
346
+ }
347
+ catch (e) {
348
+ if (!(e instanceof SceneConflictError))
349
+ throw e;
350
+ unionIntoMap(st.elements, e.freshElements);
351
+ if ((!st.files || st.files.length === 0) && e.freshFiles) {
352
+ st.files = Array.isArray(e.freshFiles) ? e.freshFiles : [e.freshFiles];
353
+ }
354
+ st.sha = e.freshSha;
355
+ st.sha = await putFile(repo, SCENE_PATH, doc(), msg, CANVAS_BRANCH, st.sha || undefined);
356
+ }
357
+ try {
358
+ await syncAssets(repo, st.files);
359
+ }
360
+ catch (e) {
361
+ log('warn', 'asset sync failed: ' + e.message);
362
+ }
363
+ log('info', `Committed ${st.elements.size} elements to ${repo}`);
364
+ return { sha: st.sha, count: st.elements.size };
365
+ }
366
+ function rAddFile(file) { const { st } = current(); st.files.push(file); scheduleCommit(); return file; }
367
+ function rCreate(el) {
368
+ const { st } = current();
369
+ const full = { ...el, id: el.id || generateId() };
370
+ st.elements.set(full.id, full);
371
+ scheduleCommit();
372
+ return full;
373
+ }
374
+ function rBatch(els) { return els.map(rCreate); }
375
+ function rUpdate(id, updates) {
376
+ const { st } = current();
377
+ const cur = st.elements.get(id);
378
+ if (!cur)
379
+ throw new Error(`Element ${id} not found`);
380
+ const u = { ...cur, ...updates };
381
+ st.elements.set(id, u);
382
+ scheduleCommit();
383
+ return u;
384
+ }
385
+ function rDelete(id) { const { st } = current(); st.elements.delete(id); scheduleCommit(); }
386
+ function rGet(id) { return current().st.elements.get(id) || null; }
387
+ function rList(type) {
388
+ return Array.from(current().st.elements.values()).filter((e) => !type || e.type === type);
389
+ }
390
+ function rClear() { const { st } = current(); const n = st.elements.size; st.elements.clear(); scheduleCommit(); return n; }
391
+ const RELAY_BUDGET_PER_DAY = Number(process.env.EXCALIDROP_RELAY_BUDGET || 1000);
392
+ let relayCalls = 0;
393
+ let relayDay = new Date().toISOString().slice(0, 10);
394
+ async function relayRpc(repo, method, args, timeoutMs = 30000) {
395
+ const day = new Date().toISOString().slice(0, 10);
396
+ if (day !== relayDay) {
397
+ relayDay = day;
398
+ relayCalls = 0;
399
+ }
400
+ if (relayCalls >= RELAY_BUDGET_PER_DAY)
401
+ throw new Error(`Relay daily budget exceeded (${RELAY_BUDGET_PER_DAY}). Use describe_scene first, or raise EXCALIDROP_RELAY_BUDGET.`);
402
+ const token = resolveToken();
403
+ if (!token)
404
+ throw new Error('No GitHub token. Run `gh auth login` or `npx excalidrop login` first.');
405
+ relayCalls += 1;
406
+ const ctl = new AbortController();
407
+ const t = setTimeout(() => ctl.abort(), timeoutMs);
408
+ try {
409
+ const r = await fetch(`${RELAY_URL}/rpc/${repo}/${method}`, {
410
+ method: 'POST',
411
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': 'excalidrop' },
412
+ body: JSON.stringify(args || {}),
413
+ signal: ctl.signal,
414
+ });
415
+ if (r.status === 503)
416
+ throw new Error(`No viewer connected for ${repo} — open the canvas URL in a browser first.`);
417
+ if (r.status === 403)
418
+ throw new Error(`No access to ${repo} with this GitHub token.`);
419
+ if (r.status === 429)
420
+ throw new Error('Relay rate-limited. Wait a minute and retry (use describe_scene first).');
421
+ if (!r.ok)
422
+ throw new Error(`Relay ${r.status}: ${(await r.text()).slice(0, 200)}`);
423
+ return r.json();
424
+ }
425
+ catch (e) {
426
+ if (e.name === 'AbortError')
427
+ throw new Error(`Relay timed out after ${timeoutMs / 1000}s (is a viewer tab open?)`);
428
+ throw e;
429
+ }
430
+ finally {
431
+ clearTimeout(t);
432
+ }
433
+ }
434
+ async function queueRpc(repo, method, args, timeoutMs = 60000) {
435
+ const reqId = generateId();
436
+ await putFile(repo, `commands/${reqId}.json`, { reqId, method, args, createdAt: new Date().toISOString() }, `excalidrop: queue ${method} ${reqId}`, CANVAS_BRANCH);
437
+ const deadline = Date.now() + timeoutMs;
438
+ while (Date.now() < deadline) {
439
+ await new Promise((r) => setTimeout(r, 4000));
440
+ const res = await getFile(repo, `results/${reqId}.json`, CANVAS_BRANCH).catch(() => null);
441
+ if (res?.content)
442
+ return res.content;
443
+ }
444
+ throw new Error(`Command ${method} timed out — is a viewer tab open on this repo?`);
445
+ }
446
+ function useRelay() {
447
+ return !process.argv.includes('--no-relay') && process.env.EXCALIDROP_NO_RELAY !== '1';
448
+ }
449
+ async function screenshotRepo(repo, opts = {}) {
450
+ const args = { background: opts.background ?? true, format: opts.format || 'png' };
451
+ if (useRelay()) {
452
+ try {
453
+ const out = await relayRpc(repo, 'screenshot', args);
454
+ return { format: out.format || 'png', data: out.data };
455
+ }
456
+ catch (e) {
457
+ log('warn', 'relay screenshot failed, queue fallback: ' + e.message);
458
+ }
459
+ }
460
+ const out = await queueRpc(repo, 'screenshot', args);
461
+ return { format: out.format || 'png', data: out.data };
462
+ }
463
+ async function viewportRepo(repo, args) {
464
+ if (useRelay()) {
465
+ try {
466
+ return await relayRpc(repo, 'viewport', args);
467
+ }
468
+ catch (e) {
469
+ log('warn', 'relay viewport failed, queue fallback: ' + e.message);
470
+ }
471
+ }
472
+ return queueRpc(repo, 'viewport', args);
473
+ }
474
+ async function deviceStart(clientId) {
475
+ const r = await fetch('https://github.com/login/device/code', {
476
+ method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
477
+ body: JSON.stringify({ client_id: clientId }),
478
+ });
479
+ if (!r.ok)
480
+ throw new Error('device flow start failed');
481
+ return r.json();
482
+ }
483
+ async function repoId(owner, repo) {
484
+ try {
485
+ const r = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'excalidrop' } });
486
+ if (!r.ok)
487
+ return null;
488
+ return (await r.json()).id ?? null;
489
+ }
490
+ catch {
491
+ return null;
492
+ }
493
+ }
494
+ async function devicePoll(clientId, deviceCode, intervalSec, timeoutSec = 300, repositoryId) {
495
+ const deadline = Date.now() + timeoutSec * 1000;
496
+ while (Date.now() < deadline) {
497
+ await new Promise((r) => setTimeout(r, intervalSec * 1000));
498
+ const r = await fetch('https://github.com/login/oauth/access_token', {
499
+ method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
500
+ body: JSON.stringify({ client_id: clientId, device_code: deviceCode, grant_type: 'urn:ietf:params:oauth:grant-type:device_code', ...(repositoryId ? { repository_id: repositoryId } : {}) }),
501
+ });
502
+ const j = await r.json();
503
+ if (j.access_token) {
504
+ storeToken(j.access_token);
505
+ return j.access_token;
506
+ }
507
+ if (j.error && j.error !== 'authorization_pending' && j.error !== 'slow_down')
508
+ throw new Error(j.error_description || j.error);
509
+ }
510
+ throw new Error('device login timed out');
511
+ }
512
+ const ALLOWED_EXPORT_DIR = process.env.EXCALIDRAW_EXPORT_DIR || process.cwd();
513
+ function sanitizeFilePath(filePath) {
514
+ const resolved = path.resolve(filePath);
515
+ const allowedDir = path.resolve(ALLOWED_EXPORT_DIR);
516
+ if (!resolved.startsWith(allowedDir + path.sep) && resolved !== allowedDir) {
517
+ throw new Error(`Path traversal blocked: "${filePath}" resolves outside "${allowedDir}". Set EXCALIDRAW_EXPORT_DIR to change it.`);
518
+ }
519
+ return resolved;
520
+ }
521
+ const groups = new Map();
522
+ const PointSchema = z.union([z.object({ x: z.number(), y: z.number() }), z.tuple([z.number(), z.number()])]);
523
+ function normalizePoints(points) {
524
+ return points.map((p) => (Array.isArray(p) ? p : [p.x, p.y]));
525
+ }
526
+ const ElementSchema = z.object({
527
+ id: z.string().optional(),
528
+ type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES)),
529
+ x: z.number(), y: z.number(),
530
+ width: z.number().optional(), height: z.number().optional(),
531
+ points: z.array(PointSchema).optional(),
532
+ backgroundColor: z.string().optional(), strokeColor: z.string().optional(),
533
+ strokeWidth: z.number().optional(), roughness: z.number().optional(), opacity: z.number().optional(),
534
+ text: z.string().optional(), fontSize: z.number().optional(),
535
+ fontFamily: z.union([z.string(), z.number()]).optional(),
536
+ groupIds: z.array(z.string()).optional(), locked: z.boolean().optional(),
537
+ strokeStyle: z.string().optional(),
538
+ roundness: z.object({ type: z.number(), value: z.number().optional() }).nullable().optional(),
539
+ fillStyle: z.string().optional(), elbowed: z.boolean().optional(),
540
+ startElementId: z.string().optional(), endElementId: z.string().optional(),
541
+ endArrowhead: z.string().optional(), startArrowhead: z.string().optional(),
542
+ });
543
+ const DIAGRAM_DESIGN_GUIDE = `# Excalidraw Diagram Design Guide
544
+
545
+ ## Color Palette
546
+
547
+ ### Stroke Colors (use for borders & text)
548
+ | Name | Hex | Use for |
549
+ |---------|-----------|-----------------------------|
550
+ | Black | #1e1e1e | Default text & borders |
551
+ | Red | #e03131 | Errors, warnings, critical |
552
+ | Green | #2f9e44 | Success, approved, healthy |
553
+ | Blue | #1971c2 | Primary actions, links |
554
+ | Purple | #9c36b5 | Services, middleware |
555
+ | Orange | #e8590c | Async, queues, events |
556
+ | Cyan | #0c8599 | Data stores, databases |
557
+ | Gray | #868e96 | Annotations, secondary |
558
+
559
+ ### Fill Colors (use for backgroundColor — pastel fills)
560
+ | Name | Hex | Pairs with stroke |
561
+ |--------------|-----------|-------------------|
562
+ | Light Red | #ffc9c9 | #e03131 |
563
+ | Light Green | #b2f2bb | #2f9e44 |
564
+ | Light Blue | #a5d8ff | #1971c2 |
565
+ | Light Purple | #eebefa | #9c36b5 |
566
+ | Light Orange | #ffd8a8 | #e8590c |
567
+ | Light Cyan | #99e9f2 | #0c8599 |
568
+ | Light Gray | #e9ecef | #868e96 |
569
+ | White | #ffffff | #1e1e1e |
570
+
571
+ ## Sizing Rules
572
+
573
+ - **Minimum shape size**: width >= 120px, height >= 60px
574
+ - **Font sizes**: body text >= 16, titles/headers >= 20, small labels >= 14
575
+ - **Padding**: leave at least 20px inside shapes for text breathing room
576
+ - **Arrow length**: minimum 80px between connected shapes
577
+ - **Consistent sizing**: keep same-role shapes identical dimensions
578
+
579
+ ## Layout Patterns
580
+
581
+ - **Grid snap**: align to 20px grid for clean layouts
582
+ - **Spacing**: 40–80px gap between adjacent shapes
583
+ - **Flow direction**: top-to-bottom (vertical) or left-to-right (horizontal)
584
+ - **Hierarchy**: important nodes larger or higher; left-to-right = temporal order
585
+ - **Grouping**: cluster related elements visually; use background rectangles as zones
586
+
587
+ ## Arrow Binding Best Practices
588
+
589
+ - **Always bind**: use \`startElementId\` / \`endElementId\` to connect arrows to shapes
590
+ - **Dashed arrows**: use \`strokeStyle: "dashed"\` for async, optional, or event flows
591
+ - **Dotted arrows**: use \`strokeStyle: "dotted"\` for weak dependencies or annotations
592
+ - **Arrowheads**: default "arrow" for directed flow; "dot" for data stores; null for lines
593
+ - **Label arrows**: set \`text\` on arrows to describe the relationship (e.g., "HTTP", "publishes")
594
+
595
+ ## Anti-Patterns to Avoid
596
+
597
+ 1. **Overlapping elements** — always leave gaps; use distribute_elements
598
+ 2. **Cramped spacing** — minimum 40px between shapes
599
+ 3. **Tiny fonts** — never below 14px; prefer 16+
600
+ 4. **Manual arrow coordinates** — always use startElementId/endElementId binding
601
+ 5. **Too many colors** — limit to 3–4 fill colors per diagram
602
+ 6. **Inconsistent sizes** — same-role shapes should be same width/height
603
+ 7. **No labels** — every shape and meaningful arrow should have text
604
+ 8. **Flat layouts** — use zones/groups to create visual hierarchy
605
+
606
+ ## Drawing Order (Recommended)
607
+
608
+ 1. **Background zones** — large rectangles with light fill, low opacity
609
+ 2. **Primary shapes** — services, entities, steps (with labels via \`text\`)
610
+ 3. **Arrows** — connect shapes using binding IDs
611
+ 4. **Annotations** — standalone text elements for notes, titles
612
+ 5. **Refinement** — align, distribute, adjust spacing, screenshot to verify
613
+ `;
614
+ const TYPES = Object.values(EXCALIDRAW_ELEMENT_TYPES);
615
+ const tools = [
616
+ { name: 'create_element', description: 'Create a new element on the remote canvas. For arrows, use startElementId/endElementId to bind to shapes.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, type: { type: 'string', enum: TYPES }, x: { type: 'number' }, y: { type: 'number' }, width: { type: 'number' }, height: { type: 'number' }, backgroundColor: { type: 'string' }, strokeColor: { type: 'string' }, strokeWidth: { type: 'number' }, strokeStyle: { type: 'string' }, roughness: { type: 'number' }, opacity: { type: 'number' }, text: { type: 'string' }, fontSize: { type: 'number' }, fontFamily: { type: ['string', 'number'] }, startElementId: { type: 'string' }, endElementId: { type: 'string' }, endArrowhead: { type: 'string' }, startArrowhead: { type: 'string' } }, required: ['type', 'x', 'y'] } },
617
+ { name: 'update_element', description: 'Update an existing element on the remote canvas', inputSchema: { type: 'object', properties: { id: { type: 'string' }, type: { type: 'string', enum: TYPES }, x: { type: 'number' }, y: { type: 'number' }, width: { type: 'number' }, height: { type: 'number' }, backgroundColor: { type: 'string' }, strokeColor: { type: 'string' }, strokeWidth: { type: 'number' }, strokeStyle: { type: 'string' }, roughness: { type: 'number' }, opacity: { type: 'number' }, text: { type: 'string' }, fontSize: { type: 'number' }, fontFamily: { type: ['string', 'number'] } }, required: ['id'] } },
618
+ { name: 'delete_element', description: 'Delete an element from the remote canvas', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
619
+ { name: 'query_elements', description: 'Query elements with optional filters', inputSchema: { type: 'object', properties: { type: { type: 'string', enum: TYPES }, filter: { type: 'object', additionalProperties: true }, bbox: { type: 'object', properties: { x_min: { type: 'number' }, x_max: { type: 'number' }, y_min: { type: 'number' }, y_max: { type: 'number' } } } } } },
620
+ { name: 'get_resource', description: 'Get a canvas resource (scene, elements, library, theme)', inputSchema: { type: 'object', properties: { resource: { type: 'string', enum: ['scene', 'library', 'theme', 'elements'] } }, required: ['resource'] } },
621
+ { name: 'group_elements', description: 'Group multiple elements together', inputSchema: { type: 'object', properties: { elementIds: { type: 'array', items: { type: 'string' } } }, required: ['elementIds'] } },
622
+ { name: 'ungroup_elements', description: 'Ungroup a group of elements', inputSchema: { type: 'object', properties: { groupId: { type: 'string' } }, required: ['groupId'] } },
623
+ { name: 'align_elements', description: 'Align elements to a specific position', inputSchema: { type: 'object', properties: { elementIds: { type: 'array', items: { type: 'string' } }, alignment: { type: 'string', enum: ['left', 'center', 'right', 'top', 'middle', 'bottom'] } }, required: ['elementIds', 'alignment'] } },
624
+ { name: 'distribute_elements', description: 'Distribute elements evenly', inputSchema: { type: 'object', properties: { elementIds: { type: 'array', items: { type: 'string' } }, direction: { type: 'string', enum: ['horizontal', 'vertical'] } }, required: ['elementIds', 'direction'] } },
625
+ { name: 'lock_elements', description: 'Lock elements to prevent modification', inputSchema: { type: 'object', properties: { elementIds: { type: 'array', items: { type: 'string' } } }, required: ['elementIds'] } },
626
+ { name: 'unlock_elements', description: 'Unlock elements to allow modification', inputSchema: { type: 'object', properties: { elementIds: { type: 'array', items: { type: 'string' } } }, required: ['elementIds'] } },
627
+ { name: 'create_from_mermaid', description: 'Convert a Mermaid diagram to elements via the connected viewer (viewer tab must be open).', inputSchema: { type: 'object', properties: { mermaidDiagram: { type: 'string' }, config: { type: 'object' } }, required: ['mermaidDiagram'] } },
628
+ { name: 'batch_create_elements', description: 'Create multiple elements at once. Assign custom id to shapes so arrows can reference them via startElementId/endElementId.', inputSchema: { type: 'object', properties: { elements: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, type: { type: 'string', enum: TYPES }, x: { type: 'number' }, y: { type: 'number' }, width: { type: 'number' }, height: { type: 'number' }, backgroundColor: { type: 'string' }, strokeColor: { type: 'string' }, strokeWidth: { type: 'number' }, strokeStyle: { type: 'string' }, roughness: { type: 'number' }, opacity: { type: 'number' }, text: { type: 'string' }, fontSize: { type: 'number' }, fontFamily: { type: ['string', 'number'] }, startElementId: { type: 'string' }, endElementId: { type: 'string' }, endArrowhead: { type: 'string' }, startArrowhead: { type: 'string' } }, required: ['type', 'x', 'y'] } } }, required: ['elements'] } },
629
+ { name: 'get_element', description: 'Get a single element by ID', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
630
+ { name: 'clear_canvas', description: 'Clear all elements from the remote canvas (commits to GitHub)', inputSchema: { type: 'object', properties: {} } },
631
+ { name: 'export_scene', description: 'Export the remote canvas to .excalidraw JSON. Optionally write to a file.', inputSchema: { type: 'object', properties: { filePath: { type: 'string' } } } },
632
+ { name: 'import_scene', description: 'Import elements from a .excalidraw JSON file or raw JSON data', inputSchema: { type: 'object', properties: { filePath: { type: 'string' }, data: { type: 'string' }, mode: { type: 'string', enum: ['replace', 'merge'] } }, required: ['mode'] } },
633
+ { name: 'export_to_image', description: 'Export the remote canvas to PNG/SVG via the connected viewer (viewer tab must be open).', inputSchema: { type: 'object', properties: { format: { type: 'string', enum: ['png', 'svg'] }, filePath: { type: 'string' }, background: { type: 'boolean' } }, required: ['format'] } },
634
+ { name: 'duplicate_elements', description: 'Duplicate elements with a configurable offset', inputSchema: { type: 'object', properties: { elementIds: { type: 'array', items: { type: 'string' } }, offsetX: { type: 'number' }, offsetY: { type: 'number' } }, required: ['elementIds'] } },
635
+ { name: 'add_image', description: 'Add an image to the remote canvas from a local file path, URL, or dataURL.', inputSchema: { type: 'object', properties: { source: { type: 'string' }, dataURL: { type: 'string' }, filename: { type: 'string' }, x: { type: 'number' }, y: { type: 'number' }, width: { type: 'number' }, height: { type: 'number' } } } },
636
+ { name: 'snapshot_scene', description: 'Save a named snapshot of the remote canvas (stored on the excalidrop branch under snapshots/)', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
637
+ { name: 'restore_snapshot', description: 'Restore the remote canvas from a named snapshot', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
638
+ { name: 'describe_scene', description: 'AI-readable description of the remote canvas: types, positions, connections, layout, bounding box.', inputSchema: { type: 'object', properties: {} } },
639
+ { name: 'get_canvas_screenshot', description: 'Screenshot of the remote canvas via the connected viewer (viewer tab must be open; ~10s).', inputSchema: { type: 'object', properties: { background: { type: 'boolean' } } } },
640
+ { name: 'read_diagram_guide', description: 'Design guide for beautiful diagrams: colors, sizing, layout, arrows, anti-patterns.', inputSchema: { type: 'object', properties: {} } },
641
+ { name: 'export_to_excalidraw_url', description: 'Export the remote canvas to a shareable excalidraw.com URL (encrypted upload).', inputSchema: { type: 'object', properties: {} } },
642
+ { name: 'switch_remote', description: 'Switch this MCP session to a different repo canvas (owner/repo). Commits land on GitHub.', inputSchema: { type: 'object', properties: { target: { type: 'string' } } } },
643
+ { name: 'current_canvas', description: 'Show which remote repo canvas this session points at.', inputSchema: { type: 'object', properties: {} } },
644
+ { name: 'github_login', description: 'GitHub device-flow login (2FA via GitHub). Step 1 (no args): returns code + URL. Step 2 (device_code): polls, stores token.', inputSchema: { type: 'object', properties: { device_code: { type: 'string' }, interval: { type: 'number' }, site: { type: 'string' }, repo: { type: 'string' } } } },
645
+ { name: 'commit_scene', description: 'Immediately commit the remote scene to GitHub (otherwise autosyncs ~10s after last edit).', inputSchema: { type: 'object', properties: { message: { type: 'string' } } } },
646
+ { name: 'set_viewport', description: 'Control the viewer viewport via the connected viewer (viewer tab must be open).', inputSchema: { type: 'object', properties: { scrollToContent: { type: 'boolean' }, scrollToElementId: { type: 'string' }, zoom: { type: 'number' }, offsetX: { type: 'number' }, offsetY: { type: 'number' } } } },
647
+ ];
648
+ const server = new Server({ name: 'excalidrop', version: '2.0.0', description: 'Remote GitHub-backed Excalidraw canvas' }, { capabilities: { tools: Object.fromEntries(tools.map((t) => [t.name, { description: t.description, inputSchema: t.inputSchema }])) } });
649
+ function toLabel(el) {
650
+ const { text, ...rest } = el;
651
+ if (text && el.type !== 'text')
652
+ return { ...rest, label: { text } };
653
+ return el;
654
+ }
655
+ function buildElement(d) {
656
+ const { startElementId, endElementId, id: customId, ...rest } = d;
657
+ const el = {
658
+ id: customId || generateId(), ...rest,
659
+ points: rest.points ? normalizePoints(rest.points) : undefined,
660
+ ...(startElementId ? { start: { id: startElementId } } : {}),
661
+ ...(endElementId ? { end: { id: endElementId } } : {}),
662
+ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), version: 1,
663
+ };
664
+ if (el.fontFamily !== undefined)
665
+ el.fontFamily = normalizeFontFamily(el.fontFamily);
666
+ if ((startElementId || endElementId) && !rest.points)
667
+ el.points = [[0, 0], [100, 0]];
668
+ return toLabel(el);
669
+ }
670
+ let toolQueue = Promise.resolve();
671
+ async function handleToolCall(request) {
672
+ try {
673
+ const { name, arguments: args } = request.params;
674
+ log('info', `tool: ${name}`);
675
+ switch (name) {
676
+ case 'create_element': {
677
+ await ensureActiveRepo();
678
+ const el = rCreate(buildElement(ElementSchema.parse(args)));
679
+ return { content: [{ type: 'text', text: `Element created on remote canvas!\n\n${JSON.stringify(el, null, 2)}\n\n✅ Will commit to GitHub (~10s, or commit_scene now)` }] };
680
+ }
681
+ case 'update_element': {
682
+ await ensureActiveRepo();
683
+ const { id, points: rawPoints, ...updates } = z.object({ id: z.string() }).merge(ElementSchema.partial()).parse(args);
684
+ if (!id)
685
+ throw new Error('Element ID is required');
686
+ const payload = toLabel({ id, ...updates, points: rawPoints ? normalizePoints(rawPoints) : undefined, updatedAt: new Date().toISOString() });
687
+ if (payload.fontFamily !== undefined)
688
+ payload.fontFamily = normalizeFontFamily(payload.fontFamily);
689
+ const el = rUpdate(id, payload);
690
+ return { content: [{ type: 'text', text: `Element updated on remote canvas!\n\n${JSON.stringify(el, null, 2)}` }] };
691
+ }
692
+ case 'delete_element': {
693
+ await ensureActiveRepo();
694
+ const { id } = z.object({ id: z.string() }).parse(args);
695
+ rDelete(id);
696
+ return { content: [{ type: 'text', text: `Element ${id} deleted (will commit to GitHub).` }] };
697
+ }
698
+ case 'query_elements': {
699
+ await ensureActiveRepo();
700
+ const { type, filter, bbox } = z.object({
701
+ type: z.enum(TYPES).optional(),
702
+ filter: z.record(z.any()).optional(),
703
+ bbox: z.object({ x_min: z.number().optional(), x_max: z.number().optional(), y_min: z.number().optional(), y_max: z.number().optional() }).optional(),
704
+ }).parse(args || {});
705
+ let results = rList(type);
706
+ if (bbox) {
707
+ results = results.filter((el) => (bbox.x_min === undefined || el.x >= bbox.x_min) &&
708
+ (bbox.x_max === undefined || el.x <= bbox.x_max) &&
709
+ (bbox.y_min === undefined || el.y >= bbox.y_min) &&
710
+ (bbox.y_max === undefined || el.y <= bbox.y_max));
711
+ }
712
+ if (filter)
713
+ results = results.filter((el) => Object.entries(filter).every(([k, v]) => el[k] === v));
714
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
715
+ }
716
+ case 'get_resource': {
717
+ await ensureActiveRepo();
718
+ const { resource } = z.object({ resource: z.enum(['scene', 'library', 'theme', 'elements']) }).parse(args);
719
+ if (resource === 'theme')
720
+ return { content: [{ type: 'text', text: JSON.stringify({ theme: 'light' }, null, 2) }] };
721
+ if (resource === 'scene') {
722
+ const els = rList();
723
+ return { content: [{ type: 'text', text: JSON.stringify({ count: els.length }, null, 2) }] };
724
+ }
725
+ return { content: [{ type: 'text', text: JSON.stringify({ elements: rList() }, null, 2) }] };
726
+ }
727
+ case 'group_elements': {
728
+ await ensureActiveRepo();
729
+ const { elementIds } = z.object({ elementIds: z.array(z.string()) }).parse(args);
730
+ const groupId = generateId();
731
+ groups.set(groupId, elementIds);
732
+ let n = 0;
733
+ for (const id of elementIds) {
734
+ const el = rGet(id);
735
+ if (!el)
736
+ continue;
737
+ rUpdate(id, { groupIds: [...(el.groupIds || []), groupId] });
738
+ n += 1;
739
+ }
740
+ if (!n) {
741
+ groups.delete(groupId);
742
+ throw new Error('No elements grouped (ids not found)');
743
+ }
744
+ return { content: [{ type: 'text', text: JSON.stringify({ groupId, elementIds, successCount: n }, null, 2) }] };
745
+ }
746
+ case 'ungroup_elements': {
747
+ await ensureActiveRepo();
748
+ const { groupId } = z.object({ groupId: z.string() }).parse(args);
749
+ const ids = groups.get(groupId) || rList().filter((e) => e.groupIds?.includes(groupId)).map((e) => e.id);
750
+ if (!ids.length)
751
+ throw new Error(`Group ${groupId} not found`);
752
+ groups.delete(groupId);
753
+ let n = 0;
754
+ for (const id of ids) {
755
+ const el = rGet(id);
756
+ if (!el)
757
+ continue;
758
+ rUpdate(id, { groupIds: (el.groupIds || []).filter((g) => g !== groupId) });
759
+ n += 1;
760
+ }
761
+ return { content: [{ type: 'text', text: JSON.stringify({ groupId, ungrouped: true, elementIds: ids, successCount: n }, null, 2) }] };
762
+ }
763
+ case 'align_elements': {
764
+ await ensureActiveRepo();
765
+ const { elementIds, alignment } = z.object({ elementIds: z.array(z.string()), alignment: z.enum(['left', 'center', 'right', 'top', 'middle', 'bottom']) }).parse(args);
766
+ const els = elementIds.map((id) => rGet(id)).filter(Boolean);
767
+ if (els.length < 2)
768
+ throw new Error('Need at least 2 elements to align');
769
+ let fn;
770
+ switch (alignment) {
771
+ case 'left': {
772
+ const v = Math.min(...els.map((e) => e.x));
773
+ fn = () => ({ x: v });
774
+ break;
775
+ }
776
+ case 'right': {
777
+ const v = Math.max(...els.map((e) => e.x + (e.width || 0)));
778
+ fn = (e) => ({ x: v - (e.width || 0) });
779
+ break;
780
+ }
781
+ case 'center': {
782
+ const cs = els.map((e) => e.x + (e.width || 0) / 2);
783
+ const a = cs.reduce((x, y) => x + y, 0) / cs.length;
784
+ fn = (e) => ({ x: a - (e.width || 0) / 2 });
785
+ break;
786
+ }
787
+ case 'top': {
788
+ const v = Math.min(...els.map((e) => e.y));
789
+ fn = () => ({ y: v });
790
+ break;
791
+ }
792
+ case 'bottom': {
793
+ const v = Math.max(...els.map((e) => e.y + (e.height || 0)));
794
+ fn = (e) => ({ y: v - (e.height || 0) });
795
+ break;
796
+ }
797
+ case 'middle': {
798
+ const ms = els.map((e) => e.y + (e.height || 0) / 2);
799
+ const a = ms.reduce((x, y) => x + y, 0) / ms.length;
800
+ fn = (e) => ({ y: a - (e.height || 0) / 2 });
801
+ break;
802
+ }
803
+ }
804
+ for (const el of els)
805
+ rUpdate(el.id, fn(el));
806
+ return { content: [{ type: 'text', text: JSON.stringify({ aligned: true, elementIds, alignment, successCount: els.length }, null, 2) }] };
807
+ }
808
+ case 'distribute_elements': {
809
+ await ensureActiveRepo();
810
+ const { elementIds, direction } = z.object({ elementIds: z.array(z.string()), direction: z.enum(['horizontal', 'vertical']) }).parse(args);
811
+ const els = elementIds.map((id) => rGet(id)).filter(Boolean);
812
+ if (els.length < 3)
813
+ throw new Error('Need at least 3 elements to distribute');
814
+ if (direction === 'horizontal') {
815
+ els.sort((a, b) => a.x - b.x);
816
+ const first = els[0];
817
+ const last = els[els.length - 1];
818
+ const gap = (last.x + (last.width || 0) - first.x - els.reduce((s, e) => s + (e.width || 0), 0)) / (els.length - 1);
819
+ let x = first.x;
820
+ for (const el of els) {
821
+ rUpdate(el.id, { x });
822
+ x += (el.width || 0) + gap;
823
+ }
824
+ }
825
+ else {
826
+ els.sort((a, b) => a.y - b.y);
827
+ const first = els[0];
828
+ const last = els[els.length - 1];
829
+ const gap = (last.y + (last.height || 0) - first.y - els.reduce((s, e) => s + (e.height || 0), 0)) / (els.length - 1);
830
+ let y = first.y;
831
+ for (const el of els) {
832
+ rUpdate(el.id, { y });
833
+ y += (el.height || 0) + gap;
834
+ }
835
+ }
836
+ return { content: [{ type: 'text', text: JSON.stringify({ distributed: true, elementIds, direction, count: els.length }, null, 2) }] };
837
+ }
838
+ case 'lock_elements':
839
+ case 'unlock_elements': {
840
+ await ensureActiveRepo();
841
+ const { elementIds } = z.object({ elementIds: z.array(z.string()) }).parse(args);
842
+ const locked = name === 'lock_elements';
843
+ for (const id of elementIds)
844
+ rUpdate(id, { locked });
845
+ return { content: [{ type: 'text', text: JSON.stringify({ [locked ? 'locked' : 'unlocked']: true, elementIds }, null, 2) }] };
846
+ }
847
+ case 'batch_create_elements': {
848
+ await ensureActiveRepo();
849
+ const { elements } = z.object({ elements: z.array(ElementSchema) }).parse(args);
850
+ const out = rBatch(elements.map(buildElement));
851
+ return { content: [{ type: 'text', text: `${out.length} elements created on remote canvas!\n\n${JSON.stringify(out, null, 2)}` }] };
852
+ }
853
+ case 'get_element': {
854
+ await ensureActiveRepo();
855
+ const { id } = z.object({ id: z.string() }).parse(args);
856
+ const el = rGet(id);
857
+ if (!el)
858
+ throw new Error(`Element ${id} not found`);
859
+ return { content: [{ type: 'text', text: JSON.stringify(el, null, 2) }] };
860
+ }
861
+ case 'clear_canvas': {
862
+ await ensureActiveRepo();
863
+ const n = rClear();
864
+ return { content: [{ type: 'text', text: `Canvas cleared (${n} elements removed, will commit to GitHub).` }] };
865
+ }
866
+ case 'export_scene': {
867
+ await ensureActiveRepo();
868
+ const { filePath } = z.object({ filePath: z.string().optional() }).parse(args || {});
869
+ const els = rList();
870
+ const json = JSON.stringify({ type: 'excalidraw', version: 2, source: 'excalidrop', elements: els, appState: { viewBackgroundColor: '#ffffff', gridSize: null } }, null, 2);
871
+ if (filePath) {
872
+ const p = sanitizeFilePath(filePath);
873
+ fs.writeFileSync(p, json, 'utf-8');
874
+ return { content: [{ type: 'text', text: `Scene exported to ${p} (${els.length} elements)` }] };
875
+ }
876
+ return { content: [{ type: 'text', text: json }] };
877
+ }
878
+ case 'import_scene': {
879
+ await ensureActiveRepo();
880
+ const { filePath, data, mode } = z.object({ filePath: z.string().optional(), data: z.string().optional(), mode: z.enum(['replace', 'merge']) }).parse(args);
881
+ let doc;
882
+ if (filePath)
883
+ doc = JSON.parse(fs.readFileSync(sanitizeFilePath(filePath), 'utf-8'));
884
+ else if (data)
885
+ doc = JSON.parse(data);
886
+ else
887
+ throw new Error('Either filePath or data must be provided');
888
+ const incoming = (Array.isArray(doc) ? doc : doc.elements || []).map((el) => ({ ...el, id: el.id || generateId(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), version: 1 }));
889
+ if (!incoming.length)
890
+ throw new Error('No elements found in the import data');
891
+ if (mode === 'replace')
892
+ rClear();
893
+ if (doc.files && typeof doc.files === 'object') {
894
+ for (const f of Object.values(doc.files))
895
+ if (f?.id)
896
+ rAddFile(f);
897
+ }
898
+ const out = rBatch(incoming);
899
+ return { content: [{ type: 'text', text: `Imported ${out.length} elements (mode: ${mode})\n\n✅ Will commit to GitHub` }] };
900
+ }
901
+ case 'add_image': {
902
+ await ensureActiveRepo();
903
+ const p = z.object({ source: z.string().optional(), dataURL: z.string().optional(), filename: z.string().optional(), x: z.number().optional(), y: z.number().optional(), width: z.number().optional(), height: z.number().optional() }).parse(args);
904
+ const raw = p.dataURL || p.source;
905
+ if (!raw)
906
+ throw new Error('add_image requires "source" or "dataURL"');
907
+ let finalDataURL;
908
+ let mimeHint;
909
+ if (raw.startsWith('data:')) {
910
+ finalDataURL = raw;
911
+ mimeHint = /^data:([^;]+);base64,/.exec(raw)?.[1];
912
+ }
913
+ else if (/^https?:\/\//.test(raw)) {
914
+ const r = await fetch(raw);
915
+ if (!r.ok)
916
+ throw new Error(`Failed to download image: ${r.status}`);
917
+ const buf = Buffer.from(await r.arrayBuffer());
918
+ if (buf.length > 10 * 1024 * 1024)
919
+ throw new Error('Image too large (max 10MB)');
920
+ mimeHint = r.headers.get('content-type') || 'image/png';
921
+ finalDataURL = `data:${mimeHint};base64,${buf.toString('base64')}`;
922
+ }
923
+ else {
924
+ const abs = path.resolve(raw);
925
+ if (!fs.existsSync(abs))
926
+ throw new Error(`Image file not found: ${raw}`);
927
+ const buf = fs.readFileSync(abs);
928
+ if (buf.length > 10 * 1024 * 1024)
929
+ throw new Error('Image too large (max 10MB)');
930
+ const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' };
931
+ mimeHint = mimeMap[path.extname(abs).toLowerCase()];
932
+ if (!mimeHint)
933
+ throw new Error('Unsupported image type (use png/jpg/gif/webp/svg)');
934
+ finalDataURL = `data:${mimeHint};base64,${buf.toString('base64')}`;
935
+ }
936
+ const mm = /^data:([^;]+);base64,(.+)$/s.exec(finalDataURL);
937
+ if (!mm)
938
+ throw new Error('Invalid image data');
939
+ const fileId = generateId();
940
+ rAddFile({ id: fileId, dataURL: finalDataURL, mimeType: mimeHint || mm[1], created: Date.now() });
941
+ const now = new Date().toISOString();
942
+ const el = rCreate({ id: generateId(), type: 'image', x: p.x ?? 0, y: p.y ?? 0, width: p.width ?? 400, height: p.height ?? 300, fileId, status: 'saved', scale: [1, 1], createdAt: now, updatedAt: now, version: 1 });
943
+ return { content: [{ type: 'text', text: `Image saved to remote scene (fileId ${fileId})\n\n${JSON.stringify(el, null, 2)}` }] };
944
+ }
945
+ case 'snapshot_scene': {
946
+ const repo = await ensureActiveRepo();
947
+ const { name: snapName } = z.object({ name: z.string() }).parse(args);
948
+ const els = rList();
949
+ await putFile(repo, `snapshots/${snapName}.json`, { name: snapName, elements: els, createdAt: new Date().toISOString() }, `excalidrop: snapshot ${snapName} (${els.length} elements)`, CANVAS_BRANCH);
950
+ return { content: [{ type: 'text', text: `Snapshot "${snapName}" saved (${els.length} elements) on branch ${CANVAS_BRANCH}.` }] };
951
+ }
952
+ case 'restore_snapshot': {
953
+ const repo = await ensureActiveRepo();
954
+ const { name: snapName } = z.object({ name: z.string() }).parse(args);
955
+ const snap = await getFile(repo, `snapshots/${snapName}.json`, CANVAS_BRANCH);
956
+ if (!snap)
957
+ throw new Error(`Snapshot "${snapName}" not found`);
958
+ const els = snap.content.elements || [];
959
+ rClear();
960
+ rBatch(els.map((e) => ({ ...e, id: e.id || generateId() })));
961
+ return { content: [{ type: 'text', text: `Snapshot "${snapName}" restored (${els.length} elements). Will commit to GitHub.` }] };
962
+ }
963
+ case 'describe_scene': {
964
+ await ensureActiveRepo();
965
+ const els = rList();
966
+ if (!els.length)
967
+ return { content: [{ type: 'text', text: 'The canvas is empty. No elements to describe.' }] };
968
+ const counts = {};
969
+ for (const el of els)
970
+ counts[el.type] = (counts[el.type] || 0) + 1;
971
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
972
+ for (const el of els) {
973
+ minX = Math.min(minX, el.x);
974
+ minY = Math.min(minY, el.y);
975
+ maxX = Math.max(maxX, el.x + (el.width || 0));
976
+ maxY = Math.max(maxY, el.y + (el.height || 0));
977
+ }
978
+ const lines = [`## Canvas Description`, `Total elements: ${els.length}`, `Types: ${Object.entries(counts).map(([t, c]) => `${t}(${c})`).join(', ')}`, `Bounding box: (${Math.round(minX)}, ${Math.round(minY)}) to (${Math.round(maxX)}, ${Math.round(maxY)})`, '', '### Elements:'];
979
+ for (const el of [...els].sort((a, b) => Math.floor(a.y / 50) - Math.floor(b.y / 50) || a.x - b.x)) {
980
+ const parts = [`[${el.id}] ${el.type}`, `at (${Math.round(el.x)}, ${Math.round(el.y)})`];
981
+ if (el.width || el.height)
982
+ parts.push(`size ${Math.round(el.width || 0)}x${Math.round(el.height || 0)}`);
983
+ if (el.text)
984
+ parts.push(`text: "${el.text}"`);
985
+ if (el.label?.text)
986
+ parts.push(`label: "${el.label.text}"`);
987
+ lines.push(' ' + parts.join(' | '));
988
+ }
989
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
990
+ }
991
+ case 'get_canvas_screenshot':
992
+ case 'export_to_image': {
993
+ const repo = await ensureActiveRepo();
994
+ const isShot = name === 'get_canvas_screenshot';
995
+ const p = isShot
996
+ ? z.object({ background: z.boolean().optional() }).parse(args || {})
997
+ : z.object({ format: z.enum(['png', 'svg']), filePath: z.string().optional(), background: z.boolean().optional() }).parse(args);
998
+ const format = isShot ? 'png' : p.format;
999
+ const out = await screenshotRepo(repo, { background: p.background ?? true, format });
1000
+ if (!isShot && p.filePath) {
1001
+ const fp = sanitizeFilePath(p.filePath);
1002
+ if (format === 'svg')
1003
+ fs.writeFileSync(fp, out.data, 'utf-8');
1004
+ else
1005
+ fs.writeFileSync(fp, Buffer.from(out.data, 'base64'));
1006
+ return { content: [{ type: 'text', text: `Image exported to ${fp} (format: ${format})` }] };
1007
+ }
1008
+ if (format === 'svg')
1009
+ return { content: [{ type: 'text', text: out.data }] };
1010
+ return { content: [{ type: 'image', data: out.data, mimeType: 'image/png' }, { type: 'text', text: 'Remote canvas screenshot (viewer render).' }] };
1011
+ }
1012
+ case 'create_from_mermaid': {
1013
+ const repo = await ensureActiveRepo();
1014
+ const { mermaidDiagram } = z.object({ mermaidDiagram: z.string(), config: z.any().optional() }).parse(args);
1015
+ try {
1016
+ const out = await queueRpc(repo, 'mermaid', { mermaidDiagram });
1017
+ if (Array.isArray(out?.elements) && out.elements.length) {
1018
+ rBatch(out.elements.map((e) => ({ ...e, id: e.id || generateId() })));
1019
+ }
1020
+ return { content: [{ type: 'text', text: `Mermaid converted via viewer (${out?.count || out?.elements?.length || 0} elements).` }] };
1021
+ }
1022
+ catch (e) {
1023
+ throw new Error(`Mermaid needs a connected viewer tab: ${e.message}`);
1024
+ }
1025
+ }
1026
+ case 'read_diagram_guide': {
1027
+ return { content: [{ type: 'text', text: DIAGRAM_DESIGN_GUIDE }] };
1028
+ }
1029
+ case 'export_to_excalidraw_url': {
1030
+ await ensureActiveRepo();
1031
+ const els = rList();
1032
+ if (!els.length)
1033
+ throw new Error('Canvas is empty — nothing to export');
1034
+ const cleaned = [];
1035
+ const boundTexts = [];
1036
+ let idx = 0;
1037
+ for (const el of els) {
1038
+ const { createdAt, updatedAt, syncedAt, source, syncTimestamp, label, start, end, text, version: _v, ...rest } = el;
1039
+ const base = { ...rest, angle: 0, strokeColor: '#1e1e1e', backgroundColor: 'transparent', fillStyle: 'solid', strokeWidth: 2, strokeStyle: 'solid', roughness: 1, opacity: 100, groupIds: [], frameId: null, index: `a${idx++}`, roundness: null, seed: 1, version: 1, versionNonce: 1, isDeleted: false, boundElements: null, updated: Date.now(), link: null, locked: false };
1040
+ if (el.type === 'text') {
1041
+ base.text = text ?? '';
1042
+ base.originalText = text ?? '';
1043
+ base.fontSize = 20;
1044
+ base.fontFamily = 1;
1045
+ cleaned.push(base);
1046
+ continue;
1047
+ }
1048
+ const labelText = label?.text || text;
1049
+ if (labelText) {
1050
+ const textId = `${base.id}-label`;
1051
+ base.boundElements = [...(Array.isArray(base.boundElements) ? base.boundElements : []), { type: 'text', id: textId }];
1052
+ boundTexts.push({ id: textId, type: 'text', x: base.x + 10, y: base.y + 10, width: 100, height: 24, angle: 0, strokeColor: '#1e1e1e', backgroundColor: 'transparent', fillStyle: 'solid', strokeWidth: 1, strokeStyle: 'solid', roughness: 1, opacity: 100, groupIds: [], frameId: null, index: `a${idx++}`, roundness: null, seed: 1, version: 1, versionNonce: 1, isDeleted: false, boundElements: null, updated: Date.now(), link: null, locked: false, text: labelText, originalText: labelText, fontSize: 16, fontFamily: 1, textAlign: 'center', verticalAlign: 'middle', autoResize: true, lineHeight: 1.25, containerId: base.id });
1053
+ }
1054
+ cleaned.push(base);
1055
+ }
1056
+ cleaned.push(...boundTexts);
1057
+ const enc = new TextEncoder();
1058
+ const concat = (...bufs) => {
1059
+ let total = 4;
1060
+ for (const b of bufs)
1061
+ total += 4 + b.length;
1062
+ const out = new Uint8Array(total);
1063
+ const dv = new DataView(out.buffer);
1064
+ dv.setUint32(0, 1);
1065
+ let off = 4;
1066
+ for (const b of bufs) {
1067
+ dv.setUint32(off, b.length);
1068
+ off += 4;
1069
+ out.set(b, off);
1070
+ off += b.length;
1071
+ }
1072
+ return out;
1073
+ };
1074
+ const inner = concat(enc.encode('{}'), enc.encode(JSON.stringify({ type: 'excalidraw', version: 2, source: 'https://excalidraw.com', elements: cleaned, appState: { viewBackgroundColor: '#ffffff', gridSize: null }, files: {} })));
1075
+ const compressed = deflateSync(Buffer.from(inner));
1076
+ const key = await webcrypto.subtle.generateKey({ name: 'AES-GCM', length: 128 }, true, ['encrypt']);
1077
+ const iv = webcrypto.getRandomValues(new Uint8Array(12));
1078
+ const ct = new Uint8Array(await webcrypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, compressed));
1079
+ const payload = concat(enc.encode(JSON.stringify({ version: 2, compression: 'pako@1', encryption: 'AES-GCM' })), iv, ct);
1080
+ const up = await fetch('https://json.excalidraw.com/api/v2/post/', { method: 'POST', body: Buffer.from(payload) });
1081
+ if (!up.ok)
1082
+ throw new Error(`Upload failed: ${up.status}`);
1083
+ const { id } = await up.json();
1084
+ const jwk = await webcrypto.subtle.exportKey('jwk', key);
1085
+ return { content: [{ type: 'text', text: `Diagram exported!\n\nShareable URL: https://excalidraw.com/#json=${id},${jwk.k}` }] };
1086
+ }
1087
+ case 'switch_remote': {
1088
+ const { target: input } = z.object({ target: z.string().optional() }).parse(args || {});
1089
+ if (!input)
1090
+ throw new Error('Remote-only mode: pass { target: "owner/repo" }.');
1091
+ const repo = parseRepo(input);
1092
+ activeRepo = repo;
1093
+ const st = await ensureLoaded(repo);
1094
+ persistRepo(repo);
1095
+ return { content: [{ type: 'text', text: `Switched to remote canvas ${repo} (${st.elements.size} elements). Edits commit to GitHub.` }] };
1096
+ }
1097
+ case 'current_canvas': {
1098
+ const repo = activeRepo || loadSavedRepo();
1099
+ return { content: [{ type: 'text', text: repo ? `remote:${repo}` : 'no canvas selected — pass --repo=owner/repo' }] };
1100
+ }
1101
+ case 'github_login': {
1102
+ const { device_code, interval, site, repo } = z.object({ device_code: z.string().optional(), interval: z.number().optional(), site: z.string().optional(), repo: z.string().optional() }).parse(args || {});
1103
+ if (!device_code) {
1104
+ if (resolveToken())
1105
+ return { content: [{ type: 'text', text: 'Already logged in to GitHub (token from env/gh CLI/stored).' }] };
1106
+ const dev = await deviceStart(APP_CLIENT_ID);
1107
+ return { content: [{ type: 'text', text: `Open ${dev.verification_uri}, enter code ${dev.user_code}, then call github_login with { device_code: "${dev.device_code}", interval: ${dev.interval} }` }] };
1108
+ }
1109
+ let repositoryId;
1110
+ const ref = repo || site;
1111
+ if (ref) {
1112
+ try {
1113
+ const [o, r] = parseRepo(ref).split('/');
1114
+ repositoryId = (await repoId(o, r)) || undefined;
1115
+ }
1116
+ catch { /* unrestricted */ }
1117
+ }
1118
+ const token = await devicePoll(APP_CLIENT_ID, device_code, interval || 5, 300, repositoryId);
1119
+ const link = site ? `\nBrowser login link: ${site.replace(/\/$/, '')}/#token=${token}` : '';
1120
+ return { content: [{ type: 'text', text: `Logged in to GitHub (token stored).${link}` }] };
1121
+ }
1122
+ case 'commit_scene': {
1123
+ const { message } = z.object({ message: z.string().optional() }).parse(args || {});
1124
+ const out = await commitNow(message);
1125
+ return { content: [{ type: 'text', text: `Committed ${out.count} elements (${out.sha.slice(0, 7)}).` }] };
1126
+ }
1127
+ case 'set_viewport': {
1128
+ const repo = await ensureActiveRepo();
1129
+ const p = z.object({ scrollToContent: z.boolean().optional(), scrollToElementId: z.string().optional(), zoom: z.number().min(0.1).max(10).optional(), offsetX: z.number().optional(), offsetY: z.number().optional() }).parse(args || {});
1130
+ const out = await viewportRepo(repo, p);
1131
+ return { content: [{ type: 'text', text: `Viewport updated.\n\n${JSON.stringify(out, null, 2)}` }] };
1132
+ }
1133
+ default:
1134
+ throw new Error(`Unknown tool: ${name}`);
1135
+ }
1136
+ }
1137
+ catch (error) {
1138
+ log('error', `tool failed: ${error.message}`);
1139
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
1140
+ }
1141
+ }
1142
+ server.setRequestHandler(CallToolRequestSchema, (request) => {
1143
+ const task = toolQueue.then(() => handleToolCall(request));
1144
+ toolQueue = task.then(() => undefined, () => undefined);
1145
+ return task;
1146
+ });
1147
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
1148
+ async function runServer() {
1149
+ const repo = loadSavedRepo();
1150
+ if (repo) {
1151
+ try {
1152
+ const st = await ensureLoaded(repo);
1153
+ activeRepo = repo;
1154
+ log('info', `Remote canvas ${repo} (${st.elements.size} elements)`);
1155
+ }
1156
+ catch (e) {
1157
+ log('warn', 'Remote preload failed: ' + e.message);
1158
+ }
1159
+ }
1160
+ else {
1161
+ log('warn', 'No repo selected — pass --repo=owner/repo.');
1162
+ }
1163
+ await server.connect(new StdioServerTransport());
1164
+ log('info', 'excalidrop MCP (remote-only) running on stdio');
1165
+ process.stdin.resume();
1166
+ }
1167
+ process.on('uncaughtException', (e) => { log('error', 'uncaught: ' + e.message); setTimeout(() => process.exit(1), 1000); });
1168
+ process.on('unhandledRejection', (r) => { log('error', 'unhandled: ' + String(r)); setTimeout(() => process.exit(1), 1000); });
1169
+ function entryPath(p) {
1170
+ if (!p)
1171
+ return null;
1172
+ try {
1173
+ return fs.realpathSync(p);
1174
+ }
1175
+ catch {
1176
+ return path.resolve(p);
1177
+ }
1178
+ }
1179
+ if (entryPath(fileURLToPath(import.meta.url)) === entryPath(process.argv[1])) {
1180
+ runServer().catch((e) => { log('error', 'start failed: ' + e.message); process.exit(1); });
1181
+ }
1182
+ export default runServer;
1183
+ //# sourceMappingURL=mcp.js.map