Skip to main content
Glama
nnishad

open-splitwise

by nnishad
README.md
<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

A4.4/5.0

Scored across 7 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues