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.
@@ -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 can be critical to visually inspect the design. Remember to use the `export_shape` tool for this purpose!
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 code = `return penpotUtils.exportImage(${shapeCode}, "${args.mode}", ${asSvg});`;
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", express2.static(pluginDistPath));
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) => {
@@ -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 n=e.fills;for(const r of n)if(r.fillImage)return r.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}
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})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-penpot",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for Penpot design tool integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",