Skip to main content
Glama
sandeepanmukherjee

trading212-mcp-guardrails

README.md
# trading212-mcp-guardrails

An [MCP](https://modelcontextprotocol.io) server that exposes the
[Trading 212](https://www.trading212.com) API to Claude (or any MCP client)
— with a hard-enforced safety layer that Trading 212's own API doesn't give
you, and that no other Trading 212 MCP server currently implements.

**This connects to a real brokerage account. `place_market_order` and
friends can spend real money if `DRY_RUN=false`.** Read the whole "Safety
model" section before pointing this at a live account.

## Why this one

There are several Trading 212 MCP servers already. They're solid for read-only use and for pointing an agent at
T212's own `demo` (paper-trading) environment. What none of them do is add
any protection *on top of* the raw API for **live** trading — as of this
writing, none implement request-level dedup, a loss-based circuit breaker,
or position-size enforcement independent of what the model asks for:

| | This project | Other T212 MCP servers |
|---|---|---|
| Duplicate-order protection | Same-day ledger keyed by full order payload — a retried tool call submits a byte-identical order and is refused, while distinct orders on the same ticker (e.g. a buy then a protective stop) still go through | Not implemented; T212's order API is [documented as non-idempotent](https://t212public-api-docs.redoc.ly/), so a retry can silently duplicate a trade |
| Circuit breaker | Halts *all* trading (persisted to disk, survives restarts) if daily equity drop exceeds a configurable threshold, until a human manually clears it | Not implemented |
| Position limits | Hard-rejects (not silently shrinks) any buy that would exceed configured max position count or max % of equity per position | Not implemented |
| Paper trading | `T212_ENV=demo` (T212's own demo environment) | Several also support this |
| DRY_RUN simulation | Local simulate-only mode that still validates against your *real* account balance/positions before "placing" — lets you sanity-check exact tool behavior without touching T212's demo environment | Not implemented |
| Credential redaction in logs | Regex filter strips Basic-auth tokens from every log line as defense in depth | Not checked/not applicable (most don't log to disk) |

None of this is about having more tools — it's about the tools you already
have not being able to hurt you by accident: a retried MCP call, an agent
that free-associates a position size, or a bad day in the market.

## Tools

Read-only: `get_account_summary`, `get_positions`, `get_pending_orders`,
`get_order`, `get_order_history`, `get_dividend_history`,
`search_instruments`, `get_halt_status`.

Trading (all gated by the safety layer below): `place_market_order`,
`place_limit_order`, `place_stop_order`, `place_stop_limit_order`,
`cancel_order`.

## Safety model

1. **Idempotency ledger.** Every order actually submitted is recorded in
   `state/orders_<date>.json`, keyed by a hash of the full order payload
   (type, ticker, signed quantity, and any prices/flags). Re-submitting a
   byte-identical order the same day is refused outright — this is the only
   thing standing between a retried tool call and a duplicate order, since
   T212's API is documented as non-idempotent. Genuinely different orders on
   the same ticker (a buy followed by a protective stop-loss sell, or
   averaging into a position) hash differently, so they aren't false-flagged
   as duplicates.
2. **Circuit breaker.** Before any trading tool executes, it re-fetches
   account equity and compares it to the first equity reading of the day.
   If the drop exceeds `MAX_DAILY_LOSS_PCT`, it writes `state/HALT` and
   every subsequent call — this run, this process, any other process that
   imports this code — refuses to trade until a human deletes that file by
   hand. Nothing in this codebase clears it automatically.
3. **Position limits.** Buys are hard-rejected (not silently resized) if
   they'd open a position beyond `MAX_POSITIONS` distinct tickers, or push
   any single position above `MAX_POSITION_PCT` of total account equity.
4. **`DRY_RUN`.** Controlled solely by `.env` — no tool accepts a `dry_run`
   argument, so an agent can't be talked into flipping it mid-conversation.
   When true, every trading tool runs its full validation (funds check,
   position limits, circuit breaker) against your *real* account data and
   logs/ledgers exactly what it would have submitted, but never calls the
   order endpoint.
5. **Credential redaction.** A logging filter strips any `Basic <token>`
   auth header from every log line before it's written, as defense in depth
   on top of application code never logging the raw key/secret.

None of this is advisory — every check above runs inside the tool call
itself, every time, regardless of what a connected model asks for.

## Setup

```bash
git clone https://github.com/sandeepanmukherjee/trading212-mcp-guardrails
cd trading212-mcp-guardrails
python -m venv .venv
.venv\Scripts\activate       # Windows
# source .venv/bin/activate  # macOS/Linux
pip install -r requirements.txt
cp .env.example .env         # then fill in T212_API_KEY / T212_API_SECRET
```

Get API credentials from Trading 212 under Settings → API (Beta). Leave
`DRY_RUN=true` until you've reviewed exactly what a tool call would have
submitted.

### Claude Code

```bash
claude mcp add trading212 -- /path/to/.venv/bin/python /path/to/mcp_server.py
```

### Claude Desktop

Add to `claude_desktop_config.json` (Developer settings → Local MCP
servers → Edit Config):

```json
{
  "mcpServers": {
    "trading212": {
      "command": "/path/to/.venv/bin/python",
      "args": ["/path/to/mcp_server.py"]
    }
  }
}
```

### Any other MCP client

This is a stock stdio MCP server built on the standard Python `mcp` SDK —
nothing Claude-specific about it. Any client that speaks MCP over stdio can
launch it the same way: run `/path/to/.venv/bin/python /path/to/mcp_server.py`
as the command, no arguments needed. In practice that means pointing the
client at that command + interpreter path in whatever config format it uses:

**OpenAI Codex CLI** — either run:

```bash
codex mcp add trading212 -- /path/to/.venv/bin/python /path/to/mcp_server.py
```

or add directly to `~/.codex/config.toml`:

```toml
[mcp_servers.trading212]
command = "/path/to/.venv/bin/python"
args = ["/path/to/mcp_server.py"]
```

**Cursor** — `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):

```json
{
  "mcpServers": {
    "trading212": {
      "command": "/path/to/.venv/bin/python",
      "args": ["/path/to/mcp_server.py"]
    }
  }
}
```

**Windsurf** — `~/.codeium/windsurf/mcp_config.json`
(`%USERPROFILE%\.codeium\windsurf\mcp_config.json` on Windows), same
`mcpServers` shape as Cursor above.

**Cline (VS Code extension)** — via the Cline panel's MCP settings, or
directly in `cline_mcp_settings.json` (path varies by OS — see
[Cline's docs](https://docs.cline.bot/mcp/mcp-overview)), same `mcpServers`
shape as Cursor above.

**VS Code (native MCP support)** — `.vscode/mcp.json` in the workspace:

```json
{
  "servers": {
    "trading212": {
      "type": "stdio",
      "command": "/path/to/.venv/bin/python",
      "args": ["/path/to/mcp_server.py"]
    }
  }
}
```

**Gemini CLI** — `mcpServers` in `settings.json`, same shape as Cursor above.

For every client above: if `T212_API_KEY`/`T212_API_SECRET` are set as OS
environment variables rather than in `.env`, add them explicitly under that
server's `"env"` block instead of relying on inheritance. GUI-launched apps
in particular often don't pick up environment variables added after the app
(or its launcher process) last started — if the server can authenticate from
a terminal but a GUI client reports it disconnected or missing credentials,
that mismatch is almost always why. `env: {}` (or an explicit `env` block
with the two keys) sidesteps it entirely.

## Testing

```bash
pytest tests/
```

Every guarantee in the safety model above has tests behind it, with mocked
HTTP calls — no live API access or credentials required:

- **Idempotency ledger** — an identical retry is refused on every order type;
  a different type, or the same type at a different size, on the same ticker
  is *not* false-flagged; and a rejected order (4xx) leaves nothing in the
  ledger, so a failed submit doesn't block a legitimate retry for the rest of
  the day.
- **Circuit breaker** — halts on a breach, stays halted on the next call even
  if equity recovers, is never cleared automatically, and gates every trading
  tool via `_check_trading_allowed`.
- **Position limits** — both directions of the contract: breaches of
  `MAX_POSITIONS` / `MAX_POSITION_PCT` are refused rather than resized, and
  legitimate orders aren't over-refused (topping up a held position at the
  count cap, quantity-0 closed positions ignored in the count). The `%` cap is
  judged on projected total, and an order value that can't be verified warns
  without becoming a way around the count cap.
- **`DRY_RUN`** — the order endpoint is never called, for any order type.
- **Pagination** — `cursor` reaches the API from both history tools.

CI runs the suite on Python 3.10–3.12. There's no integration test against
the real T212 API; run with `DRY_RUN=true` against your real account for that.

## Disclaimer

This is not financial advice and not an official Trading 212 product. You
are responsible for anything it does to your account. Start with
`DRY_RUN=true`, review the simulated output, and only flip to live trading
once you're satisfied it's behaving the way you expect.

Maintenance

ActivitySlowing
ResponsivenessNo issues