# MCP/API Issue list

**URL:** <https://community.penpot.app/t/mcp-api-issue-list/10700>\
**Category:** Help us improve Penpot\
**Tags:** bug\
**Created:** [June 28, 2026, 1:36pm UTC](https://community.penpot.app/t/mcp-api-issue-list/10700 "2026-06-28T13:36:19Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![HeyTC](https://avatars.discourse-cdn.com/v4/letter/h/e79b87/32.png) [@HeyTC](https://community.penpot.app/u/HeyTC)\
**Post date:** [June 28, 2026, 1:36pm UTC](https://community.penpot.app/t/mcp-api-issue-list/10700/1 "2026-06-28T13:36:19Z")

</div>

Hiya…

…I know, I know, I know it’s Claude bug reporting but as it’s real-world experience with Penpot / MCP it seems it should be of value. If it’s not, then LMK.

**# Penpot Plugin API — Discovered Issues**

Compiled from production use building a multi-file design system with a real-world component library (VL Design System → VL module files). All issues were encountered via the MCP `execute_code` interface, which runs code in the plugin executor context (`penpotUtils` / `penpot` globals).

Issues are grouped by severity. Each entry includes reproduction steps, observed behaviour, expected behaviour, and the workaround used.

**—**

**## Critical — Data Loss / Corruption**

**### 1. Large `execute_code` calls silently roll back with no error**

**Severity:** Critical

**Area:** Plugin executor / persistence

**Description:**

When a single plugin call creates many shapes (≥ ~25), the call returns successfully, shapes are findable _within the same call_, but **nothing persists** to the next call. No error is thrown; the promise resolves normally. This is indistinguishable from success at the call boundary.

**Reproduction:**

1. Open a Penpot file that has had a freeze/disconnect event in its history (or has been exported and re-imported).

2. Write a single `execute_code` call that creates ~25+ boards and text shapes, appends them hierarchically, and logs their IDs.

3. In a subsequent call, read `penpot.currentPage.root.children` — the shapes are absent.

**Expected:** Either the call commits atomically, or it throws an error.

**Observed:** Silent rollback. Logs show IDs. Next call shows zero new children.

**Workaround:**

- Keep each call to ≤ ~8 new shapes.

- Create the parent board in its own tiny call, capture its `.id`.

- Add children in small batches, re-finding the parent by `.id` each call via `root.children`.

- Always verify persistence by reading `parent.children.length` in the _next_ call before continuing.

**—**

**### 2. `layoutCell.columnSpan` corrupts grid layout — auto-expands column count and scrambles cell positions**

**Severity:** Critical

**Area:** Grid layout API

**Description:**

After building a grid with `addGridLayout()`, `addColumn()`, `addRow()`, and `appendChild(child, row, col)`, setting `layoutCell.columnSpan` on any cell causes Penpot to **silently expand the column count** (e.g. 7 → 9) and **scramble all cell column assignments**. Attempting cleanup via `removeColumn()` or re-assigning `layoutCell` properties triggers a freeze/crash on the page.

**Reproduction:**

1. Create a board, call `board.addGridLayout()`.

2. Add N columns, add rows, place children at (row, col) positions.

3. Set `someChild.layoutChild.columnSpan = 2` (or any span \> 1).

4. Observe column count in Penpot UI — it changes from N to N+2 or similar.

5. All other cells shift to wrong columns.

**Expected:** `columnSpan` spans the cell across multiple columns without affecting other cells or column count.

**Observed:** Column corruption + freeze on attempted recovery.

**Workaround:**

Do not use `columnSpan`. For full-width rows (e.g. row highlight backgrounds), use a separate background layer positioned behind the grid board (absolute coordinates), rather than a spanning grid cell. Use `grid.setParentIndex(n)` (not `bringToFront()`) to ensure the grid renders in front of background layers.

**—**

**### 3. `switchVariant()` on instances inside cloned trees freezes the plugin bridge**

**Severity:** Critical

**Area:** Component instance API

**Description:**

Calling `instance.switchVariant(propertyName, value)` on component instances that exist inside a _cloned_ board (i.e. a board that was itself produced by cloning another board containing instances) hangs the plugin bridge indefinitely. The `execute_code` call does not resolve or reject — it times out after ~30s, then the entire bridge freezes requiring a manual browser tab reload. Plugin storage is wiped on disconnect.

**Reproduction:**

1. Have a component with variants (e.g. nav-panel-item with active/inactive state).

2. Build an app shell board and clone it (`.clone()`).

3. Walk the clone’s children, find an instance, call `instance.switchVariant('state', 'active')`.

4. The call hangs; browser tab becomes unresponsive.

**Expected:** `switchVariant` updates the variant on the instance.

**Observed:** Bridge freeze, tab reload required, storage wiped.

**Workaround:**

Avoid `switchVariant` on instances in bulk or cloned trees. Instead: set the shape’s `fills` directly to the selected-state colour, or delete the instance and re-create it using the desired variant’s component.

**—**

**## High — Incorrect Behaviour**

**### 4. `penpot.currentPage.findShapes(predicate)` always returns an empty array**

**Severity:** High

**Area:** Shape querying API

**Description:**

`penpot.currentPage.findShapes(pred)` returns `[]` regardless of predicate, even when many shapes exist on the page and are visible in the Penpot UI. The shapes have not been removed — they are present and accessible via `root.children`.

**Reproduction:**

1. Create several boards on the current page.

2. Call `penpot.currentPage.findShapes(s => true)`.

3. Observe: returns `[]`.

4. Call `penpot.currentPage.root.children` — shapes are present.

**Expected:** Returns all shapes matching the predicate.

**Observed:** Always `[]`.

**Workaround:**

Use `penpot.currentPage.root.children` for top-level shapes. For nested lookups, walk `.children` recursively. Store shape `.id` values in `penpot.storage` to re-find them across calls.

**Note:** `penpotUtils.findShapes(pred, root)` (the utility variant) does work, but **includes the root shape itself** if it matches the predicate — scope predicates with `s !== root` to avoid accidentally mutating the container.

**—**

**### 5. `board.width` and `board.height` are getter-only — assignment silently fails or throws**

**Severity:** High

**Area:** Shape geometry API

**Description:**

Assigning `board.width = 300` throws `"setting getter-only property 'width'"` (or silently fails depending on strict mode). This applies to all board-like shapes (frames, components). The property appears writable in documentation/autocomplete but is not.

**Reproduction:**

1. `const b = penpot.createBoard(); b.width = 300;`

2. Observe: TypeError or silent no-op.

**Expected:** `board.width = n` sets the width.

**Observed:** Throws or no-ops.

**Workaround:**

Use `board.resize(width, height)` for all dimension changes. **Critical ordering constraint:** call `resize()` BEFORE `addFlexLayout()`. Once flex layout is added, calling `resize()` on the board again makes `width`/`height` getter-only even via `resize()` (the flex container takes over sizing). Set dimensions first, then add layout.

**—**

**### 6. `text.fontColor` assignment throws “Object is not extensible”**

**Severity:** High

**Area:** Text styling API

**Description:**

Attempting to set `text.fontColor = '#rrggbb'` throws `"Object is not extensible"`. The property does not exist on the text shape; the object is sealed.

**Reproduction:**

1. `const t = penpot.createText('hello'); t.fontColor = '#677081';`

2. Observe: TypeError.

**Expected:** Sets the text colour.

**Observed:** Throws.

**Workaround:**

Use the fills array: `t.fills = [{ fillType: 'solid', fillColor: '#677081', fillOpacity: 1 }]`.

**—**

**### 7. `text.characters = ""` throws a validation error**

**Severity:** High

**Area:** Text content API

**Description:**

Setting `text.characters = ""` (empty string) throws `"Value not valid: . Code: :characters"`. Empty string is not accepted as a valid characters value.

**Reproduction:**

1. `const t = penpot.createText('hello'); t.characters = '';`

2. Observe: throws validation error.

**Expected:** Clears the text content.

**Observed:** Throws.

**Workaround:**

Use `text.hidden = true` to hide the text instead of clearing it. If the text must be empty, delete the shape entirely.

**—**

**### 8. Library component instances cannot be created programmatically**

**Severity:** High

**Area:** Component / Library API

**Description:**

There is no plugin API method to create a linked component instance from a library. `penpot.library.connected[i].components[j].mainInstance()` returns the main component shape, and calling `.clone()` on it returns a shape with `type: null, name: null`. Appending this null-typed shape via `parentBoard.appendChild(nullShape)` silently drops it — nothing is added, no error thrown.

No `penpot.createInstance()`, `component.instantiate()`, or equivalent exists.

**Reproduction:**

1. Have a connected library with components.

2. `const comp = penpot.library.connected[0].components[0]; const inst = comp.mainInstance().clone();`

3. `someBoard.appendChild(inst);`

4. Observe: `someBoard.children.length` is unchanged.

**Expected:** A linked instance of the component is added to the board.

**Observed:** `clone()` returns a null shape; `appendChild` silently ignores it.

**Impact:**

Patterns built via the plugin cannot use library component instances. They must be built from raw shapes, losing the component-linking benefit. The only way to place a real linked instance is to drag it from the Assets panel in the Penpot UI.

**Suggested API addition:** `penpot.createInstance(component)` → returns a live, linked instance shape.

**—**

**### 9. Spacing tokens cannot be applied to flex padding via `applyToken`**

**Severity:** High

**Area:** Token binding API

**Description:**

`shape.applyToken(spacingToken, ['paddingLeft'])` (and all other padding properties) throws `"Value not valid: Field message is invalid"`. Including any padding property in a multi-property array fails the entire call.

Gap properties (`columnGap`, `rowGap`) work correctly.

**Reproduction:**

1. Have a spacing token in the local library.

2. `board.applyToken(tok, ['paddingLeft'])` → throws.

3. `board.applyToken(tok, ['columnGap'])` → works.

**Expected:** Token is bound to the padding property, analogous to gap binding.

**Observed:** Throws for all padding properties.

**Workaround:**

Set padding as raw numeric values: `board.paddingLeft = 8`. Can be manually token-bound in the Penpot UI via right-click → Assign Token.

**—**

**## Medium — Unexpected Behaviour**

**### 10. `penpot.openPage()` requires its own call — page switch is not synchronous**

**Severity:** Medium

**Area:** Page navigation API

**Description:**

After calling `penpot.openPage(page)`, reading `penpot.currentPage.name` in the same call still returns the old page name. The switch takes a tick to propagate. Any shape edits in the same call as `openPage` target the old page.

**Reproduction:**

1. `penpot.openPage(otherPage); console.log(penpot.currentPage.name);` — logs old page name.

2. Run `openPage` in call 1, read `currentPage.name` in call 2 — logs new page name.

**Expected:** `openPage` is synchronous, or returns a Promise that resolves after the switch.

**Observed:** Switch is deferred; same-call reads still see old page.

**Workaround:**

Always call `openPage` alone in its own `execute_code` call, then edit in the next call.

**Additional note:** Edits to shapes on a non-active page silently revert. Always verify `currentPage` before editing.

**—**

**### 11. `page.name` not set by `penpot.createPage(name)` argument**

**Severity:** Medium

**Area:** Page management API

**Description:**

`penpot.createPage('v2 - Project Data')` creates a page but the name comes out as `"Page 2"`, `"Page 3"` etc. — the argument is ignored.

**Reproduction:**  
[HeyTC: Have the Tokens panel open when new page is being called.]  
1. `const p = penpot.createPage('My Page Name'); console.log(p.name);` → logs `"Page 2"`.

**Expected:** Page is created with the given name.

**Observed:** Name argument is ignored; page gets a generic sequential name.

**Workaround:**

Set the name separately after creation: `p.name = 'My Page Name';` — this does work.

**—**

**### 12. `layoutChild` properties are null when accessing from a subsequent call**

**Severity:** Medium

**Area:** Flex layout / child sizing API

**Description:**

`child.layoutChild.horizontalSizing = 'fill'` only works when the parent board was created in the **same** `execute_code` call. If the parent was created in a previous call and is re-found via `root.children`, then appending a new child and accessing `child.layoutChild` returns `null`.

**Reproduction:**

1. Call 1: create parent board with flex layout, capture its `.id`.

2. Call 2: find parent via `root.children.find(c => c.id === id)`, create a new child board, `parentBoard.appendChild(child)`.

3. `child.layoutChild.horizontalSizing = 'fill'` → TypeError (null access).

**Expected:** `layoutChild` is accessible on any child of a flex container, regardless of when the parent was created.

**Observed:** `layoutChild` is null for children added to parents created in previous calls.

**Workaround:**

Build entire parent + children trees within a single call. For shapes that must be added to existing flex containers across calls, use hardcoded `resize(w, h)` values instead of `fill` sizing.

**—**

**### 13. Inter font only supports weights 200, 300, 400, 700, 900 in Penpot**

**Severity:** Medium

**Area:** Text styling API

**Description:**

Setting `text.fontWeight = 500` or `600` throws `"Value not valid: Font weight '500' not supported for the current font."`. This is not documented; discovery is only by trial and error.

**Reproduction:**

1. `const t = penpot.createText('test'); t.fontWeight = 500;` → throws.

2. `t.fontWeight = 400;` → works.

**Expected:** All standard CSS font weights accepted (or a clear error/enum at the API level).

**Observed:** Runtime throw for unsupported weights; only 200/300/400/700/900 are valid for Inter.

**Workaround:**

Use 400 (regular) and 700 (bold) as the safe pair. Expose the valid weight set in API docs or as a typed enum.

**—**

**### 14. Theme switching via plugin freezes and rolls back**

**Severity:** Medium

**Area:** Token theme API

**Description:**

Toggling token sets (`TokenSet.toggleActive()`) to change the active module/mode triggers heavy propagation that freezes the plugin bridge and then rolls back — the active set reverts to its previous state. Even mid-toggle, already-bound shapes do not recolour (`fills[0].fillColor` stays the old resolved value).

**Reproduction:**

1. Have multiple token sets active (e.g. `v2/Theme/Light` + `v2/Module/DataQuality`).

2. Call `penpot.library.local.tokens.sets.find(s => s.name === 'v2/Module/EnvFootprint').toggleActive()`.

3. Bridge freezes for ~20–30s, then reverts.

**Expected:** Active set changes; bound shapes recolour to the new resolved values.

**Observed:** Freeze + rollback. No recolouring occurs.

**Workaround:**

Theme switching must be done in the Penpot UI (Tokens → Themes panel). Components are correctly token-bound; only the render won’t follow a plugin toggle.

**—**

**### 15. `penpotUtils.findShapes(pred, root)` includes the root shape if it matches**

**Severity:** Low / Footgun

**Area:** Shape querying utility

**Description:**

`penpotUtils.findShapes(pred, root)` walks the entire subtree rooted at `root`, and **includes `root` itself** in the results if `pred(root)` is true. When used to find all filled descendants (e.g. to apply a fill token to all children), this inadvertently mutates the container board itself.

**Example:** Applying `color.accent.contrast` to all filled paths inside a tile board accidentally overwrites the tile board’s own accent fill with white.

**Workaround:**

Always exclude the root: `penpotUtils.findShapes(s => s !== root && yourPredicate(s), root)`.

**—**

**### 16. `bringToFront()` does not reliably reorder shapes in grid/flex containers**

**Severity:** Low

**Area:** Z-order API

**Description:**

In some layouts (observed with grid layout boards with background stripe layers), `shape.bringToFront()` does not change the render order. The shape remains visually behind others.

**Workaround:**

Use `shape.setParentIndex(n)` with a high index value (higher = rendered in front). This reliably controls z-order.

**—**

**## API Gaps (Feature Requests)**

**### A. No `createInstance(component)` API**

As noted in issue #8, there is no way to place a linked component instance programmatically. This is the single most impactful gap for plugin-based pattern building. A `penpot.createInstance(component)` method that returns a live, linked instance shape would unlock the full FCP (Frame-Component-Pattern) workflow via plugins.

**### B. `applyToken` does not support padding properties**

As noted in issue #9. Padding token binding requires manual UI interaction. Parity with gap properties (`columnGap`, `rowGap`) would complete the spacing token binding surface.

**### C. `addFlexLayout()` / `addGridLayout()` should accept an initial size**

Because `resize()` cannot be called after `addFlexLayout()`, callers must remember to size first. An optional parameter `board.addFlexLayout({ width: 300, height: 200 })` would prevent the common ordering mistake.

**### D. `fontWeight` should accept a typed enum or expose valid values**

Inter’s valid weights in Penpot (200/300/400/700/900) differ from CSS standard (100–900). Exposing this as an enum (e.g. `penpot.fontWeights.inter`) or as a validation error at the property setter level (before the async throw) would reduce friction.

**—**

_Discovered during VL Design System build — June 2026. All issues reproduced in the Penpot plugin executor (MCP `execute_code` interface) against a Penpot cloud instance._

---

<div class="post-metadata">

**Author:** ![alotor](https://dub1.discourse-cdn.com/flex017/user_avatar/community.penpot.app/alotor/32/42_2.png) [@alotor](https://community.penpot.app/u/alotor)\
**Post date:** [July 3, 2026, 10:59am UTC](https://community.penpot.app/t/mcp-api-issue-list/10700/2 "2026-07-03T10:59:32Z")

</div>

Hi @HeyTC,

First, thank you for the detailed report. This is exactly the kind of real-world MCP/API feedback that helps us improve Penpot.

Sorry for the late reply. We reviewed the full list against the next release branch. Most of the issues are already fixed there, and your report helped us confirm a few bugs that we have now fixed.

## Confirmed bugs

**#9: Spacing tokens cannot be applied to padding via `applyToken`**

Confirmed.

In the current build, this no longer throws, but the token binding is stored under the wrong key, so the token is not actually applied to `paddingLeft`.

Gap properties like `columnGap` and `rowGap` work correctly.

This will be fixed in the next release. Padding is already intended to be supported, so this was a bug rather than a missing feature.

**#16: `bringToFront()` is unreliable in layout containers**

Partially confirmed.

We reproduced the issue inside flex containers. `bringToFront()` was not reordering the child correctly there.

It worked on plain boards and, in our tests, also inside grid containers. The flex case is now fixed.

Your `setParentIndex(n)` workaround is the right approach until the fix is released.

**`resize()` after adding a layout did not behave like the UI**

We found this while investigating #5 and feature request C.

`resize()` can be called after `addFlexLayout()` or `addGridLayout()`, but it did not correctly switch auto-sized layouts to fixed sizing.

That meant:

- Flex boards could snap back to hugged content.

- Grid boards could apply the size once, but return to auto sizing on the next relayout.

This is now fixed. `resize()` now switches the affected axes to fixed sizing first, so the requested size sticks on both flex and grid boards.

While fixing this, we also found that `GridLayout` was missing its `horizontalSizing` and `verticalSizing` properties. That is fixed too.

## Could not reproduce on the current development branch

We added tests for the cases below using the reproductions you described. They all pass on the current development branch.

A large batch of Plugin API fixes is queued for the next release, so some of these may already be resolved. If you can still reproduce any of them after the release, a small snippet would help us investigate.

**#2: `layoutCell.columnSpan` corrupts the grid**

We tested a 3×2 grid with children and set `columnSpan = 2`.

The column count stayed the same, and the other cells kept their positions.

**#3: `switchVariant()` on instances inside cloned boards freezes**

We cloned a board containing a variant instance and called `switchVariant(0, value)` on the cloned instance. It worked without hanging.

One detail to note: the signature is:

`switchVariant(pos: number, value: string)`

The first argument is the property position, not the property name. So `switchVariant('state', 'active')` passes an invalid first argument.

**#8: Library component instances cannot be created programmatically**

The API for this already exists:

`component.instance()`

For example:

`penpot.library.connected[i].components[j].instance()`

That returns a linked instance of the component and is the supported way to place component instances from a plugin.

We also could not reproduce the broken clone behavior. `mainInstance().clone()` returns a valid shape in our tests.

One thing that may explain what you saw: `clone()` already attaches the copy to the same parent as the original. If you then call `appendChild` on that same parent, `children.length` will not increase, so it can look like the shape was dropped even though it was already there.

**#12: `layoutChild` is `null` for children added to parents from earlier calls**

We tested this by re-finding a flex board with `currentPage.getShapeById(id)`, appending a new child, and setting:

`child.layoutChild.horizontalSizing = 'fill'`

That worked correctly.

If this still happens after the next release, please share the exact sequence that triggers it.

**#14: Theme switching via plugin freezes and rolls back**

`TokenSet.toggleActive()` works and persists correctly in our tests when tokens from the set are bound to shapes.

That said, large themed files may behave differently. If you still see the freeze after the next release, the file would help us reproduce it. If sharing the file is not possible, an approximate size would also help: number of token sets, tokens, and bound shapes.

## Working as designed

These match the current typed API in `@penpot/plugin-types`, but some of them clearly need better docs or clearer errors.

**#4: `currentPage.findShapes()` always returns `[]`**

`findShapes` takes a criteria object, not a predicate function.

Use:

`findShapes({ name: 'exampleName' })`

Calling it with no arguments returns all shapes.

A predicate like `s => true` belongs to the MCP utility `penpotUtils.findShapes(pred, root)`, which is a different function.

**#5: `board.width` and `board.height` are getter-only**

This is intentional. `width` and `height` are readonly.

Use `resize(w, h)` to change dimensions.

However, your note about `resize()` not working after adding a layout helped us find a real bug. See the confirmed `resize()` fix above.

**#7: `text.characters = ""` throws a validation error**

This is intentional. A text shape must always have content, so an empty string is rejected.

The supported options are:

- Hide the shape with `hidden = true`

- Remove the shape

We agree the error message should be clearer.

**#6: `text.fontColor` throws “Object is not extensible”**

There is no `fontColor` property in the Plugin API.

The plugin sandbox seals API objects, so writing to an unknown property throws. Using `fills`, as you did, is the supported way to set text color.

**#10: `openPage()` is not synchronous**

You are right. In the version you tested, there was no way to wait for the page switch.

This is fixed in the next release. `openPage(page)` now returns a `Promise<void>`, so you can do:

`await penpot.openPage(page)`

After that, you can safely mutate shapes on the new page in the same `execute_code` call.

Until then, your one-call-per-`openPage` workaround is the right approach.

Also related: in the next release, edits to shapes on a non-active page will raise a validation error instead of silently reverting.

**#11: `createPage(name)` ignores the name**

`createPage()` currently takes no arguments, so the name argument is ignored.

The intended flow is:

`const page = penpot.createPage();`  
`page.name = 'My Page Name';`

Accepting an optional name is a reasonable improvement, and we have noted it.

**#13: Inter only supports weights 200/300/400/700/900**

Valid weights depend on the font. They come from the variants the font provides, not from the full CSS weight range.

You can inspect the available variants through `penpot.fonts`.

We also improved the error message for this. See feature request D below.

## MCP tooling

**#1: Large `execute_code` calls silently roll back**

We found issues in the MCP connection when the browser froze the tab. It could fail silently or leave the agent in an infinite loop.

Both cases are fixed for the next release.

**#15: `penpotUtils.findShapes(pred, root)` includes the root**

We changed this behavior.

The root shape is no longer included in the returned list.

## Feature requests

**A. `createInstance(component)`**

This already exists as `component.instance()`. See #8 above.

**B. `applyToken` padding support**

Padding is already part of the intended API surface. The current issue is a binding bug, and it will be fixed. See #9.

**C. Initial size for `addFlexLayout()` / `addGridLayout()`**

We found that the real issue was `resize()` not switching auto-sized layouts to fixed sizing.

That is now fixed, along with the missing grid sizing properties.

With those fixes, this flow works as expected:

`addFlexLayout()` / `addGridLayout()`  
`resize(width, height)`

So we fixed the underlying bugs instead of adding a size parameter.

**D. Expose valid font weights**

Done.

The validation error now lists the supported weights for the current font.

Example:

`Font weight '500' not supported for the current font. Supported weights: 200, 300, 400, 700, 900`

This will ship with the next release.

The full variant data is also available through `penpot.fonts`.

Thanks again for taking the time to write this up. Your report helped us confirm Plugin API bugs, fix MCP reliability issues, and identify a few places where the docs and error messages need to be clearer.

---

<div class="post-metadata">

**Author:** ![radziszcze](https://dub1.discourse-cdn.com/flex017/user_avatar/community.penpot.app/radziszcze/32/10744_2.png) [@radziszcze](https://community.penpot.app/u/radziszcze)\
**Post date:** [July 3, 2026, 11:08am UTC](https://community.penpot.app/t/mcp-api-issue-list/10700/3 "2026-07-03T11:08:20Z")

</div>

Hi @alotor!

I’ve been using the MCP through both Claude Code and Codex and I’ve reported few of the bugs listed here myself, too.

You wrote that few of those have been fixed already when compared with the next release branch. Two questions from me:

1. Is this a release branch for self-hosted or for cloud? I don’t know if both share the same codebase.

2. When is the next cloud update due?

---

<div class="post-metadata">

**Author:** ![HeyTC](https://avatars.discourse-cdn.com/v4/letter/h/e79b87/32.png) [@HeyTC](https://community.penpot.app/u/HeyTC)\
**Post date:** [July 4, 2026, 2:08am UTC](https://community.penpot.app/t/mcp-api-issue-list/10700/4 "2026-07-04T02:08:13Z")

</div>

Awesome & thank you. I’ll wait until the next release to get Claude to re-run the battery of tests.

I have realised that Claude can analyse error reports post-crash to pinpoint the steps to reproduce crash errors and come up with work-arounds (it does my head in how quickly it does this).

_Here’s a crash-causing issue discovered using this technique:_  
**Cloning a component’s main instance leaves it internally tracked as a “copy,” which crashes the server on the next unrelated persist.**

`.clone()` on a `mainInstance()` looks like a clean independent duplicate — commits fine, renders fine, survives several unrelated edits — but internally keeps a `:shape-ref` back to the original component. That makes it an orphaned “copy” shape, which Penpot’s backend only permits nested inside another component’s structure. The plugin API never rejects this at clone or append time; the crash only fires on the _next_ persist that happens to touch the shape, which is why it took 3 reproductions to trace back to the real cause instead of whatever edit triggered it.

`report.txt` is the actual server crash trace confirming this (`:code :nested-copy-not-allowed`, shape named `"input-error"`, Penpot 2.16.2-RC1) — it matches exactly the crash I hit and fixed on **2026-07-03** while building `controls / input`’s error/focus/disabled variants.

Fix: always `.detach()` immediately after cloning a main instance, before any other edit.

---

<div class="post-metadata">

**Author:** ![alotor](https://dub1.discourse-cdn.com/flex017/user_avatar/community.penpot.app/alotor/32/42_2.png) [@alotor](https://community.penpot.app/u/alotor)\
**Post date:** [July 6, 2026, 9:32am UTC](https://community.penpot.app/t/mcp-api-issue-list/10700/5 "2026-07-06T09:32:30Z")

</div>

@radziszcze We’re releasing this week, unless there is a last minute blocker. Usually we release cloud first and when everything is working fine (there is always some surprises when deploying) we generate the images for self-hosted.

@HeyTC Thanks for the details, I’ll look into it

---

<div class="post-metadata">

**Author:** ![radziszcze](https://dub1.discourse-cdn.com/flex017/user_avatar/community.penpot.app/radziszcze/32/10744_2.png) [@radziszcze](https://community.penpot.app/u/radziszcze)\
**Post date:** [July 7, 2026, 10:52am UTC](https://community.penpot.app/t/mcp-api-issue-list/10700/6 "2026-07-07T10:52:56Z")

</div>

@alotor will the release be announced? I’m asking, because I’ve reported a few bugs and I’m interested in testing them right after the release gets shipped to production.

---

<div class="post-metadata">

**Author:** ![alotor](https://dub1.discourse-cdn.com/flex017/user_avatar/community.penpot.app/alotor/32/42_2.png) [@alotor](https://community.penpot.app/u/alotor)\
**Post date:** [July 7, 2026, 12:23pm UTC](https://community.penpot.app/t/mcp-api-issue-list/10700/7 "2026-07-07T12:23:47Z")

</div>

@HeyTC I’ve investigated the report and we could find the issue. The plugins API could change the structure of component instances and this is something that the model forbids. We’ll add the change in the release.

> [@radziszcze](#):
>
> @alotor will the release be announced? I’m asking, because I’ve reported a few bugs and I’m interested in testing them right after the release gets shipped to production.

@radziszcze yes, of course

---

<div class="post-metadata">

**Author:** ![HeyTC](https://avatars.discourse-cdn.com/v4/letter/h/e79b87/32.png) [@HeyTC](https://community.penpot.app/u/HeyTC)\
**Post date:** [July 10, 2026, 12:01am UTC](https://community.penpot.app/t/mcp-api-issue-list/10700/8 "2026-07-10T00:01:48Z")

</div>

Wow - Thank you!

**# Penpot 2.17 Retest Report - Tracked Plugin-API Issues**  
Retested 2026-07-10 against a fresh test project/library (`Test Design File` / `Test Design System`), 15 isolated repro calls, each verified on a **fresh** `execute_code` call (not just same-call output, per this project’s established discipline).

**## Fixed (9)**

| Title | Description | Status |
| --- | --- | --- |
| Fills/strokes array elements read-only | `shape.fills\[0\].fillColor = ...` direct mutation now persists | **FIXED** - matches CHANGES.md #8357 |
| `openPage()` not synchronous | `await penpot.openPage(page)` now switches `currentPage` within the same call | **FIXED** - #10195 |
| `applyToken` fails on padding | `applyToShapes(shapes, \['paddingLeft'\])` no longer throws, binds and resolves correctly | **FIXED** - #10077 |
| Token application when target set is inactive | No longer throws; binds correctly, resolves once the set is activated | **FIXED** - #10070/#10071 |
| `TokenSet.active` toggle doesn’t refresh bound shapes | Activating a set now retroactively updates already-bound shapes’ resolved values with no re-apply needed | **FIXED** - closes a long-standing gotcha |
| Numeric token values rejected | `addToken({value: 24})` (raw number) no longer throws | **FIXED** - #10073 |
| `Board.combineAsVariants(ids)` fails 4/4 | Retested the exact failing pattern - now builds a real `VariantContainer` correctly | **FIXED** |
| Appending a child to a still-linked instance silently orphans it | Now throws immediately (`"Cannot change the structure of a component copy"`) instead of silently corrupting state and crashing later | **FIXED** (fails loud, not silent) |
| Editing a descendant’s property of a still-linked instance no-ops | `.characters` writes on a nested text now persist without detaching first | **FIXED** - closes the most-frequently-hit gotcha in the project |
| Large-batch `applyToShapes` (30+ shapes) silently no-ops | 40/40 shapes bound correctly in one call | **FIXED** |
| Cross-page/non-active-page mutation fails silently | Now throws a clear `"Cannot modify a page that is not currently active"` error instead of silent no-op | **IMPROVED** - active-page rule still applies, but now fails loud |
| Clone of a component main instance crashes later, unless detached | Simple edits now persist safely undetached; structural edits now fail immediately instead of corrupting the file on a later unrelated persist | **IMPROVED** |
