Skip to main content
Glama
wilderfield

plaid-mcp

by wilderfield

plaid-mcp

Persistent Plaid MCP server for an AI assistant (Elowen) running in an ephemeral container.

plaid-mcp is a long-lived, externally hosted service that owns the Plaid secret and the encrypted access tokens for every linked institution. The assistant calls mcp__plaid__* tools at runtime; it never sees the raw access tokens, only opaque item_id and account_id values that Plaid already considers public.

Elowen (ephemeral container)
  └─ calls mcp__plaid__* tools
        └─ plaid-mcp (persistent, nanoclaw-hosted)
              ├─ Plaid SDK + PLAID_SECRET (never leaves this service)
              ├─ access_token store (SQLite, AES-256-GCM at rest)
              └─ /link/start, /link/callback (HTTPS, browser-facing)
                    └─ Plaid REST API / Plaid Link JS

Surfaces

A single Node.js process exposes two completely separate surfaces:

  1. MCP server. Either stdio (the agent spawns this binary as a subprocess) or http (Streamable HTTP at POST /mcp, bearer-gated). Choose with MCP_TRANSPORT. For the family-budget use case described above, you want http so a fleet of ephemeral agent containers can share one persistent server.

  2. HTTPS link mini-app at /link/*. Used only during the one-time bank link flow — the user opens a URL the assistant gives them, logs into their bank inside Plaid Link, and is done. After that the browser is never needed again for that institution.

Related MCP server: plaid-mcp

MCP tools

Tool

What it does

list_linked_institutions()

Every linked Item, with needs_relink health flag (calls /item/get per Item).

list_accounts(item_id?)

Cached account list (type, subtype, mask, last balance) for one or all institutions.

get_balances(account_ids?)

Real-time balances via /accounts/balance/get (paid Plaid endpoint).

get_transactions(start_date, end_date, account_ids?, cursor?)

Date-range transactions, ~250 per page, opaque pagination cursor.

search_transactions(query, since?, until?, min_amount?, max_amount?, category?)

Server-side filtered transaction search. Returns compact rows.

get_monthly_summary(month, group_by?)

Pre-aggregated monthly totals grouped by category or merchant. Keeps LLM context small.

get_investment_holdings(account_ids?)

Position snapshot (ticker, qty, market value, cost basis).

get_investment_transactions(start_date, end_date, account_ids?)

Buys/sells/dividends in a window.

get_liabilities(account_ids?)

Credit-card APRs/statements, student loans, mortgage details.

initiate_link(institution_hint?)

Returns { url, session_id, expires_at } — give the URL to the user.

link_status(session_id)

Poll until succeeded (with new item_id), failed, or expired.

remove_institution(item_id)

Revoke the Plaid Item and delete the local token.

All tool responses are JSON inside a single text content item (works on every MCP client, including ones that don't surface structuredContent).

  1. Elowen calls initiate_link({ institution_hint: "Chase" }). The server:

    • calls Plaid /link/token/create,

    • stores a link_sessions row (status pending),

    • returns { url: "https://<LINK_BASE_URL>/link/start?s=<uuid>&sig=<hmac>", session_id, expires_at }.

  2. Elowen sends the URL to the user.

  3. The user opens it in a browser. The page loads Plaid Link JS from the official CDN with that link_token and presents an "Open Plaid Link" button.

  4. Plaid Link's onSuccess POSTs { public_token, institution } plus the signed session id back to /link/callback.

  5. /link/callback exchanges public_tokenaccess_token + item_id, AES-256-GCM-encrypts the access token, persists it, and marks the session succeeded.

  6. Elowen polls link_status(session_id), sees succeeded with the item_id, and proceeds.

The signed URL params (s, sig) are HMAC-SHA256-keyed by LINK_SESSION_SECRET. The DB row is the source of truth — the HMAC just cheaply rejects garbage requests before we touch SQLite.

Configuration

All config is via environment variables (loaded from .env).

Variable

Required

Default

Description

PLAID_CLIENT_ID

Yes

From the Plaid dashboard

PLAID_SECRET

Yes

From the Plaid dashboard. Never leaves this service.

PLAID_ENV

No

sandbox

sandbox | development | production

PLAID_API_VERSION

No

2020-09-14

Pinned API version

PLAID_PRODUCTS

No

transactions

Comma list. Common: transactions,investments,liabilities

PLAID_COUNTRY_CODES

No

US

Comma list of ISO country codes

PLAID_USER_ID

No

family-default

Stable client_user_id sent to Plaid

PLAID_ENCRYPTION_KEY

Yes

32 bytes hex (openssl rand -hex 32). AES-256-GCM key for tokens at rest.

LINK_SESSION_SECRET

Yes

≥ 32 bytes hex. HMAC key for signed link URLs.

LINK_SESSION_TTL_SECONDS

No

900

Link session lifetime

LINK_BASE_URL

Yes

Public HTTPS base URL the browser will hit (e.g. https://plaid.example.com)

PORT

No

3333

HTTP port. TLS terminates upstream at nanoclaw.

ADMIN_TOKEN

No

If set, gates /link/admin/* introspection routes

MCP_TRANSPORT

No

http

stdio | http

MCP_BEARER_TOKEN

Yes if MCP_TRANSPORT=http

Bearer required on POST /mcp

DB_PATH

No

./data/plaid-mcp.sqlite (Docker: /data/plaid-mcp.sqlite)

SQLite path. Mount a persistent volume here.

LOG_LEVEL

No

info

Pino log level. All logs go to stderr.

Generate secrets with:

make keys

Storage

SQLite (better-sqlite3) at $DB_PATH. Two tables matter:

  • itemsitem_id PK, encrypted access_token_blob BLOB, institution name/id, status, consent expiration.

  • link_sessions — short-lived, expire automatically when read after their expires_at and during a 60s background sweep.

Access tokens are stored as [1-byte version][12-byte IV][16-byte GCM tag][N-byte ciphertext]. Decryption fails closed if the GCM tag doesn't verify.

Security model

  • The MCP HTTP transport requires Authorization: Bearer $MCP_BEARER_TOKEN on every request. Without it the agent fleet would expose every linked bank account to the internet.

  • The browser-facing /link/* routes are signed (HMAC) and bound to a short-lived DB-backed session.

  • TLS is expected to terminate upstream (at nanoclaw / Caddy / whatever your edge is). The container speaks plain HTTP internally; expose it only through the proxy.

  • Every Plaid token is encrypted at rest. Even with the SQLite file in hand, an attacker without PLAID_ENCRYPTION_KEY cannot use the tokens.

  • The MCP tools never return access tokens to the agent. Only opaque item_id / account_id strings cross the MCP boundary.

Local development

npm install
make setup           # creates .env from env.example
make keys >> .env    # append fresh PLAID_ENCRYPTION_KEY / LINK_SESSION_SECRET / MCP_BEARER_TOKEN
# edit .env: PLAID_CLIENT_ID, PLAID_SECRET, LINK_BASE_URL
npm run dev          # tsx with hot reload

For local link testing you'll need an HTTPS tunnel (Plaid Link onSuccess won't fire from http://localhost). cloudflared, ngrok, or a real Caddy reverse proxy all work; whatever public hostname they give you goes into LINK_BASE_URL.

Docker

make build
make up
make logs

The compose file mounts ./data:/data so the SQLite DB survives restarts. In a nanoclaw deployment, replace that bind-mount with the cluster-managed persistent volume.

Wiring the agent to a hosted instance

Inside the agent container's MCP client config:

{
  "mcpServers": {
    "plaid": {
      "url": "https://plaid-mcp.your-domain.example/mcp",
      "headers": {
        "Authorization": "Bearer <MCP_BEARER_TOKEN>"
      }
    }
  }
}

The agent gets the bearer token through whatever secret-injection mechanism nanoclaw already uses for its other agent secrets. It does not ever see PLAID_SECRET or any access token.

License

Internal.

Maintenance

ActivityInactive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Self-hosted MCP server enabling Claude to query bank accounts, balances, and transactions through Plaid with OAuth and TLS.
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    A local MCP server that provides read-only SQL access to financial accounts via Plaid, enabling natural language queries about transactions, balances, and holdings.
    MIT
  • F
    license
    A
    quality
    C
    maintenance
    Personal finance MCP server that integrates Plaid bank data with local SQLite memory for conversational budgeting, goal tracking, and transaction management.
    15
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    MCP server that exposes banking data (connections, accounts, balances, transactions) and agent skills, allowing AI agents to query and refresh financial data via stdio.
    139
    6
    Apache 2.0