firstrade-mcp-server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@firstrade-mcp-server preview a limit buy order for 10 shares of AAPL at $250"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
firstrade-mcp-server
A Model Context Protocol server that gives an LLM (Claude, or any other MCP host) read/write access to a Firstrade brokerage account: live positions, balances, quotes, option chains/greeks, order history, and — if you choose to enable it — order placement (stocks, single-leg options, and two-leg option spreads).
Built on top of the community firstrade
Python package, which reverse-engineers Firstrade's internal api3x web API.
This project is not affiliated with, endorsed by, or supported by Firstrade.
⚠️ Read this before you use it
Firstrade has no official public trading API. This server (like the
firstradepackage it depends on) works by driving the same private endpoints the Firstrade web app uses, authenticated with your real login. That is very likely outside the spirit — and possibly the letter — of Firstrade's Terms of Service around automated / unauthorized access. Your account could be flagged, rate-limited, or suspended. Use at your own risk, on an account you're prepared to lose access to.The order-placement tools (
place_stock_order,place_option_order,place_option_spread) send real orders with real money. There is no simulated/paper mode. This is enforced server-side, not just by convention:place_*is disabled unlessFT_ALLOW_LIVE_ORDERS=trueis set in.env, and every call additionally requires aconfirm_tokenminted by the matchingpreview_*tool for the identical order — a mismatched or missing token is rejected before anything is sent. See Live order safety model.This is a personal tool the author built for their own workflow and is sharing as-is. It is not a product, has no support SLA, and comes with no warranty of any kind (see LICENSE). Nothing here is investment advice.
Options trading requires an appropriately approved options level on your Firstrade account (e.g. naked calls need Level 2+ margin approval) — the broker enforces this server-side and will reject anything you're not approved for.
If any of that gives you pause, it should — read it twice before you put real
credentials in .env.
Related MCP server: Alpaca API MCP Server
What it does
Tool | Purpose |
| Live stock + option positions, all accounts |
| Equity, cash, buying power |
| Transaction history (fills, dividends, interest, transfers) — presets or a custom date range |
| Open/filled/cancelled orders, with the Firstrade order id |
| Real-time quote(s) |
| Broker's own option chain; omit the expiration to list available expirations |
| Broker-computed delta/gamma/theta/vega/rho + IV for a chain |
| Stock orders — buy, sell, sell_short, buy_to_cover; limit/market/stop/stop-limit/trailing |
| Single-leg option orders — buy_to_open, sell_to_open, sell_to_close, buy_to_close |
| Two-leg option spreads (debit or credit), priced by net price |
| Cancel an open order by id |
Every place_* tool has a matching preview_* tool that runs the identical
request in dry-run mode. The intended usage pattern for an LLM host is:
always preview first, show the user the preview, only place after explicit
confirmation — and the server enforces this, it doesn't just document it (see
below).
Live order safety model
place_stock_order, place_option_order, and place_option_spread are gated
by two independent checks, both server-side:
Kill switch. They refuse to run unless
FT_ALLOW_LIVE_ORDERS=trueis set in.env. Unset (the default) or anything else, and everyplace_*call returns an error without touching the network —preview_*still works, so you can wire this up and see previews before ever flipping the switch.Preview→place confirmation token. Every
preview_*call mints a one-timeconfirm_tokenbound to the exact order arguments (symbol, side, quantity, price, duration, etc.), valid for 10 minutes. The matchingplace_*call must pass that token back unchanged. A missing token, an expired token, or a token minted for different order arguments (e.g. the LLM previewed 10 shares but tries to place 100) is rejected before the order reaches Firstrade. Tokens live in-process only — a server restart invalidates every pending preview.
This closes the gap where "preview first" was only a docstring instruction an LLM host could skip or a permissions layer could bypass; now placing an order that was never (or differently) previewed is impossible at the code level.
Two more things worth knowing:
durationon stock orders defaults togt90(Firstrade's ~90-day GTC), which the confirm-token flow forces you to see in the preview before it can be sent. Passduration="day"explicitly if you don't want a resting GTC order.If your login has more than one Firstrade account, order/quote/cancel tools refuse to guess which one you mean — set
FT_ACCOUNT_NUMBERin.env.
See docs/option-order-api.md for the reverse-engineered
schema of Firstrade's single-leg and multi-leg option order endpoints (error
codes, field validation behavior, GTC vs day-only constraints), discovered via
the probe scripts in tools/.
Prerequisites
Python 3.12+
uv (recommended; plain
pip install -e .also works)A Firstrade account, and an authenticator app if you want headless session refresh (see below)
Setup
git clone https://github.com/PatrickSUDO/firstrade-mcp-server.git
cd firstrade-mcp-server
uv sync
cp .env.example .env
# edit .env: fill in FT_USERNAME, FT_PASSWORD, FT_PIN, FT_EMAIL
# leave FT_ALLOW_LIVE_ORDERS unset until you've reviewed the safety model belowFirst login (interactive)
Firstrade requires 2FA (OTP or authenticator MFA) on every fresh login. Run this once to establish a session:
uv run python3 tools/ft_setup.py step1
# → sends an OTP / prompts for your authenticator code
uv run python3 tools/ft_setup.py step2 <CODE>This saves session cookies to ~/.local/share/firstrade-session. server.py
reuses that saved session on every tool call, so you don't re-auth per request.
Optional: headless session refresh
Firstrade sessions expire periodically. If you set FT_TOTP_SECRET in .env
(the seed your authenticator app was set up with — not the 6-digit code), the
server self-heals automatically: it detects a dead session (401) and re-runs
the login + TOTP flow without a human typing anything. You can also trigger
this manually:
uv run python3 tools/ft_setup.py autoWithout FT_TOTP_SECRET, a dead session falls back to the manual step1 /
step2 <code> flow above.
Register with an MCP host
Claude Code:
claude mcp add firstrade-server -- uv --directory /absolute/path/to/firstrade-mcp-server run server.pyGeneric mcp.json / host config:
{
"mcpServers": {
"firstrade-server": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/firstrade-mcp-server", "run", "server.py"]
}
}
}Credentials are read from firstrade-server/.env at startup — you don't need
to (and shouldn't) put them in the MCP host config.
Security notes
No credentials are hardcoded anywhere in the source.
server.pyandtools/ft_setup.pyboth loadFT_*values from a local.envfile that is git-ignored.FT_TOTP_SECRET, if you set it, is your authenticator's full seed — not a 6-digit code. Anyone with it (or with your.env) can mint valid login codes for your account indefinitely, no phone required. Treat.envlike a password, not a config file:chmod 600it, don't sync it anywhere shared.The saved session (
~/.local/share/firstrade-session) and the transient login-flow state (~/.local/share/firstrade-session-tmp/) hold live cookies / tokens. Both are written with0600/0700perms and kept out of/tmp(which is world-readable on most multi-user machines).ft_setup.pyalso never prints raw tokens/cookies to stdout — only redacted shapes — since a failed headless re-auth surfaces its tail through the MCP error channel.uv.lockis committed andfirstradeis pinned to an exact version, not a floating>=. This wraps an unofficial, reverse-engineered API client that runs against a live brokerage account — bump it deliberately, after testing, not automatically onuv sync.tools/probe-out/(raw API responses captured while reverse-engineering the option order schema) is git-ignored too — even though account numbers in those responses are masked by Firstrade itself, it's still your own live session output.Review
.gitignorebefore you fork/extend this and make sure your own.env, session cache, and any new debug-output directories stay out of git.
License
MIT. Provided as-is, for personal/educational use. Not investment advice. Not affiliated with Firstrade.
Available Tools
15 toolscancel_orderA
Cancel an open order by order_id (e.g. 'G42621-1569').
Args: order_id: The order ID returned by place_stock_order or place_option_order.
Returns JSON with cancellation result.
Inputs: order_id (required). Returns: result
get_account_balanceA
Get account equity, cash, and balance overview for all accounts.
Inputs: no named parameters. Returns: result
get_account_historyA
Get transaction history (fills, dividends, interest, transfers).
Args: date_range: today|1w|1m|2m|mtd|ytd|ly|cust. Use "ly" for the trailing year, or "cust" with custom_from/custom_to for an explicit window. custom_from: Window start, "YYYY-MM-DD". Required when date_range="cust"; supplying it also implies "cust" so the range arg can be left alone. custom_to: Window end, "YYYY-MM-DD". Defaults to custom_from when omitted.
Returns JSON keyed by account number.
Inputs: custom_to, date_range, custom_from. Returns: result
get_account_positionA
Get current stock and options positions for all accounts.
Inputs: no named parameters. Returns: result
get_option_chainA
Get the broker's own option chain for a stock symbol.
Args: symbol: Underlying ticker, e.g. 'NVDA'. exp_date: Expiration as 'YYYYMMDD' (or 'YYYY-MM-DD'). Omit to list the available expiration dates instead of returning a chain. strike_min / strike_max: Optional inclusive strike filter (0 = no bound). Full chains on NVDA/MU/CRWD exceed the MCP result-size cap (~100-140k chars); pass a band around spot (e.g. ±15%) to keep the payload small.
Returns JSON: {"items": [{exp_date, day_left, exp_type}, ...]} when exp_date is omitted, else the chain for that expiration (filtered if bounds given).
Inputs: symbol (required), exp_date, strike_max, strike_min. Returns: result
get_option_greeksA
Get broker-computed greeks (delta/gamma/theta/vega/rho, IV) for an option chain.
Prefer this over locally derived greeks when sizing or comparing legs.
Args: symbol: Underlying ticker, e.g. 'TSLA'. exp_date: Expiration as 'YYYYMMDD' (or 'YYYY-MM-DD'). Get valid dates from get_option_chain with exp_date omitted. strike_min / strike_max: Optional inclusive strike filter (0 = no bound); full greeks on liquid names exceed the MCP result-size cap.
Returns JSON {"chains": [{strike, cp, side, symbol, iv, delta, gamma, rho, theta, vega}, ...]}. Illiquid strikes report "--" rather than a number.
Inputs: symbol (required), exp_date (required), strike_max, strike_min. Returns: result
get_ordersA
List orders and their status (open / filled / cancelled) for all accounts.
Each entry carries the Firstrade order id (e.g. 'G42621-1601'), which is the authoritative link between a fill and the GTC ladder it came from. Use this to see resting GTC orders, detect dead orders, and attribute fills to a plan.
Args: per_page: Orders per page. 0 (default) returns all.
Returns JSON keyed by account number.
Inputs: per_page. Returns: result
get_single_quoteB
Get real-time quote for a stock symbol.
Inputs: symbol (required). Returns: result
get_watchlist_quoteA
Get real-time quotes for multiple symbols (comma-separated, e.g. 'AAPL,NVDA,MU').
Inputs: symbols (required). Returns: result
place_option_orderA
Place a real option order (dry_run=False). Requires FT_ALLOW_LIVE_ORDERS=true in .env AND a confirm_token from preview_option_order called with these exact same arguments — the server rejects the order otherwise, it does not just rely on the caller having "meant to" preview first.
Args: option_symbol: OCC format symbol (e.g. 'AAPL250620C00150000'). order_type: buy_to_open | sell_to_close | sell_to_open | buy_to_close ('buy' = buy_to_open, 'sell' = sell_to_open; to exit a long option you MUST use sell_to_close, otherwise Firstrade treats it as opening a short and rejects with ref 1103). contracts: Number of contracts. confirm_token: Token returned by preview_option_order for this exact order. price_type: limit | market | stop | stop_limit duration: day | day_ext | gt90 price: Limit price per contract (required for limit orders). stop_price: Stop trigger price (required for stop/stop_limit orders).
Returns JSON with order confirmation. This sends a real order to Firstrade.
Inputs: contracts (required), order_type (required), confirm_token (required), option_symbol (required), +4 more. Returns: result
place_option_spreadA
Place a real two-leg option spread (dry_run=False). Requires FT_ALLOW_LIVE_ORDERS=true in .env AND a confirm_token from preview_option_spread called with these exact same arguments — the server rejects the order otherwise, it does not just rely on the caller having "meant to" preview first.
Same arguments as preview_option_spread, plus confirm_token. DAY order only; 7AM–4PM ET window. Returns JSON with order confirmation. This sends a real order to Firstrade.
Inputs: symbol1 (required), symbol2 (required), net_price (required), limit_type (required), +5 more. Returns: result
place_stock_orderA
Place a real stock order (dry_run=False). Requires FT_ALLOW_LIVE_ORDERS=true in .env AND a confirm_token from preview_stock_order called with these exact same arguments — the server rejects the order otherwise, it does not just rely on the caller having "meant to" preview first.
Args: symbol: Ticker symbol (e.g. 'NVDA'). order_type: buy | sell | sell_short | buy_to_cover quantity: Number of shares. confirm_token: Token returned by preview_stock_order for this exact order. price_type: limit | market | stop | stop_limit | trailing_stop_dollar | trailing_stop_percent duration: day | day_ext | overnight | gt90 (gt90 ≈ GTC, 90-day) price: Limit price (required for limit/stop_limit orders). stop_price: Stop trigger price (required for stop/stop_limit orders).
Returns JSON with order confirmation. This sends a real order to Firstrade.
Inputs: symbol (required), quantity (required), order_type (required), confirm_token (required), +4 more. Returns: result
preview_option_orderA
Preview an option order WITHOUT sending it (dry_run=True). Always call this first.
Args: option_symbol: OCC format symbol (e.g. 'AAPL250620C00150000'). order_type: buy_to_open | sell_to_close | sell_to_open | buy_to_close ('buy' = buy_to_open, 'sell' = sell_to_open; to exit a long option you MUST use sell_to_close, otherwise Firstrade treats it as opening a short and rejects with ref 1103). contracts: Number of contracts. price_type: limit | market | stop | stop_limit duration: day | day_ext | gt90 price: Limit price per contract (required for limit orders). stop_price: Stop trigger price (required for stop/stop_limit orders).
Returns JSON with order preview confirmation data, plus "confirm_token": pass that token unchanged to place_option_order (with the identical order arguments) to actually send it. The token expires in 10 minutes and works once.
Inputs: contracts (required), order_type (required), option_symbol (required), price, +3 more. Returns: result
preview_option_spreadA
Preview a two-leg option spread WITHOUT sending it (dry_run=True). Always call this first.
Args: symbol1 / symbol2: OCC symbols of the two legs (e.g. 'NVDA261120C00270000'). transaction1 / transaction2: buy_to_open | sell_to_open | sell_to_close | buy_to_close per leg (e.g. debit call spread = leg1 buy_to_open lower strike, leg2 sell_to_open higher strike). limit_type: 'debit' (you pay net_price) or 'credit' (you receive net_price). net_price: Net limit price per spread. contracts1 / contracts2: Contracts per leg (default 1 each).
Notes: complex orders are DAY only (no GTC) and accepted by Firstrade only 7AM–4PM ET (ref 1110 otherwise). Returns JSON preview plus "confirm_token": pass that token unchanged to place_option_spread (with the identical arguments) to actually send it. The token expires in 10 minutes and works once.
Inputs: symbol1 (required), symbol2 (required), net_price (required), limit_type (required), +4 more. Returns: result
preview_stock_orderA
Preview a stock order WITHOUT sending it (dry_run=True). Always call this first.
Args: symbol: Ticker symbol (e.g. 'NVDA'). order_type: buy | sell | sell_short | buy_to_cover quantity: Number of shares. price_type: limit | market | stop | stop_limit | trailing_stop_dollar | trailing_stop_percent duration: day | day_ext | overnight | gt90 (gt90 ≈ GTC, 90-day) price: Limit price (required for limit/stop_limit orders). stop_price: Stop trigger price (required for stop/stop_limit orders).
Returns JSON with order preview confirmation data, plus "confirm_token": pass that token unchanged to place_stock_order (with the identical order arguments) to actually send it. The token expires in 10 minutes and works once.
Inputs: symbol (required), quantity (required), order_type (required), price, +3 more. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 15 tools
Each tool targets a distinct responsibility: account data, quotes, option chains/greeks, order preview vs. placement, and cancellation. The preview/place pairs are clearly separated by the confirm_token workflow, and even the two option-order tools (single vs. spread) are distinct in scope.
The set follows a clear verb-first pattern: get_* for reads, preview_*/place_* for order flows, and cancel_order. Minor inconsistency exists between get_single_quote and get_watchlist_quote, which could be more uniformly get_quote/get_quotes, but the intent remains obvious.
15 tools is at the upper edge of reasonable, but the count is justified by the trading domain: separate preview/place pairs for stocks, options, and spreads plus account data. Could be slightly consolidated (e.g., one quote tool), but it is not bloated.
The set covers the core brokerage workflow well: quotes, positions, balances, history, option chains/greeks, order preview/placement for stocks/options/spreads, and cancellation. Missing order modification/replacement, but cancel+recreate covers most needs.
Maintenance
Related MCP Connectors
Agentic brokerage access to a US brokerage account: quotes, orders, positions, cash and documents.
Connect brokerage accounts to AI assistants: holdings, orders, asset lookup, and approved trading.
Unified financial infrastructure connecting AI agents directly to trade live/demo brokerage accounts, Web3 non-custodial wallets, real-time market data across equities, ETFs, crypto, forex, options, DeFi swaps, and prediction markets, institutional research feeds, and algorithmic strategy backtesters.
OpenFinance gives any AI agent easy trading across every major market through one hosted MCP server. Trade crypto spot and perpetuals on Hyperliquid with market, limit and TWAP orders, leverage and margin control. Trade stocks, indices, commodities, forex and pre-IPO names as Hyperliquid HIP-3 perps. Trade Polymarket prediction markets with odds, order books, positions, PnL and trader leaderboards. Swap and bridge across 40+ EVM chains and Solana with Relay and Jupiter, find memecoins across 200+ chains, launch tokens on Robinhood Chain and Solana, and buy crypto with card, bank or UPI. Write actions are designed to ask for your confirmation first. Connect with OAuth (sign in to OpenFinance and approve) or an API key. Works with Claude, ChatGPT, Cursor, VS Code, Codex and any MCP client. No install. Home: https://openfinance.tech/agents · Docs: https://docs.openfinance.tech · GitHub: https://github.com/openfinance-tech/mcp
Related MCP Servers
- AlicenseBqualityCmaintenanceEnables AI assistants to interact with Interactive Brokers trading accounts to retrieve market data, check positions, and place trades. Includes pre-configured IB Gateway and handles OAuth authentication automatically.14138 npm228MIT
- AlicenseNot gradedqualityDmaintenanceEnables trading and portfolio management through the Alpaca API, allowing users to place orders, manage positions and watchlists, access market data, and retrieve account information through natural language.22 npm4ISC
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to securely interact with Charles Schwab accounts through OAuth authentication, providing access to account balances, real-time market quotes, options chains, transaction history, order management, and comprehensive market data.1MIT
- FlicenseNot gradedqualityDmaintenanceEnables LLMs to retrieve real-time stock and options market data through the E\*TRADE API using natural language. It features secure OAuth 1.0 authentication, persistent token management, and comprehensive support for stock quotes, options chains, and Greeks.2-