mirror of
https://github.com/penpot/penpot.git
synced 2026-08-06 04:48:39 +00:00
🎉 Add MCP plugin embedded execution (#8368)
* ✨ Add core changes for mcp server * ✨ Changes to plugins-runtime to add mcp extensions * ✨ Changes to MCP plugin * ✨ Changes post-review and ci fixes
This commit is contained in:
parent
b87d7e3de0
commit
c32a336c50
@ -87,3 +87,20 @@
|
|||||||
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
||||||
:columns [:id :name :perms :type :created-at :updated-at :expires-at]})
|
:columns [:id :name :perms :type :created-at :updated-at :expires-at]})
|
||||||
(mapv decode-row)))
|
(mapv decode-row)))
|
||||||
|
|
||||||
|
|
||||||
|
(def ^:private schema:get-current-mcp-token
|
||||||
|
[:map {:title "get-current-mcp-token"}])
|
||||||
|
|
||||||
|
(sv/defmethod ::get-current-mcp-token
|
||||||
|
{::doc/added "2.15"
|
||||||
|
::sm/params schema:get-current-mcp-token}
|
||||||
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id ::rpc/request-at]}]
|
||||||
|
(->> (db/query pool :access-token
|
||||||
|
{:profile-id profile-id
|
||||||
|
:type "mcp"}
|
||||||
|
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
||||||
|
:columns [:token :expires-at]})
|
||||||
|
(remove #(ct/is-after? (:expires-at %) request-at))
|
||||||
|
(map decode-row)
|
||||||
|
(first)))
|
||||||
|
|||||||
@ -107,4 +107,18 @@
|
|||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(let [results (:result out)]
|
(let [results (:result out)]
|
||||||
(t/is (= 2 (count results))))))))
|
(t/is (= 2 (count results))))))
|
||||||
|
|
||||||
|
(t/testing "get mcp token"
|
||||||
|
(let [_ (th/command! {::th/type :create-access-token
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:type "mcp"
|
||||||
|
:name "token 1"
|
||||||
|
:perms ["get-profile"]})
|
||||||
|
{:keys [error result]}
|
||||||
|
(th/command! {::th/type :get-current-mcp-token
|
||||||
|
::rpc/profile-id (:id prof)})]
|
||||||
|
;; (th/print-result! result)
|
||||||
|
(t/is (nil? error))
|
||||||
|
(t/is (string? (:token result)))))))
|
||||||
|
|
||||||
|
|||||||
@ -42,12 +42,13 @@
|
|||||||
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
||||||
"watch": "exit 0",
|
"watch": "exit 0",
|
||||||
"watch:app": "pnpm run clear:shadow-cache && pnpm run build:wasm && concurrently --kill-others-on-fail \"pnpm run watch:app:assets\" \"pnpm run watch:app:main\" \"pnpm run watch:app:libs\"",
|
"watch:app": "pnpm run clear:shadow-cache && pnpm run build:wasm && concurrently --kill-others-on-fail \"pnpm run watch:app:assets\" \"pnpm run watch:app:main\" \"pnpm run watch:app:libs\"",
|
||||||
"watch:storybook": "pnpm run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\""
|
"watch:storybook": "pnpm run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\"",
|
||||||
|
"postinstall": "(cd ../plugins/libs/plugins-runtime; pnpm install; pnpm run build)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@penpot/draft-js": "workspace:./packages/draft-js",
|
"@penpot/draft-js": "workspace:./packages/draft-js",
|
||||||
"@penpot/mousetrap": "workspace:./packages/mousetrap",
|
"@penpot/mousetrap": "workspace:./packages/mousetrap",
|
||||||
"@penpot/plugins-runtime": "1.4.2",
|
"@penpot/plugins-runtime": "link:../plugins/dist/plugins-runtime",
|
||||||
"@penpot/svgo": "penpot/svgo#v3.2",
|
"@penpot/svgo": "penpot/svgo#v3.2",
|
||||||
"@penpot/text-editor": "workspace:./text-editor",
|
"@penpot/text-editor": "workspace:./text-editor",
|
||||||
"@penpot/tokenscript": "workspace:./packages/tokenscript",
|
"@penpot/tokenscript": "workspace:./packages/tokenscript",
|
||||||
|
|||||||
47
frontend/pnpm-lock.yaml
generated
47
frontend/pnpm-lock.yaml
generated
@ -20,8 +20,8 @@ importers:
|
|||||||
specifier: workspace:./packages/mousetrap
|
specifier: workspace:./packages/mousetrap
|
||||||
version: link:packages/mousetrap
|
version: link:packages/mousetrap
|
||||||
'@penpot/plugins-runtime':
|
'@penpot/plugins-runtime':
|
||||||
specifier: 1.4.2
|
specifier: link:../plugins/dist/plugins-runtime
|
||||||
version: 1.4.2
|
version: link:../plugins/dist/plugins-runtime
|
||||||
'@penpot/svgo':
|
'@penpot/svgo':
|
||||||
specifier: penpot/svgo#v3.2
|
specifier: penpot/svgo#v3.2
|
||||||
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b
|
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b
|
||||||
@ -581,15 +581,6 @@ packages:
|
|||||||
'@dabh/diagnostics@2.0.8':
|
'@dabh/diagnostics@2.0.8':
|
||||||
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
|
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':
|
'@esbuild/aix-ppc64@0.21.5':
|
||||||
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -1258,12 +1249,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
|
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
|
||||||
engines: {node: '>= 10.0.0'}
|
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':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@ -4636,9 +4621,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
|
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
ses@1.14.0:
|
|
||||||
resolution: {integrity: sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==}
|
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -5499,9 +5481,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.0 || ^4.0.0
|
zod: ^3.25.0 || ^4.0.0
|
||||||
|
|
||||||
zod@3.25.76:
|
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
|
||||||
|
|
||||||
zod@4.3.6:
|
zod@4.3.6:
|
||||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||||
|
|
||||||
@ -5775,12 +5754,6 @@ snapshots:
|
|||||||
enabled: 2.0.0
|
enabled: 2.0.0
|
||||||
kuler: 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':
|
'@esbuild/aix-ppc64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -6297,14 +6270,6 @@ snapshots:
|
|||||||
'@parcel/watcher-win32-x64': 2.5.6
|
'@parcel/watcher-win32-x64': 2.5.6
|
||||||
optional: true
|
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':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -10000,12 +9965,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
set-function-length@1.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
define-data-property: 1.1.4
|
define-data-property: 1.1.4
|
||||||
@ -10974,6 +10933,4 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
zod: 4.3.6
|
zod: 4.3.6
|
||||||
|
|
||||||
zod@3.25.76: {}
|
|
||||||
|
|
||||||
zod@4.3.6: {}
|
zod@4.3.6: {}
|
||||||
|
|||||||
@ -119,6 +119,10 @@
|
|||||||
(normalize-uri (or (obj/get global "penpotPublicURI")
|
(normalize-uri (or (obj/get global "penpotPublicURI")
|
||||||
(obj/get location "origin"))))
|
(obj/get location "origin"))))
|
||||||
|
|
||||||
|
(def mcp-ws-uri
|
||||||
|
(or (some-> (obj/get global "penpotMcpServerURI") u/uri)
|
||||||
|
(u/join public-uri "mcp/ws")))
|
||||||
|
|
||||||
(def rasterizer-uri
|
(def rasterizer-uri
|
||||||
(or (some-> (obj/get global "penpotRasterizerURI") normalize-uri)
|
(or (some-> (obj/get global "penpotRasterizerURI") normalize-uri)
|
||||||
public-uri))
|
public-uri))
|
||||||
|
|||||||
@ -65,8 +65,23 @@
|
|||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
|
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
|
||||||
|
|
||||||
|
(defn start-plugin!
|
||||||
|
[{:keys [plugin-id name version description host code permissions allow-background]} ^js extensions]
|
||||||
|
(.ɵloadPlugin
|
||||||
|
^js ug/global
|
||||||
|
#js {:pluginId plugin-id
|
||||||
|
:name name
|
||||||
|
:version version
|
||||||
|
:description description
|
||||||
|
:host host
|
||||||
|
:code code
|
||||||
|
:allowBackground (boolean allow-background)
|
||||||
|
:permissions (apply array permissions)}
|
||||||
|
nil
|
||||||
|
extensions))
|
||||||
|
|
||||||
(defn- load-plugin!
|
(defn- load-plugin!
|
||||||
[{:keys [plugin-id name description host code icon permissions]}]
|
[{:keys [plugin-id name description host code icon permissions] :as params}]
|
||||||
(try
|
(try
|
||||||
(st/emit! (save-current-plugin plugin-id)
|
(st/emit! (save-current-plugin plugin-id)
|
||||||
(reset-plugin-flags plugin-id))
|
(reset-plugin-flags plugin-id))
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
[app.main.data.workspace.layers :as dwly]
|
[app.main.data.workspace.layers :as dwly]
|
||||||
[app.main.data.workspace.layout :as layout]
|
[app.main.data.workspace.layout :as layout]
|
||||||
[app.main.data.workspace.libraries :as dwl]
|
[app.main.data.workspace.libraries :as dwl]
|
||||||
|
[app.main.data.workspace.mcp :as mcp]
|
||||||
[app.main.data.workspace.notifications :as dwn]
|
[app.main.data.workspace.notifications :as dwn]
|
||||||
[app.main.data.workspace.pages :as dwpg]
|
[app.main.data.workspace.pages :as dwpg]
|
||||||
[app.main.data.workspace.path :as dwdp]
|
[app.main.data.workspace.path :as dwdp]
|
||||||
@ -212,7 +213,8 @@
|
|||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ _ _]
|
||||||
(rx/of (dp/check-open-plugin)
|
(rx/of (dp/check-open-plugin)
|
||||||
(fdf/fix-deleted-fonts-for-local-library file-id)))))
|
(fdf/fix-deleted-fonts-for-local-library file-id)
|
||||||
|
(mcp/init-mcp-connexion)))))
|
||||||
|
|
||||||
(defn- bundle-fetched
|
(defn- bundle-fetched
|
||||||
[{:keys [file file-id thumbnails] :as bundle}]
|
[{:keys [file file-id thumbnails] :as bundle}]
|
||||||
|
|||||||
58
frontend/src/app/main/data/workspace/mcp.cljs
Normal file
58
frontend/src/app/main/data/workspace/mcp.cljs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.main.data.workspace.mcp
|
||||||
|
(:require
|
||||||
|
[app.common.logging :as log]
|
||||||
|
[app.common.uri :as u]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.main.data.plugins :as dp]
|
||||||
|
[app.main.repo :as rp]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
|
(log/set-level! :info)
|
||||||
|
|
||||||
|
(def ^:private default-manifest
|
||||||
|
{:code "plugin.js"
|
||||||
|
:name "Penpot MCP Plugin"
|
||||||
|
:version 2
|
||||||
|
: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"}})
|
||||||
|
|
||||||
|
(defn init-mcp!
|
||||||
|
[]
|
||||||
|
(->> (rp/cmd! :get-current-mcp-token)
|
||||||
|
(rx/subs!
|
||||||
|
(fn [{:keys [token]}]
|
||||||
|
(when token
|
||||||
|
(dp/start-plugin!
|
||||||
|
(assoc default-manifest
|
||||||
|
:url (str (u/join cf/public-uri "plugins/mcp/manifest.json"))
|
||||||
|
:host (str (u/join cf/public-uri "plugins/mcp/")))
|
||||||
|
|
||||||
|
;; API extension for MCP server
|
||||||
|
#js {:mcp
|
||||||
|
#js
|
||||||
|
{:getToken (constantly token)
|
||||||
|
:getServerUrl #(str cf/mcp-ws-uri)
|
||||||
|
:setMcpStatus
|
||||||
|
(fn [status]
|
||||||
|
;; TODO: Visual feedback
|
||||||
|
(log/info :hint "MCP STATUS" :status status))}}))))))
|
||||||
|
|
||||||
|
(defn init-mcp-connexion
|
||||||
|
[]
|
||||||
|
(ptk/reify ::init-mcp-connexion
|
||||||
|
ptk/EffectEvent
|
||||||
|
(effect [_ state _]
|
||||||
|
(when (and (contains? cf/flags :mcp)
|
||||||
|
(-> state :profile :props :mcp-status))
|
||||||
|
(init-mcp!)))))
|
||||||
11
mcp/packages/plugin/src/index.d.ts
vendored
Normal file
11
mcp/packages/plugin/src/index.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
interface McpOptions {
|
||||||
|
getToken(): string;
|
||||||
|
getServerUrl(): string;
|
||||||
|
setMcpStatus(status: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const mcp: undefined | McpOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@ -19,12 +19,19 @@ const statusElement = document.getElementById("connection-status");
|
|||||||
* @param isConnectedState - whether the connection is in a connected state (affects color)
|
* @param isConnectedState - whether the connection is in a connected state (affects color)
|
||||||
* @param message - optional additional message to append to the status
|
* @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) {
|
if (statusElement) {
|
||||||
const displayText = message ? `${status}: ${message}` : status;
|
const displayText = message ? `${status}: ${message}` : status;
|
||||||
statusElement.textContent = displayText;
|
statusElement.textContent = displayText;
|
||||||
statusElement.style.color = isConnectedState ? "var(--accent-primary)" : "var(--error-700)";
|
statusElement.style.color = isConnectedState ? "var(--accent-primary)" : "var(--error-700)";
|
||||||
}
|
}
|
||||||
|
parent.postMessage(
|
||||||
|
{
|
||||||
|
type: "update-connection-status",
|
||||||
|
status: code,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,25 +51,24 @@ function sendTaskResponse(response: any): void {
|
|||||||
/**
|
/**
|
||||||
* Establishes a WebSocket connection to the MCP server.
|
* Establishes a WebSocket connection to the MCP server.
|
||||||
*/
|
*/
|
||||||
function connectToMcpServer(): void {
|
function connectToMcpServer(baseUrl?: string, token?: string): void {
|
||||||
if (ws?.readyState === WebSocket.OPEN) {
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
updateConnectionStatus("Already connected", true);
|
updateConnectionStatus("connected", "Already connected", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let wsUrl = PENPOT_MCP_WEBSOCKET_URL;
|
let wsUrl = baseUrl || PENPOT_MCP_WEBSOCKET_URL;
|
||||||
if (isMultiUserMode) {
|
if (isMultiUserMode && token) {
|
||||||
// TODO obtain proper userToken from penpot
|
wsUrl += `?userToken=${encodeURIComponent(token)}`;
|
||||||
const userToken = "dummyToken";
|
|
||||||
wsUrl += `?userToken=${encodeURIComponent(userToken)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ws = new WebSocket(wsUrl);
|
ws = new WebSocket(wsUrl);
|
||||||
updateConnectionStatus("Connecting...", false);
|
updateConnectionStatus("connecting", "Connecting...", false);
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log("Connected to MCP server");
|
console.log("Connected to MCP server");
|
||||||
updateConnectionStatus("Connected to MCP server", true);
|
updateConnectionStatus("connected", "Connected to MCP server", true);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
@ -79,19 +85,19 @@ function connectToMcpServer(): void {
|
|||||||
ws.onclose = (event: CloseEvent) => {
|
ws.onclose = (event: CloseEvent) => {
|
||||||
console.log("Disconnected from MCP server");
|
console.log("Disconnected from MCP server");
|
||||||
const message = event.reason || undefined;
|
const message = event.reason || undefined;
|
||||||
updateConnectionStatus("Disconnected", false, message);
|
updateConnectionStatus("disconnected", "Disconnected", false, message);
|
||||||
ws = null;
|
ws = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error("WebSocket error:", error);
|
console.error("WebSocket error:", error);
|
||||||
// note: WebSocket error events typically don't contain detailed error messages
|
// note: WebSocket error events typically don't contain detailed error messages
|
||||||
updateConnectionStatus("Connection error", false);
|
updateConnectionStatus("error", "Connection error", false);
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to connect to MCP server:", error);
|
console.error("Failed to connect to MCP server:", error);
|
||||||
const message = error instanceof Error ? error.message : undefined;
|
const message = error instanceof Error ? error.message : undefined;
|
||||||
updateConnectionStatus("Connection failed", false, message);
|
updateConnectionStatus("error", "Connection failed", false, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,10 +107,14 @@ document.querySelector("[data-handler='connect-mcp']")?.addEventListener("click"
|
|||||||
|
|
||||||
// Listen plugin.ts messages
|
// Listen plugin.ts messages
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
if (event.data.source === "penpot") {
|
if (event.data.type === "start-server") {
|
||||||
|
connectToMcpServer(event.data.url, event.data.token);
|
||||||
|
} else if (event.data.source === "penpot") {
|
||||||
document.body.dataset.theme = event.data.theme;
|
document.body.dataset.theme = event.data.theme;
|
||||||
} else if (event.data.type === "task-response") {
|
} else if (event.data.type === "task-response") {
|
||||||
// Forward task response back to MCP server
|
// Forward task response back to MCP server
|
||||||
sendTaskResponse(event.data.response);
|
sendTaskResponse(event.data.response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
parent.postMessage({ type: "ui-initialized" }, "*");
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler";
|
import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler";
|
||||||
import { Task, TaskHandler } from "./TaskHandler";
|
import { Task, TaskHandler } from "./TaskHandler";
|
||||||
|
|
||||||
|
mcp?.setMcpStatus("connecting");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry of all available task handlers.
|
* Registry of all available task handlers.
|
||||||
*/
|
*/
|
||||||
@ -11,12 +13,24 @@ declare const IS_MULTI_USER_MODE: boolean;
|
|||||||
const isMultiUserMode = typeof IS_MULTI_USER_MODE !== "undefined" ? IS_MULTI_USER_MODE : false;
|
const isMultiUserMode = typeof IS_MULTI_USER_MODE !== "undefined" ? IS_MULTI_USER_MODE : false;
|
||||||
|
|
||||||
// Open the plugin UI (main.ts)
|
// Open the plugin UI (main.ts)
|
||||||
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}&multiUser=${isMultiUserMode}`, { width: 158, height: 200 });
|
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}&multiUser=${isMultiUserMode}`, {
|
||||||
|
width: 158,
|
||||||
|
height: 200,
|
||||||
|
hidden: !!mcp,
|
||||||
|
} as any);
|
||||||
|
|
||||||
// Handle messages
|
// 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
|
// Handle plugin task requests
|
||||||
if (typeof message === "object" && message.task && message.id) {
|
if (mcp && typeof message === "object" && message.type === "ui-initialized") {
|
||||||
|
penpot.ui.sendMessage({
|
||||||
|
type: "start-server",
|
||||||
|
url: mcp?.getServerUrl(),
|
||||||
|
token: mcp?.getToken(),
|
||||||
|
});
|
||||||
|
} else if (typeof message === "object" && message.type === "update-connection-status") {
|
||||||
|
mcp?.setMcpStatus(message.status || "unknown");
|
||||||
|
} else if (typeof message === "object" && message.task && message.id) {
|
||||||
handlePluginTaskRequest(message).catch((error) => {
|
handlePluginTaskRequest(message).catch((error) => {
|
||||||
console.error("Error in handlePluginTaskRequest:", error);
|
console.error("Error in handlePluginTaskRequest:", error);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import baseConfig from "./vite.config";
|
|||||||
export default mergeConfig(
|
export default mergeConfig(
|
||||||
baseConfig,
|
baseConfig,
|
||||||
defineConfig({
|
defineConfig({
|
||||||
base: "./",
|
|
||||||
plugins: [],
|
plugins: [],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
2
plugins/libs/plugin-types/index.d.ts
vendored
2
plugins/libs/plugin-types/index.d.ts
vendored
@ -23,7 +23,7 @@ export interface Penpot extends Omit<
|
|||||||
open: (
|
open: (
|
||||||
name: string,
|
name: string,
|
||||||
url: string,
|
url: string,
|
||||||
options?: { width: number; height: number },
|
options?: { width: number; height: number; hidden: boolean },
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
size: {
|
size: {
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
"ses": "^1.1.0",
|
"ses": "^1.1.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"module": "./index.mjs",
|
"module": "./dist/index.js",
|
||||||
"typings": "./index.d.ts",
|
"typings": "./dist/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|||||||
@ -24,6 +24,10 @@ export function createModal(
|
|||||||
inlineStart: window.innerWidth - width - 290,
|
inlineStart: window.innerWidth - width - 290,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (options?.hidden) {
|
||||||
|
modal.style.setProperty('display', 'none');
|
||||||
|
}
|
||||||
|
|
||||||
modal.style.setProperty(
|
modal.style.setProperty(
|
||||||
'--modal-block-start',
|
'--modal-block-start',
|
||||||
`${initialPosition.blockStart}px`,
|
`${initialPosition.blockStart}px`,
|
||||||
|
|||||||
@ -83,7 +83,7 @@ describe('createPlugin', () => {
|
|||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
);
|
);
|
||||||
expect(createSandbox).toHaveBeenCalledWith(mockPluginManager);
|
expect(createSandbox).toHaveBeenCalledWith(mockPluginManager, undefined);
|
||||||
expect(mockSandbox.evaluate).toHaveBeenCalled();
|
expect(mockSandbox.evaluate).toHaveBeenCalled();
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
plugin: mockPluginManager,
|
plugin: mockPluginManager,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export async function createPlugin(
|
|||||||
context: Context,
|
context: Context,
|
||||||
manifest: Manifest,
|
manifest: Manifest,
|
||||||
onCloseCallback: () => void,
|
onCloseCallback: () => void,
|
||||||
|
apiExtensions?: object,
|
||||||
) {
|
) {
|
||||||
const evaluateSandbox = async () => {
|
const evaluateSandbox = async () => {
|
||||||
try {
|
try {
|
||||||
@ -30,7 +31,7 @@ export async function createPlugin(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const sandbox = createSandbox(plugin);
|
const sandbox = createSandbox(plugin, apiExtensions);
|
||||||
|
|
||||||
evaluateSandbox();
|
evaluateSandbox();
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,10 @@ import type { Penpot } from '@penpot/plugin-types';
|
|||||||
import type { createPluginManager } from './plugin-manager';
|
import type { createPluginManager } from './plugin-manager';
|
||||||
import { createApi } from './api';
|
import { createApi } from './api';
|
||||||
import { ses } from './ses.js';
|
import { ses } from './ses.js';
|
||||||
|
|
||||||
export function createSandbox(
|
export function createSandbox(
|
||||||
plugin: Awaited<ReturnType<typeof createPluginManager>>,
|
plugin: Awaited<ReturnType<typeof createPluginManager>>,
|
||||||
|
apiExtensions?: object,
|
||||||
) {
|
) {
|
||||||
ses.hardenIntrinsics();
|
ses.hardenIntrinsics();
|
||||||
|
|
||||||
@ -51,7 +53,7 @@ export function createSandbox(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const publicPluginApi = {
|
let publicPluginApi = {
|
||||||
penpot: proxyApi,
|
penpot: proxyApi,
|
||||||
fetch: ses.harden(safeFetch),
|
fetch: ses.harden(safeFetch),
|
||||||
setTimeout: ses.harden(
|
setTimeout: ses.harden(
|
||||||
@ -123,6 +125,10 @@ export function createSandbox(
|
|||||||
structuredClone: ses.harden(window.structuredClone),
|
structuredClone: ses.harden(window.structuredClone),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (apiExtensions) {
|
||||||
|
publicPluginApi = Object.assign(publicPluginApi, apiExtensions);
|
||||||
|
}
|
||||||
|
|
||||||
const compartment = ses.createCompartment(publicPluginApi);
|
const compartment = ses.createCompartment(publicPluginApi);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -78,6 +78,7 @@ describe('plugin-loader', () => {
|
|||||||
mockContext,
|
mockContext,
|
||||||
manifest,
|
manifest,
|
||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(mockPluginApi.plugin.close).not.toHaveBeenCalled();
|
expect(mockPluginApi.plugin.close).not.toHaveBeenCalled();
|
||||||
expect(getPlugins()).toHaveLength(1);
|
expect(getPlugins()).toHaveLength(1);
|
||||||
@ -130,6 +131,7 @@ describe('plugin-loader', () => {
|
|||||||
mockContext,
|
mockContext,
|
||||||
manifest,
|
manifest,
|
||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -144,6 +146,7 @@ describe('plugin-loader', () => {
|
|||||||
mockContext,
|
mockContext,
|
||||||
manifest,
|
manifest,
|
||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,7 +19,10 @@ export const getPlugins = () => plugins;
|
|||||||
|
|
||||||
const closeAllPlugins = () => {
|
const closeAllPlugins = () => {
|
||||||
plugins.forEach((pluginApi) => {
|
plugins.forEach((pluginApi) => {
|
||||||
pluginApi.plugin.close();
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
if (!(pluginApi.manifest as any)?.allowBackground) {
|
||||||
|
pluginApi.plugin.close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
plugins = [];
|
plugins = [];
|
||||||
@ -38,6 +41,7 @@ window.addEventListener('message', (event) => {
|
|||||||
export const loadPlugin = async function (
|
export const loadPlugin = async function (
|
||||||
manifest: Manifest,
|
manifest: Manifest,
|
||||||
closeCallback?: () => void,
|
closeCallback?: () => void,
|
||||||
|
apiExtensions?: object,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const context = contextBuilder && contextBuilder(manifest.pluginId);
|
const context = contextBuilder && contextBuilder(manifest.pluginId);
|
||||||
@ -58,6 +62,7 @@ export const loadPlugin = async function (
|
|||||||
closeCallback();
|
closeCallback();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
apiExtensions,
|
||||||
);
|
);
|
||||||
|
|
||||||
plugins.push(plugin);
|
plugins.push(plugin);
|
||||||
@ -70,8 +75,9 @@ export const loadPlugin = async function (
|
|||||||
export const ɵloadPlugin = async function (
|
export const ɵloadPlugin = async function (
|
||||||
manifest: Manifest,
|
manifest: Manifest,
|
||||||
closeCallback?: () => void,
|
closeCallback?: () => void,
|
||||||
|
apiExtensions?: object,
|
||||||
) {
|
) {
|
||||||
loadPlugin(manifest, closeCallback);
|
loadPlugin(manifest, closeCallback, apiExtensions);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ɵloadPluginByUrl = async function (manifestUrl: string) {
|
export const ɵloadPluginByUrl = async function (manifestUrl: string) {
|
||||||
|
|||||||
@ -3,4 +3,5 @@ import { z } from 'zod';
|
|||||||
export const openUISchema = z.object({
|
export const openUISchema = z.object({
|
||||||
width: z.number().positive(),
|
width: z.number().positive(),
|
||||||
height: z.number().positive(),
|
height: z.number().positive(),
|
||||||
|
hidden: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
873
plugins/pnpm-lock.yaml
generated
873
plugins/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user