Skip to main content
Glama
pyalgobot

angelone-mcp

by pyalgobot
README.md
# angelone-mcp

[![Listed on mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/pyalgobot/angelone-mcp.git)

An MCP (Model Context Protocol) server that wraps [Angel One's SmartAPI](https://smartapi.angelone.in/docs) —
trading, portfolio, market data, GTT rules, and margin/brokerage — so any MCP
client (Claude, Claude Code, etc.) can query your account and place orders
through natural conversation.

⚠️ **This places real orders on a real trading account.** Test with small
quantities first, and keep in mind Angel One (like most brokers) does not
let you "undo" a filled order.

## What's included

- `angelone_mcp/client.py` – REST client for every documented SmartAPI route:
  auth, orders, positions/holdings, GTT rules, historical candles/OI,
  quotes, option greeks, gainers/losers, margin calculator, brokerage
  estimator. Handles TOTP login, auto re-login on token expiry, and paces
  itself against SmartAPI's documented rate limits (see "Rate limiting"
  below).
- `angelone_mcp/server.py` – MCP server exposing 32 tools built on top of
  the client (see full list below).

## 1. Prerequisites

- Python 3.10+
- An Angel One trading account with SmartAPI access
- A SmartAPI app created at https://smartapi.angelone.in/ (gives you an API key)
- TOTP set up on your Angel One account, and the **base32 secret** used to
  set up that authenticator (not the 6-digit code — the secret behind it).
  You get this once, when you first scan the QR code to enable TOTP; if you
  don't have it saved, you'll need to reset/reconfigure TOTP on your account
  to get a fresh secret.

## 2. Install

```bash
cd angelone-mcp
python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -r requirements.txt
```

## 3. Configure credentials

Set these environment variables (e.g. in a `.env` file you source, or
directly in your MCP client config):

| Variable | Description |
|---|---|
| `ANGELONE_API_KEY` | API key from your SmartAPI app |
| `ANGELONE_CLIENT_CODE` | Your Angel One client/trading account code |
| `ANGELONE_PIN` | Your login PIN |
| `ANGELONE_TOTP_SECRET` | Base32 TOTP secret for your account |

**Never commit these to source control.** Treat `ANGELONE_TOTP_SECRET` and
`ANGELONE_PIN` like passwords — anyone with them plus your API key can trade
on your account.

### Optional: running behind an HTTP proxy

If your machine/network requires an outbound HTTP proxy to reach the
internet, set:

| Variable | Description |
|---|---|
| `ANGELONE_HTTP_PROXY` | Proxy URL used for `http://` requests, e.g. `http://user:pass@proxyhost:8080` |
| `ANGELONE_HTTPS_PROXY` | Proxy URL used for `https://` requests (this is the one that matters — SmartAPI is https-only). Falls back to `ANGELONE_HTTP_PROXY` if unset. |
| `ANGELONE_NO_PROXY` | Optional comma-separated list of hosts to bypass the proxy for |

These are only needed if the standard `HTTP_PROXY` / `HTTPS_PROXY` environment
variables aren't already visible to the server process. That's commonly the
case for MCP servers, since MCP clients usually launch the server with an
explicit `env` block (like the JSON below) instead of inheriting your shell's
environment — so a proxy configured in your shell won't reach the server
unless you either add it to that `env` block yourself under `HTTPS_PROXY`, or
use the `ANGELONE_*` variables above. If neither `ANGELONE_HTTP_PROXY` nor
`ANGELONE_HTTPS_PROXY` is set, the server falls back to the standard
`HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` variables automatically.

## 4. Run it

Standalone (for testing):

```bash
python -m angelone_mcp.server
```

It speaks MCP over stdio, so it's meant to be launched by an MCP client, not
run interactively.

### Claude Desktop / Claude Code config

Add to your MCP client's config (e.g. `claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "angelone": {
      "command": "/absolute/path/to/angelone-mcp/.venv/bin/python",
      "args": ["-m", "angelone_mcp.server"],
      "cwd": "/absolute/path/to/angelone-mcp",
      "env": {
        "ANGELONE_API_KEY": "your_api_key",
        "ANGELONE_CLIENT_CODE": "your_client_code",
        "ANGELONE_PIN": "your_pin",
        "ANGELONE_TOTP_SECRET": "your_base32_totp_secret",
        "ANGELONE_HTTPS_PROXY": "http://user:pass@proxyhost:8080"
      }
    }
  }
}
```

## Tools exposed

**Session**
`login`, `logout`, `get_profile`

**Orders**
`place_order`, `modify_order`, `cancel_order`, `get_order_book`,
`get_trade_book`, `get_individual_order_details`

**Portfolio / funds**
`get_positions`, `get_holdings`, `get_all_holdings`, `get_rms_limit`,
`convert_position`

**GTT (Good Till Triggered) rules**
`gtt_create_rule`, `gtt_modify_rule`, `gtt_cancel_rule`, `gtt_details`,
`gtt_list`

**Market data**
`get_ltp`, `get_market_quote`, `search_scrip`, `get_candle_data`,
`get_oi_data`, `get_option_greeks`, `get_gainers_losers`,
`get_put_call_ratio`, `get_oi_buildup`, `get_nse_intraday_data`,
`get_bse_intraday_data`

**Margin & brokerage**
`get_margin`, `estimate_charges`

## How auth works

`AngelOneClient` logs in lazily on the first tool call using
`clientcode` + `pin` + a TOTP generated on the fly from
`ANGELONE_TOTP_SECRET` (via `pyotp`). It caches the resulting `jwtToken`,
`refreshToken`, and `feedToken` in memory for the life of the process. If any
call comes back with a 401/403 or a `TokenException`, it transparently
re-logs-in once and retries — you don't need to call `login` yourself unless
you want to force a fresh session.

Sessions issued by SmartAPI are valid until midnight IST regardless of
activity, so a long-running server may still need a fresh login the next day
— the auto-retry logic handles that automatically on the next call.

### Session persistence across restarts

A successful login is also cached to a file on disk, so a fresh server
process doesn't need a fresh TOTP-based login every time it starts (handy
since TOTP requires the code to be freshly generated — restarting the server
several times in a row otherwise means several real logins in a row).

On startup, before serving any tool calls, the server calls
`AngelOneClient.restore_session()`, which:

1. Looks for a previously saved session file. If there isn't one, it does
   nothing further — the client stays in its normal lazy mode and logs in on
   the first tool call, same as before this feature existed.
2. If a saved session is found, it loads the cached tokens and verifies them
   with a real `getProfile` call.
3. If that verification succeeds, the restored session is used as-is — no
   fresh login needed.
4. If it fails for any reason (expired token, revoked session, corrupt file,
   etc.), the cached tokens are discarded and a normal fresh login runs
   instead.

Every successful login (fresh or via the automatic 401/403 retry described
above) re-saves the session file, so it stays current across the whole time
the server runs, not just at startup. `logout` deletes the file.

| Variable | Description |
|---|---|
| `ANGELONE_SESSION_PERSIST` | Set to `false`/`0`/`no`/`off` to disable session persistence entirely (default: enabled) |
| `ANGELONE_SESSION_FILE` | Override the file path used to persist the session. Default: a file under the OS temp directory, named from a hash of your client code (so multiple accounts on the same machine don't collide) |

The session file holds a live access token — not your PIN or TOTP secret,
but enough to call the API as you until it expires. It's written with
owner-only file permissions where the OS supports it; treat it as sensitive
the same way you'd treat any cached login session.

## Rate limiting

`AngelOneClient` paces every outgoing call against
[SmartAPI's documented per-endpoint rate limits](https://smartapi.angelone.in/docs/RateLimit)
— login and most portfolio reads at 1 request/sec, `getProfile` at 3/sec,
quotes/GTT/order-detail lookups at 10/sec, order placement at 20/sec, and so
on. Limits are per SmartAPI endpoint, not global, so calling different tools
back-to-back is never slowed down by this — only a *repeat* call to the same
endpoint made faster than SmartAPI's own limit allows gets held back, which
you'd want anyway.

If SmartAPI reports its own limit was hit regardless (HTTP 403/429, "Access
denied because of exceeding access rate"), the call backs off and retries a
few times with increasing delay before giving up — and that response no
longer gets misread as an expired session and doesn't trigger a spurious
extra login the way it used to.

This applies to every tool automatically; there's nothing to configure to
get it. To turn client-side pacing off entirely (SmartAPI still enforces its
own limits server-side either way — this only controls whether the client
tries to stay under them proactively):

| Variable | Description |
|---|---|
| `ANGELONE_RATE_LIMIT_DISABLED` | Set to `true`/`1`/`yes`/`on` to disable proactive pacing (default: enabled) |

## Testing

```bash
pip install -e ".[test]"

# Offline: verifies the server registers the expected tools. No credentials
# or network access needed.
python -m pytest tests/test_tool_registration.py -v

# Offline: unit tests for session persistence (login state cached to disk,
# restored + verified via get_profile on restart, falls back to a fresh
# login when the cache is missing/invalid). Uses a fake HTTP layer - no
# credentials or network access needed.
python -m pytest tests/test_session_persistence.py -v

# Offline: unit tests for AngelOneClient's own rate limiting (pacing per
# ROUTE_MIN_INTERVAL, backoff/retry on a 403/429 rate-limit response, and
# that such a response is never misread as an expired session). Uses a fake
# HTTP layer - no credentials or network access needed.
python -m pytest tests/test_client_rate_limiting.py -v

# Live, read-only smoke test against your real account. Calls get_profile,
# get_order_book, get_holdings, search_scrip, get_ltp, etc. through the
# actual MCP server subprocess, plus a check that a session survives a
# restart of the server without calling the "login" tool again. Never calls
# place_order/modify_order/cancel_order/gtt_create_rule/gtt_modify_rule/
# gtt_cancel_rule/convert_position/logout - a SafeSession wrapper
# hard-asserts those are never invoked. On top of the server's own rate
# limiting (see "Rate limiting" above), the test itself also paces its tool
# calls and backs off/retries if the API reports one was hit anyway (see
# "Rate limiting in the live test" below) - belt and suspenders. Requires
# ANGELONE_API_KEY/ANGELONE_CLIENT_CODE/ANGELONE_PIN/ANGELONE_TOTP_SECRET
# to be set; skips automatically if they aren't.
python -m pytest tests/test_readonly_live.py -v -s
# or, for a plain-text report without pytest:
python tests/test_readonly_live.py
```

### Rate limiting in the live test

The live test (`tests/test_readonly_live.py`) calls a real account against
the real SmartAPI. The server it drives already paces itself (see "Rate
limiting" above), but the test adds its own independent pacing on top -
useful because it also exercises things the server-side limiter doesn't see
by itself, like two separate server subprocesses (the session-persistence
check) hitting the same account back to back:

- A `RateLimiter` tracks the last time each MCP tool was called and, before
  calling it again, waits out the rest of that endpoint's minimum interval
  (1/req-per-second-limit, plus a ~20% safety margin). Distinct tools hit
  distinct SmartAPI endpoints with independent limits, so this only ever
  delays a *repeat* call to the same tool (e.g. `get_profile` being called
  again by the second server spawn in the session-persistence check) - a
  normal single pass through the suite, where every tool is called once or
  twice, isn't slowed down by it in practice.
- If SmartAPI reports a rate limit was hit anyway (HTTP 403, "Access denied
  because of exceeding access rate"), the test backs off and retries a
  couple of times with increasing delay instead of failing outright.
- This governs the test suite's own request pace only - it has no effect on
  how the MCP server behaves for a real MCP client (Claude, etc.); SmartAPI
  still enforces its limits server-side either way.

## Notes / limitations

- Order params (`price`, `quantity`, etc.) are passed as strings, matching
  what SmartAPI's `placeOrder` expects.
- `get_margin` and `estimate_charges` take a list of position/order dicts —
  see the SmartAPI docs for exact field names per instrument type
  (https://smartapi.angelone.in/docs/Margin, .../Brokerage).
- Rate limits are enforced by Angel One per endpoint; see
  https://smartapi.angelone.in/docs/RateLimit. This server does not do its
  own client-side rate limiting.
- Not affiliated with or endorsed by Angel One / Angel Broking.

TDQS

B3.4/5.0

Scored across 32 tools

Disambiguation4/5

Most tools target distinct actions and resources—orders, positions, holdings, GTT rules, market data, and analytics are clearly separated. Minor overlap exists between get_holdings and get_all_holdings, and between get_ltp and get_market_quote, but descriptions clarify their scope.

Naming Consistency4/5

The naming mostly follows a clear verb_noun snake_case pattern, such as place_order, cancel_order, get_positions, and search_scrip. The gtt_* tools deviate by leading with the domain prefix instead of the verb, and a few names like get_all_holdings vs get_holdings are slightly inconsistent, but overall the pattern is predictable.

Tool Count2/5

With 32 tools, the surface feels heavy and exceeds the 25-tool threshold that typically signals over-expansion. Many market-data and analytics tools could be consolidated or grouped, though the breadth is understandable for a full brokerage API.

Completeness4/5

The tool surface covers the core brokerage lifecycle well: authentication, order placement/modification/cancellation, order and trade books, positions, holdings, GTT rules, margin checks, and charge estimation. Some gaps exist such as historical order history or bulk order capabilities, but agents can accomplish most common trading workflows without dead ends.

Maintenance

ActivityMaintained
ResponsivenessNo issues