Skip to main content
Glama
GitMasterJatin

AlgoTrade-MCP

AlgoTrade-MCP

CI

An MCP server that lets a language model trade on your Zerodha account.

Five tools — buy, sell, cancel, and look at your orders and portfolio — over stdio, so any MCP client (Claude Desktop, Claude Code, your own) can drive a real brokerage account.

Wiring a model to a trading API is about fifty lines. The rest of this repo is about what happens when that goes wrong, because when it does, it costs money.


Setup

You need Bun and a Zerodha Kite Connect app for the API key and secret.

bun install
export KITE_API_KEY=your_key
export KITE_API_SECRET=your_secret

Orders are refused until you also set this:

export KITE_TRADING_ENABLED=true

It's off by default so an accidental run can't spend anything, and it's the switch to flip when something looks wrong. Reads and cancels keep working while it's off — the point is to stop new exposure, and not being able to close a position you already hold would be the wrong failure.

Single orders are also capped, at ₹10,000 unless you say otherwise:

export KITE_MAX_ORDER_VALUE=25000

The default is deliberately small, so turning trading on without thinking about limits can't do much damage.

Orders also need your explicit approval, one at a time. Set KITE_REQUIRE_APPROVAL=false only if you want the model trading unattended.

Kite's login hands you a request token that works exactly once and expires in minutes, which you trade for an access token good until 6am the next day. So logging in is two steps:

bun run login

That prints a URL. Open it, log in, and Zerodha bounces you to a redirect carrying ?request_token=... in the address bar. Copy that value:

bun run login <request_token>

The access token lands in .kite-session.json (gitignored, mode 600) and the server reuses it until Zerodha expires it. Set KITE_SESSION_FILE if you'd rather keep it somewhere outside the repo.

Then point your MCP client at it:

{
  "mcpServers": {
    "zerodha": {
      "command": "bun",
      "args": ["run", "/absolute/path/to/AlgoTrade-MCP/index.ts"],
      "env": {
        "KITE_API_KEY": "...",
        "KITE_API_SECRET": "...",
        "KITE_TRADING_ENABLED": "true"
      }
    }
  }
}

Related MCP server: Zerodha MCP Server

The tools

Tool

What it does

buy_stock / sell_stock

Place an order. Needs KITE_TRADING_ENABLED=true and your approval.

cancel_order

Cancel a pending order by id.

get_orders

Today's order book, with status and fill quantity.

show_portfolio

Holdings and positions.

Buy, sell and cancel are tagged destructiveHint so a client can warn you before the model spends anything. Reads are tagged readOnlyHint.


The parts worth reading

A request that fails isn't the same as an order that didn't happen

This is the one that actually matters.

You POST an order. The connection dies. Did it go through? You genuinely don't know — Zerodha may have accepted it and lost the response on the way back. If you retry, you might just buy the stock twice.

So every order goes out stamped with a unique tag. If the request fails, the server goes looking for that tag in the order book before it answers you, and retries the lookup a few times because the book takes a moment to catch up. You get one of three answers, never a shrug:

  • placed — here's the order id

  • placed anyway — the request failed, but the order is live. Don't retry.

  • not placed — with Zerodha's actual reason. Safe to retry.

There's a fourth case, and keeping it separate is the whole point: if the order book itself can't be read, the server says UNKNOWN and hands you the tag to check by hand. It never guesses "not placed," because a live order reported as unplaced is the mistake that costs you money.

Getting a secret past the model

Every order needs a human to approve it, which sounds simple until you notice the model is the only thing talking to this server. Any code handed back in a tool response, it can just read and echo. So the code never appears there.

Ask for an order and nothing is placed. Instead the server writes this to its stderr and to .pending-approval, where you read it and the model can't:

=== APPROVAL REQUIRED ===
BUY 10 INFY on NSE (MARKET, CNC)
Estimated value: 1050
Code: 88E6BEFF62
Expires: 7:40:39 PM
=========================

You tell the model the code, it calls again, the order goes through. The notice shows what you're approving rather than just a code, because an approval you can't read isn't one.

The code is bound to a hash of that exact order — side, symbol, quantity, product, type, price. A code you gave for one share cannot be spent on a thousand, or on a different stock, or flipped from buy to sell. It works once, expires in five minutes, and is void if the price moved more than 2% since you looked, because an approval given at one price shouldn't execute at another.

