npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

whoowes

v0.1.1

Published

MCP server for trip/event expense ledgers: shared expenses, settlements, multi-currency with retroactive weighted-average rates

Downloads

183

Readme

whoowes

MCP server for self-contained trip/event expense ledgers. You talk to a model ("we paid 200k naira for the hotel, 20% timi 80% george", "george sent me 20k", "I converted another £60 and got 150k"), the model calls typed tools, and the ledger does the arithmetic with decimal.js. The model never computes a number.

Model

  • A tab is a self-contained event (a trip, a house project) with a base currency.
  • Everything is an event: expense, settlement, conversion, or rate. Every event carries an id and an optional free-text note.
  • All state (balances, rates) is derived by refolding the full event log on every read. Nothing is stored and incremented, so retroactive revaluation is correct by construction.
  • The log is append-mostly: edit_event and remove_event can correct it, but an edit patches an event in place, keeping its position (see below).
  • The exchange rate per currency is the weighted average of your real conversions (total base spent / total foreign received). Adding a conversion retroactively revalues every expense in that currency.
  • A manually declared rate (declare_rate, as foreign units per 1 base unit) overrides the conversions average while in force: expenses revalue retroactively and later settlements lock at it. Clearing the declaration falls back to the average. A declared rate needs no conversions, so a currency you never converted into can still be valued.
  • Settlements lock at the rate in force when they were recorded; they do not float afterwards. Money that actually moved keeps its value.
  • Balances always sum to zero across participants.

Storage

A single JSON file at ~/.whoowes/ledger.json (override with the WHOOWES_DIR env var). It is the event log; back it up by copying it.

Build

npm install
npm run build     # tsc -> dist/
npm run smoke     # runs the worked scenario with assertions

smoke and test:concurrency are checkout-only: they run TypeScript under tsx (a devDependency) against scripts/, and the files allow-list ships neither. They stay in the manifest because they are the development entry points; running them inside an installed copy of the package will not work.

Publishing

dist/ is gitignored and built by two lifecycle hooks, because npm picks a different one depending on how the package is being installed:

  • prepack runs on npm pack and npm publish, so the tarball you inspect locally is compiled the same way the published one is and cannot ship a stale build.
  • prepare runs on a git-URL install (npm i github:Zaida-3dO/whoowes), which does not fire prepack. Without it such an install lands with no dist/ and no way to build one, so npm skips the whoowes bin shim and npx github:Zaida-3dO/whoowes fails. npm skips prepare for registry-tarball installs, so it costs consumers of the published package nothing. (A file:/local-directory dependency is symlinked into place, and prepare still runs against the linked checkout — so it builds dist/ there, but only if that checkout's devDependencies are already installed. Otherwise the install fails on a missing tsc.)

The files allow-list ships dist/, README.md and LICENSE only — no sources, no scripts, no fixtures.

npm pack                    # inspect the tarball first
npm publish --access public

The whoowes binary is dist/server.js, which carries a #!/usr/bin/env node shebang from src/server.ts (tsc preserves it and marks the output executable).

Transports

Stdio is the only transport:

| Entry | Command | Use | | --- | --- | --- | | stdio (published) | npx -y -p whoowes whoowes | One client spawns its own copy, no checkout needed. The usual way in. | | stdio (from a checkout) | node dist/server.js | The same entry point, run from a local build. |

There was once a streamable-HTTP entry point (dist/http.js, serving POST /mcp, GET /health and GET /view) for running whoowes as one shared process. It was retired along with its container in September 2026, and src/http.ts, the express dependency and the Dockerfile were removed with it. The HTML page it served lives on as the display tool below.

Both stdio forms are the same program: dist/server.js is the package's whoowes binary. Standard output is the protocol stream, so the server writes its startup line (and everything else human-readable) to standard error — one stray line on stdout would corrupt the JSON-RPC framing for every message after it.

display — the rendered page

The display tool writes a read-only HTML page of a tab to a file and returns its path: position cards and a per-currency waterfall for one participant, then every entry with its share assignment, the net per person, and the settlements. It folds the log at render time, so unlike a hand-built snapshot it cannot go stale — but it is a snapshot once written, so re-render to pick up later events.

  • display with no tab — the only open tab, or a list of all of them
  • display with tab: <name> — a specific tab
  • display with tab: <name>, who: <participant> — focus the cards and waterfall on someone

Because the output is a standalone file opened over file://, the page carries no navigation: there is no server to answer a link. Participant names and tab names are rendered as plain marks, and each row shows the who/tab argument that renders that view instead. Re-run display with those arguments to move around.

It states a combined cross-currency figure only when every currency involved has a real rate behind it; otherwise it names the missing rate rather than assuming one.

Only ever run one writer against a given WHOOWES_DIR. Within a single process every tool handler does load → mutate → save synchronously in one tick and save() is compare-and-swap (it refuses to overwrite a ledger that moved since it was read), so a concurrent writer loses nothing silently. Two separate instances on the same file still race at the check-to-rename window — see the note in src/store.ts.

Register

From npm (recommended)

No checkout, no build, no path to keep correct — npx fetches the published package and runs its binary. In an .mcp.json (or Claude Desktop's claude_desktop_config.json):

{
  "mcpServers": {
    "whoowes": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "-p", "whoowes", "whoowes"]
    }
  }
}

