Skip to main content
Glama
diofthecr3ed

Broker-mcp

by diofthecr3ed
README.md
# Broker-mcp — Broker MCP Server (Phase 1 · read-only · stdio)

A first-party [Model Context Protocol](https://modelcontextprotocol.io) server that connects a
customer's **Broker** (Broker by Parent Company) trading account to any MCP-compatible AI client —
Claude Desktop, Cursor, VS Code and others. Built to **PRD v1.3** ("Broker MCP — Product
Requirements Document"), this is the **Phase 1 deliverable: read-only, local (stdio) only.**

> **This server cannot trade.** Order placement/modification/cancellation tools are not merely
> disabled — they **do not exist in this codebase** (FR-G1, the Zerodha excluded-tools pattern).
> Nothing here can change account state at Broker, move funds, or sell holdings. Phase 2 (trade
> tools behind guardrails) is a separate, compliance-gated build.

---

## 1. What is implemented (PRD traceability)

### Tools (21, all annotated `readOnlyHint` for account data)

| Tool                                                             | What it does                                                                                                         | PRD       |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------- |
| `get_login_url` / `complete_login` / `session_status` / `logout` | Daily login handoff: browser link → Broker's own login page → one-time request token → encrypted local session       | FR-A1–A5  |
| `get_profile`                                                    | Account profile                                                                                                      | FR-R1     |
| `get_margins`                                                    | Equity + commodity funds/margins (optionally per segment)                                                            | FR-R1     |
| `get_holdings`                                                   | Demat holdings with avg cost, LTP, day & overall P&L                                                                 | FR-R2     |
| `get_positions`                                                  | Net + day positions with P&L                                                                                         | FR-R2     |
| `get_ltp` / `get_ohlc` / `get_quote`                             | Prices for many instruments; batch limits 1,000 / 1,000 / 500 enforced with transparent chunking                     | FR-R3, V3 |
| `resolve_symbol`                                                 | "BANKNIFTY next expiry 52000 CE" → exactly one instrument, or candidates to disambiguate (never guesses)             | FR-R4, V2 |
| `get_expiries` / `get_option_chain`                              | Option expiries and chain (strikes × LTP/OI/volume/bid/ask around ATM). Greeks/IV honestly reported as unavailable   | FR-R5     |
| `get_historical`                                                 | Daily/intraday candles (8 documented intervals) with range guards                                                    | FR-R6     |
| `get_orders` / `get_order` / `get_trades`                        | Order book, order history, trade book — **read only**                                                                | FR-R7     |
| `calculate_margin`                                               | Pre-trade margin for hypothetical orders; uses Broker's margin API when live, else a clearly-labelled rough estimate | FR-R8     |
| `server_status` / `refresh_instruments`                          | Diagnostics, per-tool metrics, instrument-master refresh                                                             | FR-O4, V2 |

### Guardrails & controls in this build

- **Read-only by construction** — no write tool registered anywhere (FR-G1; guardrail stage 3).
- **Audit trail** — every tool call written to append-only JSONL with UTC timestamp, unique
  correlation ID (`mcp-…`), user id, redacted arguments, outcome and latency (FR-G7; stage 10).
  Location: `<data dir>/audit/audit-YYYYMMDD.jsonl`.
- **Assistant framing** — server instructions and tool descriptions state: _knowledgeable
  assistant, not an autonomous trader; no Broker-authored advice; never fabricate Greeks/IV;
  credentials only on Broker's own pages_ (FR-G8, R7 mitigation).
- **Readable errors** — raw 4xx/5xx bodies never reach the model; failures follow the PRD §4.7
  message patterns ("Your Broker session has expired — reconnect to continue", "Insufficient
  margin…", "I couldn't find that contract — did you mean…") (FR-O1).
- **Rate limiting** — client-side token bucket (default 3 req/s, far below anything order-loop
  shaped) plus exponential backoff and a readable message on upstream 429 (FR-O2). Broker
  publishes no rate limits; the cap is deliberately conservative.
- **Timestamps + source labels** — every response is wrapped in an envelope with `as_of_utc`,
  `as_of_ist`, a `data_source` label ("point-in-time snapshot, not a live stream" / "MOCK SANDBOX
  DATA") and a market-session hint (FR-O3).
- **Token security** — the daily access token is stored **only on the user's machine**, encrypted
  at rest (Fernet), never transmitted to any Broker-hosted MCP infrastructure (none exists in
  Phase 1). Corrupt stores are discarded, forcing a clean reconnect (FR-A5). Daily expiry is
  handled gracefully with a reconnect prompt (FR-A4).
- **Input validation** — typed schemas on every tool (pydantic), field-level rejection messages
  (V1); instrument references must resolve to exactly one contract or the tool asks (V2); batch
  limits chunked transparently (V3); session freshness checked at call time (V14).

### Verification status (FR-O6)

- **57 unit tests, all passing** (`pytest`): schemas, chunking, symbol-resolution grammar, option
  chain composition, error translation, token-store encryption round-trip, audit redaction, plus
  end-to-end tool calls through the real FastMCP layer.
- **stdio protocol smoke test passing**: the server was spawned by a real MCP client
  (`mcp.client.stdio`), initialized (protocol `2025-11-25`), tools listed with annotations, tools
  called, error paths exercised — the equivalent of the MCP Inspector check the PRD requires
  before client wiring.
- A **multi-agent adversarial code review** (PRD-compliance / correctness / security / SDK-usage)
  was run on this codebase; confirmed findings were fixed.

---

## 2. What works right now vs. what is gated

### Works today, no dependencies: mock sandbox mode

`Broker_MCP_MODE=mock` (or `Broker-mcp --mock`) runs the full server against a deterministic
built-in sandbox — no credentials, no network. Every tool works: a realistic instrument master
(NIFTY/BANKNIFTY option chains, equities, futures), holdings, positions, an order book with
COMPLETE/OPEN/REJECTED orders, historical candles. All responses are loudly labelled
**"MOCK SANDBOX DATA — synthetic values… NOT real market data."** This is the evaluation,
demo and client-integration surface, and it satisfies the PRD's sandbox-before-live posture.

### Live mode — gated on the Broker open-API programme reaching GA

The live client is fully implemented against the **verified** Broker API surface (checked
19 Jul 2026 against developer.Broker.in and the official PyBrokerAPI SDK source): host
`api.Broker.in`, `Authorization: token api_key:access_token`, checksum-based token exchange,
gzipped daily instruments CSV, quote batch limits, historical intervals. To go live you need,
from the **Broker Developer Portal**: an app (API key + secret) with its **redirect URL** and
**static IPs** registered — i.e. Phase 0 of the PRD ("open APIs at GA") must be true for the
account. Until then, live calls fail with readable configuration/session errors.

### Known open items (flagged in code, degrade gracefully)

| Item                                           | Status                                                                              | Behaviour today                                                                                                                       |
| ---------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /optionchain`, `/optionchain/expirylist` | In the official SDK, **not on the docs site** — PRD says _confirm GA with API team_ | Tried first; on failure the chain is **composed** from instrument master + batched quotes (OI/volume live, Greeks/IV honestly absent) |
| `POST /margins/orders`, `/margins/basket`      | Same "confirm GA" flag                                                              | Tried first; falls back to a **clearly-labelled rough estimate** (`authoritative: false`)                                             |
| Access-token expiry time                       | Broker's own docs contradict themselves (12 AM vs 6 AM IST)                         | Session marked _suspect_ after midnight, _expired_ at 6 AM; the API's own 401 always wins and produces a reconnect prompt             |
| API version header                             | Docs majority `X-Version: 3`, SDK sends `X-Broker-Version: 3`                       | Both sent; smoke-test at GA                                                                                                           |
| Rate limits                                    | Not documented by Broker anywhere                                                   | Conservative client-side cap (3 req/s)                                                                                                |
| Refresh-token / connect-once UX (PRD Q5)       | Open decision for API/Security                                                      | Not built; daily reconnect flow implemented                                                                                           |

---

## 3. Quickstart

**Windows (PowerShell):**

```powershell
cd Broker-mcp
python -m venv .venv
.venv\Scripts\python -m pip install -e ".[dev]"
.venv\Scripts\python .venv\Scripts\pywin32_postinstall.py -install   # required: places pywin32 DLLs
.venv\Scripts\python -m pytest                                       # 57 tests
.venv\Scripts\Broker-mcp --mock                                      # run the server (stdio) in sandbox mode
```

> The `pywin32_postinstall` step is **required on Windows**: the official `mcp` SDK imports
> `pywintypes` for its stdio transport, and pip alone does not always place the loader DLL. Skip it
> and both `pytest` and the server fail with `ImportError: DLL load failed while importing
_win32sysloader`. The COM-registration warnings it prints (needs admin) are harmless.

**macOS / Linux:**

```bash
cd Broker-mcp
python -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/python -m pytest            # 57 tests
.venv/bin/Broker-mcp --mock           # run the server (stdio) in sandbox mode
```

> **Note:** the running server prints nothing and waits silently — that is correct. An MCP stdio
> server is not a REPL; it waits for a client (Claude Desktop, Inspector) to speak JSON-RPC on
> stdin. Press `Ctrl+C` to stop it. Clients launch their own copy of the server; you do not run it
> by hand for them.

### Troubleshooting the environment

| Symptom                                                                                                                          | Cause                                                                                                                                                | Fix                                                                                                                                                                                                                               |
| -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ModuleNotFoundError: No module named 'pydantic_core._pydantic_core'` (or a stale `*.cp3XX-win_amd64.pyd` for the wrong version) | The `.venv` mixes wheels built for a different Python (e.g. a 3.12 wheel under a 3.13 interpreter — often from a venv reused after a Python upgrade) | Recreate the venv with the current Python: delete `.venv`, then repeat the steps above. Quick check: `.venv\Scripts\python --version` must match the `cp3XX` tag on the `.pyd` files in `.venv\Lib\site-packages\pydantic_core\`. |
| `ImportError: DLL load failed while importing _win32sysloader`                                                                   | The `pywin32` post-install step was skipped                                                                                                          | Run `.venv\Scripts\python .venv\Scripts\pywin32_postinstall.py -install`                                                                                                                                                          |

### Claude Desktop (mock sandbox — works immediately)

`%APPDATA%\Claude\claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "Broker": {
      "command": "C:\\path\\to\\Broker-mcp\\.venv\\Scripts\\Broker-mcp.exe",
      "args": ["--mock"]
    }
  }
}
```

### Claude Desktop (live)

```json
{
  "mcpServers": {
    "Broker": {
      "command": "C:\\path\\to\\Broker-mcp\\.venv\\Scripts\\Broker-mcp.exe",
      "env": {
        "Broker_API_KEY": "your_app_api_key",
        "Broker_API_SECRET": "your_app_api_secret"
      }
    }
  }
}
```

Then in chat: _"Connect my Broker account"_ → the assistant calls `get_login_url`, you log in on
Broker's own page (mobile + OTP + MPIN — **never** in the chat), copy the `request_token` from
the redirect URL, and say _"complete login with token …"_. The session lasts until the daily
regulatory expiry; the server prompts to reconnect after that.

Cursor and VS Code configs, plus sample prompts: [docs/client-setup.md](docs/client-setup.md)
and [docs/sample-prompts.md](docs/sample-prompts.md).

### Test it end-to-end (mock mode — no credentials)

Two ways, quickest first.

**MCP Inspector** — a browser UI to click each tool and inspect raw responses. Best for methodical
tool-by-tool testing; needs Node.js.

```powershell
npx @modelcontextprotocol/inspector .venv\Scripts\Broker-mcp.exe --mock
```

It prints a `http://localhost:…` URL. Open it → **Connect** → **Tools** tab → pick a tool (e.g.
`get_holdings`) → **Run Tool**. The Inspector launches and talks to the server for you.

**Claude Desktop** — the real client experience:

1. Add the mock config above to `%APPDATA%\Claude\claude_desktop_config.json` (merge into any
   existing `mcpServers` block — don't overwrite the file).
2. **Fully quit** Claude Desktop from the system-tray icon (right-click → Quit) — closing the
   window is not enough; it keeps running in the tray and won't reload the config.
3. Reopen Claude Desktop, start a new chat, click the **tools icon** in the input box — `Broker`
   should appear with 21 tools.
4. Ask a sample prompt; approve the tool-use prompt the first time it fires:
   - _"How is my portfolio doing today?"_ → holdings, labelled **MOCK SANDBOX DATA**
   - _"Show the NIFTY option chain for the nearest expiry"_ → chain with OI/volume; Greeks/IV honestly absent
   - _"Buy 2 lots of NIFTY futures"_ → refused (no order tools exist in Phase 1 — proves read-only)

You do **not** start the server yourself for a client — each client (Claude Desktop, Inspector)
launches its own copy from the `command` path. If `Broker` does not appear, check
`%APPDATA%\Claude\logs\mcp-server-Broker.log`. More prompts: [docs/sample-prompts.md](docs/sample-prompts.md).

---

## 4. Regulatory position — read before any real-world use

> This section is a product-level summary of PRD §8. **It is not legal advice.** Phase 1 must not
> be offered to customers until Broker Compliance & Legal have signed off.

### Why Phase 1 (this build) is the low-risk posture

Read-only tools never touch order routing, so under SEBI's algo-trading framework (circular of
4 Feb 2025, fully mandatory since 1 Apr 2026) **no Algo-ID tagging, no algo registration and no
Research-Analyst question arises** — there are no orders. Phase 1 rides on the controls the
Broker open-API programme itself must already satisfy (client-specific API keys, OAuth + 2FA,
static-IP whitelisting, 5-year audit, VAPT). Six brokers (Zerodha, 5paisa, Groww, Dhan, FYERS,
Upstox) already operate official MCPs; the read-only posture matches Zerodha/Upstox/FYERS.

### Approvals required BEFORE customer launch of this read-only build

| #   | Approval / action                                                                                                        | Who grants it                                                                             | Status                                                         |
| --- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| 1   | Broker open APIs at GA, SEBI-compliant (static IP, OAuth 2.1, 2FA, audit, VAPT) — the gating dependency the MCP inherits | Exchanges (NSE/BSE/MCX) via the existing API-programme approval; owned by API/infra teams | **Gating** — outside this repo                                 |
| 2   | Standard API review for the MCP as an API client of that programme                                                       | Broker internal (API team + Compliance)                                                   | Required                                                       |
| 3   | Consent & disclosure language shown at connection (read scopes, risk/non-advice statement, fee disclosure)               | Broker Compliance, against SEBI + exchange norms                                          | Required — copy in this repo is engineering draft only         |
| 4   | Confirmation of the "confirm with API team" endpoints and the token-expiry time                                          | Broker API team                                                                           | Required before customer docs                                  |
| 5   | 5-year retention pipeline for the audit logs this server writes                                                          | Broker ops/compliance                                                                     | Required (server writes the records; retention is operational) |

### Additionally required before Phase 2 (trade tools — NOT in this build)

- **Written confirmation** that a human-confirmed, sub-10-orders/sec, user-instructed MCP order is
  ordinary API trading, **not** a registrable algo (PRD Q2) — Compliance/Legal, with NSE/BSE if needed.
- **Written comfort** that an LLM in the order path does not trigger black-box-algo /
  RA-licence duties (PRD Q3).
- Exchange **Algo-ID tagging**, static-IP enforcement on the write path, mock/simulation session
  sign-off, sandbox order-path testing, kill switch, idempotency chaos-tests, 100% confirmation
  coverage.
- Any material change to an approved order flow requires fresh review.

### And before any hosted (Phase 3) endpoint

Security review + external **VAPT** by a CERT-In-empanelled auditor, plus resolution of how a
hosted endpoint satisfies static-IP requirements for writes (may end up read-only-hosted). This
build deliberately ships **no network listener** — stdio only.

### Standing non-goals baked into this build

No autonomous trading, no order loops, no Broker-authored advice or strategy, no bespoke LLM.
The model analyses the user's own data at the user's explicit request.

---

## 5. Architecture

```
AI client (Claude/Cursor/VS Code)
        │ stdio (JSON-RPC, MCP)
        ▼
Broker-mcp  ──  server.py     FastMCP tools · audit shell · readable errors
                session.py    login handoff · encrypted token store · daily expiry
                backend.py    LiveBackend (api.Broker.in) ⇄ MockBackend (sandbox)
                instruments.py daily master cache · symbol resolution · chain composition
                ratelimit.py  token bucket + backoff      audit.py  JSONL + metrics
                routes.py     every endpoint path, with confirm-GA flags
        │ HTTPS (Authorization: token api_key:access_token)
        ▼
api.Broker.in  (the SEBI-compliant Broker open-API gateway — static IP, 2FA,
                Algo-ID machinery all live there; the MCP never bypasses it)
```

Data locations (per-user, overridable via `Broker_MCP_DATA_DIR`):

- Encrypted session: `<data dir>/session.enc` + local key `token.key`
- Instrument cache: `<cache dir>/instruments-YYYYMMDD.csv`
- Audit log: `<data dir>/audit/audit-YYYYMMDD.jsonl` (ship to WORM storage for the 5-year SEBI retention)

## 6. Configuration reference

| Env var                                | Meaning                                                 | Default                 |
| -------------------------------------- | ------------------------------------------------------- | ----------------------- |
| `Broker_API_KEY` / `Broker_API_SECRET` | App credentials from the Broker Developer Portal        | — (required for live)   |
| `Broker_ACCESS_TOKEN`                  | Directly inject a daily access token (skips login flow) | —                       |
| `Broker_MCP_MODE`                      | `live` or `mock`                                        | `live`                  |
| `Broker_BASE_URL`                      | Override API host (e.g. UAT)                            | `https://api.Broker.in` |
| `Broker_MCP_DATA_DIR`                  | Session/audit/cache location                            | OS per-user data dir    |
| `Broker_MCP_LOG_LEVEL`                 | Server log level                                        | `INFO`                  |

## 7. Development

```bash
.venv\Scripts\python -m pytest -v          # test suite
npx @modelcontextprotocol/inspector .venv\Scripts\Broker-mcp.exe --mock   # MCP Inspector
```

Layout: `src/Broker_mcp/` (see §5), `tests/` (57 tests), `docs/` (client setup, sample prompts).
Python ≥3.10. Dependencies: `mcp` (official SDK), `httpx`, `pydantic`, `cryptography`, `platformdirs`.

## 8. FAQ

### What does it mean for the server to "run"?

An MCP server is **not** a web app you open in a browser, and **not** a program with a menu you
type commands into. It is a small process that speaks **JSON-RPC over stdio** (standard
input/output). "Running" means: the process has started and is **blocked, waiting** for an AI
client to send it a message on stdin. It prints nothing and does nothing on its own — a silent,
blinking cursor is the _correct_ running state, not a hang.

You will almost never start it yourself. Each AI client (Claude Desktop, Cursor, VS Code, the MCP
Inspector) **launches its own copy** from the `command` path in its config, exchanges JSON-RPC
messages with it (`initialize` → `tools/list` → `tools/call`), and shuts it down when you close the
client. When you ran `Broker-mcp --mock` in a terminal by hand, you started an _orphan_ copy with
nothing connected to it — harmless, but it just sits there until you press `Ctrl+C`. The only time
you launch it directly is under the MCP Inspector, which then plays the role of the client for you.

So "is the server running?" is rarely the right question — the right question is "has my client
launched it and listed its tools?" (check the tools icon in Claude Desktop, or the Tools tab in the
Inspector).

