MCP server: One MCP server.

Petty

@wawrepoMCP server · External

What you keep and where: money, things, notes. End-to-end encrypted; decrypts on your computer.

Personal financev1.5.19AGPL-3.0TypeScript
Freeno charge per call

Runs on your machine from its package; the steps are below.

Home screen: the total, the places as a picture, and the drawers grouped by place
From the README · 1 of 3

What it does

  1. Reads
  2. Writes
  3. High impact

At most: creates or changes data: records, files, settings.

What it costs

Free

Odel charges nothing per call. The publisher may charge for its own service.

Runs locally over stdio

This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).

MCP Bundlehttps://github.com/WawRepo/petty/releases/download/v1.5.19/petty.mcpb

Distributed as an MCP bundle (`https://github.com/WawRepo/petty/releases/download/v1.5.19/petty.mcpb`) — download it and import the bundle in your MCP client.

README12 min read
Petty

Petty

Everything you keep, in its place. Petty is a private record of what you keep and where: cash in a tin, tools in the basement, passports in the safe, the drill you lent out. Each drawer sits in a place, holds items — money, counted things or notes — and is shared only with the people you choose. Everything is encrypted on your device before it is sent.

Try it at petty.kropka.studio — nothing to install, it works in your browser. Or run your own.

Home screen: the total, the places as a picture, and the drawers grouped by place A drawer of things: tools counted by the piece, keys and a torch as single items Places: rooms, shelves and boxes as a tree you can drag

What it does

  • Money, things and notes. Cash in any currency, things counted by the piece, and single items with a note: a passport, a drill, a spare key. Tag items and filter by tag.
  • Drawers in places. Home › Bedroom › Wardrobe › Safe. A drawer lives in a place; moving a room moves its drawers. Search finds an item by its name, note or tag.
  • Shared, with roles. A drawer is shared only with the people you pick. Writers add entries; readers only look. The server enforces this.
  • Count, check, never erase. Mark a drawer as checked after a real count. Mistakes are reversed, not deleted.
  • Works with AI apps. Claude Desktop (a one-click add-on), Claude Code, Cursor, VS Code or any MCP app can read and update your drawers. It decrypts on your computer; the server still sees only ciphertext. A Claude skill teaches it the good habits (one total per currency, ask before it writes), and the add-on is listed in the MCP Registry as io.github.WawRepo/petty. See docs/agent.md.
  • A command line. petty signs in through your browser, like gh auth login, then reads and writes drawers from a terminal, scripts or AI agents, with --json and Tab completion. See docs/cli.md.
  • Passkey first, works offline. Face ID, Touch ID or Windows Hello opens your vault. It installs like an app and keeps working without a network.
  • English, Polish, German, Spanish and French — the app, its emails and the sign-in pages.

Made for

  • Cash at home — every tin and envelope in its room, counted and checked. Where Petty started.
  • The workshop — the zip ties are in Home › Basement › Workshop, in the desk drawer.
  • A trip kitty — one drawer per trip, one item per person; nobody can quietly change what they paid.
  • Yearly accounts — a drawer per account, checked once a year, the date kept with it.
  • Lent out — who has your drill, and since when.
  • The family safe — where the passports, keys and papers are, shared read-only with the people who may one day need it.

Documentation

ForRead
Usersthis README, the in-app privacy page, docs/agent.md (use Petty from Claude Desktop or another AI app), docs/cli.md (the petty command line)
Operators (self-hosting)docs/deploy.md · docs/monitoring.md · docs/auth-clerk.md
ContributorsCONTRIBUTING.md · docs/decisions.md · CLAUDE.md — the engineering rules; plain text, no AI tool needed to read or follow them
Security reviewersSECURITY.md · docs/threat-model.md · docs/security-review-2026-09.md · docs/README.md (how keys and tokens work, with diagrams)
History, not maintainedpetty-app-spec.md (frozen 2026-09-25), petty-spec-review.md, SPEC-ISSUES.md — the original specification, its review and the reasoning behind the first decisions · petty.html — the pre-rewrite prototype

Ticket IDs such as PETTY-123 in docs and commit messages refer to the maintainers' internal tracker. The public trail for changes is this repository's issues, pull requests and CHANGELOG.md.

Security in five lines

  1. Drawer content is encrypted in the browser (AES-256-GCM). The server stores that content only as ciphertext — it still sees account details (email, display name) and activity metadata (who wrote which entry, when); the in-app privacy page lists exactly what.
  2. Each drawer has its own key, wrapped for each member with ECDH P-256. Your private keys never leave your device unencrypted.
  3. Your keys sit in a vault opened by a passkey or a passphrase (Argon2id), with a recovery code.
  4. Every ciphertext is bound to its row, drawer, line and author, and entries form a signed hash chain, so a server cannot move, re-attribute or silently drop them.
  5. Permissions and append-only history are enforced by the server and the database, not by the UI.

The threat model is docs/threat-model.md; the settled design decisions are docs/decisions.md. The latest security review is docs/security-review-2026-09.md. Report problems privately as described in SECURITY.md.

Run your own

