binance-mcp

binance-mcp

A Model Context Protocol server that gives an LLM read-only access to Binance spot market data, allowing models to reason about real prices without the ability to place orders.

Category
Visit Server

README

binance-mcp

CI License: MIT

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.

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:

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.

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.

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

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

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured