Skip to main content
Glama
Kris-Wang05

ibkr-mcp

by Kris-Wang05
README.md
# ibkr-mcp — an LLM-driven trading agent for Interactive Brokers

[![CI](https://github.com/Kris-Wang05/ibkr-mcp/actions/workflows/ci.yml/badge.svg)](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