Compare commits

..

1 Commits

Author SHA1 Message Date
alonso.torres
91fd6b9662 WIP 2026-02-05 09:32:51 +01:00
24 changed files with 227 additions and 114 deletions

View File

@@ -77,6 +77,10 @@ services:
- PENPOT_LDAP_ATTRS_FULLNAME=cn
- PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto
# MCP
- PENPOT_MCP_SERVER_LISTEN_ADDRESS=0.0.0.0
- PENPOT_MCP_SERVER_ADDRESS=0.0.0.0
networks:
default:
aliases:

View File

@@ -47,7 +47,7 @@
"devDependencies": {
"@penpot/draft-js": "workspace:./packages/draft-js",
"@penpot/mousetrap": "workspace:./packages/mousetrap",
"@penpot/plugins-runtime": "1.4.2",
"@penpot/plugins-runtime": "workspace:../plugins/dist/plugins-runtime",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "workspace:./text-editor",
"@playwright/test": "1.58.0",

View File

@@ -20,8 +20,8 @@ importers:
specifier: workspace:./packages/mousetrap
version: link:packages/mousetrap
'@penpot/plugins-runtime':
specifier: 1.4.2
version: 1.4.2
specifier: workspace:../plugins/dist/plugins-runtime
version: link:../plugins/dist/plugins-runtime
'@penpot/svgo':
specifier: penpot/svgo#v3.2
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b
@@ -461,15 +461,6 @@ packages:
'@dabh/diagnostics@2.0.8':
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
'@endo/cache-map@1.1.0':
resolution: {integrity: sha512-owFGshs/97PDw9oguZqU/px8Lv1d0KjAUtDUiPwKHNXRVUE/jyettEbRoTbNJR1OaI8biMn6bHr9kVJsOh6dXw==}
'@endo/env-options@1.1.11':
resolution: {integrity: sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA==}
'@endo/immutable-arraybuffer@1.1.2':
resolution: {integrity: sha512-u+NaYB2aqEugQ3u7w3c5QNkPogf8q/xGgsPaqdY6pUiGWtYiTiFspKFcha6+oeZhWXWQ23rf0KrUq0kfuzqYyQ==}
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
@@ -924,36 +915,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -977,12 +974,6 @@ packages:
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
engines: {node: '>= 10.0.0'}
'@penpot/plugin-types@1.4.2':
resolution: {integrity: sha512-O8wU6RSYE8bIVU7g8cSTYi32ppxs3R13dq7X3Nn9tmDaJjBOKOBpVLuoRPIp3fJC65fv8/7om0sdrtFoL5v19g==}
'@penpot/plugins-runtime@1.4.2':
resolution: {integrity: sha512-y9TDZOnb96JBW9E33dHKpmTMeAPXLtHDIZruUVjtM8hBJWZK7RCv+vAGDGxeoZJC/OB2YAHrCZG+mukePBzcuQ==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -1035,24 +1026,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@resvg/resvg-js-linux-x64-musl@2.6.2':
resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
@@ -1149,121 +1144,145 @@ packages:
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-gnueabihf@4.56.0':
resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm-musleabihf@4.56.0':
resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.54.0':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-gnu@4.56.0':
resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.54.0':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-musl@4.56.0':
resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.54.0':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-gnu@4.56.0':
resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.56.0':
resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.56.0':
resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.56.0':
resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.56.0':
resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.54.0':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-musl@4.56.0':
resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.54.0':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.56.0':
resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.54.0':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.56.0':
resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.54.0':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-x64-musl@4.56.0':
resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.56.0':
resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==}
@@ -3784,48 +3803,56 @@ packages:
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [linux]
libc: glibc
sass-embedded-linux-arm@1.97.1:
resolution: {integrity: sha512-48VxaTUApLyx1NXFdZhKqI/7FYLmz8Ju3Ki2V/p+mhn5raHgAiYeFgn8O1WGxTOh+hBb9y3FdSR5a8MNTbmKMQ==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [linux]
libc: glibc
sass-embedded-linux-musl-arm64@1.97.1:
resolution: {integrity: sha512-kD35WSD9o0279Ptwid3Jnbovo1FYnuG2mayYk9z4ZI4mweXEK6vTu+tlvCE/MdF/zFKSj11qaxaH+uzXe2cO5A==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [linux]
libc: musl
sass-embedded-linux-musl-arm@1.97.1:
resolution: {integrity: sha512-FUFs466t3PVViVOKY/60JgLLtl61Pf7OW+g5BeEfuqVcSvYUECVHeiYHtX1fT78PEVa0h9tHpM6XpWti+7WYFA==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [linux]
libc: musl
sass-embedded-linux-musl-riscv64@1.97.1:
resolution: {integrity: sha512-ZgpYps5YHuhA2+KiLkPukRbS5298QObgUhPll/gm5i0LOZleKCwrFELpVPcbhsSBuxqji2uaag5OL+n3JRBVVg==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [linux]
libc: musl
sass-embedded-linux-musl-x64@1.97.1:
resolution: {integrity: sha512-wcAigOyyvZ6o1zVypWV7QLZqpOEVnlBqJr9MbpnRIm74qFTSbAEmShoh8yMXBymzuVSmEbThxAwW01/TLf62tA==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [linux]
libc: musl
sass-embedded-linux-riscv64@1.97.1:
resolution: {integrity: sha512-9j1qE1ZrLMuGb+LUmBzw93Z4TNfqlRkkxjPVZy6u5vIggeSfvGbte7eRoYBNWX6SFew/yBCL90KXIirWFSGrlQ==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [linux]
libc: glibc
sass-embedded-linux-x64@1.97.1:
resolution: {integrity: sha512-7nrLFYMH/UgvEgXR5JxQJ6y9N4IJmnFnYoDxN0nw0jUp+CQWQL4EJ4RqAKTGelneueRbccvt2sEyPK+X0KJ9Jg==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [linux]
libc: glibc
sass-embedded-unknown-all@1.97.1:
resolution: {integrity: sha512-oPSeKc7vS2dx3ZJHiUhHKcyqNq0GWzAiR8zMVpPd/kVMl5ZfVyw+5HTCxxWDBGkX02lNpou27JkeBPCaneYGAQ==}
@@ -3887,9 +3914,6 @@ packages:
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
engines: {node: '>= 18'}
ses@1.14.0:
resolution: {integrity: sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -4681,9 +4705,6 @@ packages:
resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
engines: {node: '>=12.20'}
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
snapshots:
'@acemir/cssom@0.9.30': {}
@@ -4901,12 +4922,6 @@ snapshots:
enabled: 2.0.0
kuler: 2.0.0
'@endo/cache-map@1.1.0': {}
'@endo/env-options@1.1.11': {}
'@endo/immutable-arraybuffer@1.1.2': {}
'@esbuild/aix-ppc64@0.21.5':
optional: true
@@ -5237,14 +5252,6 @@ snapshots:
'@parcel/watcher-win32-x64': 2.5.1
optional: true
'@penpot/plugin-types@1.4.2': {}
'@penpot/plugins-runtime@1.4.2':
dependencies:
'@penpot/plugin-types': 1.4.2
ses: 1.14.0
zod: 3.25.76
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -8332,12 +8339,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
ses@1.14.0:
dependencies:
'@endo/cache-map': 1.1.0
'@endo/env-options': 1.1.11
'@endo/immutable-arraybuffer': 1.1.2
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -9196,5 +9197,3 @@ snapshots:
yargs-parser: 21.1.1
yocto-queue@1.2.2: {}
zod@3.25.76: {}

View File

@@ -119,6 +119,10 @@
(normalize-uri (or (obj/get global "penpotPublicURI")
(obj/get location "origin"))))
(def mcp-server-uri
(normalize-uri (or (obj/get global "penpotMcpServerURI")
(str "ws://" (obj/get location "hostname")":4402" ))))
(def rasterizer-uri
(or (some-> (obj/get global "penpotRasterizerURI") normalize-uri)
public-uri))

View File

@@ -65,8 +65,23 @@
(update [_ state]
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
(defn start-plugin!
[{:keys [plugin-id name description host code icon permissions allow-background]} ^js extensions]
(.ɵloadPlugin
^js ug/global
#js {:pluginId plugin-id
:name name
:description description
:host host
:code code
:icon icon
:allowBackground (boolean allow-background)
:permissions (apply array permissions)}
nil
extensions))
(defn- load-plugin!
[{:keys [plugin-id name description host code icon permissions]}]
[{:keys [plugin-id name description host code icon permissions] :as params}]
(try
(st/emit! (save-current-plugin plugin-id)
(reset-plugin-flags plugin-id))

View File

@@ -23,6 +23,7 @@
[app.common.types.components-list :as ctkl]
[app.common.types.shape :as cts]
[app.common.types.variant :as ctv]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.changes :as dch]
@@ -212,7 +213,34 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dp/check-open-plugin)
(fdf/fix-deleted-fonts-for-local-library file-id)))))
(fdf/fix-deleted-fonts-for-local-library file-id)))
ptk/EffectEvent
(effect [_ _ _]
(dp/start-plugin!
{:url (str (u/join cf/public-uri "mcp/manifest.json"))
:code "plugin.js"
:host (str (u/join cf/public-uri "mcp/"))
:name "Penpot MCP Plugin"
:plugin-id "96dfa740-005d-8020-8007-55ede24a2bae"
:description "This plugin enables interaction with the Penpot MCP server"
:allow-background true
:permissions
#{"library:read"
"library:write"
"comment:read"
"content:write"
"comment:write"
"content:read"}}
#js
{:mcp
#js
{:getToken (fn [] "xxxxx")
:getServerUrl (fn [] "ws://localhost:4402")
:setMcpStatus (fn [status]
(when-let [node (.getElementById ^js js/document "mcp-status")]
(set! (.-textContent node) status)))}}))))
(defn- bundle-fetched
[{:keys [file file-id thumbnails] :as bundle}]

View File

@@ -95,10 +95,10 @@
[]
(let [plugins-state* (mf/use-state #(preg/plugins-list))
plugins-state (deref plugins-state*)
plugins-state @plugins-state*
plugin-url* (mf/use-state "")
plugin-url (deref plugin-url*)
plugin-url* (mf/use-state "")
plugin-url @plugin-url*
fetching-manifest? (mf/use-state false)

View File

@@ -230,7 +230,9 @@
:aria-pressed (contains? layout :debug-panel)
:aria-label (tr "workspace.toolbar.debug")
:tooltip-placement "bottom"
:on-click toggle-debug-panel}]])]]
:on-click toggle-debug-panel}]])
[:span {:id "mcp-status"} "disconnected"]]]
[:button {:title (tr "workspace.toolbar.toggle-toolbar")
:aria-label (tr "workspace.toolbar.toggle-toolbar")

View File

@@ -38,7 +38,6 @@
desc (obj/get manifest "description")
code (obj/get manifest "code")
icon (obj/get manifest "icon")
vers (d/nilv (obj/get manifest "version") 1)
permissions (into #{} (obj/get manifest "permissions" []))
permissions
@@ -56,13 +55,9 @@
(u/uri plugin-url)
origin
(if (= vers 1)
(-> plugin-url
(assoc :path "/")
(str))
(-> plugin-url
(u/join ".")
(str)))
(-> plugin-url
(u/join ".")
(str))
prev-plugin
(->> (:data @registry)

View File

@@ -1,7 +1,6 @@
{
"name": "Penpot MCP Plugin",
"code": "plugin.js",
"version": 2,
"description": "This plugin enables interaction with the Penpot MCP server",
"permissions": ["content:read", "content:write", "library:read", "library:write", "comment:read", "comment:write"]
}

5
mcp/packages/plugin/src/index.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare global {
const mcp: any;
}
export {};

View File

@@ -19,12 +19,19 @@ const statusElement = document.getElementById("connection-status");
* @param isConnectedState - whether the connection is in a connected state (affects color)
* @param message - optional additional message to append to the status
*/
function updateConnectionStatus(status: string, isConnectedState: boolean, message?: string): void {
function updateConnectionStatus(code: string, status: string, isConnectedState: boolean, message?: string): void {
if (statusElement) {
const displayText = message ? `${status}: ${message}` : status;
statusElement.textContent = displayText;
statusElement.style.color = isConnectedState ? "var(--accent-primary)" : "var(--error-700)";
}
parent.postMessage(
{
type: "update-connection-status",
status: code,
},
"*"
);
}
/**
@@ -44,25 +51,23 @@ function sendTaskResponse(response: any): void {
/**
* Establishes a WebSocket connection to the MCP server.
*/
function connectToMcpServer(): void {
function connectToMcpServer(url: string, token: string): void {
if (ws?.readyState === WebSocket.OPEN) {
updateConnectionStatus("Already connected", true);
updateConnectionStatus("connected", "Already connected", true);
return;
}
try {
let wsUrl = PENPOT_MCP_WEBSOCKET_URL;
if (isMultiUserMode) {
// TODO obtain proper userToken from penpot
const userToken = "dummyToken";
wsUrl += `?userToken=${encodeURIComponent(userToken)}`;
url += `?userToken=${encodeURIComponent(token)}`;
}
ws = new WebSocket(wsUrl);
updateConnectionStatus("Connecting...", false);
ws = new WebSocket(url);
updateConnectionStatus("connecting", "Connecting...", false);
ws.onopen = () => {
console.log("Connected to MCP server");
updateConnectionStatus("Connected to MCP server", true);
updateConnectionStatus("connected", "Connected to MCP server", true);
};
ws.onmessage = (event) => {
@@ -79,32 +84,37 @@ function connectToMcpServer(): void {
ws.onclose = (event: CloseEvent) => {
console.log("Disconnected from MCP server");
const message = event.reason || undefined;
updateConnectionStatus("Disconnected", false, message);
updateConnectionStatus("disconnected", "Disconnected", false, message);
ws = null;
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
// note: WebSocket error events typically don't contain detailed error messages
updateConnectionStatus("Connection error", false);
updateConnectionStatus("error", "Connection error", false);
};
} catch (error) {
console.error("Failed to connect to MCP server:", error);
const message = error instanceof Error ? error.message : undefined;
updateConnectionStatus("Connection failed", false, message);
updateConnectionStatus("error", "Connection failed", false, message);
}
}
document.querySelector("[data-handler='connect-mcp']")?.addEventListener("click", () => {
connectToMcpServer();
});
// document.querySelector("[data-handler='connect-mcp']")?.addEventListener("click", () => {
// connectToMcpServer();
// });
// Listen plugin.ts messages
window.addEventListener("message", (event) => {
if (event.data.source === "penpot") {
console.log("event", event.data);
if (event.data.type === "init-server") {
connectToMcpServer(event.data.url, event.data.token);
} else if (event.data.source === "penpot") {
document.body.dataset.theme = event.data.theme;
} else if (event.data.type === "task-response") {
// Forward task response back to MCP server
sendTaskResponse(event.data.response);
}
});
parent.postMessage({ type: "ui-initialized" }, "*");

View File

@@ -1,6 +1,11 @@
import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler";
import { Task, TaskHandler } from "./TaskHandler";
console.log("TOKEN", mcp.getToken());
console.log("SERVER", mcp.getServerUrl());
mcp.setMcpStatus("connecting");
/**
* Registry of all available task handlers.
*/
@@ -11,12 +16,26 @@ declare const IS_MULTI_USER_MODE: boolean;
const isMultiUserMode = typeof IS_MULTI_USER_MODE !== "undefined" ? IS_MULTI_USER_MODE : false;
// Open the plugin UI (main.ts)
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}&multiUser=${isMultiUserMode}`, { width: 158, height: 200 });
(penpot.ui as any).open("Penpot MCP Plugin", `?theme=${penpot.theme}&multiUser=${isMultiUserMode}`, {
width: 158,
height: 200,
hidden: true,
});
// Handle messages
penpot.ui.onMessage<string | { id: string; task: string; params: any }>((message) => {
penpot.ui.onMessage<string | { id: string; type?: string; status?: string; task: string; params: any }>((message) => {
// Handle plugin task requests
if (typeof message === "object" && message.task && message.id) {
console.log(message);
if (typeof message === "object" && message.type === "ui-initialized") {
console.log("send message");
penpot.ui.sendMessage({
type: "init-server",
url: mcp.getServerUrl(),
token: mcp.getToken(),
});
} else if (typeof message === "object" && message.type === "update-connection-status") {
mcp.setMcpStatus(message.status);
} else if (typeof message === "object" && message.task && message.id) {
handlePluginTaskRequest(message).catch((error) => {
console.error("Error in handlePluginTaskRequest:", error);
});
@@ -67,3 +86,11 @@ penpot.on("themechange", (theme) => {
theme,
});
});
// console.log("send message");
// penpot.ui.sendMessage({
// source: "penpot",
// type: "init-server",
// url: mcp.getServerUrl(),
// token: mcp.getToken(),
// })

View File

@@ -1,24 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"typeRoots": ["./node_modules/@types", "./node_modules/@penpot"],
"types": ["plugin-types"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"typeRoots": ["./node_modules/@types", "./node_modules/@penpot"],
"types": ["plugin-types"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -9,6 +9,7 @@ let WS_URI = "http://localhost:4402";
console.log("Will define PENPOT_MCP_WEBSOCKET_URL as:", JSON.stringify(WS_URI));
export default defineConfig({
base: "./",
plugins: [
livePreview({
reload: true,

View File

@@ -4,7 +4,6 @@ import baseConfig from "./vite.config";
export default mergeConfig(
baseConfig,
defineConfig({
base: "./",
plugins: [],
})
);

View File

@@ -1,3 +1,10 @@
export interface OpenModalOptions {
width: number;
height: number;
hidden: boolean;
}
/**
* These are methods and properties available on the `penpot` global object.
*
@@ -23,7 +30,7 @@ export interface Penpot extends Omit<
open: (
name: string,
url: string,
options?: { width: number; height: number },
options?: UIOptions,
) => void;
size: {

View File

@@ -1,14 +1,14 @@
{
"name": "@penpot/plugins-runtime",
"version": "0.12.0",
"version": "1.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@penpot/plugins-runtime",
"version": "0.12.0",
"version": "1.3.2",
"dependencies": {
"@penpot/plugin-types": "^0.12.0",
"@penpot/plugin-types": "^1.3.2",
"ses": "^1.1.0",
"zod": "^3.22.4"
}
@@ -19,9 +19,9 @@
"integrity": "sha512-Uqy94PwLTco90Yfign43muvDtjsYTbL6Ck4W5sSWQUqJiFOL+YP8kiBOoGyQynDsGTY0MCQvMxEPfphaJKnzlQ=="
},
"node_modules/@penpot/plugin-types": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@penpot/plugin-types/-/plugin-types-0.12.0.tgz",
"integrity": "sha512-rtSJ8memBAWxRBEx+S47NFDJN5VIvY0J7QzwTBer45Ls3BjLCaWXCnfFEcRn4SzY+Th01tmop0pwdvN5YDxkBw=="
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@penpot/plugin-types/-/plugin-types-1.3.2.tgz",
"integrity": "sha512-f0kmmZaFNs9sGtSmqmSJQYCs5Qt+KYgTD8RneUjL+Dv+zfNQnd5e4L+iHSYFJ4HWvcDvTiK7F/gya7PwMTu7WA=="
},
"node_modules/ses": {
"version": "1.8.0",

View File

@@ -24,6 +24,10 @@ export function createModal(
inlineStart: window.innerWidth - width - 290,
};
if ((options as any)?.hidden) {
modal.style.setProperty('display', 'none');
}
modal.style.setProperty(
'--modal-block-start',
`${initialPosition.blockStart}px`,

View File

@@ -7,6 +7,7 @@ export async function createPlugin(
context: Context,
manifest: Manifest,
onCloseCallback: () => void,
apiExtensions?: Object,
) {
const evaluateSandbox = async () => {
try {
@@ -30,7 +31,7 @@ export async function createPlugin(
},
);
const sandbox = createSandbox(plugin);
const sandbox = createSandbox(plugin, apiExtensions);
evaluateSandbox();

View File

@@ -2,8 +2,10 @@ import type { Penpot } from '@penpot/plugin-types';
import type { createPluginManager } from './plugin-manager';
import { createApi } from './api';
import { ses } from './ses.js';
export function createSandbox(
plugin: Awaited<ReturnType<typeof createPluginManager>>,
apiExtensions?: Object,
) {
ses.hardenIntrinsics();
@@ -51,7 +53,7 @@ export function createSandbox(
});
};
const publicPluginApi = {
let publicPluginApi = {
penpot: proxyApi,
fetch: ses.harden(safeFetch),
setTimeout: ses.harden(
@@ -123,6 +125,10 @@ export function createSandbox(
structuredClone: ses.harden(window.structuredClone),
};
if (apiExtensions) {
publicPluginApi = Object.assign(publicPluginApi, apiExtensions);
}
const compartment = ses.createCompartment(publicPluginApi);
return {

View File

@@ -19,7 +19,9 @@ export const getPlugins = () => plugins;
const closeAllPlugins = () => {
plugins.forEach((pluginApi) => {
pluginApi.plugin.close();
if (!(pluginApi.manifest as any).allowBackground) {
pluginApi.plugin.close();
}
});
plugins = [];
@@ -38,6 +40,7 @@ window.addEventListener('message', (event) => {
export const loadPlugin = async function (
manifest: Manifest,
closeCallback?: () => void,
apiExtensions?: Object,
) {
try {
const context = contextBuilder && contextBuilder(manifest.pluginId);
@@ -55,6 +58,7 @@ export const loadPlugin = async function (
plugins = plugins.filter((api) => api !== plugin);
closeCallback && closeCallback();
},
apiExtensions,
);
plugins.push(plugin);
@@ -67,8 +71,9 @@ export const loadPlugin = async function (
export const ɵloadPlugin = async function (
manifest: Manifest,
closeCallback?: () => void,
apiExtensions?: Object,
) {
loadPlugin(manifest, closeCallback);
loadPlugin(manifest, closeCallback, apiExtensions);
};
export const ɵloadPluginByUrl = async function (manifestUrl: string) {

View File

@@ -3,4 +3,5 @@ import { z } from 'zod';
export const openUISchema = z.object({
width: z.number().positive(),
height: z.number().positive(),
hidden: z.boolean().optional(),
});

View File

@@ -2,6 +2,7 @@ import { Manifest } from './models/manifest.model.js';
import { manifestSchema } from './models/manifest.schema.js';
export function getValidUrl(host: string, path: string): string {
console.log("getValidUrl", host, path, new URL(path, host).toString());
return new URL(path, host).toString();
}