Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .envrc.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export SUPABASE_PROJECT_REF=your-project-ref-here
export DATABASE_URL=postgres://splitcount:password@localhost:5432/splitcount
export PORT=3000
58 changes: 29 additions & 29 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
name: Deploy to Supabase
name: Build and Push Docker Image

on:
push:
branches:
- main

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
deploy:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- uses: actions/checkout@v4

- uses: supabase/setup-cli@v1
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
version: latest

- name: Link project
run: supabase link --project-ref "$SUPABASE_PROJECT_REF"
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_REF }}

- name: Push database migrations
run: supabase db push --password "$SUPABASE_DB_PASSWORD"
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_REF }}
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Reload PostgREST schema cache
run: |
supabase db execute --sql "SELECT pg_notify('pgrst', 'reload schema');" 2>/dev/null \
|| echo "Schema cache reload skipped — run manually if needed."
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_REF }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=raw,value=latest,enable={{is_default_branch}}

- name: Deploy mcp edge function
run: supabase functions deploy mcp --no-verify-jwt
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_REF }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
19 changes: 19 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Test

on:
pull_request:
branches:
- main
push:
branches:
- main

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run check
- run: bun run typecheck
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
.env.*
!.env.example
.envrc
supabase/.temp/
node_modules/
dist/
.DS_Store
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM oven/bun:1-alpine
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile --production
COPY src/ ./src
COPY migrations/ ./migrations
ENV PORT=3000
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Dennis Falling

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
26 changes: 21 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
.PHONY: deploy
.PHONY: install dev start typecheck docker-build docker-up docker-down

# Deploy migrations and edge function.
# Requires SUPABASE_PROJECT_REF to be set (via .envrc + direnv, or exported manually).
deploy:
./scripts/deploy.sh
install:
bun install

dev:
bun run --watch src/index.ts

start:
bun run src/index.ts

typecheck:
bun run tsc --noEmit

docker-build:
docker build -t splitcount .

docker-up:
docker compose up --build -d

docker-down:
docker compose down
96 changes: 56 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,85 @@ Expense splitting via MCP + Claude. Log expenses (including receipt photos), tra

## Architecture

- **MCP server**: Supabase Edge Function (Deno) at `supabase/functions/mcp/`
- **Database**: Supabase PostgreSQL (groups, members, expenses, splits, settlements)
- **MCP server**: Bun HTTP server at `src/`
- **Database**: PostgreSQL (self-hosted or any provider)
- **Client**: Any MCP-compatible client (Claude Desktop, Claude.ai, etc.)

## Setup
Migrations run automatically on startup. No migration tooling required.

### 1. Create a Supabase project
## Self-hosting with Docker

Go to [supabase.com](https://supabase.com), create a new project, and note your project ref (visible in the project URL or Settings → General).
The easiest way to run SplitCount is with Docker Compose. Add it to your existing stack:

### 2. Set your project ref

Add to `.envrc` (used by [direnv](https://direnv.net/)):
```bash
export SUPABASE_PROJECT_REF=<your-ref>
```yaml
services:
splitcount:
image: ghcr.io/dfalling/splitcount:latest
environment:
DATABASE_URL: postgres://user:password@your-postgres-host:5432/splitcount
ports:
- "3000:3000"
restart: unless-stopped
```

Or export it manually:
Or run the full stack including Postgres:

```bash
export SUPABASE_PROJECT_REF=<your-ref>
docker compose up -d
```

### 3. Deploy
The server will be available at `http://localhost:3000`.

```bash
make deploy
```
### Connect to Claude

This links to your project, pushes database migrations, and deploys the MCP edge function. Your server URL will be printed at the end:
In Claude Desktop or Claude.ai settings, add a custom MCP connector:
```
https://<your-ref>.supabase.co/functions/v1/mcp
http://your-server:3000
```

### 4. Add as a custom connector
## Local development

**Prerequisites**: [mise](https://mise.jdx.dev/) for version management.

```bash
mise install # installs bun
bun install # install dependencies
cp .envrc.example .envrc
# edit .envrc with your DATABASE_URL
bun run dev # start with hot reload
```

In Claude Desktop or Claude.ai settings, add a custom MCP connector with your server URL:
Test the server:
```bash
curl -X POST http://localhost:3000 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```
https://<your-ref>.supabase.co/functions/v1/mcp

Health check:
```bash
curl http://localhost:3000?health
```

## Usage

### Create a group
> "Create a new expense group called 'Barcelona Trip' with my name as Alex"

Claude will return a **6-character join code** (e.g. `XK7M2P`) and your **member_id**. Share the join code with friends.
Claude returns a **6-character join code** (e.g. `XK7M2P`) and your **member_id**. Share the join code with friends.

### Join a group
> "Join group XK7M2P, my name is Jordan"
> "Join group XK7M2P"

Claude will show you the existing member list so you can claim your slot or join as someone new.

### Log an expense
> "Log a $45 dinner expense, I paid, split equally among everyone"

**From a receipt photo**: Share the image with Claude, then ask it to log it:
**From a receipt photo**: share the image with Claude, then ask it to log it:
> "Here's my receipt [image]. Add it as an expense split equally."

Claude will read the receipt, extract the amount and description, and call `add_expense` with the details.
Claude reads the receipt, extracts the amount and description, and calls `add_expense`.

### Check balances
> "Who owes what in our group?"
Expand All @@ -70,37 +92,31 @@ Claude will read the receipt, extract the amount and description, and call `add_

## Identity model

There is no login. When you create or join a group, you receive a **member_id** (UUID). This is your permanent identity — save it. Claude stores it in conversation context and will remind you to note it down.
There is no login. When you create or join a group you receive a **member_id** (UUID). This is your permanent identity — save it. Claude stores it in conversation context and will remind you to note it down.

If you switch devices or start a new conversation, use `get_member` to confirm your identity:
If you start a new conversation, use `get_member` to confirm your identity:
> "My member ID is abc123... — what group am I in?"

## Tools

| Tool | Description |
|------|-------------|
| `create_group` | Create a group, get a join code |
| `join_group` | Join with a join code + display name |
| `join_group` | Join with a join code; shows member list to claim or create |
| `get_group` | Group info and member list |
| `add_expense` | Log an expense (equal/exact/percent splits) |
| `list_expenses` | View expenses with split details |
| `update_expense` | Edit an expense you logged |
| `delete_expense` | Soft-delete an expense you logged |
| `get_balances` | Net balances + minimum payments to settle |
| `record_settlement` | Record a payment between members |
| `get_settlement_history` | Past settlements |
| `add_member` | Pre-add a member before they join |
| `claim_member` | Identify as an existing member |
| `list_members` | List all members in a group |
| `get_member` | Look up your member info |
| `rename_member` | Change your display name |

## Local development

```bash
supabase start # starts local Postgres + Edge Functions runtime
supabase functions serve mcp --no-verify-jwt
```
## Database migrations

Test with curl:
```bash
curl -X POST http://localhost:54321/functions/v1/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```
Migrations live in `migrations/` and are applied automatically at startup in order. To add a new migration, create a file named `NNN_description.sql` (e.g. `005_add_tags.sql`).
35 changes: 35 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
Loading
Loading