### Will the live (real) server behave very differently from the mock sandbox?

**No — not in shape or behaviour.** The server is built around a single `Backend` interface with two
implementations (`LiveBackend` → `api.Broker.in`, `MockBackend` → synthetic data). Every one of the
21 tools, plus input validation, symbol resolution, option-chain composition, the margin estimate,
the response envelope (timestamps + source labels), the audit log, error translation and batch
chunking, is **backend-agnostic** — the _same code_ runs in both modes. Switching to live changes
**where the numbers come from**, not how the server acts on them.

The mock even makes the three "confirm-GA" endpoints (`option_chain`, `option_expiries`,
`order_margins`) fail on purpose, so the sandbox exercises the **fallback paths** production uses
when those endpoints aren't live: the chain is _composed_ from the instrument master + batched
quotes, and margin falls back to a labelled rough estimate.

### Then how close is the sandbox to the real thing — and what is untested?

Structurally it's a high-fidelity stand-in: you are testing the real server, just with synthetic
numbers. What the sandbox does **not** exercise (these paths exist and are written to the verified
Broker spec, but have never touched a real server, because none exists pre-GA):

| Area                     | Mock today                                                                  | Live reality                                                                                                                     |
| ------------------------ | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Data values**          | Toy option pricing, hash-derived OI/volume, sine-wave candles, no Greeks/IV | Real market prices; the confirm-GA endpoints _may_ return richer data (real OI, possibly Greeks/IV, authoritative margins)       |
| **Auth**                 | Bypassed                                                                    | Real token-exchange checksum flow + `Authorization: token key:token`                                                             |
| **HTTP / rate limiting** | Not used                                                                    | Token bucket + 429 backoff — only instantiated in `LiveBackend`, never hit in mock                                               |
| **Instruments CSV**      | Generated with Kite-convention columns                                      | Real gzipped CSV — actual column names could differ                                                                              |
| **Error translation**    | Synthetic errors                                                            | Real error envelopes / `error_type` values map to the readable messages                                                          |
| **Wire unknowns**        | N/A                                                                         | Version header (`X-Version` vs `X-Broker-Version` — code sends **both**), token-expiry time, whether option-chain/margins are GA |

