Compare commits

...

5 Commits

Author SHA1 Message Date
alonso.torres
5f634dcf46 🐛 Fix type annotation for layoutCell property in plugins 2026-02-18 15:55:33 +01:00
alonso.torres
34840941f3 Add textBounds property in plugins 2026-02-18 15:55:33 +01:00
alonso.torres
f903194f8f 🐛 Fix problem with horizontalSizing/verticalSizing in plugins 2026-02-18 13:16:26 +01:00
alonso.torres
bb03d88639 🐛 Send a keep alive message in websocket connection 2026-02-18 13:16:13 +01:00
alonso.torres
8f2eafd060 🐛 Fix permissions for mcp plugin 2026-02-18 13:15:58 +01:00
8 changed files with 76 additions and 7 deletions

View File

@@ -11,6 +11,7 @@
[app.config :as cf]
[app.main.data.plugins :as dp]
[app.main.repo :as rp]
[app.plugins.register :refer [mcp-plugin-id]]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -20,12 +21,13 @@
{:code "plugin.js"
:name "Penpot MCP Plugin"
:version 2
:plugin-id "96dfa740-005d-8020-8007-55ede24a2bae"
:plugin-id mcp-plugin-id
: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"}})
#{"library:read" "library:write"
"comment:read" "comment:write"
"content:write" "content:read"}})
(defn init-mcp!
[]

View File

@@ -261,7 +261,39 @@
:else
(let [child-id (obj/get child "$id")]
(st/emit! (dwt/move-shapes-to-frame #{child-id} id nil nil)
(ptk/data-event :layout/update {:ids [id]})))))))
(ptk/data-event :layout/update {:ids [id]})))))
:horizontalSizing
{:this true
:get #(-> % u/proxy->shape :layout-item-h-sizing (d/nilv :fix) d/name)
:set
(fn [_ value]
(let [value (keyword value)]
(cond
(not (contains? ctl/item-h-sizing-types value))
(u/display-not-valid :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-h-sizing value})))))}
:verticalSizing
{:this true
:get #(-> % u/proxy->shape :layout-item-v-sizing (d/nilv :fix) d/name)
:set
(fn [_ value]
(let [value (keyword value)]
(cond
(not (contains? ctl/item-v-sizing-types value))
(u/display-not-valid :verticalSizing value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-v-sizing value})))))}))
(defn layout-child-proxy? [p]

View File

@@ -598,3 +598,10 @@
(case axis
:y "horizontal"
:x "vertical"))
(defn format-geom-rect
[{:keys [x y width height]}]
#js {:x x
:y y
:width width
:height height})

View File

@@ -17,6 +17,10 @@
[app.util.object :as obj]
[beicon.v2.core :as rx]))
;; Needs to be here because moving it to `app.main.data.workspace.mcp` will
;; cause a circular dependency
(def mcp-plugin-id "96dfa740-005d-8020-8007-55ede24a2bae")
;; Stores the installed plugins information
(defonce ^:private registry (atom {}))
@@ -127,5 +131,6 @@
(defn check-permission
[plugin-id permission]
(or (= plugin-id "00000000-0000-0000-0000-000000000000")
(= plugin-id mcp-plugin-id)
(let [{:keys [permissions]} (dm/get-in @registry [:data plugin-id])]
(contains? permissions permission))))

View File

@@ -8,6 +8,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes.text :as gst]
[app.common.record :as crc]
[app.common.schema :as sm]
[app.common.types.shape :as cts]
@@ -651,4 +652,7 @@
(u/display-not-valid :verticalAlign "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id {:vertical-align value})))))}))
(st/emit! (dwt/update-attrs id {:vertical-align value})))))}
{:name "textBounds"
:get #(-> % u/proxy->shape gst/shape->bounds format/format-geom-rect)}))

View File

@@ -1,5 +1,7 @@
import "./style.css";
const KEEP_ALIVE_TIME = 30000; // 30 seconds
// get the current theme from the URL
const searchParams = new URLSearchParams(window.location.search);
document.body.dataset.theme = searchParams.get("theme") ?? "light";
@@ -72,8 +74,12 @@ function connectToMcpServer(baseUrl?: string, token?: string): void {
};
ws.onmessage = (event) => {
console.log("Received from MCP server:", event.data);
try {
if (event.data === "keep-alive") {
// Keep alive response, ignore it
return;
}
console.log("Received from MCP server:", event.data);
const request = JSON.parse(event.data);
// Forward the task request to the plugin for execution
parent.postMessage(request, "*");
@@ -82,8 +88,11 @@ function connectToMcpServer(baseUrl?: string, token?: string): void {
}
};
const interval = setInterval(() => ws?.send("keep-alive"), KEEP_ALIVE_TIME);
ws.onclose = (event: CloseEvent) => {
console.log("Disconnected from MCP server");
clearInterval(interval);
const message = event.reason || undefined;
updateConnectionStatus("disconnected", "Disconnected", false, message);
ws = null;

View File

@@ -72,6 +72,10 @@ export class PluginBridge {
ws.on("message", (data: Buffer) => {
this.logger.debug("Received WebSocket message: %s", data.toString());
try {
if (data.toString() === "keep-alive") {
ws.send("keep-alive");
return;
}
const response: PluginTaskResponse<any> = JSON.parse(data.toString());
this.handlePluginTaskResponse(response);
} catch (error) {

View File

@@ -3723,7 +3723,7 @@ export interface ShapeBase extends PluginData {
/**
* Layout properties for cells in a grid layout.
*/
readonly layoutCell?: LayoutChildProperties;
readonly layoutCell?: LayoutCellProperties;
/**
* Changes the index inside the parent of the current shape.
@@ -4127,6 +4127,12 @@ export interface Text extends ShapeBase {
*/
verticalAlign: 'top' | 'center' | 'bottom' | null;
/**
* Return the bounding box for the text as a (x, y, width, height) rectangle
* This is the box that covers the text even if it overflows its selection rectangle.
*/
readonly textBounds: { x: number; y: number; width: number; height: number };
/**
* Gets a text range within the text shape.
* @param start - The start index of the text range.