angelone-mcp


  • angelone-mcp

    AngelOne MCP server
    GitHub Repo

    An MCP (Model Context Protocol) server that wraps Angel One's SmartAPI
    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

    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):

    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):

    {
      "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
    — 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

    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.