Perfboard Designer MCP server
An MCP server that lets an AI agent (Claude, or any MCP
client) design perfboard and stripboard layouts for
Perfboard Designer. The agent looks up parts, writes the board
as a project file, checks it, and gives you a share link. Clicking the link opens the board in
the editor, where you can look at it and change it.
"Lay out a 555 LED blinker on a small perfboard and send me the link."
It runs on your computer (stdio), needs no account and saves nothing to disk; or it can be hosted
on Cloudflare Workers (below). While it runs it keeps the projects an agent is working on in memory, each under a projectId, so the agent sends
a project at most once and then changes it with small edits. The project file stays the source of
truth: get_project returns it, and every tool also takes a whole file instead of an id.
Setup
Node.js 22 or newer. It's on npm as
perfboard-designer-mcp, which npx
downloads the first time.
Claude Code:
Claude Desktop (claude_desktop_config.json, Settings → Developer → Edit Config) and other
clients that take a command:
To run it from this repo instead, build it and use node /path/to/perfboard-designer/mcp/dist/server.js
as the command:
Links open the public editor, https://perfboarddesigner.com/app/. To point them at another copy (a
local dev server, say), set PERFBOARD_EDITOR_URL in the server's environment:
The server works on its own, without the hosted Worker. It
only points at it for two extras, each set in the server's environment like PERFBOARD_EDITOR_URL,
public unless set, and off to do without (agents are then not offered it):
Point them at a local Worker with http://localhost:8787/render.svg and http://localhost:8787.
Tools
Every tool that reads a project takes either projectId or the whole file as project. Kept
projects last as long as the server process (the 200 most recently used); on Cloudflare, 30 days
after they were last used.
Its instructions to agents are kept short (clients cut them off at about 2 KB); the full guide is
the resource perfboard://docs/guide (src/guide.ts).
It also serves the format as resources: perfboard://docs/project-file (the project file
format, field by field), perfboard://docs/share-links (the link format), perfboard://parts
(the whole parts catalog as JSON), and example projects: perfboard://examples/555-led-blinker (a
555 LED blinker on a perfboard, all wired with jumpers) and
perfboard://examples/555-led-blinker-stripboard (the same circuit on a stripboard), read from
../examples/.
Hosting it on Cloudflare Workers
The public copy runs at https://mcp.perfboarddesigner.com/mcp; how to add it to Claude Code,
claude.ai, ChatGPT, Cursor and other clients is on https://perfboarddesigner.com/mcp-server/
(../mcp-server/index.html).
The same server also runs as a Cloudflare Worker,
so an MCP client can reach it over the web with nothing installed: Streamable HTTP at /mcp
(src/worker.ts). It's stateless, every request getting a fresh server, so kept projects can't
live in memory: each is kept in its own Durable Object (ProjectHolder), named by its
projectId, and forgotten after 30 days unused. Project ids are random, so they can't be guessed.
There's no sign-in: anyone with the address can use it.
It also draws boards: /render.svg? followed by a share link's part after the # (name=…&z=…
or json=…; src= links aren't fetched) answers with an SVG picture of that board, drawn by the
editor's own board renderer (../src/board-render/) with the part icons baked in. /render.png?
with the same parameters answers with a PNG of it instead, for places that don't show SVG, such as
chat apps and link previews. An address says everything in its picture, so it's cached for good
(src/image-requests.ts).
The PNGs, from /render.png and render_board, are drawn from the same SVG by
resvg, compiled to WebAssembly (@resvg/resvg-wasm,
MPL-2.0), so they come out the same from the Worker and from stdio (src/board-png.ts).
Drawing one takes around a tenth of a second of CPU for a small board, far more than the Workers
Free plan's 10 ms a request, so the hosted server needs a paid plan to draw them.
So pictures aren't drawn again for every request, each Cloudflare data center keeps a copy of
every picture and stored project file it serves (the Workers Cache
API), for as long as its
Cache-Control says: a year for /render.*, a day for stored projects. Repeat requests there
are answered from it (CF-Cache-Status: HIT) without drawing again or waking the stored project's
Durable Object (edgeCached() in src/worker.ts). Only answers that
worked are kept, so an unknown id is looked up again next time. After a change to how boards are
drawn, old pictures stay until they expire or the zone's cache is purged in the Cloudflare
dashboard. The cache does nothing on the workers.dev address, only on the custom domains.
Stored projects
A share link carries the whole project, so it works without any server, but it gets long. The
Worker can also store a project under a short id and serve it, at
https://projects.perfboarddesigner.com (the same Worker, at its own address so the storage could
move to a Worker of its own without breaking links). It's the project storage behind short links,
for agents (make_share_link with store) and for people (the editor's Share dialog), documented
for both in docs/share-links.md:
- Ids are lowercase letters, digits and dashes, made from the wanted id or else the name
(
"Mágneses kapcsoló!" → magneses-kapcsolo), at most 40 characters, ignoring case. If another
board has that id, a few characters of the board's SHA-256 are added (led-blinker-08196a);
storing the same board again under the same id gives the same id back. With neither, the id is
the hash. Ids shorter than 3 characters or naming the server's own addresses (mcp, render,
projects…) always get the hash added.
- A stored project never changes: storing again makes a new id. It's deleted after a year
unopened (every request for its file or pictures pushes that back).
- Only valid project files are stored, as the editor reads them, up to 1 MB.
- The short link is a
p= share link (…/app/#name=LED%20blinker&p=led-blinker), which the
editor opens from this storage without asking. Stored anywhere else (PERFBOARD_PROJECTS_URL),
it's a src= link to the file instead, which any editor opens. Anyone with a link can read the
board.
- Each stored project is a Durable Object (
StoredProjectHolder) named by its id, so two boards
stored at once can't both get an id (src/stored-projects.ts, src/project-requests.ts). The
stdio server stores through POST /projects.
There's no sign-in or rate limit yet: anyone can store projects.
Querying stored projects
The listing is public and requires no token. Deploy from mcp/:
Then query it:
Omit q to list all indexed projects. Search matches a literal substring of the id or name
(ASCII case-insensitive). limit defaults to 20 and accepts 1–100. The response is
{projects: [{id, name?, storedAt, link, file, image, png}], nextCursor}; storedAt is the
original save time in Unix milliseconds. Pass nextCursor as the URL-encoded cursor query
parameter with the same q to get the next page; null means there are no more results.
Listings are not cached and do not extend projects' lifetimes.
The StoredProjectIndex Durable Object tracks new saves and removes expired projects. Existing
projects from before this index was deployed appear when saved again or read from their storage
object (an edge-cache hit does not reach storage). This is not a complete historical inventory:
there was no central index before. Temporary MCP working projects are not included.
For local use, run npm run worker:dev and query http://localhost:8787/projects.
Try it locally (at http://localhost:8787/mcp):
Publish it to your Cloudflare account (npx wrangler login once first):
Both build first; worker:dev serves stored projects at http://localhost:8787 too. wrangler.toml
is the Worker's config: its name, its domains, the Durable Object bindings and, in [vars],
PERFBOARD_EDITOR_URL for where links open and PERFBOARD_PROJECTS_URL for where stored projects
are served. Then add it to a client by its
address:
On the Workers Free plan a request gets 10 ms of CPU time, which checking a big board can run
over; the Paid plan allows far more.
Publishing a release
The package is on npm as perfboard-designer-mcp, and the server is listed in the
MCP Registry as com.perfboarddesigner/mcp, described
by server.json: the hosted address (remotes) and the npm package (packages). The registry
checks that package.json's mcpName is that name, and that you own perfboarddesigner.com, by a
DNS TXT record on the apex domain holding a public key.
- Bump the version:
npm version patch (or minor), and set the same version in server.json,
both at the top and in its npm package entry.
npm publish (it builds first; npm asks for 2FA). Wait until npm view perfboard-designer-mcp version shows the new version.
- Deploy the Worker if the server changed:
npm run worker:deploy.
- Publish to the registry with
mcp-publisher:
mcp-publisher validate, then mcp-publisher login dns --domain perfboarddesigner.com --private-key "$PRIVATE_KEY" and mcp-publisher publish, run in mcp/.
The key pair is made once with OpenSSL 3, its public half added as a TXT record on
perfboarddesigner.com (Cloudflare DNS); keep key.pem outside the repo:
How things connect
Wires are insulated jumpers: a wire joins only the holes at its two ends. Running over a hole
doesn't connect it, and two wires crossing aren't shorted, so a crossing is fine (the editor marks
it so it doesn't pass for a joint, and the 3D view arches the upper wire). A hole takes one part's
pin or lead at most (a second part there is an error, as on a real board) plus any number of wire
ends, and everything in a hole is joined: to connect two parts, run a wire between their pins.
A jumper can lie on the solder side (side: "bottom", format version 9): it joins its end holes the same way but never crosses a top wire.
On a stripboard, every hole along a strip is joined up to the next cut, and a cut hole connects to
nothing. get_nets and validate_project work this out with the same code as the
editor's Nets panel, so they agree with what you see there.
How it's built
src/ is the server: create-server.ts puts it together, server.ts runs it over stdio and
worker.ts on Cloudflare, tools/ holds the tools by group, resources.ts
the documents it serves, project-handles.ts the kept projects (and where they're stored), edit-ops.ts and
edit-project.ts edit_project's ops, validate.ts the checks, guide.ts its instructions and
guide, board-view/ the page show_board shows the board in (an MCP App; view.ts runs in the
client's frame and speaks the few MCP Apps messages it needs itself, so the page stays a few KB). It builds on the editor's own pure code, all of it taken through editor.ts, the one file
that imports from ../src/: the project file reader, share-link code, pin positions, nets
(nets.ts) and wire crossings (wire-crossings.ts) in ../src/project-file/, the part
definitions and part geometry in ../src/parts/, and the board renderer (../src/board-render/)
for pictures of a board. That way it always agrees with the editor about the format, the parts and what's
connected. The editor never imports anything from here. npm run build bundles that code and the MCP SDK into
a single dist/server.js (and dist/worker.js for Cloudflare) with no runtime dependencies,
and bakes in the docs and examples it serves, the part icons its pictures embed and
show_board's page (board-view/view.html with view.ts bundled for the browser into it). PNG pictures
also need resvg's WebAssembly, which the build copies beside them as dist/resvg.wasm (uploaded
with the Worker), and fonts, since resvg can't use the system's: Noto Sans and Noto Sans Mono,
cut down to the characters boards use, in fonts/ (SIL Open Font License; fonts/README.md
says how to remake them).
Requirements and component identities
The shared reader supports format 8, including optional independent requirements; format 7
adds project-scoped component instanceId fields. edit_project preserves all intended nets,
gives new parts fresh identities, and includes them in the requirement inventory without
assigning connections. Removed required parts remain reportable by identity, even if another
part reuses their reference name. A schematic field from older editors is ignored.
When requirements.validate is true, checks also compare the board with the saved requirements:
missing components/pins, missing connections and unexpected joins are errors; incomplete coverage
is a warning and never described as a full requirement match. Unspecified pins remain unchecked.
This is independent of the existing complete-list expectNets contract, which is unchanged.
The project-format resource includes the requirement types.
Deploy this shared reader with the frontend before sharing version-8 files through hosted
storage. Embedded links remain usable without hosted storage.