feat(infra): bind patched Penpot MCP into designer + design-reviewer containers #65
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!65
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "dev/64"
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
penpot-mcp-server(patched build from./penpot-mcp-server/) into the worker image viapython3-pip+pip install --break-system-packages~/.config/claude-hooks/{authelia-creds,penpot-cookie,penpot-creds}and callsetPenpotMcpConfigpenpotMCP server and allowmcp__penpot__*tools only for agents with"penpot_mcp": trueinagents.json(designer + design-reviewer); boss/dev/reviewer are unaffectedscripts/smoke-creds.shto probe binary presence and Python package importability for design agentsCLAUDE.mdwith secret-file layout, which containers carry the MCP, and smoke-test invocationCloses #64
Test plan
just qapasses (typecheck + lint + format + 234 tests)docker build .succeeds —penpot-mcpbinary present at/usr/local/bin/penpot-mcpscripts/smoke-creds.sh designer design-reviewerreports OK for binary + package checksmcp__penpot__*tools instead of aborting🤖 Generated with Claude Code
Install penpot-mcp-server from ./penpot-mcp-server/ into the worker image (python3-pip + pip install --break-system-packages). Load Penpot secrets from ~/.config/claude-hooks/{authelia-creds,penpot-cookie,penpot-creds} at startup. Register the penpot MCP server (command: penpot-mcp, env: auth secrets) and allow mcp__penpot__* tools only for agents with penpot_mcp:true in agents.json — currently designer and design-reviewer. Extend smoke-creds.sh to probe binary presence and package importability for those agents. Update CLAUDE.md with secret-file layout, which containers carry the MCP, and smoke-test invocation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Review of feat(infra): bind patched Penpot MCP into designer + design-reviewer containers
CI: ✅ green (run #1573, 2m36s, success)
Acceptance criteria: all met — image bakes penpot-mcp via pip from vendored source, MCP registration is conditional on
penpot_mcp: true, all required env vars are wired, smoke script extended, CLAUDE.md updated.The implementation is structurally sound. One real bug to fix before merging.
REQUEST_CHANGES
src/main.ts—autheliaPassis unguarded; produces"username:undefined"on a malformed secret fileIf
~/.config/claude-hooks/authelia-credshas only one line (e.g. trailing newline stripped, or accidental copy-paste),split("\n")returns a one-element array andautheliaPassisundefinedat runtime (TypeScript doesn't catch this — destructuring beyond the array length givesundefined, not a type error). The template literal silently coerces it to the string"undefined", so Authelia seesuser:undefinedand returns 401 — with no startup warning, becausesetPenpotMcpConfigsucceeds.Compare with the
penpotCredsRawblock immediately below, which correctly guards both values:Fix — apply the same pattern, and optionally throw early on a clearly malformed file:
Or at minimum:
The throw form is better here because an empty password is as useless as
undefined, and failing loudly at startup is more diagnosable than a 401 on the first designer task.Non-blocking observations (no change required)
PENPOT_PUBLIC_URLis set but never read by the MCP.src/main.tspassesPENPOT_PUBLIC_URL: "https://design.jacquin.app"in the MCP env, butpenpot-mcp-server/src/penpot_mcp/config.pyhas noPENPOT_PUBLIC_URLvariable (confirmed by grep). The env var is harmless — it just has no effect. If it was intended for future deep-link generation, a comment noting that would help; otherwise it can be dropped. Not blocking.Smoke test probes binary + import rather than actual tool listing.
The issue AC said "something like
list-tools | grep -c mcp__penpot__". The PR checkstest -x /usr/local/bin/penpot-mcpandimport penpot_mcp, which is weaker but faster and avoids spinning up the full MCP stdio loop in CI. Acceptable given the AC's "something like" phrasing and the real end-to-end test being the #62 re-dispatch.autheliaPassis not guarded againstundefined. Ifauthelia-credshas only one line,split("\n")returns a one-element array,autheliaPassisundefinedat runtime, and the template literal produces"username:undefined"— silently passing a wrong credential to Authelia.Fix:
This is consistent with how a malformed
penpot-credswould fail (the?? ""guards there surface at call time, butAUTHELIA_BASIC_AUTHfails silently with a plausible-looking string).Fixed in
f18ff55. Added the early throw on malformedauthelia-credsexactly as suggested — an empty password is as useless asundefined, so failing at startup is the right behavior.CI: ✅ green (run #1574, 2m36s,
f18ff55)All acceptance criteria from #64 are met: image bakes the patched
penpot-mcp-servervia pip, MCP is gated onpenpot_mcp: true, all five env vars are wired, smoke script extended, and CLAUDE.md updated. The overall design is clean and mirrors the forgejo-mcp pattern correctly.Two issues need fixing before merge — both in
src/main.ts, both in the startup secret-loading block.Issue 1 —
penpot-credsnot validated (inconsistent with authelia-creds guard)File:
src/main.ts, startup block ~line 520authelia-credswas just hardened with an explicit guard (the head commit, "fix(infra): guard autheliaPass against undefined on malformed authelia-creds") that throws at startup if either line is missing.penpot-credsuses the same two-line format but silently falls back to"". The AC notes "empty strings cause an earlier crash per the.envcomment" — meaning a malformedpenpot-creds(e.g. single-line, or CRLF-terminated) would silently configure the MCP withPENPOT_PASSWORD="", then crash the MCP process only when a designer task actually runs, with a confusing error.Fix: apply the same pattern:
Issue 2 —
penpot-cookienot validatedFile:
src/main.ts, startup block ~line 517An empty or missing
penpot-cookiefile producesPENPOT_AUTH_TOKEN_COOKIE: "". Since this instance haslogin-with-passworddisabled (OIDC-only), the cookie is the only working auth path. An empty cookie would cause a 401 inside the running MCP, surfacing as a confusing tool error at task time rather than a clear startup failure.Fix:
Minor —
PENPOT_PUBLIC_URLmay be a dead env varFile:
src/main.tsPENPOT_PUBLIC_URL: "https://design.jacquin.app"is passed to the MCP process, but the only config.py change in this diff addsPENPOT_BASE_URLsupport — there's noPENPOT_PUBLIC_URLread anywhere in the diff. This is harmless if the MCP consumes it elsewhere in the package, but worth confirming it's actually read. If not, remove it to avoid config drift.Missing validation for
penpotPassword— ifpenpot-credshas fewer than 2 lines,penpotPasswordisundefinedand""is silently passed asPENPOT_PASSWORD. Per the AC, empty strings crash the MCP at task time. Add the same guard used for authelia-creds:Fixed in
bbb89ef— all three issues addressed:cookie, throw if empty — OIDC-only instance makes this the only working auth path.penpotEmail/penpotPasswordare now used directly without?? ""since we've asserted they're non-empty.Review: APPROVED
CI is green (
success, SHAbbb89ef, 2m41s). All acceptance criteria from #64 are met.AC checklist
Image
penpot-mcp-servervendored viaCOPY penpot-mcp-server/ ...+pip install --break-system-packages— mirrors the forgejo-mcp bake-in pattern exactly../penpot-mcp-server/in the tree; no external fetches at build time.MCP registration
penpotMCP entry registered inagent-runner.tswithcommand: "penpot-mcp", matching the forgejo-mcp invocation style.penpot_mcp: trueinagents.json— onlydesigner+design-reviewer;boss/dev/revieweruntouched.Env vars
AUTHELIA_BASIC_AUTH,PENPOT_AUTH_TOKEN_COOKIE,PENPOT_EMAIL,PENPOT_PASSWORD,PENPOT_BASE_URLall loaded and passed.PENPOT_PUBLIC_URLintentionally dropped ("drop dead PENPOT_PUBLIC_URL" in the head commit). Correct —penpot-mcp-server/src/penpot_mcp/config.pyonly readsPENPOT_BASE_URL/PENPOT_URL;PENPOT_PUBLIC_URLwas dead config.Startup behaviour
Promise.all; failure logs a warning but does not crash the service, so non-design tasks are unaffected.Smoke test
scripts/smoke-creds.shextended with binary executability +python3 -c "import penpot_mcp"checks.list-tools | grep mcp__penpot__probe was not implemented — the binary + import approach is actually more reliable here since it doesn't require a live Penpot connection at smoke time. Acceptable trade-off.Docs
CLAUDE.mdupdated with secret-file layout, which containers carry the MCP, and smoke script invocation.Code quality
penpot_mcp?: booleanadded consistently throughwebhook-config.ts→worker.ts→agent-runner.ts→main.ts. Propagation chain is clean.config.pydual-alias (PENPOT_BASE_URL||PENPOT_URL) is a sensible backwards-compat shim.args: []for the penpot-mcp invocation is consistent with howforgejo-mcpis called; correct for a stdio MCP binary.No logic bugs, unhandled errors, security issues, or scope creep found. Ready to merge.