open-splitwise
Turn Splitwise into an agent-native expense tracker.
An open Model Context Protocol (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
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_authvalidates a key live against Splitwise before storing it (wrong keys are never persisted),get_auth_statusexplains what's configured,logoutclears 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 carrydestructiveHint, 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, mode0600, atomic writes, never echoed back (masked previews only).
Quick start
git clone https://github.com/<you>/open-splitwise.git
cd open-splitwise
uv sync
Run it standalone (stdio):
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, …):
{
"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:
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
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; customowed_sharesvalidated to sum exactly; payer included by default (include_payer_in_split=falsewhen 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-currencyowed_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:falsechecked on every mutation- Money as decimal strings with 2 dp; remainder cents distributed, sums always exact
category_idmust 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
┌─────────────── 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
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:
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. 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 — open for everyone: use it, modify it, ship it, sell with it. Just keep the copyright notice.