Track every kit, tool, and terrible financial decision from pre-order to panel-lined masterpiece.
A self-hosted Gunpla/plamo collection and build tracker. Kits move across a drag-and-drop Kanban board from pre-ordered to complete; orders know which kits they turned into; nippers, cement and decal sheets get counted; and an embedded MCP server means you can just tell Claude "the Sinanju arrived" instead of clicking things.
Your data lives in your Postgres, on your hardware, and leaves as plain CSV whenever you want it to.
There is no authentication yet. Anyone who can reach the API can write to it, and that includes deleting things. Run it on a network you trust — your LAN, a VPN, or plain old localhost — and please don't put it on the internet until Milestone 6 lands.
The database schema is also still moving. Migrations are provided and tested in both directions, but export an archive before you upgrade. It takes one click, and that's exactly why it exists.
Six statuses — pre-ordered, ordered, in transit, backlog, building, complete — with two views over them. Build shows the three that matter on a Sunday afternoon (backlog → building → complete). Orders shows the money still in flight, with everything that's arrived collapsed into one Received column so it doesn't fill your screen with things you already own.
"Backlog" means in hand, not started. There is no polite word for this pile. We tried.
Order a Zaku ×2 and plamotrack creates two kit rows, because you own two physical plastic objects, not "a quantity of 2". Each one remembers which order line it came from, so fixing a typo in the order fixes it everywhere, and deleting an order cleanly undoes the whole thing.
Orders are pending until you mark them received. That matters more than it sounds: stock only lands in your inventory when the box does. No more being told you have five Gundam markers while they're demonstrably still in Osaka.
If you look closely at the inventory below, Mr. Color Thinner sits at 0 on hand — it's on that pending Mecha Supply Co order. It'll count itself the moment you hit Receive.
Three quantity-tracked catalogs, with an optional low-stock threshold so you find out you're nearly out of Extra Thin before the hobby shop closes. Upgrade parts can be applied to a specific kit, which decrements stock and records what went where.
Adding items to an order uses a search-and-pick typeahead, never a free text box. This is deliberate: give a naming-things-at-11pm hobbyist a text field and within a month you'll own "GM02 Gundam Marker", "Gundam Marker GM02", and three units of stock split between them.
Rating, packing quality, shipping speed, and a blunt would-you-order-again field. Because six months later you will not remember which one sent a $200 Perfect Grade loose in a bag.
Everything in these screenshots is invented demo data, ratings included. Mecha Supply Co isn't a real shop, and the ones that are haven't been graded by anyone — go form your own opinions, that's what the field is for.
Export the entire collection as a zip of plain CSVs with a manifest, or pull any single table as a spreadsheet. Import it back to restore, merge, or move instances — with a full preview of every change before anything is written, and no duplicating what's already there.
Coming from a spreadsheet, Notion, or Baserow? Grab the starter sheet: one row per kit, and plamotrack works out the retailers, orders, and order lines for you.
Full details in docs/import-export.md.
The API ships with a Model Context Protocol server built in — same process, same business logic, no separate thing to run. Point Claude at it and the conversation goes roughly:
You: grab that Gundam Express order confirmation from my email and add it Claude: (reads the email, calls
create_order) Added — 2 kits and a pack of sanding sponges, A$104.90, pending.You: the Sinanju arrived Claude: (calls
list_orders, thenmark_order_received) Marked received. The Sinanju Stein is now in your backlog and the markers are on hand.
There is no email-parsing feature in plamotrack and there never will be. Your agent already has a mail connector; it just needed somewhere to write.
Being honest up front beats you finding out at 11pm:
| Status | |
|---|---|
| Kits, orders, inventory, retailers, Kanban board | ✅ Built |
| CSV import / export | ✅ Built |
| MCP server | ✅ Built |
Bundled docker compose up for the whole local stack |
✅ Built |
| Internationalisation foundations | 🔨 Milestone 5.1 — English catalogue and locale-aware formatting; the configurable reference currency already shipped |
| Authentication + OAuth-compatible remote MCP | 🔨 Milestone 6 — yes, really, see the warning above |
MCP 2026-07-28 compatibility |
🔨 Milestone 6.1 — dual-era, without dropping current clients |
| Photo gallery per kit | 🔨 Milestone 7 |
| Public read-only showcase page | 🔨 Milestone 8 — after the admin and MCP paths are protected |
You'll need: Docker and about three minutes. Nothing else — the images build from this repo.
git clone https://github.com/DeusMaximus/plamotrack.git && cd plamotrack
cp .env.example .env
# open .env, replace change-me with a real password
docker compose up -d --waitOpen http://localhost:8080. That's an empty collection — head to Data → Starter sheet to pour an existing spreadsheet in, or just add an order.
The first run builds two images and takes a couple of minutes; after that it's
seconds. .env is the whole configuration: Compose reads it to start the database
and the API reads it to connect, so there's nothing to keep in sync.
Four containers, but only one open port:
| http://localhost:8080 | the app |
http://localhost:8080/api/… |
REST API — e.g. /api/kits, or /api/docs for the interactive docs |
http://localhost:8080/mcp/ |
MCP endpoint |
The API and database aren't published — they talk over Compose's internal network,
so an instance has exactly one door, and it's bound to 127.0.0.1. A migrate
container runs the database migrations and exits before the API starts; seeing it
as Exited (0) is success, not a failure.
Running it on a server and want to reach it from your laptop? That door stays on loopback by default for a reason — there's no login yet — so see Reaching it from another machine rather than just widening the bind.
Backups, restores, upgrading, and the full configuration reference live in docs/operations.md.
POSTGRES_PASSWORDerror from compose — you skippedcp .env.example .env.- Port 8080 already in use — set
WEB_PORTin.envto something free. up --waitfailed —docker compose psshows which service is unhealthy. If it'smigrate,docker compose logs migratehas the reason, and the API deliberately won't have started.- Password authentication failed — you changed
POSTGRES_PASSWORDafter the database volume was already created. Postgres only reads it when initialising an empty data directory. Either set it back, ordocker compose down -vto start clean, which deletes the database. - Pointing at a Postgres you already run — uncomment
DATABASE_URLin.env.
plamotrack speaks MCP over streamable HTTP at:
http://localhost:8080/mcp/
Keep the trailing slash as a habit. The bundled stack serves both spellings, but running the API straight from source (see Developing on it) redirects without it — and a 307 in response to a POST loses the body on clients that don't re-send it.
Edit the config file directly — Claude Desktop's Add custom connector dialog only accepts publicly reachable URLs, and a self-hosted plamotrack on your own network isn't one. Nor should it be at this stage; see the alpha warning above.
So bridge the HTTP endpoint into a stdio server with
mcp-remote. Open
claude_desktop_config.json — on macOS at
~/Library/Application Support/Claude/claude_desktop_config.json, on Windows at
%APPDATA%\Claude\claude_desktop_config.json — and add:
{
"mcpServers": {
"plamotrack": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:8080/mcp/"]
}
}
}Restart Claude Desktop. If it complains about the URL not being HTTPS, add
"--allow-http" to the end of the args array.
claude mcp add --transport http plamotrack http://localhost:8080/mcp/It's a standard streamable-HTTP MCP server, so any client that can point at a local URL
will work — give it http://localhost:8080/mcp/. Clients that only speak stdio, or
that (like Claude Desktop) only accept publicly reachable URLs, can use the mcp-remote
bridge shown above. Instructions for other specific clients are welcome as PRs; open an
issue if yours needs something unusual.
| Tool | What it does |
|---|---|
list_kits |
Filter by status or grade |
get_kit |
One kit, in full |
update_kit_status |
Move a kit along the pipeline |
search_catalog |
Search tools/consumables/upgrades — the same search the UI typeahead uses, so agents hit the same de-duplication a human does |
create_order |
Full order with lines; kits fan out, retailers are matched by name or created |
list_orders |
Optionally pending-only — how an agent finds the order a shipping email belongs to |
mark_order_received |
Applies stock, advances that order's kits to backlog |
adjust_stock |
Nudge a quantity, with a reason |
apply_upgrade |
Record an upgrade part going onto a kit |
Import and export deliberately have no MCP tools. An agent that can silently replace your entire collection is not a feature.
⚠️ The MCP server has no auth either — it's the same unauthenticated process as the REST API. Keep it on localhost or a trusted network.
Running the containers is the install path; for development you want hot reload, so run the database in Docker and the app from source.
You'll need: Docker · uv · Node 20.19+ (or 22+).
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d db --wait
# just Postgres, on fixed loopback and POSTGRES_PORT (5432 by default)
cd backend && uv sync && uv run alembic upgrade head
uv run uvicorn app.main:app # REST on :8000, MCP at :8000/mcp/
cd ../frontend && npm install && npm run dev # Vite on :5173, proxies /api to :8000Open http://localhost:5173. The API serves at the root here rather than under
/api — the Vite dev proxy strips the prefix exactly as nginx does in the container,
so the app's own fetch paths are identical either way.
The development overlay and full stack are the same Compose project, so they share
one database container. Starting the full stack without the overlay recreates the
database container without its host port; the named volume and its data survive.
What you get if both APIs are running is the container's on :8080 and yours from
source on :8000. Harmless, but confusing when a change doesn't show up where you
expected. docker compose stop web api leaves just the database.
Already have a Postgres on 5432? Set POSTGRES_PORT in .env — the development
overlay publishes on it and the source-run API connects to it. The bind stays fixed
to 127.0.0.1; deliberately remote database access requires your own override.
Tests follow the same configured connection by default, but use a sibling
<database>_test database that they create if needed and destructively reset. Set
TEST_DATABASE_URL in the test process only when tests need a different connection.
cd backend
uv run pytest # real Postgres, migrations both ways
uv run ruff check --fix . && uv run ruff format .
cd ../frontend
npm run build # type-check + production build
npm run lint
npm run test:e2e # Playwright (npx playwright install chromium)Two documents are worth reading before you change anything structural:
- docs/design.md — why the app is shaped this way. It's a record of decisions, not a spec; where it disagrees with the code, the code is right.
- AGENTS.md — the rules that actually bind, for both human and AI
contributors. The important one: all business logic lives in
app/services/, and REST routers and MCP tools are thin wrappers over it, so the two can never drift apart.
Issues and PRs welcome, especially: other MCP clients, non-Gunpla model kit taxonomies (the schema deliberately hedges — see design notes §9.1), and anyone who has opinions about grade-to-scale defaults.
Please run the lint/build/test commands above before opening a PR. A contribution guide with more ceremony arrives at Milestone 9.
MIT — see LICENSE. Build what you like with it.
Not affiliated with Bandai, Bandai Spirits, Sunrise, or anyone else who owns the things you're gluing together. "Plamo" (プラモ) is the Japanese hobbyist shorthand for plastic models, which is what this tracks, whether or not it happens to be a robot.