The equivalent for Claude Code (user scope, works in any project):

claude mcp add --scope user whoowes -- npx -y -p whoowes whoowes

To pin a version, use -p [email protected]. To point the ledger somewhere other than ~/.whoowes, set WHOOWES_DIR in the server's env block:

{
  "mcpServers": {
    "whoowes": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "-p", "whoowes", "whoowes"],
      "env": { "WHOOWES_DIR": "/path/to/your/ledger/dir" }
    }
  }
}

Each client that spawns this gets its own process. That is fine for one person on one machine, but see the one-writer warning above before pointing two spawned copies at the same WHOOWES_DIR.

From a local checkout

claude mcp add --scope user whoowes -- node /path/to/whoowes/dist/server.js

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "whoowes": {
      "command": "node",
      "args": ["/path/to/whoowes/dist/server.js"]
    }
  }
}

Against a shared HTTP instance instead (see below):

claude mcp add --scope user --transport http whoowes http://<host>:18801/mcp

Shared deployment (Docker)

The included Dockerfile builds the HTTP server. The ledger is not in the image — mount a volume at /data (the image sets WHOOWES_DIR=/data).

docker build -t whoowes .
docker run -d --name whoowes-mcp -p 18801:8000 -v /srv/whoowes-data:/data whoowes
curl http://localhost:18801/health

Clients then point at http://<host>:18801/mcp (transport streamable-http), and everyone shares the same tabs. Back up by copying ledger.json out of the mounted volume.

Tools

| Tool | Purpose | | --- | --- | | create_tab | New tab with a name and base currency | | add_participant | Register a person (global, reused across tabs) | | add_expense | Shared expense; shares by pct (sum 100) or fixed amounts (sum to total) | | add_settlement | A payment between two people; locks at the current rate | | add_conversion | A real FX conversion you made; moves the weighted-average rate | | declare_rate | Manually pin a currency's rate (overrides the average); omit rate to clear | | get_balances | Net position per participant in the base currency | | get_person | One person's obligations, payments, and settlements | | list_tabs | All tabs | | list_events | The raw event log with ids — where you get the event_id for the two below | | edit_event | Correct any event in place by id, patching only the fields you pass | | remove_event | Delete any event by id (not just the last one) | | set_tab_status | Close or reopen a tab | | set_base_currency | Rebase a tab; everything revalues retroactively | | delete_tab | Permanently delete a tab and its log (needs confirm: true) | | undo_last_event | Remove the most recent event (the cheap common case) |

Editing is in place, on purpose

edit_event keeps an event at its original position in the log, and this is load-bearing rather than an implementation detail. Settlements lock their base value at the rate in force at their point in the sequence, so an edit implemented as remove-then-append would slide the event behind later conversions and silently relock settlements at a different rate — producing a ledger that looks fine and is wrong. kind and id are immutable: an edit corrects an entry, it doesn't turn one kind of event into another.

Both mutations validate by folding, then commit: the change is applied, the whole tab is refolded, and it is only saved if the fold succeeds. That is what refuses a remove_event on a conversion some later settlement still needs — the fold throws no rate available for X, and the log is restored untouched.

Rebasing re-reads each conversion from the other side — a £110 -> ₦250,000 history reads as 0.00044 GBP per NGN on a GBP tab and 2272.73 NGN per GBP on an NGN one. It is refused if any conversion has no side in the new base, or if a rate is declared for it (a tab can't hold a rate against its own base).

Not in v1

Donation pots (collecting toward a goal rather than dividing a cost) and any messaging/settle-up flow. Computing is here; texting people is on you.