Metadata-Version: 2.5
Name: splitwise-mcp-server
Version: 0.1.0
Summary: A gold-standard Model Context Protocol server for Splitwise
Project-URL: Homepage, https://github.com/sarathfrancis90/splitwise-mcp
Project-URL: Repository, https://github.com/sarathfrancis90/splitwise-mcp
Project-URL: Issues, https://github.com/sarathfrancis90/splitwise-mcp/issues
Author: Sarath Francis
License-Expression: MIT
License-File: LICENSE
Keywords: claude,expense-sharing,llm-tools,mcp,model-context-protocol,splitwise
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: mcp[cli]>=2.1.0
Requires-Dist: pydantic>=2.7
Provides-Extra: dev
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# splitwise-mcp

A Model Context Protocol server that gives any MCP-compatible AI host full, safe control
over a [Splitwise](https://www.splitwise.com) account — expenses, groups, friends, and
balances — with guards against silent duplicate charges and unconfirmed deletions.

[![PyPI version](https://img.shields.io/pypi/v/splitwise-mcp-server.svg)](https://pypi.org/project/splitwise-mcp-server/)
[![CI](https://github.com/sarathfrancis90/splitwise-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/sarathfrancis90/splitwise-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/sarathfrancis90/splitwise-mcp/blob/main/LICENSE)

## Quickstart

No install step is required. Any MCP host that can launch a command over stdio can run
the server directly with [uv](https://docs.astral.sh/uv/):

```bash
uvx --from splitwise-mcp-server splitwise-mcp
```

Set `SPLITWISE_API_KEY` in the environment first (see below), then point your MCP host at
that command. Configuration snippets for three hosts are below.

## Getting your Splitwise API key

Most setups only need a personal API key:

1. Go to [secure.splitwise.com/apps](https://secure.splitwise.com/apps) and register an
   application (any name and description work).
2. Copy the generated API key.
3. Set it as `SPLITWISE_API_KEY` wherever you configure the server.

A personal API key authenticates as the Splitwise account that generated it — this is all
you need for a single-user setup, such as Claude Code or Gemini CLI running on your own
machine.

Reach for OAuth2 instead when one deployment of this server needs to act on behalf of
*someone else's* Splitwise account — for example, a shared bot or chat-bridge host serving
multiple people, where you don't want each user handing their API key to the operator.
To use it:

1. Register an application at the same [secure.splitwise.com/apps](https://secure.splitwise.com/apps)
   page and note its consumer key and secret.
2. Set that application's **Redirect URI / Callback URL** to
   `http://127.0.0.1:61438/callback`. `splitwise-mcp-auth` always binds its local callback
   server to this exact host and port, so it must match what's registered here or
   Splitwise will refuse the authorization request.
3. Set `SPLITWISE_CONSUMER_KEY` and `SPLITWISE_CONSUMER_SECRET` in the environment.
4. Run `splitwise-mcp-auth` once per account. It opens a browser to Splitwise's
   authorization page, receives the redirect on a local server listening at
   `127.0.0.1:61438`, and caches the resulting access token to
   `~/.splitwise-mcp/token.json` (mode 0600). The MCP server picks that cached token up
   automatically on every subsequent run. If port 61438 is already in use by something
   else, free it and run the command again -- it will not fall back to a different port.

## Configuring your MCP host

All three snippets below use the same command and the personal-API-key path. Substitute
your own key.

### Claude Code (`.mcp.json`)

```json
{
  "mcpServers": {
    "splitwise": {
      "type": "stdio",
      "command": "uvx",
      "args": ["--from", "splitwise-mcp-server", "splitwise-mcp"],
      "env": {
        "SPLITWISE_API_KEY": "your-splitwise-api-key"
      }
    }
  }
}
```

### Gemini CLI (`.gemini/settings.json` or `~/.gemini/settings.json`)

```json
{
  "mcpServers": {
    "splitwise": {
      "command": "uvx",
      "args": ["--from", "splitwise-mcp-server", "splitwise-mcp"],
      "env": {
        "SPLITWISE_API_KEY": "your-splitwise-api-key"
      }
    }
  }
}
```

### Hermes Agent (`~/.hermes/config.yaml`)

```yaml
mcp_servers:
  splitwise:
    command: "uvx"
    args: ["--from", "splitwise-mcp-server", "splitwise-mcp"]
    env:
      SPLITWISE_API_KEY: "your-splitwise-api-key"
```

## Worked example: a low-trust registration for chat channels

Some MCP hosts bridge a chat surface you don't fully control — a WhatsApp number shared
with roommates, a family group chat, a support inbox — through Hermes Agent or a similar
tool. Messages arriving over that channel may come from anyone in the chat, may be routed
through a smaller/cheaper model with weaker judgment, and may not even reach a client that
implements MCP elicitation (many chat-bridge hosts don't), which means this server's
confirm-gate on destructive tools would fall back to its "re-call with `confirm=true`"
mode instead of ever prompting a human. A local model asked "can you delete the roommate
group?" as a joke, or a prompt-injection payload hidden in an incoming message, should not
be able to act on that.

The fix is to register this server **twice** under two different names — once with the
full tool list for channels you trust, and once scoped down to read-only tools plus
`create_expense` for everything else — and wire only the second registration into the
untrusted channel's agent profile.

**Hermes Agent** (`~/.hermes/config.yaml`) supports this natively via each server's
`tools.include`/`tools.exclude` block:

```yaml
mcp_servers:
  splitwise:
    command: "uvx"
    args: ["--from", "splitwise-mcp-server", "splitwise-mcp"]
    env:
      SPLITWISE_API_KEY: "${SPLITWISE_API_KEY}"
    # Full tool list. Wire this into channels you fully control (CLI, your own account).

  splitwise_readonly:
    command: "uvx"
    args: ["--from", "splitwise-mcp-server", "splitwise-mcp"]
    env:
      SPLITWISE_API_KEY: "${SPLITWISE_API_KEY}"
    tools:
      include:
        - whoami
        - list_groups
        - get_group
        - list_friends
        - list_expenses
        - get_expense
        - create_expense
    # Read-only tools plus create_expense only. Wire this into the WhatsApp channel
    # (or any other agent profile handling messages from people you don't fully trust).
```

With `include` set, Hermes never even shows the model the excluded tools exist, so there is
no `delete_group`, `delete_expense`, or `manage_group_members` for a WhatsApp message to
reach — the worst an untrusted sender can do is add a visible, additive expense (itself
still guarded by `create_expense`'s duplicate-detection window), never rewrite or delete
shared history.

**Gemini CLI** offers the same mechanism per server, with `includeTools`/`excludeTools`
directly in `settings.json`:

```json
{
  "mcpServers": {
    "splitwise": {
      "command": "uvx",
      "args": ["--from", "splitwise-mcp-server", "splitwise-mcp"],
      "env": { "SPLITWISE_API_KEY": "your-splitwise-api-key" }
    },
    "splitwise_readonly": {
      "command": "uvx",
      "args": ["--from", "splitwise-mcp-server", "splitwise-mcp"],
      "env": { "SPLITWISE_API_KEY": "your-splitwise-api-key" },
      "includeTools": [
        "whoami", "list_groups", "get_group", "list_friends",
        "list_expenses", "get_expense", "create_expense"
      ]
    }
  }
}
```

**Claude Code** has no per-server include/exclude field in `.mcp.json` itself; register
the server twice there as shown above, then scope the low-trust name in
`.claude/settings.json` with an explicit allow-list keyed to the `mcp__<server>__<tool>`
pattern. Unlike Gemini CLI's or Hermes's include-lists, this doesn't hide the other tools
from the model — it gates the call itself, falling back to Claude Code's normal permission
prompt for anything not listed — so for a fully unattended bridge, pair it with whatever
default-deny/headless permission mode Claude Code offers rather than relying on the allow
rule alone:

```json
{
  "permissions": {
    "allow": [
      "mcp__splitwise_readonly__whoami",
      "mcp__splitwise_readonly__list_groups",
      "mcp__splitwise_readonly__get_group",
      "mcp__splitwise_readonly__list_friends",
      "mcp__splitwise_readonly__list_expenses",
      "mcp__splitwise_readonly__get_expense",
      "mcp__splitwise_readonly__create_expense"
    ]
  }
}
```

## Tool reference

| Tool | Description | Kind |
| --- | --- | --- |
| `whoami` | Return the profile of the authenticated Splitwise account. | Read-only |
| `list_groups` | List every Splitwise group the authenticated user belongs to. | Read-only |
| `get_group` | Fetch a single group's detail, including its current members. | Read-only |
| `list_friends` | List every Splitwise friend, with current balances. | Read-only |
| `list_expenses` | List expenses, optionally scoped to one group or friend, paginated. | Read-only |
| `get_expense` | Fetch one expense's full detail, including splits and its comment thread. | Read-only |
| `create_expense` | Create a new expense, split among a group or with a friend. Runs duplicate detection first. | Additive |
| `settle_up` | Record a payment from one user to another, settling part of their balance. | Additive |
| `add_comment` | Post a comment on an expense. | Additive |
| `create_group` | Create a new group and optionally add its initial members. | Additive |
| `undelete_expense` | Restore a previously deleted expense. | Additive |
| `undelete_group` | Restore a previously deleted group. | Additive |
| `manage_group_members` | Add a member (immediate) or remove one (confirm-gated). | Additive / Destructive |
| `update_expense` | Change an expense's description, amount, split, date, or category. | Destructive (confirm-gated) |
| `delete_expense` | Permanently delete an expense. | Destructive (confirm-gated) |
| `delete_comment` | Permanently delete a comment. | Destructive (confirm-gated) |
| `delete_group` | Permanently delete a group. | Destructive (confirm-gated) |

Two read-only resources are also exposed for reference data: `splitwise://categories`
(every expense category, for `create_expense`'s `category_id`) and
`splitwise://currencies` (every accepted `currency_code`).

## Safety model

**Destructive actions are confirm-gated.** `update_expense`, `delete_expense`,
`delete_comment`, `delete_group`, and `manage_group_members` (when removing a member) all
take a `confirm: bool = False` parameter. With `confirm` left `False`, the tool fetches the
current state from Splitwise, builds an exact preview of what will change, and calls the
MCP client's elicitation capability to ask for interactive yes/no confirmation. If the
connected client doesn't support elicitation at all, the tool never performs the write —
instead it raises an error containing the full preview and the instruction to re-call with
`confirm=true`. Passing `confirm=true` skips straight to the write. Additive actions
(`create_expense`, `add_comment`, `settle_up`, `create_group`, `undelete_expense`,
`undelete_group`, and adding a group member) never require confirmation.

**`create_expense` guards against duplicate writes with a reserve-then-finalize log.** A
local SQLite log at `~/.splitwise-mcp/write_log.db` (directory mode 0700) reserves a row
for every `create_expense` call before it talks to Splitwise, and only marks that row
completed once the API call actually succeeds. A repeated call for the same group/friend,
amount, and description is handled one of three ways: if an earlier call for the same
content is still in flight (reserved but not yet completed, within a 60-second guard
window), the retry is rejected outright rather than allowed to race the original request;
if a matching call completed in the last 120 seconds, it's rejected with an error
describing the existing expense unless `allow_duplicate=true` is passed; otherwise the
write proceeds. An optional `idempotency_key` lets a caller safely retry the same logical
write — a repeated key replays the original result instead of creating a second expense.
This narrows, but cannot fully eliminate, the failure window inherent to any client
retrying a call whose response was lost: a retry issued after the 60-second guard window
has elapsed, with no `idempotency_key` and against an original request that is *still*
in flight at Splitwise, can still produce a real duplicate.

**Money is exact.** Every amount is a Python `Decimal`, never a `float`. Splitting an
expense uses the largest-remainder method: each participant's ideal share is computed as
an exact fraction of the total in cents, floored, and any leftover cents are handed out one
at a time to the participants with the largest fractional remainder (ties broken by input
order) until the split sums to the total exactly, in every mode (`equal`, `exact`,
`percent`, `shares`). `exact` and `percent` splits that don't reconcile to the expense
total are rejected with the precise shortfall or excess, not silently rounded.

**Ambiguous names are never guessed.** Friend, group, and category lookups accept either a
numeric id or a case-insensitive substring of a name. Zero matches produce an error listing
close-but-imperfect suggestions; more than one match produces an error listing every
candidate by id and full name so the caller can retry with an exact id. This server never
silently picks the "best" match.

## Limitations

- **Two-decimal currencies only.** All money math assumes currencies with exactly two
  decimal places (USD, EUR, GBP, and most others). Zero-decimal currencies such as JPY are
  not handled correctly in this v1 and should not be used with `create_expense` or
  `update_expense`.
- **`settle_up` is a synthetic payment.** Splitwise's API doesn't expose the flag that
  marks an expense as a true "payment" through this endpoint, so a settlement is recorded
  as an ordinary expense where the payer owes nothing and the recipient owes the full
  amount. The balance effect is identical to a real payment, but it will display with a
  regular expense icon in the Splitwise UI rather than a payment icon.
- **Elicitation support varies by host.** The confirm-gate's interactive prompt only works
  if the connected MCP client implements elicitation. Many hosts, especially chat-bridge
  and headless integrations, don't — those fall back to the `confirm=true` re-call pattern
  for every destructive tool.
- **Duplicate detection is local and per-installation.** The write log lives on the machine
  running the server. It won't catch a duplicate created by a different instance of this
  server, or one entered directly through the Splitwise app or website.
- **Pagination has no total count.** `list_expenses`'s `has_more` flag is a lower bound (it
  is true whenever a page came back full), not a guarantee, because the Splitwise API
  doesn't report how many expenses exist in total.
- **One currency per expense.** Splits within a single expense share one `currency_code`,
  matching Splitwise's own data model; there is no cross-currency split support.

## Development

```bash
git clone https://github.com/sarathfrancis90/splitwise-mcp
cd splitwise-mcp
uv venv
source .venv/bin/activate
uv pip install -e '.[dev]'

pytest
ruff check src/ tests/
mypy src/
```
