fabplane
OfficialSupports AliExpress as a built-in purchase destination for cart items, allowing BOMs and shopping lists to route parts procurement to AliExpress.
Supports Amazon as a built-in purchase destination for cart items, allowing BOMs and shopping lists to route parts procurement to Amazon.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@fabplaneadd 10 10k 0603 resistors to my JLCPCB cart"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
fabplane-cli
Command line, MCP server and typed TypeScript client for fabplane.com: orgs, personal API tokens, carts (shopping lists / BOMs with per-item purchase destinations) and the parts inventory. It also talks to the local fabPlane desktop app (the fabdesk daemon).
fabplane: a CLI with human output by default and--jsonfor scriptsfabplane mcp: a stdio MCP server for Claude Code, Codex, Cursor and other agentsimport { FabplaneClient } from "fabplane-cli": the same API as a library
Docs: https://fabplane.com/docs/cli · API reference: https://fabplane.com/docs/api · MIT licensed.
Install
npm i -g fabplane-cli # installs the `fabplane` command
npx fabplane-cli help # or run it without installingUntil the first npm release, build it from source (npm i -g github:fabPlane/fabplane-cli does
not work: npm's global git install never has TypeScript available for the build):
git clone https://github.com/fabPlane/fabplane-cli && cd fabplane-cli
npm ci && npm pack && npm i -g ./fabplane-cli-*.tgzNode.js 20 or newer is required.
Related MCP server: a2a-mcp-bridge
Sign in
fabplane login # browser device flow: prints a URL and code, waits for approval
fabplane login --token fpk_… # store a personal API token instead
fabplane whoami
fabplane logoutCredentials are resolved in this order:
What | Source |
token |
|
API origin |
|
org |
|
The credentials file is $XDG_CONFIG_HOME/fabplane/credentials.json (default
~/.config/fabplane/credentials.json; %APPDATA%\fabplane\credentials.json on Windows). It is
written atomically with mode 0600 and keeps one profile per API origin, so you can be signed in to
production and to a self-hosted or development API at the same time. FABPLANE_CREDENTIALS_FILE
overrides the path.
Personal API tokens (fpk_…) suit CI and agents: create one with fabplane tokens create <name>,
then set FABPLANE_TOKEN. logout does not revoke fpk_ tokens; use fabplane tokens revoke <id>.
To use an API other than production, set FABPLANE_API_ORIGIN (for example
http://localhost:4000 for a local fabplane API) or pass --origin. Password login
(fabplane login --email … --password …) exists only for local/development accounts and is refused
against production.
Commands
Every command accepts --json (machine output), --org <id|slug> and --origin <url>.
fabplane help <command> prints the details.
Command | What it does |
| Sign in (device flow) or store a token |
| Forget stored credentials; revokes a device session |
| User, API origin, token source and default org |
| Print (or open) the web dashboard for the current API |
| Your orgs; |
| Create an org (you are owner) |
| Set the default org for this API origin |
| Members of the org |
| Create an invite link (admin) |
| Pending invites (admin) |
| Change a member's role |
| Accept an invite |
| Orgs your verified email domain may join, and joining one |
| Personal API tokens (the secret is shown once) |
| Connect this machine as a bot; an org admin approves a link |
| Manage the org's bots (admin; tokens are shown once) |
| Built-in fab houses and distributors plus the org's custom ones |
| Add a custom destination, e.g. a regional DigiKey site |
| Carts, filtered by git repo or client project id |
| A cart, its items and per-destination totals |
| Create a cart |
| Add one item |
| Add items from JSON, JSONL or a BOM CSV (500 per request); |
| CSV export, optionally for one destination |
| Delete a cart |
| Search inventory |
| One item with attributes and images |
| Add an item, optionally with photos |
| Bulk upsert, 500 items per request, keyed by |
| Atomic stock change, e.g. |
| Delete an item and its images |
| Items still waiting for a photo, with queue counts |
| Lease items to find photos for (default 5 for 900 s) |
| Download or read a photo, check it, upload it |
| Give up on an item (leaves the queue) or hand it back |
| Put a skipped item back in the queue (admin) |
| Run the stdio MCP server |
| The local fabPlane desktop app |
| Call a desktop tool |
| Raw request to any endpoint (escape hatch) |
|
Exit codes: 0 success, 1 API or runtime error, 2 usage error.
Carts and destinations
Each cart item has a destination: a built-in (builtin:jlcpcb, builtin:pcbway,
builtin:oshpark, builtin:digikey, builtin:mouser, builtin:lcsc, builtin:arrow,
builtin:farnell, builtin:aliexpress, builtin:amazon, builtin:manual) or one of the org's
custom destinations. Items without one go to the cart's fab house (--fab, JLCPCB by default).
carts import reads CSV headers case-insensitively and understands common BOM names: qty,
designator, mfr, manufacturer part number, LCSC Part (as sku), value, footprint,
unit price, destination, status, notes. A row with no quantity column counts its designators.
fabplane carts create "Rev B" --repo https://github.com/acme/sensor-board
fabplane carts import <cartId> bom.csv
fabplane carts add <cartId> --mpn STM32G031K8T6 --qty 10 --dest builtin:lcsc
fabplane carts export <cartId> --dest builtin:lcsc -o lcsc.csvInventory from photos: extract locally, send fields
The API stores what you send; it does not run AI on uploads. Setting serverAiProcessing: true
(--server-ai on the CLI) is answered with HTTP 501 server_ai_unavailable and nothing is
stored. Clients are expected to run a model locally (a vision model on the photo of a reel, bag
label or datasheet), extract the fields, and send them as structured data with the photo attached:
# e.g. after a local model read the label of a reel:
fabplane inventory add --name "10k 0603 resistor" --mpn RC0603FR-0710KL --manufacturer Yageo \
--qty 5000 --unit pcs --location "Lab / drawer A3" --tag passive \
--attr resistance=10k --attr tolerance=1% --attr package=0603 \
--source my-scanner --external-id reel-0042 --image reel.jpgFor batches, write one JSON object per line and import them. Rows are upserted by
(source, externalId), so re-running an import updates instead of duplicating:
{"name":"NE555 timer","mpn":"NE555P","manufacturer":"TI","quantity":25,"location":"Bin 4","externalId":"bin4-ne555","attributes":{"package":"DIP-8"}}
{"name":"100nF 0402 capacitor","mpn":"GRM155R71C104KA88D","quantity":10000,"externalId":"reel-0107"}fabplane inventory import parts.jsonl --source my-scannerRows without an externalId get one derived from name, MPN, manufacturer, SKU and location.
Photo backfill
Inventory items without an image form the org's photo queue. Filling it is client-side work:
a worker (a script with a local model, an agent through the MCP tools, or you on the command line)
identifies each part, finds a product photo on the web, and uploads it. The server stores what it is
given and runs no AI (serverAiProcessing still answers 501).
How the queue works:
An item is queued while it has no images and has not been skipped. Uploading an image removes it; deleting its last image puts it back.
photos claimleases items (1–25, default 5) for a while (60–3600 s, default 900) and counts an attempt. Leased items are hidden from other workers, so several workers can run at once. Items with the fewest attempts, then the oldest, come first.A lease ends when you attach a photo, retry (hand the item back, still queued), skip (
not_found: no photo exists; the item leaves the queue with your note), or when it expires.Each claimed item comes with a lease token. Pass it to
photos skip/photos retry(--lease-token). If your lease expired and another worker reclaimed the item, the release is refused with 409, which the CLI reports as "lease lost: another worker reclaimed this item; leave it alone".An admin can requeue a skipped item.
photos queue --alllists every item without a photo, including leased and skipped ones.
photos attach --url downloads the image on your machine. It follows redirects, gives up after 20 s
(change with --timeout <seconds>), refuses anything over 10 MiB, and accepts only png, jpeg, webp,
gif, heic or heif. The check reads the file's leading bytes, falling back to an image/* content type
when the bytes are inconclusive. The upload is tagged source: web, with sourceUrl set to
--source-url (the product page) or, by default, the image URL. --file uploads a local photo
tagged source: user.
An example worker loop in shell. find-photo stands in for your local model or search step: it
prints an image URL and the page it came from, or fails when there is no photo:
while :; do
items=$(fabplane inventory photos claim --limit 5 --worker "$(hostname)-photos" --json)
[ "$(echo "$items" | jq '.items | length')" = 0 ] && break
echo "$items" | jq -c '.items[]' | while read -r item; do
id=$(echo "$item" | jq -r .id)
lease=$(echo "$item" | jq -r .leaseToken)
if found=$(find-photo "$item"); then # e.g. "https://…/part.jpg https://…/product-page"
set -- $found
fabplane inventory photos attach "$id" --url "$1" --source-url "$2" \
|| fabplane inventory photos retry "$id" --lease-token "$lease" --note "upload failed"
else
fabplane inventory photos skip "$id" --lease-token "$lease" --note "no product photo found"
fi
done
doneThe same loop in TypeScript:
import { FabplaneClient, fetchImage, resolveDefaultOrg } from "fabplane-cli";
const client = new FabplaneClient({ token: process.env.FABPLANE_TOKEN });
const orgId = await resolveDefaultOrg(client);
for (;;) {
const { items } = await client.claimPhotoQueue(orgId, { limit: 5, worker: "photo-bot" });
if (items.length === 0) break;
for (const item of items) {
const hit = await findPhoto(item); // your local model/search: { imageUrl, pageUrl } | null
if (!hit) {
await client.releasePhotoQueueItem(orgId, item.id, { outcome: "not_found", note: "no photo found", leaseToken: item.leaseToken });
continue;
}
try {
const { image } = await fetchImage(hit.imageUrl); // redirects, 10 MiB cap, type check, timeout
await client.uploadInventoryImage(orgId, item.id, image, { source: "web", sourceUrl: hit.pageUrl });
} catch (err) {
// A 409 here means the lease was lost to another worker: leave the item alone.
await client
.releasePhotoQueueItem(orgId, item.id, { outcome: "retry", note: String(err).slice(0, 500), leaseToken: item.leaseToken })
.catch((e) => console.warn(`release ${item.id}: ${e}`));
}
}
}Agents get the same flow through the MCP tools: inventory_photo_claim, then either
inventory_photo_attach (imageUrl, or base64 data with contentType, plus sourceUrl) or
inventory_photo_release.
Bots and agents
Agents (openclaw, Hermes, CI jobs, automations) should not use a person's login. They join an org as
a bot member: a separate identity with its own fpb_… token and a role, listed among the org's
members with kind: bot. A bot can use everything inside its own org (inventory, carts, the photo
queue, destinations, the member list) with its role. It gets 403 on tokens, invites, bot management,
settings and creating or joining orgs, and cannot see other orgs.
Connect with a link (recommended). On the agent's machine, as the user the agent runs as:
fabplane bot connect --agent openclaw --name "openclaw on $(hostname)"The CLI prints a dashboard link and a code to stderr (and opens the browser unless --no-browser),
then waits. An org admin opens the link, checks the code, picks the org and role, and approves. The
CLI stores the bot token as the credential for that API origin and makes the bot's org the default
org. A denied or expired request (15 minutes) ends with a clear error, and nothing is stored.
fabplane whoami then shows kind: bot, the bot's name and its org.
Create directly (headless machines, CI). An admin runs:
fabplane bots create "nightly-bom-sync" --agent ci # prints the fpb_ token onceThen give the token to the job as FABPLANE_TOKEN, or run fabplane login --token fpb_… on its machine.
fabplane bots rotate <botId> issues a new token and revokes the old ones. fabplane bots remove <botId>
deletes the bot. fabplane logout on the bot's machine only forgets the token; to revoke access, remove
the bot.
With a bot credential loaded, fabplane mcp and every command work as usual, defaulting to the bot's org.
Ready-made agent skills live in integrations/. They are also shipped in the npm
package under integrations/:
integrations/openclaw: an openclaw skill, plus a nightlyopenclaw cron addjob for the photo backfill.integrations/hermes: a Hermes Agent skill, plus ahermes cronjob.
Both cover inventory upkeep, shopping lists and the photo backfill loop.
MCP server
fabplane mcp serves these tools over stdio. They act on your default org unless the agent passes
orgId:
Tool | |
| Your orgs (ids, slugs, roles) |
| Purchase destinations for cart items |
| Find, read and create carts (by repo or project id) |
| Edit cart items |
| Search, add and count stock |
| See and lease items that need a photo |
| Attach a found photo (URL or base64), or skip/retry |
fabplane mcp --desktop also exposes desktop_status, desktop_projects and desktop_call_tool,
which reach the fabPlane desktop app running on the same machine.
Sign in first (fabplane login) or pass FABPLANE_TOKEN in the server's environment.
Claude Code
claude mcp add fabplane -- npx -y fabplane-cli mcp
# with a token and the desktop tools:
claude mcp add fabplane -e FABPLANE_TOKEN=fpk_… -- npx -y fabplane-cli mcp --desktopCodex (~/.codex/config.toml)
[mcp_servers.fabplane]
command = "npx"
args = ["-y", "fabplane-cli", "mcp"]
# env = { FABPLANE_TOKEN = "fpk_…", FABPLANE_ORG = "my-team" }Cursor (.cursor/mcp.json or ~/.cursor/mcp.json)
{
"mcpServers": {
"fabplane": { "command": "npx", "args": ["-y", "fabplane-cli", "mcp"] }
}
}With a global install, use "command": "fabplane", "args": ["mcp"] instead of npx.
Library
import { FabplaneClient, FabplaneApiError, resolveAuth, resolveDefaultOrg, dashboardUrlFor } from "fabplane-cli";
const client = new FabplaneClient({ token: process.env.FABPLANE_TOKEN }); // origin defaults to https://api.fabplane.com
const orgId = await resolveDefaultOrg(client); // FABPLANE_ORG, else your personal org
const { cart } = await client.createCart(orgId, { name: "Rev B", repos: ["https://github.com/acme/board"] });
await client.addCartItems(orgId, cart.id, [{ mpn: "NE555P", quantity: 4, refs: ["U1", "U2", "U3", "U4"] }]);
try {
await client.adjustInventory(orgId, "item-id", -10, "built 10 boards");
} catch (err) {
if (err instanceof FabplaneApiError && err.code === "conflict") console.log("not enough stock");
else throw err;
}
console.log(dashboardUrlFor(client.origin)); // https://app.fabplane.com/dashboardEvery API operation is a method named after its OpenAPI
operationId(listOrgs,createCart,replaceCartItems,listInventory,bulkUpsertInventory,uploadInventoryImage, …) and resolves to the JSON body the API documents, e.g.{ orgs }or{ items, nextCursor }.204answers resolve toundefined;exportCartCsvresolves to CSV text.Also available:
startDeviceLogin,pollDeviceToken,waitForDeviceToken,me,logout,getSettings,putSettings,publicCatalog,config,sendPush,getPushJob, andraw/requestfor anything else.Failures throw
FabplaneApiErrorwithstatus,code(the API'serrorfield),message,body.Images:
createInventoryItem(orgId, input, [{ data, contentType, filename }])sends multipart;datamay be aUint8Array,ArrayBufferorBlob.Pass
fetchto the constructor to inject your own implementation (tests, proxies).fabplaneToolsis the MCP tool list as plain objects (name,title,description, zodinputSchemashape,annotations,handler(client, args, { orgId })) for embedding in another MCP server;createFabplaneMcpServer()builds a readyMcpServer.Bots:
startBotConnect,pollBotConnect,waitForBotConnect,getBotConnectRequest,approveBotConnect,denyBotConnect,listBots,createBot,rotateBotToken,deleteBot.Photo queue:
listPhotoQueue,claimPhotoQueue,releasePhotoQueueItem,requeuePhotoQueueItem;uploadInventoryImage(orgId, itemId, image, { source, sourceUrl });fetchImage(url)downloads and validates an image,imageFromBase64(data, contentType)validates base64 input.CredentialStoreandresolveAuth()read and write the same credentials file as the CLI.
The desktop app
import { FabdeskClient } from "fabplane-cli";
const desk = new FabdeskClient(); // finds daemon.json; or { baseUrl, token }, or FABDESK_URL + FABDESK_TOKEN
console.log(await desk.health(), await desk.projects());
const tools = await desk.toolManifest();
const result = await desk.callTool("board_stats", {}, { projectId: "…", sync: true });FabdeskClient looks for daemon.json in FABDESK_HOME, then in the desktop app's data folders
(~/Library/Application Support/{fabPlane,fabdesk,fabPlane Dev,fabdesk-dev} on macOS,
%APPDATA%\… on Windows, $XDG_CONFIG_HOME/… on Linux), preferring a daemon whose process is alive.
It covers health, projects, project, readFile, threads, createThread, messages,
sendMessage, liveRuns, toolManifest, callTool, jobs, job, waitJob, settings and
authState.
API spec sync
spec/openapi.json is a snapshot of the API's OpenAPI 3.1 document and
src/generated/operations.ts lists its operations. The Sync API spec workflow runs every Monday
at 03:00 UTC (and on demand): it fetches /v1/public/openapi.json, regenerates both files and opens
or updates a pull request when something changed, listing operations that have no FabplaneClient
method yet. A test fails while any operationId lacks a method. If the API does not serve the
document yet (404), the job ends without changes.
npm run sync-spec # fetch from FABPLANE_API_ORIGIN (default https://api.fabplane.com)
npm run sync-spec -- --offline # regenerate from the committed snapshot onlyspec/fabdesk-tools.json is a snapshot of the desktop app's tool manifest (names, toolsets,
descriptions and JSON-schema inputs, as served by GET /tools/manifest). The fabdesk repository
refreshes it with a pull request here when its tools change.
Both weekly sync jobs use a GitHub App installed on only fabPlane/fabplane-cli, with
Contents and Pull requests read/write permissions. Set FABPLANE_CLI_SYNC_APP_ID as an
Actions variable and FABPLANE_CLI_SYNC_APP_PRIVATE_KEY as an Actions secret in both this
repository and TensorFleet/fabdesk. Each run creates a short-lived installation token scoped to
fabplane-cli; the private key is never committed. The job fails early if either setting is missing.
The App does not need installation on fabdesk: that workflow requests a token for the fabPlane
installation to open a pull request here.
Contributing
npm ci
npm run build # tsc → dist/
npm test # compiles src + test and runs node --test
bun test # the same tests straight from TypeScriptTests run against a small in-memory fake of the API (test/fake-api.ts) and of the desktop daemon
(test/fake-daemon.ts); no network access is needed. Keep runtime dependencies to
@modelcontextprotocol/sdk and zod (imported as zod/v4), and keep src/ free of Bun-only APIs:
the package must run under plain Node. Bun workspaces that vendor this repo get src/index.ts
through the bun export condition.
Publishing
Releases go to npm from GitHub Actions (.github/workflows/publish.yml), which needs an npm token
that has not been added yet:
On npmjs.com, create an automation (or granular publish) token that can publish the
fabplane-clipackage.Add it as the
NPM_TOKENrepository secret: Settings → Secrets and variables → Actions.Bump
versioninpackage.jsonandsrc/version.ts, then publish a GitHub release or push av*tag (e.g.v0.1.1). The workflow builds, tests and runsnpm publish --provenance.
Without the secret the workflow fails on its first step with an error asking for it, and publishes nothing. Until the first release, install from source as described under Install.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for Flex on the Job: inventory, jobs, customers, invoices, purchasing, sales orders.
MCP server for Product Management
MCP Server for an Agent Task Marketplace
Search multi-merchant supply, checkout, and track orders via MCP.
Related MCP Servers
- AlicenseCqualityBmaintenancePrototype MCP server enabling coding agents to manage Amigo Agent Forge operations, including org credentials, entity configurations, conversation simulations, and version sets.3711 npmISC
- FlicenseNot gradedqualityDmaintenanceA stdio MCP server that allows MCP-capable agents to call an A2A endpoint, enabling agent-to-agent messaging through the A2A protocol.-
- AlicenseNot gradedqualityBmaintenanceMCP server that provides pre-checkout basket tools and a local API/viewer, enabling agents to research products and manage a shopping cart through natural language.1MIT
- FlicenseAqualityBmaintenanceMCP server that exposes read-only NetSuite tools for querying warehouse stock levels and order status, using mock data locally and a pluggable client interface.2-