mcp-server-penpot 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/prompts.yml +48 -3
- package/dist/index.js +127 -2
- package/dist/plugin/plugin.js +1 -1
- package/package.json +1 -1
package/dist/data/prompts.yml
CHANGED
|
@@ -210,9 +210,54 @@ initial_instructions: |
|
|
|
210
210
|
});
|
|
211
211
|
Always validate against the root container that is supposed to contain the shapes.
|
|
212
212
|
|
|
213
|
-
# Visual Inspection of Designs
|
|
214
|
-
|
|
215
|
-
For many tasks, it
|
|
213
|
+
# Visual Inspection of Designs (Screenshots)
|
|
214
|
+
|
|
215
|
+
For many tasks, it is CRITICAL to visually inspect the design. The `export_shape` tool is your primary tool for
|
|
216
|
+
taking screenshots and visually verifying designs. ALWAYS use it after making design changes to confirm correctness.
|
|
217
|
+
|
|
218
|
+
## When to Take Screenshots
|
|
219
|
+
|
|
220
|
+
* After creating or modifying shapes, boards, or components -- verify the result looks correct
|
|
221
|
+
* When analyzing an existing design -- screenshot key elements to understand their appearance
|
|
222
|
+
* When debugging layout issues -- screenshot the parent board to see actual rendered layout
|
|
223
|
+
* Before and after making changes -- to compare and verify the change had the desired effect
|
|
224
|
+
* When the user asks you to review, check, or verify any visual aspect of the design
|
|
225
|
+
|
|
226
|
+
## Screenshot Workflows
|
|
227
|
+
|
|
228
|
+
**Screenshot the current selection:**
|
|
229
|
+
Use `export_shape` with shapeId="selection" to screenshot whatever the user has selected.
|
|
230
|
+
|
|
231
|
+
**Screenshot a board by name:**
|
|
232
|
+
First find the board, then screenshot it:
|
|
233
|
+
```
|
|
234
|
+
// Step 1: Find the board using execute_code
|
|
235
|
+
const board = penpotUtils.findShape(s => s.type === 'board' && s.name === 'Login Screen');
|
|
236
|
+
return { id: board.id, name: board.name };
|
|
237
|
+
```
|
|
238
|
+
Then call `export_shape` with the returned shape ID.
|
|
239
|
+
|
|
240
|
+
**Screenshot after creating a component:**
|
|
241
|
+
After using `execute_code` to create shapes, always capture the parent board or the created shape
|
|
242
|
+
to verify it rendered correctly:
|
|
243
|
+
```
|
|
244
|
+
// After creating shapes, return the parent board ID for visual verification
|
|
245
|
+
return { boardId: parentBoard.id };
|
|
246
|
+
```
|
|
247
|
+
Then call `export_shape` with that board ID.
|
|
248
|
+
|
|
249
|
+
**High-resolution screenshot for detailed inspection:**
|
|
250
|
+
Use `export_shape` with scale=2 for higher resolution when checking fine details like text,
|
|
251
|
+
alignment, or small UI elements.
|
|
252
|
+
|
|
253
|
+
## Visual Verification Checklist
|
|
254
|
+
|
|
255
|
+
When visually inspecting a screenshot, check for:
|
|
256
|
+
* Layout alignment -- are elements properly aligned and spaced?
|
|
257
|
+
* Text readability -- are fonts, sizes, and colors correct?
|
|
258
|
+
* Overflow/clipping -- are any elements cut off or overflowing their containers?
|
|
259
|
+
* Color correctness -- do fills, strokes, and text colors match the intended design?
|
|
260
|
+
* Completeness -- are all required elements present and visible?
|
|
216
261
|
|
|
217
262
|
# Revising Designs
|
|
218
263
|
|
package/dist/index.js
CHANGED
|
@@ -14519,6 +14519,9 @@ var ExportShapeArgs = class {
|
|
|
14519
14519
|
mode: external_exports.enum(["shape", "fill"]).default("shape").describe(
|
|
14520
14520
|
"The export mode: either 'shape' (full shape as it appears in the design, including descendants; the default) or 'fill' (export the raw image that is used as a fill for the shape; PNG format only)"
|
|
14521
14521
|
),
|
|
14522
|
+
scale: external_exports.number().min(0.01).max(10).default(1).optional().describe(
|
|
14523
|
+
"Scale factor for the export. Use values > 1 (e.g. 2) for higher resolution screenshots. Only applies to bitmap formats (png). Default: 1."
|
|
14524
|
+
),
|
|
14522
14525
|
filePath: external_exports.string().optional().describe(
|
|
14523
14526
|
"Optional file path to save the exported image to. If not provided, the image data is returned directly for you to see."
|
|
14524
14527
|
)
|
|
@@ -14526,6 +14529,7 @@ var ExportShapeArgs = class {
|
|
|
14526
14529
|
shapeId;
|
|
14527
14530
|
format = "png";
|
|
14528
14531
|
mode = "shape";
|
|
14532
|
+
scale;
|
|
14529
14533
|
filePath;
|
|
14530
14534
|
};
|
|
14531
14535
|
var ExportShapeTool = class extends Tool {
|
|
@@ -14550,6 +14554,7 @@ var ExportShapeTool = class extends Tool {
|
|
|
14550
14554
|
if (this.mcpServer.isFileSystemAccessEnabled()) {
|
|
14551
14555
|
description += "\nAlternatively, you can save it to a file.";
|
|
14552
14556
|
}
|
|
14557
|
+
description += "\n\n**This is the primary tool for taking screenshots and visually inspecting designs.** Use it to:\n- Take a screenshot of a shape, board, or component to verify its appearance\n- Visually inspect a design after making changes to confirm correctness\n- Check layout, alignment, colors, and typography by looking at the actual rendered output\n\nTo screenshot a specific element, find its shape ID first (via `execute_code` with `penpotUtils.shapeStructure` or `penpotUtils.findShape`), then call this tool with that ID. Use 'selection' as the shapeId to screenshot whatever the user has selected.\n\nFor high-resolution screenshots, use the `scale` parameter (e.g. scale=2 for 2x resolution).";
|
|
14553
14558
|
return description;
|
|
14554
14559
|
}
|
|
14555
14560
|
async executeCore(args) {
|
|
@@ -14563,7 +14568,8 @@ var ExportShapeTool = class extends Tool {
|
|
|
14563
14568
|
shapeCode = `penpotUtils.findShapeById("${args.shapeId}")`;
|
|
14564
14569
|
}
|
|
14565
14570
|
const asSvg = args.format === "svg";
|
|
14566
|
-
const
|
|
14571
|
+
const scale = args.scale ?? 1;
|
|
14572
|
+
const code = `return penpotUtils.exportImage(${shapeCode}, "${args.mode}", ${asSvg}, ${scale});`;
|
|
14567
14573
|
const task = new ExecuteCodePluginTask({ code });
|
|
14568
14574
|
const result = await this.mcpServer.pluginBridge.executePluginTask(task);
|
|
14569
14575
|
const imageData = result.data.result;
|
|
@@ -14681,6 +14687,102 @@ var ImportImageTool = class _ImportImageTool extends Tool {
|
|
|
14681
14687
|
}
|
|
14682
14688
|
};
|
|
14683
14689
|
|
|
14690
|
+
// src/prompts/VerifyDesignPrompt.ts
|
|
14691
|
+
var VERIFY_DESIGN_PROMPT = {
|
|
14692
|
+
name: "verify-design",
|
|
14693
|
+
config: {
|
|
14694
|
+
title: "Verify Design",
|
|
14695
|
+
description: "Takes a screenshot of a design element and performs a visual review. Use after making design changes to verify correctness, or to check the current state of a shape, board, or component.",
|
|
14696
|
+
argsSchema: {
|
|
14697
|
+
shapeName: external_exports.string().optional().describe(
|
|
14698
|
+
"Name of the shape or board to verify. If omitted, verifies the user's current selection."
|
|
14699
|
+
)
|
|
14700
|
+
}
|
|
14701
|
+
},
|
|
14702
|
+
callback: async (args) => {
|
|
14703
|
+
const target = args.shapeName ? `the shape named "${args.shapeName}"` : "the user's current selection";
|
|
14704
|
+
const findStep = args.shapeName ? `1. **Find the target shape** using \`execute_code\`:
|
|
14705
|
+
\`\`\`javascript
|
|
14706
|
+
const shape = penpotUtils.findShape(s => s.name === '${args.shapeName}');
|
|
14707
|
+
if (!shape) throw new Error('Shape "${args.shapeName}" not found');
|
|
14708
|
+
return { id: shape.id, name: shape.name, type: shape.type };
|
|
14709
|
+
\`\`\`
|
|
14710
|
+
` : `1. **The target is the current selection.** Use shapeId="selection" in the next step.
|
|
14711
|
+
`;
|
|
14712
|
+
const exportStep = args.shapeName ? `2. **Take a screenshot** using \`export_shape\` with the shape ID from step 1.` : `2. **Take a screenshot** using \`export_shape\` with shapeId="selection".`;
|
|
14713
|
+
return {
|
|
14714
|
+
messages: [
|
|
14715
|
+
{
|
|
14716
|
+
role: "user",
|
|
14717
|
+
content: {
|
|
14718
|
+
type: "text",
|
|
14719
|
+
text: `Please visually verify ${target} in the Penpot design. Follow these steps:
|
|
14720
|
+
|
|
14721
|
+
${findStep}
|
|
14722
|
+
${exportStep}
|
|
14723
|
+
|
|
14724
|
+
3. **Analyze the screenshot** and check for:
|
|
14725
|
+
- Layout alignment and spacing
|
|
14726
|
+
- Text readability (fonts, sizes, colors)
|
|
14727
|
+
- Element overflow or clipping
|
|
14728
|
+
- Color correctness
|
|
14729
|
+
- Completeness (all expected elements visible)
|
|
14730
|
+
|
|
14731
|
+
4. **Report findings** with specific observations about what looks correct and any issues found.`
|
|
14732
|
+
}
|
|
14733
|
+
}
|
|
14734
|
+
]
|
|
14735
|
+
};
|
|
14736
|
+
}
|
|
14737
|
+
};
|
|
14738
|
+
|
|
14739
|
+
// src/prompts/ExploreDesignPrompt.ts
|
|
14740
|
+
var EXPLORE_DESIGN_PROMPT = {
|
|
14741
|
+
name: "explore-design",
|
|
14742
|
+
config: {
|
|
14743
|
+
title: "Explore Design",
|
|
14744
|
+
description: "Explores a Penpot design page by analyzing its structure and taking screenshots of key elements. Use to understand what a page contains and how it looks.",
|
|
14745
|
+
argsSchema: {
|
|
14746
|
+
pageName: external_exports.string().optional().describe(
|
|
14747
|
+
"Name of the page to explore. If omitted, explores the current page."
|
|
14748
|
+
)
|
|
14749
|
+
}
|
|
14750
|
+
},
|
|
14751
|
+
callback: async (args) => {
|
|
14752
|
+
const pageRef = args.pageName ? `the page named "${args.pageName}"` : "the current page";
|
|
14753
|
+
const getPageCode = args.pageName ? `const page = penpotUtils.getPageByName('${args.pageName}');
|
|
14754
|
+
if (!page) throw new Error('Page "${args.pageName}" not found');
|
|
14755
|
+
const root = page.root;` : `const root = penpot.root;`;
|
|
14756
|
+
return {
|
|
14757
|
+
messages: [
|
|
14758
|
+
{
|
|
14759
|
+
role: "user",
|
|
14760
|
+
content: {
|
|
14761
|
+
type: "text",
|
|
14762
|
+
text: `Please explore and summarize ${pageRef} in the Penpot design. Follow these steps:
|
|
14763
|
+
|
|
14764
|
+
1. **Get the page structure** using \`execute_code\`:
|
|
14765
|
+
\`\`\`javascript
|
|
14766
|
+
${getPageCode}
|
|
14767
|
+
return penpotUtils.shapeStructure(root, 3);
|
|
14768
|
+
\`\`\`
|
|
14769
|
+
|
|
14770
|
+
2. **Identify key elements**: From the structure, identify the main boards, groups, and notable shapes.
|
|
14771
|
+
|
|
14772
|
+
3. **Take screenshots of the main boards** using \`export_shape\` for each top-level board to see how they look.
|
|
14773
|
+
|
|
14774
|
+
4. **Summarize the design**: Provide a structured overview including:
|
|
14775
|
+
- Page name and overall structure
|
|
14776
|
+
- List of boards with their purpose (inferred from names and visual content)
|
|
14777
|
+
- Notable design patterns (layout systems, component usage)
|
|
14778
|
+
- Any potential issues spotted in the screenshots`
|
|
14779
|
+
}
|
|
14780
|
+
}
|
|
14781
|
+
]
|
|
14782
|
+
};
|
|
14783
|
+
}
|
|
14784
|
+
};
|
|
14785
|
+
|
|
14684
14786
|
// src/ReplServer.ts
|
|
14685
14787
|
import express from "express";
|
|
14686
14788
|
import path3 from "path";
|
|
@@ -14908,6 +15010,7 @@ var PenpotMcpServer = class {
|
|
|
14908
15010
|
this.pluginBridge = new PluginBridge(this, this.webSocketPort);
|
|
14909
15011
|
this.replServer = new ReplServer(this.pluginBridge, this.replPort);
|
|
14910
15012
|
this.registerTools();
|
|
15013
|
+
this.registerPrompts();
|
|
14911
15014
|
}
|
|
14912
15015
|
logger = createLogger("PenpotMcpServer");
|
|
14913
15016
|
server;
|
|
@@ -15003,6 +15106,17 @@ var PenpotMcpServer = class {
|
|
|
15003
15106
|
);
|
|
15004
15107
|
}
|
|
15005
15108
|
}
|
|
15109
|
+
registerPrompts() {
|
|
15110
|
+
const prompts = [VERIFY_DESIGN_PROMPT, EXPLORE_DESIGN_PROMPT];
|
|
15111
|
+
for (const prompt of prompts) {
|
|
15112
|
+
this.logger.info(`Registering prompt: ${prompt.name}`);
|
|
15113
|
+
this.server.registerPrompt(
|
|
15114
|
+
prompt.name,
|
|
15115
|
+
prompt.config,
|
|
15116
|
+
prompt.callback
|
|
15117
|
+
);
|
|
15118
|
+
}
|
|
15119
|
+
}
|
|
15006
15120
|
setupHttpEndpoints() {
|
|
15007
15121
|
this.app.all("/mcp", async (req, res) => {
|
|
15008
15122
|
const userToken = req.query.userToken;
|
|
@@ -15075,6 +15189,12 @@ var PenpotMcpServer = class {
|
|
|
15075
15189
|
const pluginDistPath = path5.join(__dirname2, "plugin");
|
|
15076
15190
|
const fs4 = await import("fs");
|
|
15077
15191
|
if (fs4.existsSync(pluginDistPath)) {
|
|
15192
|
+
pluginApp.use((_req, res, next) => {
|
|
15193
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
15194
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
15195
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
15196
|
+
next();
|
|
15197
|
+
});
|
|
15078
15198
|
pluginApp.use(express2.static(pluginDistPath));
|
|
15079
15199
|
return new Promise((resolve2) => {
|
|
15080
15200
|
pluginApp.listen(this.pluginPort, this.listenAddress, () => {
|
|
@@ -15095,7 +15215,12 @@ var PenpotMcpServer = class {
|
|
|
15095
15215
|
const pluginDistPath = path5.join(__dirname2, "plugin");
|
|
15096
15216
|
const fs4 = await import("fs");
|
|
15097
15217
|
if (fs4.existsSync(pluginDistPath)) {
|
|
15098
|
-
this.app.use("/plugin",
|
|
15218
|
+
this.app.use("/plugin", (_req, res, next) => {
|
|
15219
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
15220
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
15221
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
15222
|
+
next();
|
|
15223
|
+
}, express2.static(pluginDistPath));
|
|
15099
15224
|
this.logger.info(`Plugin files will be served at: http://${this.serverAddress}:${this.port}/plugin/`);
|
|
15100
15225
|
}
|
|
15101
15226
|
return new Promise((resolve2) => {
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
class y{constructor(e,t,o){this.requestId=e,this.taskType=t,this.params=o}isResponseSent=!1;sendResponse(e,t=void 0,o=void 0){if(this.isResponseSent){console.error("Response already sent for task:",this.requestId);return}const n={type:"task-response",response:{id:this.requestId,success:e,data:t,error:o}};penpot.ui.sendMessage(n),console.log("Sent task response:",n),this.isResponseSent=!0}sendSuccess(e=void 0){this.sendResponse(!0,e)}sendError(e){this.sendResponse(!1,void 0,e)}}class w{isApplicableTo(e){return this.taskType===e.taskType}}class f{static shapeStructure(e,t=void 0){let o;(t===void 0||t>0)&&"children"in e&&e.children&&(o=e.children.map(r=>this.shapeStructure(r,t===void 0?void 0:t-1)));const n={id:e.id,name:e.name,type:e.type,children:o};if("flex"in e&&e.flex){const r=e.flex;n.layout={type:"flex",dir:r.dir,rowGap:r.rowGap,columnGap:r.columnGap}}else if("grid"in e&&e.grid){const r=e.grid;n.layout={type:"grid",rows:r.rows,columns:r.columns,rowGap:r.rowGap,columnGap:r.columnGap}}return n}static findShapes(e,t=penpot.root){let o=new Array,n=function(r){if(r&&(e(r)&&o.push(r),"children"in r&&r.children))for(let s of r.children)n(s)};return n(t),o}static findShape(e,t=null){let o=function(n){if(!n)return null;if(e(n))return n;if("children"in n&&n.children)for(let r of n.children){let s=o(r);if(s)return s}return null};if(t===null){const n=penpot.currentFile?.pages;if(n)for(let r of n){let s=o(r.root);if(s)return s}return null}else return o(t)}static findShapeById(e){return this.findShape(t=>t.id===e)}static findPage(e){return penpot.currentFile.pages.find(e)||null}static getPages(){return penpot.currentFile.pages.map(e=>({id:e.id,name:e.name}))}static getPageById(e){return this.findPage(t=>t.id===e)}static getPageByName(e){return this.findPage(t=>t.name.toLowerCase()===e.toLowerCase())}static getPageForShape(e){for(const t of penpot.currentFile.pages)if(t.getShapeById(e.id))return t;return null}static generateCss(e){const t=this.getPageForShape(e);if(!t)throw new Error("Shape is not part of any page");return penpot.openPage(t),penpot.generateStyle([e],{type:"css",includeChildren:!0})}static isContainedIn(e,t){return e.x>=t.x&&e.y>=t.y&&e.x+e.width<=t.x+t.width&&e.y+e.height<=t.y+t.height}static setParentXY(e,t,o){if(!e.parent)throw new Error("Shape has no parent - cannot set parent-relative position");e.x=e.parent.x+t,e.y=e.parent.y+o}static addFlexLayout(e,t){const n=("children"in e&&e.children?[...e.children]:[]).sort((s,i)=>t==="row"?s.x-i.x:s.y-i.y),r=e.addFlexLayout();r.dir=t;for(const s of n)s.setParentIndex(0);return r}static analyzeDescendants(e,t,o=void 0){const n=[],r=(s,i)=>{const p=t(e,s);if(p!=null&&n.push({shape:s,result:p}),(o===void 0||i<o)&&"children"in s&&s.children)for(const a of s.children)r(a,i+1)};if("children"in e&&e.children)for(const s of e.children)r(s,1);return n}static atob(e){const t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",o=new Uint8Array(256);for(let i=0;i<t.length;i++)o[t.charCodeAt(i)]=i;let n=e.length*.75;e[e.length-1]==="="&&(n--,e[e.length-2]==="="&&n--);const r=new Uint8Array(n);let s=0;for(let i=0;i<e.length;i+=4){const p=o[e.charCodeAt(i)],a=o[e.charCodeAt(i+1)],c=o[e.charCodeAt(i+2)],d=o[e.charCodeAt(i+3)];r[s++]=p<<2|a>>4,r[s++]=(a&15)<<4|c>>2,r[s++]=(c&3)<<6|d&63}return r}static async importImage(e,t,o,n,r,s,i){const p=f.atob(e),a=await penpot.uploadMediaData(o,p,t),c=penpot.createRectangle();c.name=o;let d,u;const g=s!==void 0,h=i!==void 0;return g&&h?(d=s,u=i):g?(d=s,u=d*(a.height/a.width)):h?(u=i,d=u*(a.width/a.height)):(d=a.width,u=a.height),c.resize(d,u),n!==void 0&&(c.x=n),r!==void 0&&(c.y=r),c.fills=[{fillOpacity:1,fillImage:a}],c}static async exportImage(e,t,o){switch(t){case"shape":return e.export({type:o?"svg":"png"});case"fill":if(o)throw new Error("Image fills cannot be exported as SVG");if(!("fills"in e))throw new Error("Shape with `fills` member is required for fill export mode");const
|
|
1
|
+
class y{constructor(e,t,o){this.requestId=e,this.taskType=t,this.params=o}isResponseSent=!1;sendResponse(e,t=void 0,o=void 0){if(this.isResponseSent){console.error("Response already sent for task:",this.requestId);return}const n={type:"task-response",response:{id:this.requestId,success:e,data:t,error:o}};penpot.ui.sendMessage(n),console.log("Sent task response:",n),this.isResponseSent=!0}sendSuccess(e=void 0){this.sendResponse(!0,e)}sendError(e){this.sendResponse(!1,void 0,e)}}class w{isApplicableTo(e){return this.taskType===e.taskType}}class f{static shapeStructure(e,t=void 0){let o;(t===void 0||t>0)&&"children"in e&&e.children&&(o=e.children.map(r=>this.shapeStructure(r,t===void 0?void 0:t-1)));const n={id:e.id,name:e.name,type:e.type,children:o};if("flex"in e&&e.flex){const r=e.flex;n.layout={type:"flex",dir:r.dir,rowGap:r.rowGap,columnGap:r.columnGap}}else if("grid"in e&&e.grid){const r=e.grid;n.layout={type:"grid",rows:r.rows,columns:r.columns,rowGap:r.rowGap,columnGap:r.columnGap}}return n}static findShapes(e,t=penpot.root){let o=new Array,n=function(r){if(r&&(e(r)&&o.push(r),"children"in r&&r.children))for(let s of r.children)n(s)};return n(t),o}static findShape(e,t=null){let o=function(n){if(!n)return null;if(e(n))return n;if("children"in n&&n.children)for(let r of n.children){let s=o(r);if(s)return s}return null};if(t===null){const n=penpot.currentFile?.pages;if(n)for(let r of n){let s=o(r.root);if(s)return s}return null}else return o(t)}static findShapeById(e){return this.findShape(t=>t.id===e)}static findPage(e){return penpot.currentFile.pages.find(e)||null}static getPages(){return penpot.currentFile.pages.map(e=>({id:e.id,name:e.name}))}static getPageById(e){return this.findPage(t=>t.id===e)}static getPageByName(e){return this.findPage(t=>t.name.toLowerCase()===e.toLowerCase())}static getPageForShape(e){for(const t of penpot.currentFile.pages)if(t.getShapeById(e.id))return t;return null}static generateCss(e){const t=this.getPageForShape(e);if(!t)throw new Error("Shape is not part of any page");return penpot.openPage(t),penpot.generateStyle([e],{type:"css",includeChildren:!0})}static isContainedIn(e,t){return e.x>=t.x&&e.y>=t.y&&e.x+e.width<=t.x+t.width&&e.y+e.height<=t.y+t.height}static setParentXY(e,t,o){if(!e.parent)throw new Error("Shape has no parent - cannot set parent-relative position");e.x=e.parent.x+t,e.y=e.parent.y+o}static addFlexLayout(e,t){const n=("children"in e&&e.children?[...e.children]:[]).sort((s,i)=>t==="row"?s.x-i.x:s.y-i.y),r=e.addFlexLayout();r.dir=t;for(const s of n)s.setParentIndex(0);return r}static analyzeDescendants(e,t,o=void 0){const n=[],r=(s,i)=>{const p=t(e,s);if(p!=null&&n.push({shape:s,result:p}),(o===void 0||i<o)&&"children"in s&&s.children)for(const a of s.children)r(a,i+1)};if("children"in e&&e.children)for(const s of e.children)r(s,1);return n}static atob(e){const t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",o=new Uint8Array(256);for(let i=0;i<t.length;i++)o[t.charCodeAt(i)]=i;let n=e.length*.75;e[e.length-1]==="="&&(n--,e[e.length-2]==="="&&n--);const r=new Uint8Array(n);let s=0;for(let i=0;i<e.length;i+=4){const p=o[e.charCodeAt(i)],a=o[e.charCodeAt(i+1)],c=o[e.charCodeAt(i+2)],d=o[e.charCodeAt(i+3)];r[s++]=p<<2|a>>4,r[s++]=(a&15)<<4|c>>2,r[s++]=(c&3)<<6|d&63}return r}static async importImage(e,t,o,n,r,s,i){const p=f.atob(e),a=await penpot.uploadMediaData(o,p,t),c=penpot.createRectangle();c.name=o;let d,u;const g=s!==void 0,h=i!==void 0;return g&&h?(d=s,u=i):g?(d=s,u=d*(a.height/a.width)):h?(u=i,d=u*(a.width/a.height)):(d=a.width,u=a.height),c.resize(d,u),n!==void 0&&(c.x=n),r!==void 0&&(c.y=r),c.fills=[{fillOpacity:1,fillImage:a}],c}static async exportImage(e,t,o,n=1){switch(t){case"shape":return e.export({type:o?"svg":"png",scale:n});case"fill":if(o)throw new Error("Image fills cannot be exported as SVG");if(!("fills"in e))throw new Error("Shape with `fills` member is required for fill export mode");const r=e.fills;for(const s of r)if(s.fillImage)return s.fillImage.data();throw new Error("No fill with image data found in the shape");default:throw new Error(`Unsupported export mode: ${t}`)}}}class m{logOutput="";resetLog(){this.logOutput=""}getLog(){return this.logOutput}appendToLog(e,...t){const o=t.map(n=>typeof n=="object"?JSON.stringify(n,null,2):String(n)).join(" ");this.logOutput+=`[${e}] ${o}
|
|
2
2
|
`}log(...e){this.appendToLog("LOG",...e)}warn(...e){this.appendToLog("WARN",...e)}error(...e){this.appendToLog("ERROR",...e)}info(...e){this.appendToLog("INFO",...e)}debug(...e){this.appendToLog("DEBUG",...e)}trace(...e){this.appendToLog("TRACE",...e)}table(e){this.appendToLog("TABLE",e)}time(e){this.appendToLog("TIME",`Timer started: ${e||"default"}`)}timeEnd(e){this.appendToLog("TIME_END",`Timer ended: ${e||"default"}`)}group(e){this.appendToLog("GROUP",e||"")}groupCollapsed(e){this.appendToLog("GROUP_COLLAPSED",e||"")}groupEnd(){this.appendToLog("GROUP_END","")}clear(){}count(e){this.appendToLog("COUNT",e||"default")}countReset(e){this.appendToLog("COUNT_RESET",e||"default")}assert(e,...t){e||this.appendToLog("ASSERT",...t)}}class T extends w{taskType="executeCode";context;constructor(){super(),this.context={penpot,storage:{},console:new m,penpotUtils:f}}async handle(e){if(!e.params.code){e.sendError("executeCode task requires 'code' parameter");return}this.context.console.resetLog();const t=this.context,o=e.params.code;let n=await(async s=>new Function(...Object.keys(s),`return (async () => { ${o} })();`)(...Object.values(s)))(t);console.log("Code execution result:",n);let r={result:n,log:this.context.console.getLog()};e.sendSuccess(r)}}const k=[new T],E=!1;penpot.ui.open("Penpot MCP Plugin",`?theme=${penpot.theme}&multiUser=${E}`,{width:158,height:200});penpot.ui.onMessage(l=>{typeof l=="object"&&l.task&&l.id&&x(l).catch(e=>{console.error("Error in handlePluginTaskRequest:",e)})});async function x(l){console.log("Executing plugin task:",l.task,l.params);const e=new y(l.id,l.task,l.params),t=k.find(o=>o.isApplicableTo(e));if(t)try{console.log("Processing task with handler:",t),await t.handle(e),e.isResponseSent||(console.warn("Handler did not send a response, sending generic success."),e.sendSuccess("Task completed without a specific response.")),console.log("Task handled successfully:",e)}catch(o){console.error("Error handling task:",o);const n=o instanceof Error?o.message:"Unknown error";e.sendError(`Error handling task: ${n}`)}else console.error("Unknown plugin task:",l.task),e.sendError(`Unknown task type: ${l.task}`)}penpot.on("themechange",l=>{penpot.ui.sendMessage({source:"penpot",type:"themechange",theme:l})});
|