cp deploy/compose/.env.example deploy/compose/.env    # fill in the values
docker compose -f deploy/compose/docker-compose.yml --env-file deploy/compose/.env up -d

Make the database passwords letters and digits only (openssl rand -hex 32). Then put an HTTPS reverse proxy in front of 127.0.0.1:3000. docs/deploy.md has the details, and docs/monitoring.md covers metrics, logs and traces.

License

Copyright (C) 2026 Petty contributors. Petty is free software under the GNU Affero General Public License v3.0 (LICENSE, and NOTICE). If you run a changed version for other people, you must offer them its source. Contributions are welcome; see CONTRIBUTING.md and CODE_OF_CONDUCT.md.


Development setup

This part covers running Petty locally. Nothing here needs a cloud account or network access after pnpm install.

Try it with a seeded household (Docker only)

make demo          # pulls the published image (PETTY_TAG=<tag> for another), migrates, seeds two people and six drawers
make demo-local    # same, but builds the image from your working tree (no CI, no registry)
make demo-down     # stop it
make demo-reset    # stop it and drop the data (shares the local dev database volume)

Open http://localhost:3300 (mail at http://localhost:8025). Logins are printed by the seed: ania@petty.local / password-ania / vault passphrase vault ania 2026 drawer; bartek@petty.local / password-bartek / vault bartek 2026 drawer.

Settings: one .env at the root

Copy .env.example to .env (gitignored). The API dev server, the tests, Playwright and docker compose all read that one file; the web app needs none (it asks the API). Anything already in the environment wins over the file. The production image carries no file: a deployment passes the same names as environment variables or secrets.

Requirements (clean machine)

  • Docker Desktop (or any Docker with Compose v2)
  • Node 24 (LTS; .node-version says 24 — the image and CI use it too)
  • pnpm 11: corepack enable && corepack prepare pnpm@11.28.2 --activate (the version in package.json)
  • GNU make (preinstalled on macOS; apt install make on Debian/Ubuntu)

First run

pnpm install
make dev

make dev starts Postgres 16 and Mailpit in Docker, waits for the database, runs the migrations, then starts the API on http://127.0.0.1:3000 and the web app on http://localhost:5173. Stop everything with Ctrl-C and make stop.

Then, in a second terminal:

make seed        # three test users
curl localhost:3000/health   # {"ok":true,"db":"up"}

Test users

EmailLogin passwordVault passphrase (from Phase 2 on)
alice@petty.localpassword-alicevault alice 2026 drawer
bob@petty.localpassword-bobvault bob 2026 drawer
carol@petty.localpassword-carolvault carol 2026 drawer

Mailpit catches every email the app sends (join links, invitations, removals, ownership offers): http://localhost:8025. Set SMTP_HOST/SMTP_PORT/MAIL_FROM/APP_URL for a real provider.

Commands

CommandDoes
make devdb + mailpit + migrate + api + web
make seedthree users + a shared "Kitchen" drawer with real ciphertext (skips if alice exists; make reset first to reseed)
make testevery vitest suite (API tests need the db)
make linttypecheck + eslint
make resetdrop the database volume, migrate again
pnpm --filter @petty/api migrate:create <name>new empty SQL migration

Offline

The web app is an installable PWA. The service worker caches the app shell only; API responses are never HTTP-cached. Ciphertext (vault blob, bootstrap) and non-extractable key handles live in IndexedDB, so unlock and reading work offline. Entries and document edits made offline wait in an encrypted outbox and are sent on reconnect (idempotent by client id); the home screen then reports what changed while you were away and which queued changes were refused.

pnpm e2e runs two Playwright projects: dev (Vite dev server) and prod (the API serving the production build on :3100 under the strict security headers; fails on any CSP violation; service worker active). A third, clerk-prod, runs the Clerk sign-in against a Clerk development instance, only with CLERK_E2E=1 and its keys.

Layout

apps/api        Fastify API. All writes go through here. Connects as petty_api;
                petty_maint only inside rotation and deletion.
apps/web        Vite + React SPA. Holds keys in memory; never writes plaintext to disk.
packages/crypto Envelope encryption, AAD binding, signatures, compatibility corpus.
packages/ledger Pure domain: balance fold, chain verification, money formatting,
                line extras (icon, tags, counted) and place paths.
packages/protocol zod schemas shared by api and web.
packages/agent  The drawer tools shared by the MCP server and the command line.
apps/mcp        The MCP server for AI apps (petty-mcp.mjs, the petty.mcpb add-on).
apps/cli        petty, the command line (petty.mjs).
apps/api/migrations  Version-controlled SQL migrations (node-pg-migrate).
tests/integration    The production image under deploy/compose, over real HTTP.
deploy/compose  The production compose file; deploy/backup the pg_dump + age image.
docker/postgres/init.sql  Dev-only: creates petty_api / petty_maint with LOGIN.

Places, icons and tags

