Skip to main content
Glama
yuechen

plaid-mcp

by yuechen
README.md
# plaid-mcp

**Talk to your bank accounts in natural language.** `plaid-mcp` is a local
[Model Context Protocol](https://modelcontextprotocol.io) server that gives
any MCP-aware agent (Claude Code, Claude Desktop, Codex CLI, Cursor, Gemini
CLI, Hermes, OpenClaw…) read-only SQL access to your bank, credit-card,
brokerage, and mortgage data via [Plaid](https://plaid.com).

```
$ claude
> what did I spend on food last month?
  → plaid_query(sql=...)
  → "You spent $487 on food in April, down from $612 in March.
     Top three merchants: Whole Foods ($143), Tartine ($89), DoorDash ($72)."

> am I behind on any credit-card payments?
  → plaid_query(sql=...)
  → "No. Your Chase Sapphire next payment is $1,243 due May 28
     (statement closed May 1, 14 days remaining)."
```

Everything runs locally. Your Plaid access tokens, transactions, and
holdings live in a SQLite file at `~/.plaid-mcp/data.db` — never uploaded
anywhere, never seen by Anthropic or any LLM provider.

---

## Why?

Plaid hands you a firehose of structured financial data — transactions,
balances, holdings, liabilities — but reasoning over it in any useful way
takes either a custom dashboard or a finance-app subscription that
re-sells your own data back to you. LLMs are great at this kind of
analytical query if you put a clean schema in front of them; the
challenge is wiring it up without leaking access tokens or building a
backend.

`plaid-mcp` is the wiring. Link your banks once, run sync on a cron,
expose a single `plaid_query` tool to your agent. The agent gets a
documented set of views (`vw_plaid_transactions`, `vw_plaid_holdings`,
`vw_plaid_liabilities_mortgage`, …) with Plaid's sign conventions
already flipped to the right direction, and writes SQL against them.

---

## Quick start (Claude Desktop)

There's no fully no-terminal path — you'll open Terminal twice — but
most of the work happens in two ordinary apps: Plaid's website and
Claude Desktop. Plan on ~15 minutes including Plaid's signup.

### 1. Install plaid-mcp

Open Terminal and run:

```bash
pipx install git+https://github.com/yuechen/plaid-mcp
```

(No pipx? On macOS: `brew install pipx && pipx ensurepath`, then
restart Terminal. On other OSes:
[pipx install guide](https://pipx.pypa.io/stable/installation/).)

### 2. Get free Plaid credentials

1. Sign up at [dashboard.plaid.com](https://dashboard.plaid.com) and
   verify your email.
2. Apply for the free **Trial plan** at
   [dashboard.plaid.com/trial-plan](https://dashboard.plaid.com/trial-plan).
   Approval is usually quick. (See [Plaid plans](#plaid-plans-which-one-do-i-need)
   below if you'd rather start with sandbox/fake-bank testing.)
3. Once approved, open the Dashboard's **Keys** tab and copy your
   `client_id` and your **production** secret.

### 3. Save your credentials

Create the file `~/.plaid-mcp/.env` with these three lines, pasting
the values from step 2:

```
PLAID_CLIENT_ID=paste-your-client-id-here
PLAID_SECRET=paste-your-production-secret-here
PLAID_ENV=production
```

Easiest way: open Terminal, run `mkdir -p ~/.plaid-mcp && open
~/.plaid-mcp`, then create the `.env` file in the Finder window that
pops up (Cmd-N → save as `.env`). Or use your editor of choice.

`plaid-mcp` auto-loads this file every time it runs — you only do
this once.

### 4. Link your bank

```bash
plaid-mcp link
```

Your browser opens to a Plaid-hosted page. Pick your bank, sign in
(handle any 2FA / OAuth prompts), and you're done. Run the command
again for each additional institution you want to connect.

### 5. Connect it to Claude Desktop

1. Open **Claude Desktop**.
2. **Claude → Settings → Developer → Edit Config**. (This opens
   `claude_desktop_config.json` in your text editor.)
3. Find the path to the installed `plaid-mcp` binary — in Terminal:

   ```bash
   which plaid-mcp
   ```

   Copy what it prints.
4. Paste this into the JSON file (replacing the path with what `which`
   showed you):

   ```json
   {
     "mcpServers": {
       "plaid": {
         "command": "/paste/the/path/from/which/here",
         "args": ["serve"]
       }
     }
   }
   ```

   - If the file is empty (`{}`), replace it with the block above.
   - If the file already has a `"mcpServers"` block, just add the
     `"plaid"` entry inside it.
   - If the file has *other* top-level keys (e.g. a `"preferences"`
     block — some Claude builds store more than just MCP config in
     this file), add `"mcpServers"` as a new top-level sibling of
     whatever's already there. Don't nest it inside another key.
5. Save and **quit + reopen Claude Desktop** (not just close the
   window — fully quit so it re-reads the config).

### 6. Try it

In Claude Desktop, open a new chat and ask something like:

> *What did I spend on food last month?*

Claude will call the `plaid_query` tool. The first time, it'll ask
you to approve the tool — say yes and you're off.

### Want to try it with fake data first?

If you'd rather kick the tires with fake banks before applying for
Plaid's Trial plan, use **sandbox mode** instead. It's free, instant
(no application), and lets you exercise the whole pipeline against
Plaid's test institutions with realistic synthetic data.

Two changes versus the steps above:

1. **In step 2**, copy your `sandbox` secret from the Keys tab (the
   row labeled "Sandbox", not "Production") — sandbox access is on
   by default, no application needed.
2. **In step 3** (`~/.plaid-mcp/.env`), use these values instead:

   ```
   PLAID_CLIENT_ID=paste-your-client-id-here
   PLAID_SECRET=paste-your-sandbox-secret-here
   PLAID_ENV=sandbox
   ```

When you run `plaid-mcp link` and Plaid's institution picker opens,
search for **First Platypus Bank** (or any other "test" institution
in the list). When it prompts for a username and password, use:

| Field    | Value       |
|----------|-------------|
| Username | `user_good` |
| Password | `pass_good` |

If the sandbox prompts you for an MFA code (phone or device
verification), the canonical answer is `1111` — Plaid's sandbox
always accepts that as the correct one-time code, regardless of
which "phone number" it claims to be sending to.

You'll get a fake but realistic set of accounts, transactions,
holdings, and a mortgage to play with. Once you're satisfied, apply
for the Trial plan and swap the three `.env` values to point at
production — no other changes needed.

### Keeping your data fresh

`plaid-mcp` syncs on demand (Claude can pull fresh data mid-conversation
by calling the `plaid_sync` tool). If you want a background refresh
running quietly in the background, open Terminal and run:

```bash
plaid-mcp sync --daemon
```

Leave it running in a Terminal window or tmux. It syncs everything
every 30 minutes.

### Using a different MCP client?

`plaid-mcp serve` speaks the standard Model Context Protocol over
stdio, so it works with **Claude Code**, **Codex CLI**, **Cursor**,
**Gemini CLI**, and any other MCP-aware tool. Each has its own config
file location, but the JSON block (`command` + `args: ["serve"]`)
above is the same.

If you use **Claude Code** specifically, also drop the bundled skill
into your global skills directory so the agent knows when to use the
finance tools without prompting:

```bash
git clone https://github.com/yuechen/plaid-mcp /tmp/plaid-mcp
cp -r /tmp/plaid-mcp/skills/plaid ~/.claude/skills/plaid
```

---

## What the agent sees

A single tool, `plaid_query(sql: str)`. The tool description embeds the
full schema (views, columns, types) plus a dozen worked SQL examples
(spending by category, net-worth breakdown, top merchants in a window,
unrealized-gain leaders, …). The agent picks a query, runs it, and
gets back JSON rows.

Hard-enforced safety net:

- The connection is opened **read-only** (`?mode=ro`) — DDL/DML can't
  reach the data even if the agent tries.
- Output is capped at 1000 rows.
- 5-second statement timeout.
- The agent only sees the curated `vw_plaid_*` views, not the raw
  `plaid_*` tables — Plaid's positive-is-outflow amount convention is
  flipped into `inflow` / `outflow` / `direction` columns so the
  agent can't get it wrong.

---

## What it knows

| Plaid product   | What you get                                                     |
|-----------------|------------------------------------------------------------------|
| Accounts        | Balances, account types, mask, institution, last-refresh time    |
| Transactions    | 24 months of history, merchant enrichment, PFC categories        |
| Investments     | Holdings (with cost basis where available), trades, dividends    |
| Liabilities     | Credit-card statement state, mortgage details                    |

`plaid-mcp` does *not* and will not include:

- **Auth / Identity** — full routing numbers and PII. Not needed for
  the read-only analytics use case.
- **Money movement** — no transfers, no payments, no payouts.

Adding either is a deliberate code change requiring a fork.

> **One thing to know about transaction history.** Plaid pulls up to
> 24 months of transactions, but the window is bound to the Item at
> *first link time* and can't be extended retroactively. If you link
> Chase today, then unlink and re-link 6 months from now, you'll lose
> the original 24-month window and start over from Plaid's default
> 90-day backstop. Use `plaid-mcp relink` (Plaid Link's update mode)
> instead of unlink+link whenever possible — it re-authenticates the
> *same* Item and preserves history.

---

## Configuration

All config is via env vars (or a `.env` file in the working directory).

| Variable                | Default                | Notes                                                              |
|-------------------------|------------------------|--------------------------------------------------------------------|
| `PLAID_CLIENT_ID`       | —                      | Required.                                                          |
| `PLAID_SECRET`          | —                      | Required.                                                          |
| `PLAID_ENV`             | `sandbox`              | `sandbox` / `development` / `production`.                          |
| `PLAID_MCP_DB`          | `~/.plaid-mcp/data.db` | Where the SQLite file lives.                                       |
| `PLAID_MCP_KEY`         | auto                   | Fernet key for at-rest access-token encryption. Auto-generated and stored at `~/.plaid-mcp/key` (chmod 600) on first run if unset. |
| `PLAID_MCP_SYNC_EVERY`  | `1800`                 | Seconds between syncs when running `sync --daemon`.                |

---

## Architecture

```
┌──────────────────────────────────────────────────────┐
│                  Your AI agent                       │
│         (Claude Code / Desktop / Codex / …)          │
└────────────────────────┬─────────────────────────────┘
                         │ MCP / stdio
                         ▼
              ┌──────────────────────┐
              │  plaid-mcp serve     │ ← read-only SQLite connection
              │  (FastMCP server)    │
              └──────────┬───────────┘
                         │ SELECT only
                         ▼
              ┌──────────────────────┐
              │ ~/.plaid-mcp/data.db │
              │   plaid_* tables     │
              │   vw_plaid_* views   │
              └──────────▲───────────┘
                         │ INSERT/UPDATE
                         │
              ┌──────────┴───────────┐         ┌──────────────────┐
              │ plaid-mcp sync       │ ──────► │   Plaid API      │
              │ (one-shot or daemon) │         └──────────────────┘
              └──────────────────────┘
                         ▲
                         │ encrypted access_token
              ┌──────────┴───────────┐         ┌──────────────────┐
              │ plaid-mcp link       │ ──────► │ Plaid Hosted     │
              │ (opens plaid.com URL,│   poll  │ Link             │
              │  polls for finish)   │ ◄────── │ (browser → bank) │
              └──────────────────────┘         └──────────────────┘
```

Three loosely-coupled commands:

- **`plaid-mcp link`** — calls Plaid's `/link/token/create` with
  Hosted Link, opens the returned `plaid.com` URL in your browser,
  polls `/link/token/get` until you've finished, exchanges the
  public_token, encrypts the access_token, stores it, and runs an
  initial sync.
- **`plaid-mcp sync`** — pulls fresh data from Plaid for all linked
  items. Idempotent. Run from cron or with `--daemon` for an in-process
  loop.
- **`plaid-mcp serve`** — speaks the MCP protocol over stdio. Opens
  the SQLite DB read-only and exposes `plaid_query`, `plaid_sync`, and
  `plaid_status` tools.

---

## CLI reference

```
plaid-mcp link                          # link a NEW institution
plaid-mcp relink <item-id>              # re-auth an existing institution (update mode)
plaid-mcp unlink <item-id>              # permanently remove an institution
plaid-mcp items                         # list linked institutions
plaid-mcp sync                          # one-shot sync of all items
plaid-mcp sync --daemon                 # background loop
plaid-mcp sync --item <item-id>         # sync just one item
plaid-mcp status                        # show sync staleness + errors
plaid-mcp serve                         # MCP server on stdio
```

`<item-id>` accepts the full UUID or any unique leading prefix
(`plaid-mcp items` shows the short forms).

### When to use `relink` vs `link`

| Situation                                                  | Command                       |
|------------------------------------------------------------|-------------------------------|
| Adding your first bank, or a different institution         | `plaid-mcp link`              |
| `plaid-mcp status` shows `ITEM_LOGIN_REQUIRED` for a bank  | `plaid-mcp relink <item-id>`  |
| `PENDING_EXPIRATION` / `USER_PERMISSION_REVOKED`           | `plaid-mcp relink <item-id>`  |
| You no longer want a bank connected                        | `plaid-mcp unlink <item-id>`  |

`relink` runs Plaid Link in **update mode** — same `access_token`,
same `plaid_item_id`, same 24-month transaction-history window. A
naive `plaid-mcp link` after an error would create a duplicate Item
with only 90 days of history and silently lose your past data.

---

## Driving link / relink / unlink from your AI agent

The MCP server exposes every connection-management operation as a tool
so the agent can drive the whole flow from chat — you never have to
drop to the terminal:

| Tool                                                  | What it does                                                                                                |
|-------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| `plaid_link()`                                        | Add a new institution. Returns a `hosted_link_url`, opens it in your browser.                               |
| `plaid_relink(item_id)`                               | Re-auth an existing institution (update mode). Same browser flow.                                           |
| `plaid_link_complete(link_token, item_id?)`           | Finalize after the user finishes in the browser. Polls up to 60s; re-call if the result is `pending`. Doesn't sync — agent calls `plaid_sync(item_id)` as a follow-up. |
| `plaid_unlink(item_id, confirmed=True)`               | Permanently remove an institution. Tool enforces confirmation — a call without `confirmed=True` returns an error.                     |
| `plaid_query(sql)` / `plaid_sync()` / `plaid_status()` | Query / refresh / inspect the local cache.                                                                  |

Typical chat exchange:

> **You:** *Why don't I see any Chase transactions for the last week?*
> **Agent:** *(calls `plaid_status`)* Looks like Chase needs a re-auth
> — `ITEM_LOGIN_REQUIRED`. Want me to fix it?
> **You:** *Yes please.*
> **Agent:** *(calls `plaid_relink`)* I've opened Plaid Link in your
> browser. Sign in to Chase and let me know when you're done.
> **You:** *Done.*
> **Agent:** *(calls `plaid_link_complete`)* Re-authed Chase Sapphire
> Preferred and synced 23 new transactions. What did you want to check?

---

## Plaid plans — which one do I need?

Plaid has four account modes. Two are relevant to `plaid-mcp` users:

### Sandbox (default — kick the tires)

- **Cost:** $0 forever.
- **What it gets you:** Fake "test" institutions (First Platypus Bank,
  Houndstooth Bank, etc.) with realistic data — accounts, transactions,
  holdings, mortgages. Log in with `user_good` / `pass_good`.
- **What it doesn't get you:** Your actual money.
- **Setup:** Create a Plaid Dashboard account, copy the `client_id` and
  the **sandbox** secret from the "Keys" tab, set `PLAID_ENV=sandbox`.
- **Hosted Link works here:** Yes, fully. No approvals needed.

### Trial plan (free, real banks — what most personal users want)

In April 2026 Plaid replaced the old "Limited Production" tier with a
free **Trial plan** for new US and Canada teams. This is the right path
if you want `plaid-mcp` to know about your real Chase / Wells Fargo /
Fidelity / Amex / Cap One accounts.

- **Cost:** $0 up to 10 linked institutions ("Items"). Beyond that you
  upgrade to a paid plan.
- **What it gets you:** Live production data. Most OAuth institutions
  (Chase, Bank of America, Wells Fargo, Amex, Cap One, Citi…) are
  available *before* full Production approval. Same SDK, same endpoints
  — the only difference is `PLAID_ENV=production` and a different
  secret.
- **Setup:**
  1. Create a Plaid Dashboard account at
     [dashboard.plaid.com](https://dashboard.plaid.com) and verify your
     email.
  2. From the Dashboard homepage, click **Apply for Trial plan** (or
     go directly to
     [dashboard.plaid.com/trial-plan](https://dashboard.plaid.com/trial-plan)).
     Fill in the short form — Plaid asks what you're building, your
     contact info, etc. Approval is usually fast (minutes to a day).
  3. Once approved, the "Keys" tab will show a **production** secret.
     Copy it. Set in your shell:

     ```bash
     export PLAID_CLIENT_ID=...
     export PLAID_SECRET=<production secret>
     export PLAID_ENV=production
     ```
  4. Run `plaid-mcp link`. Pick your real bank. Done.
- **Hosted Link works here:** Yes. Plaid removed the "ask your account
  manager to enable Hosted Link" gate, so the same `plaid-mcp link`
  command works without any extra setup.
- **OAuth banks work here:** Yes. Hosted Link runs on `plaid.com`, so
  Plaid handles every OAuth redirect server-side — you don't need a
  domain, HTTPS endpoint, or `redirect_uri` registration.

### Production (paid — for hobbyists who outgrow Trial)

If you link more than 10 institutions, Plaid will ask you to upgrade.
Paid plans are usage-based ([current pricing](https://plaid.com/pricing/)).
Nothing in `plaid-mcp` changes; same env vars, same code.

### Limited Production (legacy — don't use)

The old free-tier-with-strings-attached for teams created before April
2026. New users should apply for the Trial plan instead. If your
account predates April 2026, switching to the Trial plan is a separate
application in the Dashboard.

---

## Known limitations

- **Transaction history is fixed at first-link time.** See the callout
  in *What it knows*. Plaid won't let us back-fill once an Item exists.
  Always prefer `relink` over `unlink + link` for the same institution.
- **Brokerage/bank acquisitions can create stranded data.** If an
  institution is acquired and re-issues a new `plaid_item_id`, your
  old Item will start failing with `INSTITUTION_NOT_RESPONDING` or
  similar. There's no automatic migration — unlink the dead Item and
  link the new one. The old transactions stay in the local cache
  unless you `plaid-mcp unlink` (which cascade-deletes them) — that's
  usually the right call, but worth knowing.
- **No Fernet key rotation.** The encryption key at `~/.plaid-mcp/key`
  (or `$PLAID_MCP_KEY`) is single-use. If you replace it, every
  previously linked institution becomes unrecoverable and must be
  `relink`-ed. Back up the key file alongside the DB, or set
  `$PLAID_MCP_KEY` from a password manager.
- **Sandbox only ships with credential-flow test banks.** OAuth-only
  banks (Amex, Cap One, etc.) work fully in production via Hosted Link
  but aren't reachable in sandbox testing — you'll only see the
  classic test institutions there.
- **No webhook receiver yet.** Data refresh is poll-based via
  `plaid-mcp sync` (cron or `--daemon`). The agent can also pull on
  demand by calling the `plaid_sync` MCP tool mid-conversation.
  Webhooks are on the roadmap; they require a public URL on your end.

---

## License

MIT. See [LICENSE](LICENSE).