The PRD notes the two source-of-truth documents (Broker docs vs the PyBrokerAPI SDK) _do not fully
agree_; the live code makes defensible choices (sends both version headers, tries endpoint-then-
composes) precisely so a **smoke-test at GA** can confirm the details. Expect a short integration
pass then — it is anticipated, not a defect.

### What is needed for the live (real) server to work?

1. **The gating dependency (outside this repo):** Broker's open-API programme reaches **GA and is
   SEBI-compliant** for the account — PRD Phase 0. No code substitutes for this.
2. **A registered app** in the Broker Developer Portal: API **key + secret**, a registered
   **redirect URL**, and **static IPs** whitelisted.
3. **Config:** set `Broker_API_KEY` and `Broker_API_SECRET` in the client's `env` block and drop
   `--mock` (mode defaults to `live`). See §3 "Claude Desktop (live)".
4. **Daily login handoff:** connect → `get_login_url` → log in on Broker's own page → copy the
   `request_token` from the redirect → `complete_login`. The encrypted access token lands on your
   machine and expires daily per SEBI rules.
5. **A live smoke-test** to confirm the wire unknowns above (CSV columns, error shapes, which
   endpoints are GA, token-expiry time).

### Why does the server print nothing / look frozen when I run it?

That is expected — see "What does it mean for the server to run?" above. It is waiting for stdin.
Press `Ctrl+C` to stop it. To actually _see_ it do something, use the MCP Inspector or a client.

### Why can't it place trades?

By design (PRD FR-G1, Phase 1). Order-placement tools **do not exist in this codebase** — not
disabled, absent. Nothing here can change account state. Trade tools are a separate, compliance-
gated Phase 2 build. See §1 and §4.