Perfboard Designer
RemoteAbout Perfboard Designer
Perfboard Designer is an MCP server for AI agents designing perfboard and stripboard circuits. It combines a parts catalog, layout editing, wiring checks, board rendering, and links that open projects in the editor.
Key features
- 01Look up parts and pin details
- 02Create and edit board layouts
- 03Render boards and connection nets
- 04Check wiring and circuit errors
- 05Generate editor share links
Use cases
- 01Lay out a 555 LED blinker
- 02Check component pins and wire connections
- 03Review and edit a board in the editor
- 04Load a shared board and continue editing
MCP support
The MCP server exposes tools for catalog lookup, project editing, rendering, validation, and share links. It also provides resources for the project format, parts catalog, guides, and examples.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://mcp.perfboarddesigner.com/mcpclaude mcp add --transport http com-perfboarddesigner https://mcp.perfboarddesigner.com/mcp{"mcpServers":{"com-perfboarddesigner":{"type":"http","url":"https://mcp.perfboarddesigner.com/mcp"}}}Open in Cursor{"servers":{"com-perfboarddesigner":{"type":"http","url":"https://mcp.perfboarddesigner.com/mcp"}}}Open in VS Code{"mcpServers":{"com-perfboarddesigner":{"type":"http","url":"https://mcp.perfboarddesigner.com/mcp"}}}{"mcpServers":{"com-perfboarddesigner":{"serverUrl":"https://mcp.perfboarddesigner.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
Details
From the project READMEPerfboard 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 mcp add --scope user perfboard -- npx -y perfboard-designer-mcp
Claude Desktop (claude_desktop_config.json, Settings → Developer → Edit Config) and other
clients that take a command:
{
"mcpServers": {
"perfboard": {
"command": "npx",
"args": ["-y", "perfboard-designer-mcp"]
}
}
}
To run it from this repo instead, build it and use node /path/to/perfboard-designer/mcp/dist/server.js
as the command:
cd mcp
npm install
npm run build
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:
{
"mcpServers": {
"perfboard": {
"command": "npx",
"args": ["-y", "perfboard-designer-mcp"],
"env": {"PERFBOARD_EDITOR_URL": "http://localhost:3000/app/"}
}
}
}
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):
| Setting | Extra | Default |
|---|---|---|
PERFBOARD_IMAGE_URL | make_share_link's image, a picture of the board (render_board's PNGs are drawn here and need nothing) | https://mcp.perfboarddesigner.com/render.svg |
PERFBOARD_PROJECTS_URL | make_share_link's store: a short link to a stored project. Nothing is sent unless an agent asks | https://projects.perfboarddesigner.com |
Point them at a local Worker with http://localhost:8787/render.svg and http://localhost:8787.
Tools
| Tool | What it does |
|---|---|
list_parts | Lists the parts a project can place (79 today). Without a query, every part by category, one line each (its id, name and pin count). With words such as "capacitor" or "555", the parts matching all of them, or the closest ones when none do. |
get_part | Parts in full, as many as you name in ids: which list each goes in, its pins and pin labels, polarity, value unit, holes its body covers, and an example entry to copy. |
new_project | Starts a project kept in memory and returns its projectId: a blank perfboard or stripboard of the size you give, or a copy of an example (example: "555-led-blinker"). |
edit_project | Changes a kept project with a list of ops, all or none: add_part (by part id, with at, or from/to for two-lead parts; refs are numbered if left out; or a part of the agent's own: custom, pins anywhere or a catalog part's package via shape with its own pin names, or chip, a dual-row chip or dev board, placed again later by name), update_part (move, turn, change value or ref), remove_parts, add_wire (from/to, with via for the holes it bends at, or a path of holes made into one wire per leg; any end can be a pin by name, e.g. "U1.7" or "D1.K", for the hole it's in), update_wire (ends by hole or pin too, via to replace its bends), remove_wires, set_board, add_cuts/remove_cuts, set_pad (a note on a hole). Returns what changed, where the new or moved parts' pins landed, and the board checked as validate_project does; when it has no errors, also its nets. |
get_project | The whole project file of a kept project. |
get_pin_positions | Where every placed part's pins land on the board after rotation, e.g. U1 pin 8 (VCC) at [9, 3], so wires can be routed to the right holes. |
render_board | Draws the board as text, one cell per hole with column and row numbers: pins by part key and number (a1), holes under a body, wire ends, holes a wire runs over without joining them, strip cuts and holes with notes, then which part each key is and every wire with its net. area draws only part of the board (needed above 6000 holes). format: "png" draws a picture instead, as the editor draws it (holes 50 px apart, scaled down to fit 1000 px; area crops it), for an agent to look at. |
get_nets | Which pins the board connects: each net with its name (e.g. GND), its pins (U1.8 VCC for a chip pin, D1.K or C1.+ for a polarized lead, R1.1 otherwise) and its wires, plus pins connected to nothing, junctions (holes where three or more things join) and crossings (where a wire goes over another wire or a hole without joining it). |
validate_project | Lists every problem, with where it is: unreadable entries, unknown parts, pins off the board, two parts' pins or leads in one hole, overlapping parts, wires that connect nothing (a loose end, or no pins on their net), a wire passing over a pin without ending there, IC pins and two-lead parts' leads connected to nothing, supply rails shorted together (VCC with 5V is one rail, VCC with GND a short), LEDs, electrolytic capacitors and + and − pins in backwards or a diode conducting straight across the supply (judged by the rails: pins named like VCC, GND or 5V, battery holders' terminals and holes noted +9V), and on a stripboard, pins shorted by a strip or in a cut hole. Wires crossing are listed as notes, not problems. Pass expectNets (the netlist you meant, e.g. [["U1.8", "U1.4", "R1.1", "J1.1"], ["U1.2", "U1.6", "C1.+"]]) and every missing or extra connection is an error; for a kept project it's remembered, so edit_project and make_share_link check against it too ([] forgets it). A whole project sent here is kept, and the reply gives its projectId. |
make_share_link | Validates the project (with expectNets, given or remembered) and returns the link that opens it, and image: the address of an SVG picture of the board, drawn as the editor draws it (served by the hosted Worker, below). It refuses a project with errors unless allowErrors is set. editorUrl points it at another copy of the editor, e.g. http://localhost:3000/app/; it defaults to PERFBOARD_EDITOR_URL. With store: true (or storeAs: "led-blinker", the id it'd like), it also stores the project (below) and returns stored: its id, a short link and its file and pictures. The self-contained link is made either way, and if storing fails, storeError says why. |
show_board | Shows the user a picture of the board right in the chat, as the editor draws it, with a button that opens it in the editor: an MCP App, for clients that show them (e.g. Claude, ChatGPT), which don't load Markdown images from links. The picture travels in the result's _meta, to the page and not to the agent; the page is the ui://perfboard/board-view.html resource. Clients without MCP Apps show nothing. |
read_share_link | Reads the project out of a link you share with the agent (z=, json= or src=, a stored project's short link too) and keeps it under a projectId, so the agent can change an existing board. |
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:
| Request | Answer |
|---|---|
POST /projects with {"project": {…}, "id": "led-blinker", "name": "LED blinker"} (id and name optional) | {id, link, file, image, png}: the id it got, a short editor link, the file and its pictures |
GET /projects?q=blinker&limit=20 | Stored projects matching the id or name, newest first, with links and a pagination cursor |
GET /<id>.json | the project file |
GET /<id>.png, GET /<id>.svg | pictures of it |
- 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 asrc=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 throughPOST /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/:
npm run worker:deploy
Then query it:
curl 'https://projects.perfboarddesigner.com/projects?q=blinker&limit=20'
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):
npm run worker:dev
Publish it to your Cloudflare account (npx wrangler login once first):
npm run worker:deploy
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:
claude mcp add --transport http perfboard https://perfboard-designer-mcp.<your-subdomain>.workers.dev/mcp
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(orminor), and set the sameversioninserver.json, both at the top and in its npm package entry. npm publish(it builds first; npm asks for 2FA). Wait untilnpm view perfboard-designer-mcp versionshows the new version.- Deploy the Worker if the server changed:
npm run worker:deploy. - Publish to the registry with
mcp-publisher:mcp-publisher validate, thenmcp-publisher login dns --domain perfboarddesigner.com --private-key "$PRIVATE_KEY"andmcp-publisher publish, run inmcp/.
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:
openssl genpkey -algorithm Ed25519 -out ~/.config/mcp-registry/key.pem
echo "v=MCPv1; k=ed25519; p=$(openssl pkey -in ~/.config/mcp-registry/key.pem -pubout -outform DER | tail -c 32 | base64)"
PRIVATE_KEY="$(openssl pkey -in ~/.config/mcp-registry/key.pem -noout -text | grep -A3 'priv:' | tail -n +2 | tr -d ' :\n')"
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).
| Command | What it does |
|---|---|
npm run build | Bundle dist/server.js and dist/worker.js |
npm run typecheck | Type-check the server and the editor code it imports |
npm run worker:dev | Run the Cloudflare Worker locally, at http://localhost:8787/mcp |
npm run worker:deploy | Publish the Worker to Cloudflare |
npm version patch, then npm publish | Release a new version to npm (prepublishOnly builds first; npm asks for 2FA) |
npm run inspect | Open the server in the MCP Inspector to try the tools by hand |
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.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.