Patch Penpot MCP: add export_frame_png(file_id, page_id, frame_id, scale?) #74
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks#74
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
User story
As the designer agent, I want an
export_frame_pngMCP tool sothat the
design-implement.mdskill's sanity-check step (render theframe, check for overflow / frame-fill bugs before the handoff) is
actually callable, and so that
design-reviewercan consume PNGsfor multimodal visual review.
Carved out of #69 as a follow-up; PR #71 lands every canvas write
primitive but explicitly defers this because recent Penpot builds
render PNGs client-side via wasm — there is no stable server-side
export RPC on our instance that would give us raster output in a
single call.
Context / uncertainty
Probed against our live instance on 2026-04-19 and none of the
obvious endpoints work:
So the first job of this ticket is figuring out the right path,
not writing the wrapper. Three plausible paths, pick whichever lands
first:
chromium-headlessinto theclaude-hooks:devimage. Toolopens
https://design.jacquin.app/#/render-object?file-id=…&object-id=…(Penpot's single-frame share URL), waits for load, screenshots,
returns base64 PNG. Reuses Penpot's own wasm renderer — fidelity
matches the UI exactly. Cost: ~150 MB in the image, a puppeteer /
playwright dep.
internal exporter microservice that the UI uses for SVG/PNG
export. On our self-host it's the
exportercontainer — reachableonly on the internal Docker network today. Would need a network
alias or a public route. Gives PNG-per-frame natively but adds
deployment surface.
export-shapes-svgRPC(if present — needs probing) returns SVG string; the MCP tool
rasterises via Pillow / cairosvg. Avoids the Chromium dep but
font fidelity can drift from Penpot's wasm renderer.
Need to pick one during implementation. Probably (1) — matches UI
fidelity, simplest mental model for the designer / design-reviewer.
Acceptance criteria
MCP — new tool
export_frame_png(file_id, page_id, frame_id, scale?=1)inpenpot-mcp-server/src/penpot_mcp/tools/canvas.py.Returns
{png_bytes_base64, width, height, mimetype}.scale≥ 1, ≤ 4. Retina defaults sane.PenpotAPIErrorwith a distinguishable code(
export-failed) rather than silently returning empty bytes.Implementation plumbing (option-1 path)
claude-hooks:devimage installschromium+playwright(Python binding, because the MCP is Python).Add a
just containers-rebuildnote about the image sizedelta.
scripts/smoke-creds.shextends the Penpot probe: assertplaywrightimportable +chromium --versionsucceeds insidethe
designer/design-reviewercontainers. Fails loudly ifeither is missing rather than crashing mid-dispatch.
Tests
calls
page.goto(expected_url)andpage.screenshot(…, scale).pytest -m live): create a temp page + frame on theclaude-hooks — dashboardfile, export PNG, assertwidth > 0andpng_bytes_base64decodes to valid PNGmagic bytes (
\x89PNG…). Cleanup deletes the page.Skill update
skills/design-implement.mdalready mentions the export stepas "sanity check — crashes and frame-fill bugs are easier to
catch before the reviewer sees them". Currently treated as
optional because the tool didn't exist. Re-mark it as required
on this ticket's landing.
Out of scope
Reviewer can call the tool multiple times.
References
get-file-object-thumbnail,render-wasm,get-file-thumbnail.fork): upstream Penpot repo,
backend/src/app/rpc/commands/+the
exporterservice.Dependencies
ready to merge.
design-implementsanity-check step anddesign-reviewer's multimodal image input path. Design-reviewercould manually screenshot today, but the automation needs this.
mainafter #71 lands.Probe findings (2026-04-20)
Hit the live Penpot instance at
design.jacquin.appwithAuthorization: Token <PENPOT_ACCESS_TOKEN>. Full matrix:RPC commands — all
{"type":"not-found"}Plus the three from the ticket body (
get-file-object-thumbnail,render-wasm,get-file-thumbnail) that were already known dead. Option 3 (SVG rasterize) is dead — there's no SVG-export RPC on this instance.HTTP routes — more interesting
The 502 on
/api/exportis the signal. nginx is configured to proxy that path to an upstream exporter microservice, but the upstream isn't responding. Concretely: our self-host has the route wired — theexportercontainer just isn't running or isn't reachable from the nginx host.Decision
This reframes the three options from the ticket:
claude-hooks:devdesign.jacquin.apphost; zero cost to our imageexport-shapes-svgRPC existsRecommend Option 2. If the operator can bring the
exportercontainer online on the Penpot host (likely a one-linedocker composeor missing service in the self-host stack), we get PNG export via Penpot's own wasm renderer, matching the UI exactly, with no image bloat on our side.export_frame_png(file_id, page_id, frame_id, scale?)then becomes a thin POST to/api/exportwith the right payload shape.Fallback to Option 1 only if the exporter service is hard to revive for ops reasons (e.g. upstream removed it, dependencies changed).
Next step
Operator decision: bring up the Penpot exporter service on
design.jacquin.app, or greenlight the Chromium route.Operator decision: closing as deferred.
Option 1 (Chromium in
claude-hooks:dev) is too heavy — +150MB per image rebuild, Playwright dep, Chromium auto-update maintenance, container-wide for only 2 agents.Option 2 (exporter microservice) requires Penpot host ops work the operator isn't planning to invest in now.
Option 3 (SVG rasterize) is dead — no RPC exists.
Design-reviewer's current spec-cross-check flow (validated on #62 and #70) produces useful reviews without visual PNG inspection. The quality delta doesn't justify the cost today.
Reopen when the cost/benefit flips (e.g. a design change needs pixel-level review, or Penpot upstream ships an RPC, or the Penpot host gets an exporter container on a non-claude-hooks maintenance pass).