A drawer sits in one place of a tree (Home › Kitchen › shelf): the path of names is stored in the drawer's encrypted document, the tree itself in the person's encrypted user document (apps/web/src/lib/places.ts, edited on /places). Renaming or moving a place rewrites the path on the drawers you can write. Drawers and lines carry an optional Lucide icon slug; lines carry tags and a "part of the total" flag, all inside the document. Details and the reasoning: docs/decisions.md (rows Places, Icons, Line tags); background in the frozen petty-app-spec.md, section "Places, icons and line tags".

API (apps/api)

All bodies are JSON; bytes travel as base64; the server never opens ciphertext. Session = httpOnly cookie petty_session (__Host-petty_session over HTTPS, as in the image). In the image every route sits under /api (/api/me). Access tokens and Clerk session tokens come as Authorization: Bearer …. Non-members get 404, never 403.

RouteWhoDoes
GET /config GET /health GET /health/live–sign-in mode, contact address, open sign-up, version; health with the database; the process is up
POST /auth/signup POST /auth/login POST /auth/logout–invite-only signup with vault blobs and public keys; login; logout
POST /auth/forgot POST /auth/reset–local mode: a one-hour reset link by email; a new login password (the vault is untouched)
POST /auth/provisionClerk sessionClerk mode: the first sign-in creates the Petty account with its vault blobs
POST /join-links GET /join-links/:tokenusercreate / inspect a join link (7 days)
GET /me PATCH /me GET /users/lookup?email= GET /users/:id/keysuserprofile, locale, exact-email lookup for inviting, key history
GET /me/doc PUT /me/docuserthe user's own encrypted document (key pins), version-checked
GET /bootstrapuserall drawers, documents, my wraps, members, entries since each line's last Adjust, invitations, transfers
POST /drawers GET /drawers/:id DELETE /drawers/:iduser / read / ownercreate; read; permanent delete via delete_drawer()
PUT /drawers/:id/documentwriteoptimistic lock on base_version → 409 VersionConflict; verification: true stamps last_verified_at
GET/PUT/DELETE /drawers/:id/photoread / writeown encrypted row
POST /drawers/:id/lines/:lineId/deletewritenew document + entries removed via delete_line()
POST /drawers/:id/entrieswriteappend; idempotent on client id; Adjust needs expected_head_seq → 409 RecountRequired; Reverse rules → 409 ReverseRefused
GET /drawers/:id/lines/:lineId/entries?before=&limit=readolder pages
GET /drawers/:id/members POST /drawers/:id/invitations GET /invitations `POST /invitations/:id/acceptdecline DELETE /invitations/:id`read / owner / invitee
DELETE /drawers/:id/members/:userId POST /drawers/:id/leaveowner / memberremoves wraps, sets rotation_needed
POST /drawers/:id/transfer POST /drawers/:id/transfer/accept DELETE /drawers/:id/transferowner / targetownership moves only on acceptance
POST /drawers/:id/rotation PUT /drawers/:id/rotation/batch GET /drawers/:id/rotationwritepublish new key + wraps first; re-seal in batches (petty_maint); status
GET /drawers/:id/exportowner, writeeverything for the drawer; read members get 403
GET /me/delete POST /me/deleteuserpreview; delete with per-drawer decisions (hand over / delete), password re-entered
GET /me/recovery-vault PUT /me/recovery-vault PUT /me/vaultuserrecovery-code copy; a new recovery code; replace the passphrase wrap of the same keys
POST /me/custody-challengeusera fresh challenge for a custody proof (docs/threat-model.md)
POST /me/passkeys DELETE /me/passkeys/:iduseradd / remove a passkey that opens the vault
GET /me/storageuserstorage used, against STORAGE_QUOTA_MB
GET /drawers/:id/document/history POST /drawers/:id/document/restoreownerthe drawer document's 30-day history; restore an earlier version
GET /me/tokens POST /me/tokens DELETE /me/tokens/:id GET/POST /me/tokens/:id/keysuseraccess tokens: list, make (with a custody proof), revoke, their key bundle
GET /me/token GET /me/token/bootstrap GET /me/token/keys DELETE /me/tokentokena token about itself, its drawers and its key bundle; a token ends itself
GET /users/:id/delegationsuserthe tokens a person's key vouched for, to check their signatures
POST /device/code POST /device/token–device login: a tool asks for a code, then polls for its sealed token
GET /device/:code POST /device/:code/approve POST /device/:code/denyuserthe /device page (signed in, never a token): see the request, allow it (the page seals a new token) or deny it
GET /admin/users POST /admin/users/:id/block /unblock /revoke-sessions /restore-vault /adminadminaccounts: list, block, unblock, end sessions, restore a vault from its history, make or remove an admin

Database roles

RoleUsed byMay
pettymigrations, seedeverything (owner)
petty_apievery API requestSELECT/INSERT on entries; no UPDATE/DELETE there
petty_maintrotation, line/drawer/account deletioncolumn-restricted UPDATE, DELETE via functions

Connection strings live in .env.example; copy to .env to override.

Troubleshooting

  • pnpm install says "Ignored build scripts: esbuild" → pnpm-workspace.yaml already allows it; run pnpm install again.
  • Port 5432 busy → stop the local Postgres (brew services stop postgresql) or change the port in docker-compose.yml and .env.

Similar modules

Browse Personal finance