poke-plaid-mcp
by zayedu
README.md
# poke-plaid-mcp
A custom [MCP](https://modelcontextprotocol.io) server that hooks your investment
portfolio into [Poke](https://poke.com) via [Plaid](https://plaid.com).
> **Why this exists.** Poke can't see your Wealthsimple / brokerage portfolio —
> there's no public API and no built-in Plaid integration:
>
> So here it is. Link any brokerage/retirement account through Plaid once, point
> Poke at this server, and ask things like *"what's my portfolio worth?"*,
> *"what are my biggest winners?"*, or *"how much did I get in dividends this month?"*.
It works in two modes:
- **Mock mode** (default, no credentials needed) — serves a realistic sample
Wealthsimple-style portfolio so you can wire up Poke and try every tool *right now*.
- **Plaid mode** — set `PLAID_CLIENT_ID` / `PLAID_SECRET` and link your real accounts.
---
## Tools exposed to Poke
| Tool | What it does |
| --- | --- |
| `check_connection` | Confirms the data source is connected and lists linked institutions. |
| `list_accounts` | All linked investment/brokerage/retirement accounts + last-known balances. |
| `get_balances` | Up-to-date balances (forces a fresh read from the institution when supported). |
| `portfolio_summary` | Net-worth-style snapshot: total value + unrealized gain/loss by currency, per-account breakdown, allocation by asset type, and top holdings. |
| `list_holdings` | Every position: ticker, name, quantity, price, market value, cost basis, unrealized gain/loss. Filter by account / min value; sort by value or gain. |
| `list_investment_transactions` | Buys, sells, dividends, contributions, fees, transfers over a date range. |
| `get_dividend_income` | Total dividends over a period, broken down by security and currency, plus each payment. |
All tools are **read-only** and never move money.
---
## Quick start (mock mode — 60 seconds)
```bash
npm install
npm run dev
```
The server starts on `http://localhost:3000`. Verify it end-to-end:
```bash
npm run test:mcp # boots the server, drives it with a real MCP client, calls every tool
```
Then [connect it to Poke](#connecting-to-poke). Everything works with sample data
until you add Plaid credentials.
---
## Using your real portfolio (Plaid)
### 1. Get Plaid API keys
Create a free account at [dashboard.plaid.com](https://dashboard.plaid.com) and grab
your **client_id** and **sandbox/production secret** from *Developers → Keys*.
Investments data requires the **Investments** product (request access in the
dashboard if it isn't already enabled).
### 2. Configure
```bash
cp .env.example .env
```
Fill in at least:
```ini
PLAID_CLIENT_ID=your_client_id
PLAID_SECRET=your_secret
PLAID_ENV=sandbox # or: production
PLAID_PRODUCTS=investments
PLAID_COUNTRY_CODES=US,CA # CA covers Wealthsimple
TOKEN_ENCRYPTION_KEY=<a long random string> # encrypts access tokens at rest
MCP_API_KEY=<a random string> # require this key on the /mcp endpoint
```
### 3. Link an account
```bash
npm run dev
```
Open **http://localhost:3000/setup**, click **Connect**, and complete Plaid Link.
Your bank credentials go straight to Plaid — never to this server. The resulting
access token is encrypted and stored in `./data/items.json`.
> **Sandbox shortcut (no browser):** with `PLAID_ENV=sandbox` you can link a test
> institution from the CLI:
> ```bash
> npm run link:sandbox # default sandbox institution
> npm run link:sandbox ins_XXXXX # a specific one
> ```
You can link multiple institutions; all of them are aggregated across every tool.
---
## Connecting to Poke
Poke talks to this server over **Streamable HTTP at `/mcp`**. Pick one:
### Option A — Local tunnel (recommended for personal use)
Keeps your financial data and Plaid tokens on your machine. Start the server, then
in another terminal:
```bash
npx poke@latest tunnel http://localhost:3000/mcp -n "Plaid Portfolio"
```
Leave both running. Poke syncs the tools automatically. Say *"check my portfolio
connection"* in Poke to confirm.
### Option B — Hosted server
Deploy somewhere public (see [Deployment](#deployment)), then add it at
[poke.com/integrations/new](https://poke.com/integrations/new):
- **Name:** `Plaid Portfolio`
- **MCP Server URL:** `https://your-host.example.com/mcp`
- **API Key:** the value of `MCP_API_KEY` (sent as a Bearer token)
…or via the CLI:
```bash
npx poke@latest mcp add https://your-host.example.com/mcp -n "Plaid Portfolio" -k "$MCP_API_KEY"
```
### Option C — Shareable recipe (Poke Kitchen)
To package this for others, create a recipe at [poke.com/kitchen](https://poke.com/kitchen)
with this server as a required MCP integration and some onboarding context
(e.g. first message: *"What's my portfolio worth?"*).
---
## Deployment
Any host that runs a Node service works (Fly.io, Render, Railway, a VPS, …).
```bash
docker build -t poke-plaid-mcp .
docker run -p 3000:3000 --env-file .env -v "$PWD/data:/app/data" poke-plaid-mcp
```
The image sets `HOST=0.0.0.0` and `NODE_ENV=production`, so it **won't start without
`MCP_API_KEY`** (by design). For a hosted deployment set: `MCP_API_KEY`,
`TOKEN_ENCRYPTION_KEY`, `SETUP_TOKEN`, and `PUBLIC_BASE_URL=https://your-host`.
Then link accounts at `https://your-host/setup?token=YOUR_SETUP_TOKEN`.
> Persist `/app/data` (the volume above) so your linked-account tokens survive
> restarts. On platforms with an ephemeral filesystem, use a mounted disk/volume.
---
## Configuration reference
| Var | Default | Notes |
| --- | --- | --- |
| `PORT` | `3000` | HTTP/MCP port. |
| `HOST` | `127.0.0.1` | Bind interface. Loopback by default (LAN-safe, works with `poke tunnel`). Set `0.0.0.0` for containers/hosting. |
| `NODE_ENV` | `development` | In `production`, the server refuses to start if bound publicly without `MCP_API_KEY`, or using Plaid without `TOKEN_ENCRYPTION_KEY`. |
| `PUBLIC_BASE_URL` | `http://localhost:PORT` | Also seeds the Host/Origin allowlist. Set to your public URL when hosted. |
| `MCP_API_KEY` | *(empty)* | If set, `/mcp` requires `Authorization: Bearer <key>` (or `x-api-key`). **Required for internet-facing use.** |
| `SETUP_TOKEN` | *(empty)* | Token required to use `/setup*` (pass as `?token=`). **Required to expose setup on a public host.** |
| `ALLOWED_HOSTS` / `ALLOWED_ORIGINS` | *(empty)* | Extra allowlist entries (loopback + `PUBLIC_BASE_URL` are always allowed). |
| `ALLOW_ANY_HOST` | `false` | Disable Host-header allowlisting (behind a Host-rewriting proxy). Origin checks stay on. |
| `PLAID_CLIENT_ID` / `PLAID_SECRET` | *(empty)* | Leave blank for mock mode. |
| `PLAID_ENV` | `sandbox` | `sandbox` or `production`. |
| `PLAID_PRODUCTS` | `investments` | Comma-separated Plaid products. |
| `PLAID_COUNTRY_CODES` | `US,CA` | Comma-separated country codes. |
| `PLAID_APP_NAME` | `Poke Portfolio` | Shown in Plaid Link. |
| `TOKEN_STORE_PATH` | `./data/items.json` | Where encrypted tokens live. |
| `TOKEN_ENCRYPTION_KEY` | *(empty)* | AES-256-GCM key for tokens at rest. Required in production w/ Plaid. |
---
## Security
This server touches your financial data, so it's locked down by default:
- **Read-only.** No tool can move money — only account/holding/transaction reads.
- **Loopback by default.** Binds to `127.0.0.1`, so a local install is never exposed
to your LAN. `poke tunnel` still reaches it. Hosting requires `HOST=0.0.0.0`.
- **`/mcp` auth** via `MCP_API_KEY` (timing-safe Bearer check). In `production`, the
server refuses to start if bound publicly without a key.
- **`/setup*` auth** via `SETUP_TOKEN`. Without a token, setup is only reachable from
localhost (and disabled entirely on a public host).
- **DNS-rebinding / cross-origin protection.** Requests with a disallowed `Origin`
are rejected, and the `Host` header is allowlisted — a malicious website in your
browser can't drive the server. Server-to-server callers (Poke, the tunnel) are
unaffected.
- **Tokens encrypted at rest** with AES-256-GCM (`TOKEN_ENCRYPTION_KEY`); token file
is written `0600`. The `data/` dir is git-ignored — never commit it.
- **Secrets are redacted** from logs. Your bank login is entered in Plaid Link and
never touches this server. TLS is provided by the tunnel / your host.
- `npm audit`: 0 known vulnerabilities.
For an internet-facing deployment, set **all** of: `HOST=0.0.0.0`, `NODE_ENV=production`,
`MCP_API_KEY`, `TOKEN_ENCRYPTION_KEY`, `SETUP_TOKEN`, and `PUBLIC_BASE_URL`.
---
## How it works
```
Poke ──HTTP(S) /mcp──▶ Express + MCP (Streamable HTTP, stateless)
│
▼
PortfolioProvider
┌────────────┴────────────┐
PlaidProvider MockProvider
(real Plaid API) (sample portfolio)
```
The MCP tool layer is provider-agnostic: it always receives normalized
holdings/accounts/transactions, so swapping mock ↔ Plaid changes nothing about the
tools Poke sees.
### Project structure
```
src/
index.ts # bootstrap
server.ts # Express app: /mcp (auth), /health, /setup
mcp.ts # MCP server + tool definitions
portfolio-math.ts # portfolio + dividend aggregation (pure functions)
config.ts # env config, mock/plaid mode selection
logger.ts # redacting JSON logger
store.ts # encrypted access-token storage
providers/
types.ts # normalized domain types + PortfolioProvider interface
plaid.ts # Plaid client, normalization, Link helpers
mock.ts # realistic sample data
index.ts # provider selection
routes/link.ts # Plaid Link setup endpoints
public/setup.html # Plaid Link setup UI
scripts/
sandbox-link.ts # link a Plaid sandbox institution (no browser)
test-mcp.ts # end-to-end MCP smoke test
```
---
## Notes on Wealthsimple
Wealthsimple has no public portfolio API, which is the whole reason for this server.
Plaid's coverage of Wealthsimple's *investments* data varies by account type and
region — if Plaid can link your Wealthsimple account with the Investments product,
this server surfaces it. If not, you can link any other Plaid-supported brokerage,
and the mock mode always works for building/testing the Poke experience.
## License
MIT
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues