financecontext-mcp
by ezrazhang7
README.md
# FinanceContext MCP
The open-source **finance-context layer behind [Lumi](https://github.com/ezrazhang7/lumi-landing-page)** — a standalone remote MCP server that lets LLM agents reason over a user's real financial state without ever holding elevated credentials.
Extracted from the Lumi codebase; table names (`lumi_*`) reflect the production schema it runs against.
## The security model (the interesting part)
**The agent never gets more power than the user.** The server validates Supabase-issued bearer JWTs against JWKS, then uses *the user's own token* for every Supabase read and write — so Postgres row-level security remains the single source of authority. There is no service-role key anywhere in this service. Misbehaving agent, compromised prompt, doesn't matter: the blast radius is exactly what the user themselves could do.
Layered on top:
- OAuth protected-resource metadata (`/.well-known/oauth-protected-resource`) for MCP client discovery
- Permission claims enforced on every write and approval tool — **fail-closed**: a token with no permission claims can read but cannot write or approve
- DNS-rebinding protection via a host allowlist when binding publicly
- An activity log (`list_recent_activity`) so tool usage is auditable
### Permission model
Read tools require only a valid user token. Write and decision tools require an explicit permission claim on the JWT (`permissions`, `app_metadata.permissions`, or `user_metadata.permissions`):
| Permission | Gates |
|---|---|
| `memory_write` | `remember_financial_preference`, `forget_financial_memory` |
| `rules_draft` | `create_rule_draft` |
| `approvals_submit` | `submit_approval_request` |
| `approvals_decide` | `approve_pending_change`, `reject_pending_change` |
`approvals_submit` and `approvals_decide` are **deliberately separate**: an agent token should carry `approvals_submit` (propose) but never `approvals_decide` (dispose). The decision tools exist so a *human-held* token — not the agent — can accept or reject. Grant `approvals_decide` only to a principal you trust to be the human in the loop. If you previously ran with tokens that carried no permission claims, writes now fail closed until you add the claims above.
## Tool surface (19 tools)
| Category | Tools |
|---|---|
| **Read & analyze** | `list_accounts` · `get_balance_summary` · `search_transactions` · `summarize_spend` · `get_cashflow_summary` · `list_recurring_charges` · `get_sync_status` |
| **Classification** | `list_uncertain_transactions` · `explain_transaction_classification` |
| **Agent memory** | `remember_financial_preference` · `list_financial_memory` · `forget_financial_memory` |
| **Rule drafting** | `create_rule_draft` · `preview_rule_impact` · `list_rule_drafts` |
| **Human approval** | `submit_approval_request` · `approve_pending_change` · `reject_pending_change` |
| **Audit** | `list_recent_activity` |
The write path is deliberately indirect: agents *draft* rules and *preview* their impact, then *submit* an approval request. The request only takes effect when it is approved through `approve_pending_change`, which requires the separate `approvals_decide` permission — so an agent holding only `approvals_submit` cannot approve its own change. Agents propose; a human-held token disposes.
## Stack
TypeScript · Express 5 · `@modelcontextprotocol/sdk` (Streamable HTTP) · `@supabase/supabase-js` · `jose` (JWKS validation) · `zod`
## Endpoints
`GET /health` · `GET /.well-known/oauth-protected-resource` · `POST|GET|DELETE /mcp`
## Local development
```bash
cp .env.example .env # Supabase URL + publishable key
npm install
npm run dev # tsx watch
npm run check # typecheck
npm test # unit tests (node:test via tsx)
npm run build && npm start
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues