ibkr-mcp
by Kris-Wang05
README.md
# ibkr-mcp — an LLM-driven trading agent for Interactive Brokers
[](https://github.com/Kris-Wang05/ibkr-mcp/actions/workflows/ci.yml)
A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes
an Interactive Brokers account — account data, market data, and order entry — as
a set of tools an LLM agent (Claude Desktop, Claude Code, any MCP client) can
call. It pairs that with a **pre-trade risk engine** and a **two-step
confirm-before-trade flow** so an autonomous model can't fat-finger a live
account.
The whole thing runs **offline out of the box** against a built-in market
simulator, so it's reviewable with zero setup — no TWS, no IB Gateway, no
network, not even the `mcp` package for the demo and tests.
```bash
python examples/demo_cli.py # full preview -> place -> blocked-order flow, offline
python tests/test_risk.py # 7 risk-engine tests
python tests/test_session.py # 6 session/safety-flow tests
```
<details>
<summary><b>Sample run</b> — the safety flow and a risk-blocked order (click to expand)</summary>
```text
=== 5. Preview BUY 50 NVDA (risk check) ===
{
"symbol": "NVDA", "action": "BUY", "quantity": 50, "order_type": "MKT",
"reference_price": 167.95, "estimated_notional": 8397.5,
"risk": { "approved": true, "reasons": [] },
"trading_enabled": true,
"confirmation_token": "d2bfa616602b",
"token_expires_in_seconds": 120,
"next_step": "Call place_order('d2bfa616602b') to submit."
}
=== 6. Place order via confirmation token ===
{
"order_id": "1000", "symbol": "NVDA", "action": "BUY", "quantity": 50,
"status": "FILLED", "filled_quantity": 50, "avg_fill_price": 167.36,
"message": "Filled."
}
=== 9. Preview BUY 400 TSLA -- expect REJECT (over notional cap) ===
{
"symbol": "TSLA", "action": "BUY", "quantity": 400,
"estimated_notional": 99748.0,
"risk": {
"approved": false,
"reasons": [
"Order notional $99,748 exceeds per-order cap $20,000.",
"Resulting position notional $99,748 exceeds position cap $60,000."
]
},
"confirmation_token": null,
"next_step": "Order REJECTED by risk checks; not placeable."
}
=== 10. place_order with a bogus token -> guarded ===
{ "blocked": "Unknown or already-used confirmation token. Call preview_order first." }
```
</details>
> **On the backends.** The live `ib_async` backend (`broker/ibkr.py`) is fully
> implemented; the server simply defaults to the offline simulator so the project
> is reviewable with zero setup. Pointing it at a real IBKR **paper** account is a
> two-env-var change (`IBKR_BACKEND=ib`, `IBKR_TRADING_ENABLED=true`) — see
> [Against a real (paper) IBKR account](#against-a-real-paper-ibkr-account).
## Why this design
Letting an LLM place trades is the interesting, dangerous part. Three decisions
carry the design:
1. **The agent never touches the broker SDK directly.** Tools talk to a
`TradingSession`, which talks to a `Broker` interface. Two implementations sit
behind that interface — a live `ib_async` backend and an in-memory simulator —
so the agent-facing contract is identical whether you're on a paper account or
running offline.
2. **Trading is a two-step handshake, not one tool call.** The model must
`preview_order(...)` first; that returns the live quote, the estimated
notional, a risk decision, and — only if risk passes *and* trading is enabled —
a single-use `confirmation_token`. Only `place_order(token)` submits. The order
is **re-validated against the risk limits at execution time**, because price
and position may have moved since the preview.
3. **Safe by default.** With no configuration you get the `mock` backend with
trading **disabled** (read-only). Going live is an explicit, multi-flag opt-in.
## Architecture
```
MCP client (Claude)
│ stdio / JSON-RPC
▼
┌──────────────────┐ tool docstrings = the agent's contract
│ server.py │ get_status · get_account_summary · get_positions
│ (FastMCP tools) │ get_quote · get_open_orders · get_trades
└────────┬─────────┘ preview_order ──► place_order · cancel_order
▼
┌──────────────────┐ two-step order flow, single-use confirmation tokens,
│ session.py │ execution-time re-validation
└────┬───────────┬─┘
▼ ▼
┌─────────┐ ┌──────────────────┐
│ risk.py │ │ broker/ (Broker) │
│ pre- │ │ ├─ mock.py ◄── offline simulator (default)
│ trade │ │ └─ ibkr.py ◄── live ib_async → TWS / IB Gateway
│ checks │ └──────────────────┘
└─────────┘
```
Everything except `server.py` (FastMCP) and `broker/ibkr.py` (ib_async) is pure
standard library, which is why the simulator and tests need no dependencies.
## Tool catalog
| Tool | Purpose |
|------|---------|
| `get_status` | Backend, connection, trading on/off, active risk limits |
| `get_account_summary` | Net liquidation, cash, buying power, P&L |
| `get_positions` | Open positions with cost basis and unrealized P&L |
| `get_quote(symbol)` | Bid / ask / last / mid snapshot |
| `get_open_orders` | Working (unfilled) orders |
| `get_trades` | Execution blotter |
| `preview_order(...)` | **Step 1** — risk-check an order, return a confirmation token |
| `place_order(token)` | **Step 2** — submit a previewed, re-validated order |
| `cancel_order(order_id)` | Cancel a working order |
## Risk controls (`risk.py`)
Enforced before any order is accepted, and again at execution time:
- per-order **quantity** cap
- per-order **notional** cap
- resulting **position notional** cap
- **short-selling** switch (off by default)
- optional **symbol whitelist**
- **daily order count** cap
All are configurable via environment variables (see `.env.example`).
## Running it
### Offline demo / tests (no install)
```bash
python examples/demo_cli.py
python tests/test_risk.py && python tests/test_session.py
```
### As an MCP server
```bash
pip install "mcp[cli]"
python -m ibkr_mcp.server # serves over stdio
```
Register it with an MCP client using `examples/claude_desktop_config.example.json`.
### Against a real (paper) IBKR account
1. `pip install ib_async`
2. Launch **TWS** or **IB Gateway** with the API enabled, logged into a **paper**
account (account id starts with `DU`).
3. Set the environment and run:
```bash
export IBKR_BACKEND=ib
export IBKR_TRADING_ENABLED=true
export IBKR_PORT=7497 # paper TWS
export IBKR_ACCOUNT_ID=DUxxxxxxx
python -m ibkr_mcp.server
```
> **Safety:** keep `IBKR_TRADING_ENABLED=false` for read-only analysis. Point at a
> paper account before ever enabling trades. The risk caps are the backstop, not
> the first line of defense — the read-only default is.
## Layout
```
ibkr_mcp/
models.py dataclasses: Quote, Position, Order, Trade, AccountSummary
config.py env-driven Settings + RiskLimits (safe defaults)
risk.py pure pre-trade risk engine
session.py two-step order flow, token store, re-validation
server.py FastMCP tool layer (the agent contract)
broker/
base.py abstract Broker interface
mock.py offline market simulator
ibkr.py live ib_async backend
examples/ offline demo + MCP client config
tests/ risk + session/safety-flow tests
```
## License
MIT
TDQS
A4/5.0
Scored across 9 tools
Disambiguation5/5
Each tool targets a distinct function: account summary, positions, orders, quotes, trades, status, and a two-step order placement. No overlap in purpose.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern (e.g., cancel_order, get_account_summary, place_order) using underscores.
Tool Count5/5
9 tools is well-scoped for an IBKR trading server, covering account info, order management, quoting, and trade history without being excessive.
Completeness4/5
Core workflows are covered: quoting, order preview/placement/cancellation, account summary, positions, open orders, and trade blotter. Missing perhaps order modification, but that can be handled via cancel+reorder.
Maintenance
ActivityInactive
ResponsivenessNo issues