Worth being precise about what this covers: it stops the model inventing an approval. It does not stop a model that can read your filesystem some other way — through a filesystem MCP server running alongside, say. Narrow, but real.

The value cap has to fail closed

A limit order carries its own price, so capping it is arithmetic. A market order doesn't, so it's valued off the last traded price — plus 5%, because a market order can fill worse than the last trade and the estimate should err towards refusing.

The part that matters is what happens when that price lookup fails. The order is refused. A cap that quietly switches itself off the moment a quote is unavailable isn't a cap, and "the market data feed is having a bad minute" is exactly when you'd rather not be sending orders anyway.

It applies to sells too. An oversized sell is the same fat finger, and unlike the kill switch this is a standing config you size to your account once, not an emergency lever you reach for mid-incident.

Cancelling is not one endpoint

Kite cancels at /orders/{variety}/{order_id}, and variety isn't always regular — an after-market order is amo, a cover order is co. Your book holds orders you placed from the Kite app too.

Hardcoding regular silently fails to cancel half of them. Asking the model to supply the variety is asking it to guess. So cancel_order takes only an order id, looks the order up, and uses whatever variety it actually has.

And if the order was partly filled, cancelling only kills the remainder. Cancel 100 shares that already filled 40 and you still own 40. A bare "success" here would leave you thinking you were flat, so the result says so plainly.

Orders that are already COMPLETE, CANCELLED or REJECTED are refused before Zerodha is called at all — and a filled order gets told what it actually needs, which is an opposing order, not a cancel. Otherwise a model reads "cancel failed" and tries again forever.

That check is a denylist of those three, not an allowlist of cancellable ones. Kite's docs say its status vocabulary is open-ended, and an allowlist would quietly start refusing real cancels the first time they added a state.

Errors have to look like errors

Two things were quietly swallowing failures.

The MCP SDK already turns a thrown error into a proper isError result — so the try/catch blocks wrapping every call were downgrading real failures into ordinary-looking success. A model can't tell "order rejected" from "order filled" if both come back the same shape, and it will retry a buy that already went through. Those blocks are gone rather than patched.

The other one: kiteconnect rejects with a plain { message, error_type } object instead of an Error, and the SDK stringifies anything that isn't an Error. Every broker failure — bad symbol, expired token, rejected order — was reaching the model as [object Object]. They now carry the real reason.


Tests

bun test

52 tests, no live account needed. KITE_API_ROOT points the client at a fake Zerodha (tests/helpers/fake-kite.ts) that can be told to fail in specific ways, so the paths that are otherwise impossible to reach on purpose — a placement whose response is lost, an unreadable order book, an order that's half filled — are all covered.

The MCP tests skip the internals and drive the actual server over stdio, the same way a client would.


What this doesn't do

Worth being straight about, because some of it matters:

  • Risk stops at the per-order level. Approval, the kill switch and the cap all judge one order at a time. There are no position limits, no daily loss limit, and nothing that looks at your portfolio as a whole — ask for a lakh spread over five stocks and nothing notices if the model picks fifteen. Each one is a separate approval, so you'd see it, but the server wouldn't.

  • It's never run against a funded account. Everything here is built against Kite Connect's documented v3 contract and verified against a local fake.

  • SL and SL-M are offered but can't work — they need a trigger_price that isn't in the schema yet.

  • LIMIT orders default to price 0 if you don't pass one, which Zerodha will reject.

  • One tag per attempt means this survives a dropped connection, not a model that decides to call buy_stock twice. That needs a confirmation step, which is a different problem.

If you want the version of this with the safety architecture taken seriously — a non-bypassable risk gate, single-use approval tokens, an outbox and a reconciler — that's kite-agent. This repo is the small, sharp version of the same idea.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables trading operations on Zerodha platform through natural language, supporting account management, order placement/modification, portfolio holdings, positions, margins, and stock news retrieval.
    Apache 2.0
  • A
    license
    B
    quality
    D
    maintenance
    Enables trading and portfolio management on Zerodha Kite Connect through natural language. Supports placing orders, viewing positions/holdings, accessing real-time market data, and managing GTT orders.
    24
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI models to interact with the Zerodha trading platform for portfolio management, trading, and account operations through natural language.
    3
    -