Skip to main content
Glama
binance_mcp_server.py10.5 kB
#!/usr/bin/env python3 """ Lightweight MCP-like stdio server for interacting with the Binance REST API. The server reads JSON-RPC 2.0 requests from stdin and writes responses to stdout. It focuses on a small tool-set that is useful for agents: - get_account: signed account snapshot - get_open_orders: signed open orders (optionally filtered by symbol) - get_trades: signed recent trades for a symbol - place_order: signed market/limit order placement (or test orders) - get_candles: public kline/candlestick data Authentication is pulled from BINANCE_API_KEY and BINANCE_API_SECRET environment variables unless explicitly provided in the request. """ import hmac import hashlib import json import os import sys import time import urllib.error import urllib.parse import urllib.request from typing import Any, Dict, Optional class BinanceClient: """Minimal Binance REST client with signed request support.""" def __init__( self, api_key: Optional[str] = None, api_secret: Optional[str] = None, base_url: str = "https://api.binance.com", allow_public: bool = False, ) -> None: self.api_key = api_key or os.environ.get("BINANCE_API_KEY") self.api_secret = api_secret or os.environ.get("BINANCE_API_SECRET") self.base_url = base_url.rstrip("/") self.allow_public = allow_public if not self.api_key or not self.api_secret: if not self.allow_public: raise ValueError("BINANCE_API_KEY and BINANCE_API_SECRET are required") def _sign(self, params: Dict[str, Any]) -> Dict[str, Any]: if not self.api_secret: raise ValueError("Signing requires BINANCE_API_SECRET") params = {k: v for k, v in params.items() if v is not None} params["timestamp"] = int(time.time() * 1000) query = urllib.parse.urlencode(params, doseq=True) signature = hmac.new( self.api_secret.encode("utf-8"), query.encode("utf-8"), hashlib.sha256 ).hexdigest() params["signature"] = signature return params def _request( self, method: str, path: str, params: Optional[Dict[str, Any]] = None, signed: bool = False ) -> Any: params = params or {} if signed and not self.api_key: raise ValueError("Signed request requires BINANCE_API_KEY") encoded_params = self._sign(params) if signed else {k: v for k, v in params.items() if v is not None} query = urllib.parse.urlencode(encoded_params, doseq=True) url = f"{self.base_url}{path}" data = None if method.upper() == "GET": if query: url = f"{url}?{query}" else: data = query.encode("utf-8") request = urllib.request.Request(url=url, data=data, method=method.upper()) request.add_header("User-Agent", "binance-mcp-server/1.0") if signed: request.add_header("X-MBX-APIKEY", self.api_key) request.add_header("Content-Type", "application/x-www-form-urlencoded") try: with urllib.request.urlopen(request, timeout=15) as resp: content_type = resp.headers.get("Content-Type", "") payload = resp.read() if "application/json" in content_type or payload.startswith(b"{") or payload.startswith(b"["): return json.loads(payload.decode("utf-8")) return payload.decode("utf-8") except urllib.error.HTTPError as exc: error_body = exc.read().decode("utf-8") try: parsed = json.loads(error_body) except Exception: parsed = {"msg": error_body} raise RuntimeError({"status": exc.code, "payload": parsed}) from exc except urllib.error.URLError as exc: raise RuntimeError({"status": "network_error", "payload": str(exc)}) from exc def get_account(self, recv_window: Optional[int] = None) -> Any: return self._request("GET", "/api/v3/account", {"recvWindow": recv_window}, signed=True) def get_open_orders(self, symbol: Optional[str] = None, recv_window: Optional[int] = None) -> Any: return self._request( "GET", "/api/v3/openOrders", {"symbol": symbol, "recvWindow": recv_window}, signed=True ) def get_trades( self, symbol: str, limit: Optional[int] = None, recv_window: Optional[int] = None ) -> Any: return self._request( "GET", "/api/v3/myTrades", {"symbol": symbol, "limit": limit, "recvWindow": recv_window}, signed=True, ) def place_order( self, symbol: str, side: str, order_type: str, quantity: Optional[float] = None, price: Optional[float] = None, time_in_force: Optional[str] = None, quote_order_qty: Optional[float] = None, recv_window: Optional[int] = None, test: bool = False, ) -> Any: payload: Dict[str, Any] = { "symbol": symbol.upper(), "side": side.upper(), "type": order_type.upper(), "quantity": quantity, "quoteOrderQty": quote_order_qty, "price": price, "timeInForce": time_in_force, "recvWindow": recv_window, } path = "/api/v3/order/test" if test else "/api/v3/order" return self._request("POST", path, payload, signed=True) def get_candles( self, symbol: str, interval: str, limit: Optional[int] = None, start_time: Optional[int] = None, end_time: Optional[int] = None, ) -> Any: params = { "symbol": symbol.upper(), "interval": interval, "limit": limit, "startTime": start_time, "endTime": end_time, } return self._request("GET", "/api/v3/klines", params, signed=False) class StdioMCPServer: """Tiny JSON-RPC 2.0 server over stdio focused on Binance operations.""" def __init__(self) -> None: pass def _dispatch(self, request: Dict[str, Any]) -> Dict[str, Any]: request_id = request.get("id") method = request.get("method") params = request.get("params") or {} try: result = self._handle_method(method, params) return {"jsonrpc": "2.0", "id": request_id, "result": result} except Exception as exc: # pylint: disable=broad-except error_payload = exc.args[0] if exc.args else str(exc) return {"jsonrpc": "2.0", "id": request_id, "error": {"code": -32000, "message": error_payload}} def _handle_method(self, method: str, params: Dict[str, Any]) -> Any: if method == "ping": return {"pong": True, "time": int(time.time() * 1000)} if method == "get_account": client = self._client_from_params(params) return client.get_account(recv_window=params.get("recvWindow")) if method == "get_open_orders": client = self._client_from_params(params) return client.get_open_orders( symbol=params.get("symbol"), recv_window=params.get("recvWindow") ) if method == "get_trades": client = self._client_from_params(params) symbol = params.get("symbol") if not symbol: raise ValueError("symbol is required for get_trades") return client.get_trades( symbol=symbol, limit=params.get("limit"), recv_window=params.get("recvWindow"), ) if method == "place_order": client = self._client_from_params(params) symbol = params.get("symbol") side = params.get("side") order_type = params.get("type") if not symbol or not side or not order_type: raise ValueError("symbol, side, and type are required for place_order") return client.place_order( symbol=symbol, side=side, order_type=order_type, quantity=params.get("quantity"), quote_order_qty=params.get("quoteOrderQty"), price=params.get("price"), time_in_force=params.get("timeInForce"), recv_window=params.get("recvWindow"), test=bool(params.get("test", False)), ) if method == "get_candles": symbol = params.get("symbol") interval = params.get("interval") if not symbol or not interval: raise ValueError("symbol and interval are required for get_candles") client = self._client_from_params(params, allow_missing_keys=True) return client.get_candles( symbol=symbol, interval=interval, limit=params.get("limit"), start_time=params.get("startTime"), end_time=params.get("endTime"), ) raise ValueError(f"Unknown method: {method}") @staticmethod def _client_from_params(params: Dict[str, Any], allow_missing_keys: bool = False) -> BinanceClient: api_key = params.get("apiKey") or os.environ.get("BINANCE_API_KEY") api_secret = params.get("apiSecret") or os.environ.get("BINANCE_API_SECRET") base_url = params.get("baseUrl") or "https://api.binance.com" return BinanceClient( api_key=api_key, api_secret=api_secret, base_url=base_url, allow_public=allow_missing_keys, ) def serve(self) -> None: for line in sys.stdin: line = line.strip() if not line: continue try: payload = json.loads(line) except json.JSONDecodeError: self._write({"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "invalid JSON"}}) continue response = self._dispatch(payload) self._write(response) @staticmethod def _write(message: Dict[str, Any]) -> None: sys.stdout.write(json.dumps(message, separators=(",", ":")) + "\n") sys.stdout.flush() def main() -> None: server = StdioMCPServer() server.serve() if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/valerioey/binance-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server