monarch-mcp-ui
by jsilets
README.md
# monarch-mcp-ui
Interactive UI for Monarch Money inside claude.ai, running on Monarch's **official OAuth**.
`monarch-mcp-ui` is a small Cloudflare Worker that sits in front of Monarch's official MCP
server and adds an interactive UI layer. The protocol supports this now
([MCP Apps / SEP-1865](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865),
the standard Anthropic and OpenAI co-published in late 2025), but no Monarch server ships it. You
connect the Worker in claude.ai, log into *your own* Monarch account on Monarch's consent screen,
and read and edit your finances in widgets, not plain text.
<p align="center">
<img src="assets/transactions-in-claude.png" width="840"
alt="The transactions widget rendered inline in a claude.ai chat, with the inline category picker open on a transaction.">
<br>
<em>Ask in plain English. The table renders inline, and you recategorize a transaction without leaving the chat.</em>
</p>
Three widgets render inline:
- **Transactions**: a filterable, sortable, paginated table with inline category editing. The
dropdown writes back to Monarch and reverts if the write is rejected.
- **Spending**: spending-by-category bars you click to drill into the underlying transactions.
- **Accounts & net worth**: accounts grouped by type, with balances and total net worth.
Plus broad data-tool coverage (cash flow, budgets, recurring, investments, goals, rules, bulk
recategorize, and a one-call financial overview) so the model can reason over the rest of your
Monarch data, not just what the widgets show.
## Try it live (no clone, about a minute)
A hosted instance is running. In **claude.ai** go to **Settings → Connectors → Add custom
connector** and paste:
```
https://monarch-mcp-apps.joshuasilets.workers.dev/mcp
```
Approve, and you'll be sent to **Monarch's own login and consent** screen. Back in a chat, ask
*"show my recent transactions."* The interactive table renders inline. Change a category from the
dropdown and it persists to your Monarch account.
It is multi-tenant: you log into your own Monarch account, your data is isolated from every other
user, your tokens are encrypted at rest, and you can revoke access anytime from your Monarch account
settings or by removing the connector in claude.ai. To run your own, see
[Self-host](#self-host) below.
## What's different
Two things make it different from the other ways to use Monarch from an MCP client.
**1. Official OAuth, brokered so anyone can connect their own account.** The Worker acts as an
OAuth client to Monarch's official MCP. Each person who connects logs in on Monarch's consent
screen, against their own account. No passwords touch the server, each user's Monarch tokens are
encrypted separately, and access is revocable from Monarch.
**2. The interactive UI layer.** Monarch's official MCP and the community servers return text or
JSON. This one publishes `ui://` MCP Apps resources, so claude.ai renders widgets: a table you
edit inline, a dashboard you drill into, an accounts and net-worth view. I believe it's the first
Monarch MCP with an interactive UI. If there's another, I'd like to see it.
| | Monarch's official MCP | Community MCPs | monarch-mcp-ui |
|---|---|---|---|
| Interactive widgets in claude.ai | No | No | **Yes** (MCP Apps) |
| Auth to Monarch | Official OAuth 2.1 + PKCE | Your Monarch credentials, locally | Official OAuth 2.1 (DCR + PKCE) |
| Each user connects their own account | Yes | Personal / single-user | **Yes**, multi-tenant |
| Self-host, extend, add your own UI | No (closed) | Yes | **Yes** |
Under the hood the Worker plays two OAuth roles at once: provider to claude.ai, client to Monarch.
The widgets use the official
[`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) SDK and inherit
claude.ai's design tokens (colors, type scale, light and dark), so they match the host's look.
## Prior art and credit
This builds on a lot of community work. The unofficial
[`monarchmoney`](https://github.com/hammem/monarchmoney) library and the MCP servers built on it
(robcerda, jamiew, and others) are what made Monarch programmable for most people. They are a great
fit for personal, single-user setups where you run the tool against your own credentials on your own
machine. `monarch-mcp-ui` does two things the community servers don't: official-OAuth multi-tenant
access and an interactive UI.
## Architecture
```
claude.ai ──OAuth(provider)──► Worker ──OAuth(client, upstream)──► Monarch OAuth (DCR + PKCE)
│ Streamable HTTP /mcp ├─ MonarchMcpAgent (Durable Object)
│ ├─ tools/call passthrough ─► api.monarch.com/mcp
▼ └─ ui:// widget (embedded single-file build)
renders the widget ◄── ui/notifications/tool-result ──┘
│ recategorize dropdown → app.callServerTool('recategorize_transaction') → UpdateTransaction
```
The Worker is an OAuth provider to claude.ai and an OAuth client to Monarch. Per-user Monarch tokens
are AES-GCM encrypted in KV (keyed by an opaque `userKey`); only that key rides in the token issued
to claude.ai, so a session can resolve its own Monarch tokens and nothing else. See
[`spike/FINDINGS.md`](spike/FINDINGS.md) for the Phase 0 evidence the design rests on (the OAuth
metadata, the verified write path, and the actual tool-result shapes).
## Layout
| Path | What |
|---|---|
| `src/index.ts` | OAuthProvider wiring (entrypoint) |
| `src/auth/monarch-handler.ts` | `/authorize` + `/callback` broker (claude.ai to Monarch) |
| `src/auth/{crypto,token-store}.ts` | AES-GCM + encrypted per-user token store |
| `src/monarch/oauth.ts` | DCR / PKCE / authorize / token / refresh (TS port of the spike) |
| `src/monarch/upstream-client.ts` | `tools/call` passthrough to Monarch /mcp (SSE or JSON) |
| `src/mcp/tools.ts` / `tools-data.ts` | widget-backed tools / broad data-tool coverage |
| `src/mcp/{agent,ui-resource,reshape}.ts` | MCP agent, `ui://` resources (one URI per widget), result reshaping |
| `widget/` | React widgets, one bundle that routes by view (table / spending / accounts), built single-file and embedded |
| `spike/` | Phase 0 de-risk scripts + `FINDINGS.md` (captured personal data is gitignored) |
## Tools
**Widget-backed** (render an interactive UI):
- `list_transactions`: filterable, sortable, paginated table with inline recategorize
- `spending_dashboard`: spending-by-category bars, click to drill into transactions
- `accounts_overview`: accounts grouped by type, plus net worth
**Reads:** `get_accounts`, `get_net_worth_history`, `get_cashflow`, `get_spending_by_category`,
`get_budget`, `get_recurring`, `get_investments`, `get_goals`, `list_rules`, `list_categories`,
`financial_overview` (one-call snapshot)
**Writes:** `recategorize_transaction`, `bulk_recategorize_transactions`
All proxy Monarch's official MCP with the connected user's OAuth token.
## Self-host
To run your own instance instead of the hosted one, you need Node 20+ and a Cloudflare account.
```bash
npm install
npm --prefix widget install
# 1. KV namespace: paste the printed id into wrangler.jsonc (OAUTH_KV.id)
npx wrangler kv namespace create OAUTH_KV
# 2. Encryption key (32-byte base64) for tokens at rest
openssl rand -base64 32 | npx wrangler secret put ENCRYPTION_KEY
# 3. Build the widget and deploy
npm run deploy
```
Then in **claude.ai**: Settings → Connectors → **Add custom connector** →
`https://<your-worker>.workers.dev/mcp`. Approve, get bounced to Monarch's login and consent (it
asks for read **and write**), and back in chat try *"show my recent transactions."*
## Local dev
```bash
npm run dev # builds the widget, runs `wrangler dev` (localhost:8787)
```
For the fastest UI loop, run the widget alone with mock data and no Worker:
```bash
npm --prefix widget run dev # http://localhost:5174/?view=transactions|spending|accounts
```
## How it works
The widget is built with `vite-plugin-singlefile` into one self-contained HTML string and embedded
in the Worker bundle, because Workers can't read files at runtime. The host loads it as a `ui://`
resource; inside the sandboxed iframe it speaks to claude.ai with the official
[`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) SDK. The tool's
`structuredContent` arrives via `app.ontoolresult`, and the recategorize dropdown calls back with
`app.callServerTool`.
Two non-obvious problems I hit, with fixes in the git history:
- **Distinct `ui://` URI per widget.** Hosts key widget instances by `resourceUri`. Pointing all
three tools at one shared URI made claude.ai reuse a single iframe, so the accounts tool rendered
the spending view. Each widget now has its own URI, and each iframe is told its view so a cold
re-mount fetches the right data.
- **A JS number-precision bug in Monarch's data.** `GetSpendingByCategory` returns `category_id` as
an 18-digit JSON number that exceeds `Number.MAX_SAFE_INTEGER`, so `JSON.parse` silently rounds it
and the wrong icons and drill-downs showed up. The fix resolves the correct string id by category
name against `GetCategories`, whose ids are strings.
## Status and roadmap
v0.1, with a working hosted demo. Solid today: the official-OAuth broker, per-user encrypted tokens,
16 tools, three widgets with inline write-back, and host theming. On the list:
- A disconnect tool (Monarch exposes `/oauth/revoke/`).
- More widgets: budgets, cash flow over time, recurring.
- A shared widget design system so every view uses the same components and host tokens.
## Safety and disclaimer
- **Not affiliated with or endorsed by Monarch Money.** This is an independent project.
- **Official endpoints only.** Auth goes through Monarch's own OAuth (`api.monarch.com`, OAuth 2.1 +
PKCE) and its official MCP. No scraped cookies, no stored passwords. You log in on Monarch's own
screen and can revoke anytime.
- **Your data is yours.** Per-user Monarch tokens are AES-GCM encrypted in KV and isolated per
connection. Nothing is logged or shared.
- **Writes are optimistic and reversible in the UI.** A recategorize updates the row immediately and
reverts if Monarch rejects it.
- **Monarch caps integrations at 5 writes per day** on the standard plan (shown in Monarch's own
Integrations settings; Monarch Plus removes the cap, and other fair-use limits may apply).
Recategorizations count against it, which is why `bulk_recategorize_transactions` takes a
`dry_run` to preview how many rows would change before you spend writes.
## A note to the Monarch team
Your official MCP is clean and well-built, and OAuth 2.1 + PKCE from day one is what made this broker
possible. I built `monarch-mcp-ui` to show what an interactive UI layer on top of it can feel like,
entirely on your own endpoints. MCP Apps is new, and a product with this much structured data
(transactions, budgets, accounts) is a natural fit for native widgets. If you're thinking about shipping MCP Apps natively, please feel free to take inspo from this repo / reach out.
## License
MIT. See [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing