Content
# @mgcrea/mcp-tastytrade
Model Context Protocol server for the [TastyTrade](https://tastytrade.com) brokerage API. Exposes accounts, balances, positions, orders, transactions, instruments, market metrics, watchlists, and on-demand DXLink quote snapshots as MCP tools — for use with Claude Desktop, Claude Code, or any MCP-compatible client.
## Features
- **OAuth2 personal grant** auth (client_secret + refresh_token), automatic refresh on expiry and on 401.
- **Native `fetch`** HTTP client. No axios, no `@tastytrade/api` dependency.
- **TastyTrade dash-case ↔ camelCase** transparent translation.
- **DXLink quote snapshots** over a long-lived WebSocket — `get_quote(symbol)` reuses the connection across calls and caches last-known values, so repeat queries return in <50ms instead of paying ~3–5s of auth/setup. The connection idles closed after `TASTYTRADE_DXLINK_IDLE_TIMEOUT_MS` (default 30 s) of inactivity and reconnects on the next call.
- **REST fallback mode** — set `TASTYTRADE_DISABLE_DXLINK=1` to skip the WebSocket entirely and route `get_quote`, `get_quotes`, `get_chain_with_greeks`, and `get_position_greeks` through the Tastytrade REST `/market-data/by-type` endpoint. Greeks (`delta`/`gamma`/… fields) are unavailable via REST and always `null` in this mode. `get_expected_move` throws a clear error. Useful when DXLink auth is broken or you need a stateless deployment.
- **Trading is opt-in.** Mutating tools (`place_order`, `cancel_order`, `replace_order`, `create_watchlist`, `update_watchlist`, `delete_watchlist`) are only registered when `TASTYTRADE_ALLOW_TRADING=1`. Order-placement tools additionally require `confirm: true` in the call args; otherwise they return a dry-run preview. Set `TASTYTRADE_DANGEROUSLY_ALLOW_TRADING=1` to flip the `confirm` default to `true` (auto-submit) — only use if you trust whatever's driving the MCP.
## Stack
- Node 22+, ESM, `pnpm`
- TypeScript (strict, NodeNext, ES2023)
- [`tsdown`](https://tsdown.dev) for builds
- [`oxlint`](https://oxc.rs) + [`oxfmt`](https://oxc.rs) for lint/format
- [`vitest`](https://vitest.dev) for tests
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk), [`zod`](https://zod.dev), [`ws`](https://github.com/websockets/ws)
## Install
```bash
pnpm install
pnpm build
```
## Configure
Provision a personal OAuth grant via the TastyTrade developer portal (https://developer.tastytrade.com/oauth/) and copy `.env.example` to `.env`:
```
TASTYTRADE_CLIENT_SECRET=...
TASTYTRADE_REFRESH_TOKEN=...
TASTYTRADE_SCOPE=read trade
TASTYTRADE_ENV=prod # or "cert" for the sandbox
TASTYTRADE_ALLOW_TRADING= # "1" to register mutating tools (still gated by per-call confirm:true)
TASTYTRADE_DANGEROUSLY_ALLOW_TRADING= # "1" to also flip the per-call confirm default to true (auto-submit)
TASTYTRADE_DXLINK_IDLE_TIMEOUT_MS= # how long to keep the DXLink WS open after the last quote call (default 30000)
TASTYTRADE_DISABLE_DXLINK= # "1" to skip DXLink and use REST /market-data/by-type (no Greeks)
```
## Run
```bash
pnpm start # speaks JSON-RPC over stdio
```
## Wire up to Claude Code / Claude Desktop
In `~/.claude.json` (or the desktop config):
```json
{
"mcpServers": {
"tastytrade": {
"command": "node",
"args": ["/absolute/path/to/mcp-tastytrade/dist/cli.js"],
"env": {
"TASTYTRADE_CLIENT_SECRET": "...",
"TASTYTRADE_REFRESH_TOKEN": "...",
"TASTYTRADE_SCOPE": "read trade",
"TASTYTRADE_ENV": "prod"
}
}
}
}
```
Or, after `pnpm link --global`:
```json
{ "command": "tastytrade-mcp", "args": [], "env": { "...": "..." } }
```
Inspect tool wiring with the official inspector:
```bash
npx @modelcontextprotocol/inspector node dist/cli.js
```
## Run via Docker
A multi-stage [Dockerfile](./Dockerfile) ships with the project. CI publishes a
multi-arch image (`linux/amd64`, `linux/arm64`) to GHCR on every push to `main`
and on `v*.*.*` tags:
```bash
docker pull ghcr.io/mgcrea/mcp-tastytrade:latest
```
The container runs `node /app/dist/cli.js` as PID 1 and speaks JSON-RPC over stdio. Pass credentials via `-e VAR` (forwarded from the spawning shell) or `--env-file`.
`.mcp.json` for Claude Code (project-scoped) / Claude Desktop:
```json
{
"mcpServers": {
"tastytrade": {
"type": "stdio",
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"-e",
"TASTYTRADE_CLIENT_SECRET",
"-e",
"TASTYTRADE_REFRESH_TOKEN",
"-e",
"TASTYTRADE_SCOPE",
"-e",
"TASTYTRADE_ENV",
"-e",
"TASTYTRADE_ALLOW_TRADING",
"ghcr.io/mgcrea/mcp-tastytrade:latest"
],
"env": {
"TASTYTRADE_CLIENT_SECRET": "...",
"TASTYTRADE_REFRESH_TOKEN": "...",
"TASTYTRADE_SCOPE": "read trade openid",
"TASTYTRADE_ENV": "prod"
}
}
}
}
```
Notes:
- `-i` keeps stdin open (required); do **not** add `-t` — Claude Code spawns the process without a TTY.
- `-e VAR` (no `=value`) forwards the value from the host env, which is what Claude Code sets from the `env` block above. Don't put secrets in `args`.
- `--rm` cleans up the container after each MCP session.
- Omit `TASTYTRADE_ALLOW_TRADING` (or leave it unset) to ship without order-placement / watchlist-mutation tools registered.
If you'd rather not commit secrets to `.mcp.json`, point `--env-file` at a local `.env` instead:
```json
"args": ["run", "--rm", "-i", "--env-file", "/abs/path/to/.env", "ghcr.io/mgcrea/mcp-tastytrade:latest"]
```
### Build locally
```bash
pnpm docker:build # single-arch local image
pnpm docker:buildx # multi-arch (linux/amd64,linux/arm64)
pnpm docker:release # multi-arch + push to Docker Hub (mgcrea/mcp-tastytrade)
```
The build script passes `GIT_COMMIT` / `GIT_COMMIT_DATE` as build args so the
bundle bakes in real git info even though `.git` isn't copied into the build
context.
## Tools
### Read-only (always available)
| Tool | Description |
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `list_accounts` | Customer accounts. |
| `get_account` | Single account detail. |
| `get_customer` | Customer profile. |
| `get_balances` | Cash + margin balances. |
| `get_positions` | Open / closed positions. |
| `get_position_greeks` | Per-position greeks + per-underlying and portfolio-net totals (signed-quantity × multiplier × per-contract greek). |
| `list_orders`, `get_order` | Order history / lookup. |
| `get_complex_order` | Fetch a single complex order (OTOCO/OCO/OTO bracket) including all linked child orders. |
| `list_transactions`, `get_transaction` | Account transactions. |
| `search_symbols` | Symbol search by prefix. |
| `get_equity`, `get_equity_option`, `get_future`, `get_cryptocurrency` | Instrument metadata. |
| `get_option_chain_summary` | Per-expiration summary (strike counts, min/max strike). Slim payload. |
| `get_option_chain` | Filtered chain slice (by expiration / strike range / type), one flat leg per call/put with both OCC and DXLink symbols. |
| `get_market_metrics` | IV rank/percentile, beta, liquidity, IV term structure. |
| `get_expected_move` | ATM straddle expected ±1σ move for an underlying at a given expiration (spot, ATM strike, call/put mids, straddle, IV-implied move). |
| `get_chain_with_greeks` | Chain slice (default ATM±20 strikes) enriched with quote (bid/ask/mid) + Greeks per leg, in one batched call. |
| `find_strikes_by_delta` | For each target delta, return the strike in the chain whose actual delta is closest. Positive→calls, negative→puts. Iron-condor wing picker. |
| `get_earnings_calendar` | Bundled `{symbol, expectedReportDate, timeOfDay, estimatedEarnings}` for a batch of symbols, optional date filter. |
| `get_diagnostics` | Inspect server state + last ~200 log lines (market-data provider mode/state, OAuth freshness, recent warns/errors). Use to self-debug. |
| `get_dividend_history`, `get_earnings_history` | Corporate event history. |
| `list_watchlists`, `get_watchlist`, `list_public_watchlists`, `get_public_watchlist` | Watchlists. |
| `get_quote` | Snapshot for one symbol. Quote (bid/ask/sizes) + Greeks for options (DXLink mode); bid/ask only in REST mode. Accepts OCC or DXLink format. |
| `get_quotes` | Batch snapshot for many symbols. Greeks included in DXLink mode, null in REST mode. |
### Mutating (only when `TASTYTRADE_ALLOW_TRADING=1`)
| Tool | Description |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `place_order` | Submit an order. Without `confirm: true`, returns a dry-run preview. |
| `cancel_order` | Cancel an open order. Requires `confirm: true`. |
| `cancel_all_orders` | Bulk-cancel every open order (optional `underlyingSymbol` filter). Without `confirm: true`, returns the would-cancel list. Returns `{cancelled, failed}` on submit so per-order failures surface. |
| `replace_order` | Replace an open order. Without `confirm: true`, dry-runs the replacement. |
| `place_complex_order` | Submit an OTOCO bracket (entry + linked take-profit + stop-loss), an OCO pair attached to an existing position, or an OTO chain. Without `confirm: true`, returns a dry-run preview. |
| `cancel_complex_order` | Cancel a complex order and all its linked children. Requires `confirm: true`. |
| `create_watchlist`, `update_watchlist`, `delete_watchlist` | Private watchlist management. |
### Bracket orders / stop-loss
To attach a stop-loss (and optional take-profit) to an entry in a single atomic submission, use `place_complex_order` with `type: "OTOCO"`. Filling or cancelling one child cancels the other, so the stop can't be orphaned if the entry fills.
```jsonc
// place_complex_order
{
"accountNumber": "5WX12345",
"order": {
"type": "OTOCO",
"triggerOrder": {
"timeInForce": "Day",
"orderType": "Market",
"legs": [
{ "instrumentType": "Equity", "symbol": "SPY", "quantity": 1, "action": "Buy to Open" },
],
},
"orders": [
{
"timeInForce": "GTC",
"orderType": "Limit",
"price": 620,
"priceEffect": "Credit",
"legs": [
{ "instrumentType": "Equity", "symbol": "SPY", "quantity": 1, "action": "Sell to Close" },
],
},
{
"timeInForce": "GTC",
"orderType": "Stop",
"stopTrigger": 540,
"legs": [
{ "instrumentType": "Equity", "symbol": "SPY", "quantity": 1, "action": "Sell to Close" },
],
},
],
},
"confirm": false,
}
```
For an OCO pair attached to a position you already hold, omit `triggerOrder` and use `type: "OCO"` with the two exit orders.
## Development
```bash
pnpm dev # tsdown --watch
pnpm test # vitest run
pnpm typecheck # tsc --noEmit
pnpm lint # oxlint
pnpm format # oxfmt --write .
```
## License
MIT © Olivier Louvignes
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available