# Penpot MCP — strings de error reales

Mensajes de error **literales** que devuelve el servidor MCP de Penpot cuando un `execute_code` falla.

**Por qué existe este archivo.** El grupo D de `data/raw/seeds/penpot.jsonl` (6 seeds de auto-corrección)
entrena al modelo a diagnosticar y recuperarse de errores. Si el mensaje de error de un seed es
inventado, el modelo aprende a reaccionar a un string que nunca va a ver. Este archivo es la
**allow-list de mensajes de error**: `scripts/07_lint_penpot_code.py` verifica que todo `content` de un
mensaje `tool` que representa un error sea un string presente acá, verbatim.

**Procedencia.** Los mensajes de las secciones 1-4 fueron capturados en vivo por la sesión madre el
2026-07-30, contra el archivo real del usuario, ejecutando el código de la columna "Disparador" vía
`mcp__penpot__execute_code`. Están registrados en `PLAN.md` § "La API verificada" y en la página de
Docmost de la Fase 6 § "Diagnóstico". Los de la sección 5 son mensajes estándar del runtime de
JavaScript (V8/SpiderMonkey), deterministas dada la expresión que los produce.

**Nota operativa (2026-07-30):** al intentar re-capturar estos strings durante el paso 6.1.3, el
servidor MCP devolvió `No Penpot plugin instances are currently connected. Please ensure the plugin is
running and connected.` — el plugin del usuario no está conectado. Los mensajes de abajo se conservan
tal como los verificó la sesión madre; **la re-verificación en vivo queda pendiente** y se hace junto
con el baseline de la puerta 5 (paso 6.3.17), que también requiere el plugin conectado. Ningún seed
puede usar un string de error ausente de este archivo.

---

## 1. Propiedad inexistente sobre un objeto no extensible

El objeto `Text` (y en general los shapes) **no es extensible**: asignar una propiedad que no está en
la interfaz lanza, y la excepción **aborta todo el `execute_code`**, no solo esa línea.

| Disparador | Mensaje verbatim |
| --- | --- |
| `text.textAlign = 'center'` | `Cannot add property textAlign, object is not extensible` |
| `text.color = '#...'` | `Cannot add property color, object is not extensible` |

Forma del mensaje, generalizable: `Cannot add property <nombre>, object is not extensible`.

**`text.color` (capturado en vivo, puerta 5 modo full, paso 6.4.25, prompts `g5-06-landing-pizzeria` y
`g5-01-boton-primario`, 2026-08-03):** el modelo asume que `Text` tiene una propiedad `color` de nivel
superior (analogía con CSS) en vez de usar `fills`. Es el mismo mecanismo que `textAlign` —el objeto
`Text` no es extensible— pero para una propiedad distinta, y hoy no hay ningún seed que lo enseñe (el
lint ya lo detecta como patrón prohibido, cero ejemplos de recuperación en el corpus).

La propiedad correcta para alineación horizontal de texto es **`align`**
(`'left' \| 'center' \| 'right' \| 'justify'`). Ver `penpot_api_docs.md` Captura 4.

**Este es el error más caro del conjunto**, porque el modelo suele escribir `textAlign` por analogía con
CSS, en medio de un payload de 80 líneas que construye una landing entera: la excepción tira abajo la
composición completa, no solo el título.

---

## 2. Función inexistente en el runtime

| Disparador | Mensaje verbatim |
| --- | --- |
| `penpotUtils.importImage(url)` | `penpotUtils.importImage is not a function` |
| `penpot.importImage(url)` | `penpot.importImage is not a function` |
| `typography.setFont(font)` sobre un `LibraryTypography` | `t.setFont is not a function` |

**`penpot.importImage` (capturado en vivo, puerta 5 modo full y holdout, paso 6.4.25, 2026-08-03):**
variante distinta de la ya documentada `penpotUtils.importImage` — el modelo la invoca directamente
sobre el objeto `penpot` (no sobre `penpotUtils`), en 1 de los 10 prompts de la puerta 5 modo full
(`g5-03-card-producto`) y en 4 de los 60 del holdout, siempre en prompts que piden "una foto real
traída de una URL". El único seed de recuperación existente (grupo D) enseña la variante
`penpotUtils.importImage`, no ésta — de ahí que el modelo no generalizara la lección.

Tres casos distintos, y la diferencia importa pedagógicamente:

- `penpotUtils.importImage` **nunca existió**: no está en la Captura 1 de `penpot_api_docs.md`, ni
  `import_image` está entre las herramientas MCP de este deployment (ver `data/schemas/penpot.json`).
  El overview la menciona en prosa (`Use the export_shape and import_image tools`), lo cual es un
  **anzuelo real**: la lista de herramientas que recibe el modelo es autoritativa, la prosa del
  overview no. La ruta correcta es `await penpot.uploadMediaUrl(name, url)`.
- `typography.setFont` **sí está en el tipo** (`penpot_api_docs.md` Captura 26) pero **no existe en el
  runtime de esta versión**. Es el caso canónico de "la documentación no es el runtime": hay que setear
  las propiedades de tipografía una por una (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`).
  Notar que el mensaje dice `t.setFont`, con el nombre de la variable local — no `typography.setFont`.

---

## 3. Uso de un retorno `null` sin chequear

La familia de errores más frecuente y la que produce la falla reportada por el usuario. Todas las
llamadas de creación y de búsqueda pueden devolver `null`; usar el resultado sin chequearlo produce un
`TypeError` en la **línea siguiente**, lo que hace que el stack apunte al síntoma y no a la causa.

| Disparador | Mensaje verbatim |
| --- | --- |
| `const t = penpot.createText(); t.characters = 'Hola'` | `Cannot set properties of null (setting 'characters')` |
| `const s = penpotUtils.findShapeById(page, id); s.x = 10` | `Cannot read properties of null (reading 'x')` |
| `const s = penpotUtils.findShapeById(idInexistente); return s.fills` | `Cannot read properties of null (reading 'fills')` |
| `const b = penpot.createBoolean('union', []); b.name = 'x'` | `Cannot set properties of null (setting 'name')` |
| `const r = penpot.createRectangle(); const f = r.addFlexLayout(); f.dir = 'row'` | `Cannot set properties of null (setting 'dir')` |

**`addFlexLayout()` sobre un `Rectangle` (capturado en vivo, puerta 5 modo full, `g5-02-navbar-flex`,
2026-08-03):** `addFlexLayout()` solo existe en `Board`. Llamarlo sobre un `Rectangle` (o sobre un
`Board` que ya tiene un layout asignado) devuelve `null` en silencio en vez de lanzar; el error real
aparece recién en la línea siguiente al intentar setear una propiedad del layout inexistente. Hay que
chequear el retorno de `addFlexLayout()` antes de usarlo, igual que con `createText()`.

Forma del mensaje, generalizable:
`Cannot read properties of null (reading '<prop>')` y
`Cannot set properties of null (setting '<prop>')`.

**Las tres fuentes de `null` que los seeds deben enseñar a chequear:**

1. `penpot.createText()` **sin argumento** o con `''` → `null`. Con un string no vacío → `Text`.
   El ejemplo de la documentación oficial (`penpot_api_docs.md` Captura 6) usa la forma sin argumento.
2. `penpotUtils.findShapeById(page, id)` — **la forma de dos argumentos no lanza**: devuelve `null` en
   silencio, porque `findShapeById` tiene aridad 1 y el segundo argumento se descarta mientras el
   primero (un `Page`, no un `string`) no matchea ningún id. La forma correcta es
   `penpotUtils.findShapeById(id)`.
3. `penpot.createBoolean(type, shapes)` y `penpot.createShapeFromSvg(svg)` declaran `null | T` en su
   firma (Capturas 10 y 11).

---

## 4. Fallos de red y de subida de medios

| Disparador | Mensaje verbatim |
| --- | --- |
| `await penpot.uploadMediaUrl('foto', 'https://dominio-inexistente.invalid/a.jpg')` | `Error uploading media` |

`uploadMediaUrl` devuelve una `Promise`; el fallo llega como rechazo, así que hay que envolverlo en
`try/catch` **con `await` adentro del `try`** (un `.catch()` colgado de la promesa sin `await` deja el
resto del payload corriendo con `undefined`).

---

## 5. Errores estándar del runtime de JavaScript

Deterministas dada la expresión, no específicos de Penpot. Se listan porque aparecen en los seeds del
grupo D y el lint los tiene que aceptar.

| Disparador | Mensaje verbatim |
| --- | --- |
| `undefined.foo` | `Cannot read properties of undefined (reading 'foo')` |
| `shapes.map(...)` donde `shapes` es `null` | `Cannot read properties of null (reading 'map')` |
| `JSON.parse('{')` | `Unexpected end of JSON input` |
| `board.appendChild()` sin argumento | `Cannot read properties of undefined (reading 'id')` |

---

## 6. Errores a nivel de servidor MCP (no de `execute_code`)

No son excepciones de JavaScript: los devuelve el servidor MCP antes de llegar al plugin. **No pueden
aparecer como resultado de un `execute_code` en un seed**, porque no dejan al modelo en una situación
de "diagnosticá tu código" — significan que no hay con qué hablar.

| Situación | Mensaje verbatim |
| --- | --- |
| El plugin de Penpot no está abierto / no está conectado al servidor MCP | `No Penpot plugin instances are currently connected. Please ensure the plugin is running and connected.` |

Capturado en vivo el 2026-07-30 durante el paso 6.1.3 de esta fase.

---

## 7. Fallos SILENCIOSOS — no producen ningún error

**La categoría más peligrosa**, y la razón por la que el invariante R8 (`leer de vuelta el efecto`)
existe. Estas operaciones **no lanzan**, **no devuelven `null`**, y dejan el diseño mal. Un modelo que
solo maneja excepciones no las detecta nunca.

| Operación | Comportamiento real | Cómo detectarlo |
| --- | --- | --- |
| `flex.horizontalSizing = 'auto'` | Se lee de vuelta como `'auto'` pero **el board no crece** (200×200 sigue 200×200 tras insertar un hijo de 300 de ancho). Bug conocido penpot#8520 | Leer `board.width` tras insertar; hacer `board.resize()` a mano |
| `layoutChild.zIndex = 10` | Se lee de vuelta como `0`. **Se ignora** | Leer `layoutChild.zIndex`; usar el orden del array `children` para z-order |
| `text.letterSpacing = '-0.02'` | Se lee de vuelta como `"0"`. Los valores negativos en unidades tipo `em` **se descartan** | Leer `text.letterSpacing`; usar unidades tipo px (`'-1'`) |
| `penpot.createRectangle()` sin asignar `fills` | Queda con el fill por defecto **`#B1B2B5`** — el gris exacto del problema reportado | Invariante R1: `fills` explícito en todo shape creado |
| `penpotUtils.findShapeById(page, id)` | Devuelve `null`, **sin lanzar** | Chequear el retorno antes de usarlo |
| `board.layout` | `undefined`; `'layout' in shape === false`. La propiedad **no existe** | Usar `board.flex` / `board.grid`, que devuelven `null` cuando no hay layout |
| `generateStyle(shapes, {withChildren: true})` | La opción real es `includeChildren`; `withChildren` **se ignora** y el CSS sale sin los hijos | Ver `penpot_api_docs.md` Captura 8 |
| `grid.appendChild(shape, 0, 0)` | El `0` **se clampea a 1**: se lee de vuelta `[1,1]`. Los índices son 1-based | Leer `shape.layoutCell.row` / `.column` |
| `text.resize(w, h)` | Setea `growType` a `'fixed'` en silencio; el texto desborda su caja | Invariante R6: restaurar `growType` tras `resize()` |
| Leer `text.width` justo después de setear `characters` | El auto-sizing **no es inmediato**: se lee `1` | Dormir ~120 ms antes de leer el bounding box |
