open-splitwise
<div align="center">
# open-splitwise
**Turn [Splitwise](https://www.splitwise.com) into an agent-native expense tracker.**
An open [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets
any AI agent — Hermes, Claude Desktop, Claude Code, Cursor, or anything that speaks MCP —
read balances, split expenses from messy natural language, diagnose its own auth problems,
and never think about rate limits.
`Python 3.11+` · `MCP spec 2026-07-28` · `stdio transport` · `33 tools · lazy-loaded`
</div>
---
## Why
Existing Splitwise integrations hand the model a raw API mirror and hope for the best.
That fails in predictable ways: the model invents category IDs, mis-splits ₹300 three ways,
believes Splitwise's `200 OK` when the request actually failed, or treats a rate-limit
response as a bug to retry aggressively.
**open-splitwise** fixes this at the server layer:
| Problem for agents | What open-splitwise does |
|---|---|
| "Split dinner with Alice" requires 3–4 API calls + arithmetic | `quick_add_expense` resolves names → IDs, computes cent-exact shares, picks the category, posts once |
| Two Alices in your friends list | `resolve_users` returns candidate lists so the agent asks *you* which one |
| "What do I owe?" needs multi-endpoint aggregation | `money_summary` returns per-currency totals in one call |
| Splitwise returns `200 OK` with an `errors` object | Server checks it; failures surface as tool errors with actionable text — never false success |
| HTTP 429 rate limits | Retried invisibly (`Retry-After` honored, exponential backoff fallback) |
| Key revoked / logged out mid-session | Errors tell the agent the cause and to run `setup_auth`; new keys apply instantly, no restart |
| 33 tool schemas burn ~4k tokens in every prompt | **Lazy tool discovery**: only 7 essential tools are exposed by default; `search_tools("expenses")` loads the rest on demand with full schemas |
## Features
- **Complete API coverage** — all 27 endpoints of the official Splitwise OpenAPI 3.0 spec,
one tool each, faithful names.
- **Workflow layer** — high-level tools so a single utterance maps to a single call.
- **Self-service auth lifecycle** — `setup_auth` validates a key live against Splitwise
before storing it (wrong keys are never persisted), `get_auth_status` explains what's
configured, `logout` clears credentials. Re-auth works mid-session.
- **Honest errors** — every failure mode (unresolved person, share-sum mismatch, unknown
category, revoked key, exhausted retries) returns text telling the agent exactly what
happened and what to do next.
- **Safe-by-default annotations** — reads carry `readOnlyHint`, destructive deletes carry
`destructiveHint`, per MCP 2026-07-28 semantics. Tools register in deterministic order
for cache-friendly discovery.
- **Local-first secrets** — API key stored at `~/.config/splitwise-mcp/credentials.json`,
mode `0600`, atomic writes, never echoed back (masked previews only).
## Quick start
```bash
git clone https://github.com/<you>/open-splitwise.git
cd open-splitwise
uv sync
```
Run it standalone (stdio):
```bash
uv run open-splitwise # starts with no key configured — see auth below
```
Get an API key at <https://secure.splitwise.com/apps> *(Account Settings → API keys)*.
### Connect any MCP client
Generic stdio block (Claude Desktop `claude_desktop_config.json`, Claude Code `.mcp.json`,
Cursor, …):
```json
{
"mcpServers": {
"splitwise": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/open-splitwise", "run", "open-splitwise"],
"env": { "SPLITWISE_API_KEY": "<optional: preconfigure>" }
}
}
}
```
### Connect Hermes Agent
Add to `~/.hermes/config.yaml`:
```yaml
mcp_servers:
splitwise:
command: "uv"
args: ["--directory", "/absolute/path/to/open-splitwise", "run", "open-splitwise"]
env:
SPLITWISE_API_KEY: "<optional>"
tools:
include: [quick_add_expense, resolve_users, money_summary, get_auth_status]
prompts: false
resources: false
```
Then `/reload-mcp`. Start with the four workflow/auth tools above; add raw API tools only
when needed — Hermes' per-server filtering keeps the tool surface small.
## Authentication lifecycle
The server is designed so **agents diagnose and fix auth themselves**, asking you only for
the secret:
| Situation | Agent-visible behavior |
|---|---|
| No key anywhere | Every tool fails with: *"No Splitwise API key is configured. Ask the user to generate one at secure.splitwise.com/apps, then call setup_auth."* |
| User provides a key | `setup_auth(api_key)` probes `/get_current_user` first — invalid keys are **rejected, not stored**; valid keys are saved and who they belong to is reported |
| Key revoked / account logged out (HTTP 401/403) | Tools fail with *"key may have been revoked, expired, or the account was logged out… ask the user for a fresh key and call setup_auth"* |
| Diagnosis | `get_auth_status()` → `{configured, source: stored\|environment, masked_key}` |
| Switching accounts | `logout()` deletes the stored credential |
Key resolution happens **per request**: stored credential → `SPLITWISE_API_KEY` env var →
none. A freshly saved key takes effect immediately in the running process — zero restarts.
Credentials live at `~/.config/splitwise-mcp/credentials.json` (mode `0600`). Override the
directory with `SPLITWISE_MCP_CONFIG_DIR` (handy for tests or multi-profile setups).
## Agent ergonomics
```text
You: "add dinner 900 split with alice and bob@x.com, groceries"
Agent: quick_add_expense(description="Dinner", cost="900.00",
participants=["alice", "bob@x.com"],
category_name="groceries")
Server: resolves alice→12? two matches! → error listing Alice A (id 10), Alice Wood (id 12)
Agent: "Which Alice?" → you answer → re-call succeeds
Server: { status: created, expense_id: 99123,
splits: [ "Nikhil paid 900.00 INR",
"Alice A owes 300.00 INR",
"Bob B owes 300.00 INR" ] }
```
- **`quick_add_expense`** — names/partial-names/emails/IDs accepted; equal shares computed
with remainder cents distributed deterministically; custom `owed_shares` validated to sum
exactly; payer included by default (`include_payer_in_split=false` when they didn't
consume); currency defaults from your profile.
- **`resolve_users`** — email exact-match, full-name match, unique first-name, substring
fallback; ambiguity returns candidates instead of guessing.
- **`money_summary`** — per-currency `owed_to_you` / `you_owe` / `net`, friend-level
balances, and group simplified debts involving you.
## Tool reference (33)
| Group | Tools |
|---|---|
| **Workflows** | `quick_add_expense` · `resolve_users` · `money_summary` |
| Users | `get_current_user` · `get_user` · `update_user` |
| Groups | `get_groups` · `get_group` · `create_group` · `delete_group`* · `undelete_group` · `add_user_to_group` · `remove_user_from_group` |
| Friends | `get_friends` · `get_friend` · `create_friend` · `create_friends` · `delete_friend`* |
| Expenses | `get_expenses` · `get_expense` · `create_expense` · `update_expense` · `delete_expense`* · `undelete_expense` |
| Comments | `get_comments` · `create_comment` · `delete_comment`* |
| Notifications | `get_notifications` |
| Other | `get_currencies` · `get_categories` |
| Auth | `setup_auth` · `get_auth_status` · `logout`* |
\* annotated `destructiveHint=true`; all `get_*` tools annotated `readOnlyHint=true`.
Prefer workflow tools over their raw counterparts whenever both exist.
## Rate limiting
Splitwise answers `HTTP 429` when throttled. open-splitwise retries automatically:
`Retry-After` header honored verbatim; otherwise exponential backoff (0.5 s doubling,
capped at 30 s), up to 3 attempts by default. Agents see an error only if every attempt is
exhausted — and that error says to slow down, not retry blindly.
## Configuration
| Env var | Default | Purpose |
|---|---|---|
| `SPLITWISE_API_KEY` | – | Bootstrap key (stored credentials take precedence) |
| `SPLITWISE_MCP_CONFIG_DIR` | `~/.config/splitwise-mcp` | Where `credentials.json` lives |
| `SPLITWISE_MCP_MAX_RETRIES` | `3` | 429 retry attempts before surfacing |
| `SPLITWISE_MCP_LAZY` | `on` | `off` registers all 33 tools upfront |
## Splitwise quirks handled for you
- Array params flattened to Splitwise's odd `users__{index}__{property}` encoding
- `200 OK ≠ success`: `errors{}` / `success:false` checked on every mutation
- Money as decimal **strings** with 2 dp; remainder cents distributed, sums always exact
- `category_id` must be a **subcategory** — enforced via fuzzy name resolution
- Balances/debts read from pre-computed `balance[]` / `simplified_debts` (never recomputed)
- "Settle up" is just an expense with `payment:true` (no dedicated endpoint exists)
- OAuth2 exists but is deliberately out of scope: personal API keys fit the
agent-asks-user flow; OAuth needs a redirect URI + browser (hosted deployments only)
## Architecture
```text
┌─────────────── any MCP client ───────────────┐
│ Hermes / Claude Desktop / Cursor / … │
└──────────────────┬───────────────────────────┘
│ JSON-RPC over stdio
┌──────────────────▼───────────────────────────┐
│ server.py — FastMCP app, 33 tools │
│ workflows · raw endpoints · auth lifecycle │
├──────────────────────────────────────────────┤
│ client.py — async REST client │
│ bearer auth (per-request key resolution) │
│ param flattening · success verification │
│ transparent 429 retry/backoff │
├──────────────────────────────────────────────┤
│ auth.py — credentials.json (0600, atomic) │
└──────────────────┬───────────────────────────┘
│ HTTPS
secure.splitwise.com/api/v3.0
```
## Development
```bash
uv run pytest # 54 tests: client, rate limits, auth, workflows, lazy loading, MCP semantics
uv run python scripts/smoke_stdio.py # real subprocess: handshake, discovery, live auth-failure paths
```
Built test-first (strict TDD): every behavior above has a failing-test-first provenance.
Layout:
```text
src/open_splitwise/
client.py # REST client: auth provider, flattening, retry, error mapping
auth.py # credential storage
server.py # FastMCP definitions: workflows + raw + auth tools
tests/
scripts/smoke_stdio.py
```
## Terms of use
Splitwise's self-serve API is **non-commercial** per their [API terms](https://dev.splitwise.com/#section/Terms-of-Use).
Your API key grants full access to your account — treat it like a password. This project
is an independent integration and is not affiliated with or endorsed by Splitwise Inc.
## Roadmap
- [ ] Receipt upload on expense creation
- [ ] Multi-currency expense helper with conversion awareness
- [ ] Recurring-expense summaries as an MCP prompt
- [ ] Optional Streamable HTTP transport for hosted/multi-user deployments (+OAuth2)
- [ ] Publish to PyPI (`uvx open-splitwise`)
## Contributing
PRs welcome — please keep the TDD discipline (tests fail first, then pass), keep tool
descriptions written *for models*, and never log secrets.
## License
[MIT](LICENSE) — open for everyone: use it, modify it, ship it, sell with it. Just keep
the copyright notice.
TDQS
Scored across 7 tools
Each visible tool has a clearly distinct role: auth setup/status/logout form a lifecycle, resolve_users handles identifier mapping, quick_add_expense creates expenses, and money_summary provides balances. search_tools is explicitly a discovery/meta tool, so there is no meaningful overlap between tools.
Most tools follow a clear verb_noun snake_case pattern such as setup_auth, get_auth_status, resolve_users, quick_add_expense, and search_tools. logout and money_summary deviate slightly as a bare verb and a noun-noun phrase, but the naming remains easily predictable.
Seven tools is a well-scoped count for this server. It covers authentication, user lookup, expense creation, financial summary, and dynamic tool discovery without unnecessary bloat, and the lazy-loading design keeps the default surface compact.
The visible high-level tools cover auth, user resolution, expense creation, and summary, which handles common Splitwise workflows. search_tools explicitly exposes hidden raw tools for expenses, groups, and notifications, mitigating most gaps, though direct visible listing/update/delete expense helpers would make common management tasks more straightforward.