feat(penpot-mcp): add export_frame_png/svg via Penpot's /api/export #186
No reviewers
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!186
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/185"
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?
Summary
Brings frame export back to the
designer/design-reviewerflow nowthat the self-hosted Penpot exporter (CT 245) is online (2026-04-20).
Two new MCP tools call Penpot's exporter directly at
${PENPOT_BASE_URL}/api/export, bypassing upstream MCP's stale/assets/by-id/<id>path that 404s on Penpot 2.14.mcp__penpot__export_frame_png(file_id, page_id, object_id, scale=1.0)—posts the export spec, reads PNG bytes from the response, returns
{content_base64, mime_type="image/png", width, height}. Width/heightcome from the PNG's IHDR chunk — no extra image-parsing dependency.
mcp__penpot__export_frame_svg(...)— parallel tool for vector output.Returns
{content, mime_type="image/svg+xml", width, height}withcontentas a plain UTF-8 string (no base64 round-trip).Exporter failures surface as typed
PenpotExporterErrorwith codes soagents can branch without string-matching:
exporter-unavailable(502) → exporter container is down; surfacedso the agent stops retrying.
unable-to-upload-resource→ appends the host-side chown hint(
UID 1001:1001on/var/lib/penpot/assets) so operators fix theright layer.
Shared HTTP plumbing lives in
services.api.PenpotAPI.export_shape(...), reusing the existingAuthorization: Tokenclient.Changes
penpot-mcp-server/src/penpot_mcp/tools/export.py— new module.penpot-mcp-server/src/penpot_mcp/services/api.py—export_shapehelper +
PenpotExporterError+_parse_exporter_error.penpot-mcp-server/src/penpot_mcp/server.py— registers the newtool module.
penpot-mcp-server/src/penpot_mcp/tools/canvas.py— stale"export deferred" note removed; docstring points at the new module.
penpot-mcp-server/tests/tools/test_export.py— 19 tests coveringtool-layer, HTTP-layer, and helpers (happy + error paths).
penpot-mcp-server/pyproject.toml— version bumped to0.7.0+claude-hooks.1.penpot-mcp-server/CHANGELOG.md— 0.7.0 entry.CLAUDE.md— "Penpot MCP auth" section calls out the new tools.scripts/smoke-creds.sh— adds a presence probe for the new toolson design containers + opt-in live-export probe gated on
PENPOT_SMOKE_{FILE,PAGE,OBJECT}_ID.Test plan
PYTHONPATH=src pytest tests/tools/test_export.py— 19 passedPYTHONPATH=src pytest tests/tools/ tests/test_canvas.py tests/test_get_design_tokens.py— 40 passedbun x turbo run typecheck— 3/3 cached greenbun x turbo run test— 719 pass (server + web + shared)bun x biome check .— cleanbash -n scripts/smoke-creds.sh— syntax OKscripts/smoke-creds.sh designer design-reviewerto confirmexport_frame_png/_svgimport cleanly in the container.export_frame_pngagainst the seeded framefbb66a40-7b05-40bc-88a6-886a66a80a05in file689d7fa4-f94b-81d4-8007-e38d1256f1ae, confirm PNG writes to disk and matches the Penpot UI rendering.Notes
tests/test_tokens.pyfailures (FastMCP's_toolsprivate attribute renamed upstream) are unchanged by thisPR — verified via
git stash+ re-run before my changes landed.updating #181's handoff AC and rebuilding the design-agent images
are separate commits.
Closes #185.
🤖 Generated with Claude Code
Bring frame export back to the designer / design-reviewer flow now that the self-hosted exporter (CT 245) is online. New tools call Penpot's exporter directly at ${PENPOT_BASE_URL}/api/export, skipping upstream MCP's stale /assets/by-id/<id> path that 404s on Penpot 2.14. Surfaces exporter-unavailable (502) and unable-to-upload-resource as typed PenpotExporterError so the agent can branch without string-matching. Closes #185. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Review — Round 1 ✅ APPROVED
CI: ✅ run #1787 (
success) on head7305b49.All acceptance criteria from issue #185 are satisfied. Implementation is clean and safe to merge.
Acceptance criteria — all met
export_frame_png+export_frame_svgtools intools/export.py/api/exportdirectly — avoids stale/assets/by-id/<id>path{content_base64, mime_type, width, height}/{content, mime_type, width, height}Authorization: TokenHTTP plumbing withservices/api.pyexporter-unavailable(502) andunable-to-upload-resourcehandled as typedPenpotExporterErrorserver.pytests/tools/test_export.py— 19 tests, happy + all error pathscanvas.pystale "export deferred" note removedCHANGELOG.md0.7.0 entry,pyproject.tomlbumped,CLAUDE.mdupdatedsmoke-creds.shpresence probe + opt-in live probeCode review — no blockers
PNG parsing (
_png_dimensions): reads IHDR width/height from bytes 16–23 per spec §11.2.2 — correct and dependency-free. The 24-byte minimum guard and signature check are tight and correct.SVG dimension regex (
_svg_dimensions):_SVG_ROOT_TAGscopes the attribute search to the opening<svg>tag, so inner<rect width="...">attrs cannot leak. Falls back to(0, 0)when explicit dimensions are absent — documented, harmless, and tested.Error handling layering: 502 is intercepted before
is_success, yielding a namedexporter-unavailableerror; all other non-2xx go through_parse_exporter_errorwhich handles non-JSON bodies, bare arrays, and structured Penpot error objects. TheValueErrorfrom_png_dimensionson a corrupt PNG is deliberately unguarded — surfacing the bug rather than encoding garbage to the agent is the right call.Test monkeypatch:
monkeypatch.setattr(api, "_get_client", _get_client)sets an instance attribute on the singleton, soawait self._get_client()invokes the zero-arg async stub correctly (noselfpassed, by Python's instance-attribute lookup semantics). ✅Smoke live probe: omits
-e PENPOT_ACCESS_TOKENfromdocker execbecause the token is already present in the container env via the credentials bind dir —api._get_client()readsconfig.PENPOT_ACCESS_TOKENat call time, so the probe is correct.30s httpx timeout is inherited by
export_shapefrom the shared client. Tight for slow headless-browser renders of complex frames, but acceptable for an initial implementation; operator can increase if needed once load patterns are known.Post-merge checklist (from PR itself, not blocking merge)
penpot-mcp-serverDocker build stage; rebuilddesigner/design-reviewerimages.scripts/smoke-creds.sh designer design-reviewerto confirmexport_frame_png/_svgimport cleanly in the new images.export_frame_pngagainst framefbb66a40-7b05-40bc-88a6-886a66a80a05in file689d7fa4-f94b-81d4-8007-e38d1256f1ae, confirm PNG output and visual match.