binance-mcp
# binance-mcp
[](https://github.com/iwaqaruddin/binance-mcp/actions/workflows/ci.yml)
[](LICENSE)
A Model Context Protocol server that gives an LLM read-only access to Binance spot
market data, for people who want a model to reason about real prices without giving
it the ability to place an order.
## Why this exists
A model can already be told to fetch `api.binance.com/api/v3/klines` itself. That
works until it doesn't, and the failures are quiet ones.
**Symbols have to be guessed.** Binance has 3,680 spot pairs. The name is a
concatenation with no separator, so a model writes `BTC-USDT` or `btcusdt` and gets
back `HTTP 400 {"code": -1100, "msg": "Illegal characters found in parameter
'symbol'"}`. That is a lowercase rejection, not a missing pair, and the model cannot
tell the difference. It retries with another guess. This server uppercases and strips
separators before the call, and it exposes a search tool so a guess can be resolved
instead of repeated.
**Out-of-range limits are honoured silently.** Ask `/klines` for 5000 candles and
Binance returns `HTTP 200` with 1000 rows. Nothing in the response says it was
truncated. A model that asked for a year of hourly candles gets six weeks and
reasons about it as if it were a year. This server clamps limits itself and states
the cap in the tool schema, so the model knows what it asked for.
**Rate limits are weighted, and the penalty is a ban.** Binance budgets 6000
request weight per minute per IP, not 6000 requests. `/exchangeInfo` costs 20.
`/depth` costs 5 for 100 levels and 25 for 101. Asking for 5000 levels costs 250,
so twenty-four of those calls exhaust the minute. Cross the line and you get
`HTTP 429`; ignore the 429 and you get `HTTP 418` and a temporary IP ban. A model
polling in a loop has no idea any of this is happening. This server reads the
`X-MBX-USED-WEIGHT-1M` header on every response and stops before the budget is
gone.
**Every failure looks the same.** A misspelled symbol, an exhausted rate limit and
a Binance outage all reach the model as "the tool failed". They call for three
different responses: fix the symbol, wait, and give up. This server returns a
typed error with a code and a `retriable` flag.
**The payloads are shaped for machines that already know the schema.** Numbers
arrive as strings, keys are camelCase, and the 24 hour ticker carries 21 fields.
The full `/exchangeInfo` document is 16.7 MB, which is not something to put near a
context window. This server renames, coerces and drops, and reduces the exchange
listing to the four fields search needs.
## Quickstart
Python 3.11 or newer. No API key, because nothing here needs one.
```bash
git clone https://github.com/iwaqaruddin/binance-mcp.git
cd binance-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```
Confirm the checkout is sound:
```bash
ruff check . && mypy && pytest -q
```
The tests mock every HTTP call, so that command works offline.
Start the server by hand to see it boot. It speaks MCP over stdio, so it will look
like it is hanging with no prompt. That is correct; press Ctrl-C to stop it.
```bash
binance-mcp
```
Then point a client at it. For Claude Desktop, add this to
`claude_desktop_config.json` and restart the app. Use the absolute path to the
executable inside the virtualenv; the config is not run through a shell, so `~`
and `$PATH` lookups do not apply.
```json
{
"mcpServers": {
"binance": {
"command": "/absolute/path/to/binance-mcp/.venv/bin/binance-mcp",
"env": {
"BINANCE_MCP_MARKET_TTL_SECONDS": "2",
"BINANCE_MCP_LOG_LEVEL": "INFO"
}
}
}
}
```
Configuration comes from the process environment. `.env.example` documents every
variable and its default; the server does not read a `.env` file itself, so set
values in the `env` block above or export them before launching. All of them have
working defaults, so the `env` block can be omitted entirely.
## The tools
| Tool | Arguments | Returns |
| --- | --- | --- |
| `get_ticker` | `symbol` | Last price and the 24 hour window: open, high, low, price change and percent, weighted average, best bid and ask with sizes, volume in both assets, trade count, window start and end. |
| `get_klines` | `symbol`, `interval` (enum, default `1h`), `limit` (default 100, capped at 500) | OHLCV candles, oldest first, as positional rows plus a `columns` list naming the nine fields. |
| `get_order_book` | `symbol`, `depth` (default 20, capped at 100) | A book snapshot: `last_update_id`, and `bids` and `asks` as price and quantity pairs. |
| `get_symbol_info` | `symbol` | Trading status, base and quote assets, precisions, permitted order types, and the tick size, lot step size and minimum notional as exact strings. |
| `search_symbols` | `query`, `limit` (default 10, capped at 25) | Ranked symbol matches with base asset, quote asset, status and a score, plus the number of symbols searched. |
`symbol` is case-insensitive and separators are stripped, so `btc/usdt` and
`BTCUSDT` are the same request. `interval` is one of `1s`, `1m`, `3m`, `5m`, `15m`,
`30m`, `1h`, `2h`, `4h`, `6h`, `8h`, `12h`, `1d`, `3d`, `1w`, `1M`.
Every failed call returns the same shape instead of a message:
```json
{
"error": "rate_limited",
"message": "Local request-weight budget is exhausted for this minute.",
"retriable": true,
"retry_after_seconds": 41.2,
"used_weight": 4802,
"weight_limit": 6000,
"remaining_weight": 1198
}
```
`error` is one of `invalid_symbol`, `invalid_argument`, `rate_limited`,
`upstream_unavailable` or `internal_error`.
## Design decisions
### Read-only by design
There is no order placement, no account data and no signing code. The rejected
alternative was one server with a `BINANCE_API_KEY` and a `--enable-trading` flag,
which is how most exchange integrations are built.
The trade-off is that this repo cannot grow into a trading agent. That is the
point. A server that can trade has to be trusted with prompt injection as a threat
model: any text the model reads becomes a possible instruction to sell. A server
with no key and no write path fails that attack by construction, and the security
argument is a one-line audit rather than a policy document. Anyone who wants
execution should build a separate server with its own approval path, and should
have to think about it separately.
### A TTL cache, not no cache and not Redis
Responses are held in a fixed-size in-process dictionary: two seconds for market
data, sixty for exchange metadata.
No cache at all was the first alternative. It loses because models re-ask. A model
comparing four pairs will often fetch the same ticker twice in one turn, and each
repeat spends weight on data that has not changed. The second alternative was
Redis, which buys a cache shared across processes and survives restarts. It loses
on operational cost: it turns a `pip install` into a service dependency, and a
market-data cache with a two-second lifetime has almost nothing to share. The
trade-off accepted is that every process keeps its own copy and a restart starts
cold. At this TTL, a cold start costs one request.
The market TTL is 2s rather than something longer because that is roughly the
window in which a stale price is still the same price for reasoning purposes. It is
configurable, and raising it is the correct move if several clients poll one symbol.
### Reading the weight header, not retrying on failure
Every response carries the weight spent in the current minute. The server tracks
that number, and once the configured share of the budget is spent it waits for the
window to roll over. If the wait is longer than `MAX_BACKOFF_SECONDS` it fails the
call instead and reports how much budget is left.
The alternative was retry with exponential backoff on 429, which is the default
answer for most HTTP clients. It is the wrong answer here. Binance escalates
repeated 429s from one IP into an `HTTP 418` ban, so a retry loop converts a
recoverable throttle into an outage that outlives the process. This server never
retries a 429. It treats one as evidence the budget is gone and blocks the next
call locally.
Two trade-offs come with this. A tool call can block for up to five seconds before
returning, which is visible latency inside a client's timeout. And the local
estimate can drift, because other processes on the same IP spend from the same
budget without this server seeing it. The header corrects the count on every
response, so drift lasts one request. Setting `BINANCE_MCP_WEIGHT_LIMIT` below 6000
is the fix when the IP is genuinely shared.
### No indicator calculation
No moving averages, no RSI, no spread, not even a mid price from the order book
this server just fetched.
The alternative is a `get_rsi` tool, and it is the single most requested feature of
servers like this. It loses on two counts. Every indicator has parameters and
conventions — Wilder smoothing or simple, close or typical price, how to seed the
first value — and a tool that picks silently produces numbers the caller cannot
reconcile with their charting platform. Worse, it moves the analysis inside an
opaque function: the model reports RSI 71 and neither it nor the user can see which
candles produced it. Serving the candles keeps the arithmetic in the transcript
where it can be checked.
The trade-off is real and it is token cost. A model computing a 200-period moving
average has to read 200 candles to do it. That is the price of an auditable number,
and it is why the next decision exists.
### Candles as rows, not objects
`get_klines` returns `[[1700000000000, 67000.1, ...], ...]` with a `columns` list,
rather than a list of objects with nine named keys each.
The alternative is self-describing objects, which are easier to read in a log and
need no lookup. It loses on arithmetic: 500 candles as objects spends roughly a
third of its tokens repeating the same nine keys, and context is the scarce
resource in a tool that exists to feed a model. The trade-off is that a row is
meaningless without its header, so the header ships in the same response and the
output schema pins the type of every position.
This is the one place where the wire format is optimised for the consumer rather
than for readability, and it is contained to a single tool.
### Floats for prices, strings for tick sizes
Prices, volumes and percentages are converted to JSON numbers. Tick size, lot step
size and minimum notional stay strings, exactly as Binance published them.
Converting everything to numbers was the alternative, and it is more consistent.
It loses where exactness is load-bearing. A tick size of `0.01000000` describes a
decimal grid, and a value that survives a float round-trip a few times stops
landing on that grid; an order priced off a drifted tick size is rejected by the
exchange or, worse, quietly rounded. Leaving everything as strings was the other
alternative, and it loses because it makes the model parse before it can compare,
which it does inconsistently.
The trade-off accepted: a quote volume above roughly 10^15 loses its last digits to
float representation. No spot pair is close, and the failure is a rounding error in
a number nobody trades on, rather than a rejected order.
## When you should not use this
**You are polling faster than once every couple of seconds.** The cache returns
identical data within its TTL, so a tighter loop gets the same answer while still
paying MCP round-trip cost. If you need every tick, you need the websocket streams
and this server does not have them.
**You need sub-second freshness.** A cached ticker can be two seconds old, an order
book snapshot is stale the moment it is serialised, and the request itself takes
tens of milliseconds. Do not build anything that acts on a price this returns.
Market making, arbitrage, and stop placement all need a direct websocket feed and a
clock you control.
**You need private account data.** Balances, open orders, fills, positions and
funding history all require a signed request. There is no code path here that can
produce one.
**You want the model to execute.** No tool here places, modifies or cancels an
order, and adding one is out of scope rather than not yet done.
**You need the full order book.** Depth is capped at 100 levels per side, which is
a slice of the book. Reconstructing a full book means the depth snapshot endpoint
plus the diff stream, which is a different program.
**You are running this in a multi-tenant service.** The weight budget is tracked
per process, and the exchange enforces it per IP. Several instances behind one
egress IP will oversubscribe the budget until a 429 arrives. Lowering
`BINANCE_MCP_WEIGHT_LIMIT` divides the budget but does not coordinate it.
## Limitations
The cache does not deduplicate concurrent identical requests. Two tool calls for
the same symbol arriving in the same event-loop tick both reach the network, and
the second populates a cache entry the first already wrote.
There is no request coalescing or coordination across processes, for the reason in
the section above.
Spot only. Futures, options and margin endpoints live on different hosts with
different weight budgets and are not covered.
`search_symbols` matches ticker strings, not names. A query for `bitcoin` finds
nothing; `btc` finds everything. Ranking prefers pairs quoted in the more liquid
stablecoins, which is a judgement call about the current spot market rather than a
property of the data.
Klines are the most recent N candles. There is no start or end time argument, so
historical windows that do not end at the present cannot be requested.
The weight table for each endpoint is hardcoded. If Binance repriced an endpoint,
the first request of a minute would be mis-estimated. The response header corrects
it immediately, so the consequence is one optimistic request rather than a ban.
## Roadmap
Single-flight deduplication of in-flight identical requests, which is the cheapest
remaining weight saving.
A `start_time` and `end_time` pair on `get_klines`, so historical windows can be
requested without walking backwards.
Optional `--transport http` so one server process can back several clients, which
also makes a shared weight budget tractable.
Testnet documentation. `BINANCE_MCP_BASE_URL` already points at
`testnet.binance.vision` if you set it, but nothing here verifies that the testnet
symbol list behaves the same way.
## Licence
MIT. See [LICENSE](LICENSE).
TDQS
Scored across 5 tools
Each tool targets a clearly distinct data surface: current statistics, historical candles, order book depth, symbol metadata, and symbol resolution. There is no meaningful overlap or ambiguity between them.
All tools follow a consistent get_ or search_ pattern with clear resource nouns: get_ticker, get_klines, get_order_book, get_symbol_info, search_symbols. No mixed conventions or vague verbs.
Five tools is well-scoped for a read-only Binance market data server. Each tool provides a distinct, necessary capability without unnecessary bloat or thinness.
The tool surface covers the core public market data needs: symbol discovery, trading rules, current pricing, historical candles, and order book depth. There are no obvious gaps for the apparent read-only market data scope.