# Directory Structure
```
├── .gitignore
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── setup.sh
├── src
│ ├── __tests__
│ │ └── binance-ws.test.ts
│ ├── config.ts
│ ├── connectors
│ │ ├── binance-rest.ts
│ │ └── binance-ws.ts
│ ├── index.ts
│ ├── types
│ │ ├── api-types.ts
│ │ ├── market-data.ts
│ │ └── ws-stream.ts
│ └── utils
│ └── logger.ts
├── tsconfig.json
└── tsconfig.test.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules/
build/
*.log
.env*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Binance MCP Server
A Model Context Protocol (MCP) server implementation for Binance market data with WebSocket support.
## Features
- Real-time market data streaming via WebSocket
- Support for both spot and futures markets
- Automatic reconnection with exponential backoff
- Type-safe message handling
- Comprehensive error handling
## Installation
```bash
npm install
```
## Usage
### Starting the Server
```bash
npm start
```
### WebSocket Stream Types
The following stream types are supported:
- `trade`: Real-time trade data
- `ticker`: 24-hour rolling window price change statistics
- `bookTicker`: Best bid/ask price and quantity
- `kline`: Candlestick data
- `markPrice`: Mark price and funding rate (futures only)
- `fundingRate`: Funding rate data (futures only)
### Example Usage in Claude Desktop
```typescript
// Subscribe to trade and ticker streams for BTC/USDT
await server.subscribe('BTCUSDT', 'spot', ['trade', 'ticker']);
// Handle incoming data
server.onStreamData('BTCUSDT', 'trade', (data) => {
console.log('New trade:', data);
});
```
## Development
### Running Tests
```bash
npm test
```
### Building
```bash
npm run build
```
## License
Private
```
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
```json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"],
"isolatedModules": false
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
testMatch: ['**/__tests__/**/*.test.ts'],
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: 'tsconfig.test.json'
}]
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
};
```
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
```typescript
import winston from 'winston';
export const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext", // Changed from ES2022
"moduleResolution": "NodeNext",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts", "build"]
}
```
--------------------------------------------------------------------------------
/src/types/market-data.ts:
--------------------------------------------------------------------------------
```typescript
export interface MarketData {
symbol: string;
exchange: string;
type: 'spot' | 'futures';
price: number;
timestamp: number;
volume24h: number;
volumeDelta24h?: number;
priceChange24h: number;
priceChange1h?: number;
price24hHigh: number;
price24hLow: number;
tradeCount24h: number;
bidAskSpread?: number;
openInterest?: number;
fundingRate?: number;
liquidations24h?: number;
}
export interface OrderBookData {
symbol: string;
type: 'spot' | 'futures';
bids: [number, number][];
asks: [number, number][];
timestamp: number;
lastUpdateId: number;
}
export interface TradeData {
symbol: string;
type: 'spot' | 'futures';
price: number;
quantity: number;
timestamp: number;
isBuyerMaker: boolean;
tradeId: number;
}
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
export const config = {
// Server config
NAME: 'binance-market-data',
VERSION: '1.0.0',
// REST endpoints
SPOT_REST_URL: 'https://api.binance.com/api/v3',
FUTURES_REST_URL: 'https://fapi.binance.com/fapi/v1',
// WebSocket endpoints
SPOT_WS_URL: 'wss://stream.binance.com:9443/ws',
FUTURES_WS_URL: 'wss://fstream.binance.com/ws',
// API credentials
API_KEY: process.env.BINANCE_API_KEY || '',
API_SECRET: process.env.BINANCE_API_SECRET || '',
// Constants
DEFAULT_ORDER_BOOK_LIMIT: 100,
DEFAULT_TRADE_LIMIT: 1000,
// Rate limits
SPOT_RATE_LIMIT: 1200,
FUTURES_RATE_LIMIT: 1200,
// WebSocket configurations
WS_PING_INTERVAL: 3 * 60 * 1000, // 3 minutes
WS_RECONNECT_DELAY: 5000, // 5 seconds
WS_CONNECTION_TIMEOUT: 10000, // 10 seconds
WS_MAX_RECONNECT_ATTEMPTS: 5,
// HTTP configurations
HTTP_TIMEOUT: 10000,
HTTP_MAX_RETRIES: 3,
HTTP_RETRY_DELAY: 1000,
ERRORS: {
RATE_LIMIT_EXCEEDED: 'Rate limit exceeded',
INVALID_SYMBOL: 'Invalid trading pair symbol',
WS_CONNECTION_ERROR: 'WebSocket connection error',
WS_SUBSCRIPTION_ERROR: 'WebSocket subscription error'
}
} as const;
export type Config = typeof config;
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "binance-mcp-server",
"version": "0.1.0",
"description": "Binance market data provider with WebSocket support",
"private": true,
"type": "module",
"bin": {
"binance-mcp-server": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "rimraf build && tsc -p tsconfig.json",
"postbuild": "chmod +x build/index.js",
"prepare": "npm run build",
"watch": "tsc --watch -p tsconfig.json",
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
"start": "node build/index.js",
"test": "jest --config=jest.config.js",
"test:watch": "jest --watch --config=jest.config.js",
"test:coverage": "jest --coverage --config=jest.config.js",
"type-check": "tsc --noEmit -p tsconfig.test.json",
"lint": "eslint 'src/**/*.{js,ts}'"
},
"dependencies": {
"@modelcontextprotocol/sdk": "0.6.0",
"@types/ws": "^8.5.10",
"axios": "^1.6.7",
"ws": "^8.16.0",
"winston": "^3.11.0"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.24",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"eslint": "^8.57.0",
"jest": "^29.7.0",
"rimraf": "^5.0.5",
"ts-jest": "^29.1.2",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/src/types/api-types.ts:
--------------------------------------------------------------------------------
```typescript
export interface MarketDataParams {
symbol: string;
type: 'spot' | 'futures';
}
export interface KlineParams {
symbol: string;
type: 'spot' | 'futures';
interval: string;
limit?: number;
}
export interface StreamParams {
symbol: string;
type: 'spot' | 'futures';
streams: string[];
}
export interface FuturesDataParams {
symbol: string;
}
export class APIError extends Error {
constructor(message: string, public readonly cause?: Error) {
super(message);
this.name = 'APIError';
}
}
// Type guards
export function isMarketDataParams(params: any): params is MarketDataParams {
return (
typeof params === 'object' &&
typeof params.symbol === 'string' &&
(params.type === 'spot' || params.type === 'futures')
);
}
export function isKlineParams(params: any): params is KlineParams {
return (
typeof params === 'object' &&
typeof params.symbol === 'string' &&
(params.type === 'spot' || params.type === 'futures') &&
typeof params.interval === 'string' &&
(params.limit === undefined || typeof params.limit === 'number')
);
}
export function isStreamParams(params: any): params is StreamParams {
return (
typeof params === 'object' &&
typeof params.symbol === 'string' &&
(params.type === 'spot' || params.type === 'futures') &&
Array.isArray(params.streams)
);
}
export function isFuturesDataParams(params: any): params is FuturesDataParams {
return (
typeof params === 'object' &&
typeof params.symbol === 'string'
);
}
```
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
# Check if we're in the correct directory (has package.json)
if [ ! -f "package.json" ]; then
echo -e "${RED}Error: package.json not found. Are you in the correct directory?${NC}"
exit 1
fi
# Function to create directory if it doesn't exist
create_dir() {
if [ ! -d "$1" ]; then
mkdir -p "$1"
echo -e "${GREEN}Created directory: $1${NC}"
else
echo "Directory already exists: $1"
fi
}
# Function to create file if it doesn't exist
create_file() {
if [ ! -f "$1" ]; then
touch "$1"
# Add basic export statement to TypeScript files
if [[ $1 == *.ts ]]; then
echo "// $1" > "$1"
echo "export {}" >> "$1"
fi
echo -e "${GREEN}Created file: $1${NC}"
else
echo "File already exists: $1"
fi
}
# Create directory structure
create_dir "src/connectors"
create_dir "src/types"
create_dir "src/utils"
# Create files
create_file "src/connectors/binance-rest.ts"
create_file "src/connectors/binance-ws.ts"
create_file "src/types/market-data.ts"
create_file "src/types/api-types.ts"
create_file "src/utils/logger.ts"
create_file "src/config.ts"
echo -e "\n${GREEN}Directory structure created successfully!${NC}"
echo -e "Next steps:"
echo "1. Implement the TypeScript interfaces in types/"
echo "2. Set up the configuration in config.ts"
echo "3. Implement the connectors in connectors/"
echo "4. Set up logging in utils/logger.ts"
echo "5. Update main server implementation in index.ts"
# Print final directory tree
echo -e "\nFinal directory structure:"
tree
```
--------------------------------------------------------------------------------
/src/types/ws-stream.ts:
--------------------------------------------------------------------------------
```typescript
export type StreamEventType =
| 'trade'
| 'ticker'
| 'bookTicker'
| 'kline'
| 'depth'
| 'forceOrder' // Liquidation orders
| 'markPrice' // Mark price and funding rate
| 'openInterest'; // Open interest updates
export interface WebSocketMessage<T> {
stream: string;
data: T;
timestamp: number;
}
export type StreamEventData =
| TradeData
| TickerData
| BookTickerData
| KlineData
| DepthData
| ForceOrderData
| MarkPriceData
| OpenInterestData;
// Existing interfaces
export interface TradeData {
e: 'trade';
E: number;
s: string;
t: number;
p: string;
q: string;
b: number;
a: number;
T: number;
m: boolean;
}
export interface TickerData {
e: '24hrTicker';
E: number;
s: string;
p: string;
P: string;
w: string;
c: string;
Q: string;
o: string;
h: string;
l: string;
v: string;
q: string;
}
export interface BookTickerData {
e: 'bookTicker';
u: number;
s: string;
b: string;
B: string;
a: string;
A: string;
}
export interface KlineData {
e: 'kline';
E: number;
s: string;
k: {
t: number;
T: number;
s: string;
i: string;
f: number;
L: number;
o: string;
c: string;
h: string;
l: string;
v: string;
n: number;
x: boolean;
q: string;
V: string;
Q: string;
};
}
export interface DepthData {
e: 'depthUpdate';
E: number;
s: string;
U: number;
u: number;
b: [string, string][];
a: [string, string][];
}
// New Futures-specific interfaces
export interface ForceOrderData {
e: 'forceOrder';
E: number; // Event time
o: {
s: string; // Symbol
S: 'SELL' | 'BUY'; // Side
o: 'LIMIT'; // Order type
f: 'IOC'; // Time in force
q: string; // Original quantity
p: string; // Price
ap: string; // Average price
X: 'FILLED'; // Order status
l: string; // Last filled quantity
z: string; // Cumulative filled quantity
T: number; // Trade time
};
}
export interface MarkPriceData {
e: 'markPriceUpdate';
E: number; // Event time
s: string; // Symbol
p: string; // Mark price
i: string; // Index price
P: string; // Estimated settle price
r: string; // Funding rate
T: number; // Next funding time
}
export interface OpenInterestData {
e: 'openInterest';
E: number; // Event time
s: string; // Symbol
o: string; // Open interest
T: number; // Transaction time
}
```
--------------------------------------------------------------------------------
/src/__tests__/binance-ws.test.ts:
--------------------------------------------------------------------------------
```typescript
import { BinanceWebSocketManager } from '../connectors/binance-ws';
import WebSocket from 'ws';
import { StreamEventData, TradeData } from '../types/ws-stream';
jest.mock('ws');
type MockWebSocket = jest.Mocked<WebSocket> & {
readyState: number;
};
describe('BinanceWebSocketManager', () => {
let wsManager: BinanceWebSocketManager;
let mockWs: MockWebSocket;
beforeEach(() => {
wsManager = new BinanceWebSocketManager();
mockWs = {
readyState: WebSocket.CONNECTING,
on: jest.fn(),
once: jest.fn(),
emit: jest.fn(),
close: jest.fn(),
ping: jest.fn(),
send: jest.fn(),
terminate: jest.fn(),
removeListener: jest.fn(),
removeAllListeners: jest.fn(),
setMaxListeners: jest.fn(),
getMaxListeners: jest.fn(),
listeners: jest.fn(),
rawListeners: jest.fn(),
listenerCount: jest.fn(),
eventNames: jest.fn(),
addListener: jest.fn(),
off: jest.fn(),
prependListener: jest.fn(),
prependOnceListener: jest.fn(),
} as unknown as MockWebSocket;
(WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs);
});
afterEach(() => {
wsManager.close();
jest.clearAllMocks();
});
it('should successfully subscribe to stream', () => {
const symbol = 'BTCUSDT';
const streams = ['trade', 'ticker'] as const;
wsManager.subscribe(symbol, 'spot', streams);
expect(WebSocket).toHaveBeenCalledWith(
expect.stringContaining(`btcusdt@trade/btcusdt@ticker`)
);
});
it('should handle incoming messages correctly', (done) => {
const symbol = 'BTCUSDT';
const mockData = {
stream: 'btcusdt@trade',
data: {
e: 'trade',
E: 123456789,
s: 'BTCUSDT',
p: '50000.00',
q: '1.0'
} as TradeData
};
wsManager.subscribe(symbol, 'spot', ['trade']);
wsManager.onStreamData(symbol, 'trade', (data: StreamEventData) => {
expect(data).toEqual(mockData.data);
done();
});
// Simulate receiving a message
mockWs.emit('message', JSON.stringify(mockData));
});
it('should handle reconnection on connection close', () => {
const symbol = 'BTCUSDT';
wsManager.subscribe(symbol, 'spot', ['trade']);
// Simulate connection close
mockWs.emit('close');
// Verify that a new connection attempt is made
expect(WebSocket).toHaveBeenCalledTimes(2);
});
it('should clean up resources on unsubscribe', () => {
const symbol = 'BTCUSDT';
wsManager.subscribe(symbol, 'spot', ['trade']);
wsManager.unsubscribe(symbol);
expect(mockWs.close).toHaveBeenCalled();
});
it('should handle multiple stream subscriptions', () => {
const symbol = 'BTCUSDT';
const streams = ['trade', 'ticker', 'bookTicker'] as const;
wsManager.subscribe(symbol, 'spot', streams);
expect(WebSocket).toHaveBeenCalledWith(
expect.stringContaining('btcusdt@trade/btcusdt@ticker/btcusdt@bookTicker')
);
});
it('should properly maintain connection state', () => {
const symbol = 'BTCUSDT';
wsManager.subscribe(symbol, 'spot', ['trade']);
// Update mockWs.readyState which is now properly typed
mockWs.readyState = WebSocket.OPEN;
mockWs.emit('open');
expect(wsManager.getConnectionState(symbol)).toBe(WebSocket.OPEN);
});
});
```
--------------------------------------------------------------------------------
/src/connectors/binance-ws.ts:
--------------------------------------------------------------------------------
```typescript
import WebSocket from 'ws';
import { config } from '../config.js';
import { logger } from '../utils/logger.js';
import {
WebSocketMessage,
StreamEventType,
StreamEventData,
TradeData,
TickerData,
BookTickerData,
KlineData,
ForceOrderData,
MarkPriceData,
OpenInterestData
} from '../types/ws-stream.js';
type WSReadyState = number;
interface StreamSubscription {
symbol: string;
type: 'spot' | 'futures';
streams: StreamEventType[];
reconnectAttempts: number;
reconnectTimeout?: NodeJS.Timeout;
}
type MessageHandler = (data: StreamEventData) => void;
export class BinanceWebSocketManager {
private connections: Map<string, WebSocket>;
private pingIntervals: Map<string, NodeJS.Timeout>;
private messageCallbacks: Map<string, Map<StreamEventType, MessageHandler[]>>;
private subscriptions: Map<string, StreamSubscription>;
private readonly MAX_RECONNECT_ATTEMPTS = 5;
private readonly RECONNECT_DELAY = config.WS_RECONNECT_DELAY || 5000;
constructor() {
this.connections = new Map();
this.pingIntervals = new Map();
this.messageCallbacks = new Map();
this.subscriptions = new Map();
}
public subscribe(symbol: string, type: 'spot' | 'futures', streams: StreamEventType[]): void {
const subscription: StreamSubscription = {
symbol,
type,
streams,
reconnectAttempts: 0
};
if (!this.messageCallbacks.has(symbol)) {
this.messageCallbacks.set(symbol, new Map());
}
const symbolCallbacks = this.messageCallbacks.get(symbol)!;
streams.forEach(stream => {
if (!symbolCallbacks.has(stream)) {
symbolCallbacks.set(stream, []);
}
});
this.subscriptions.set(symbol, subscription);
this.connectWebSocket(subscription);
}
private connectWebSocket(subscription: StreamSubscription): void {
const { symbol, type, streams } = subscription;
const wsUrl = type === 'spot' ? config.SPOT_WS_URL : config.FUTURES_WS_URL;
// Handle special futures streams
const streamNames = streams.map(stream => {
if (type === 'futures') {
switch (stream) {
case 'forceOrder':
return `${symbol.toLowerCase()}@forceOrder`;
case 'markPrice':
return `${symbol.toLowerCase()}@markPrice@1s`; // 1s update frequency
case 'openInterest':
return `${symbol.toLowerCase()}@openInterest@1s`;
default:
return `${symbol.toLowerCase()}@${stream}`;
}
}
return `${symbol.toLowerCase()}@${stream}`;
});
try {
const ws = new WebSocket(`${wsUrl}/${streamNames.join('/')}`);
ws.on('open', () => {
logger.info(`WebSocket connected for ${symbol} ${streams.join(', ')}`);
subscription.reconnectAttempts = 0;
this.setupPingInterval(symbol, ws);
});
ws.on('message', (data: WebSocket.Data) => {
try {
const message = JSON.parse(data.toString()) as WebSocketMessage<StreamEventData>;
this.handleStreamMessage(symbol, message);
} catch (error) {
logger.error('Error parsing WebSocket message:', error);
}
});
ws.on('error', (error: Error) => {
logger.error(`WebSocket error for ${symbol}:`, error);
});
ws.on('close', () => {
logger.info(`WebSocket closed for ${symbol}`);
this.cleanup(symbol);
this.handleReconnection(subscription);
});
ws.on('pong', () => {
logger.debug(`Received pong from ${symbol} WebSocket`);
});
this.connections.set(symbol, ws);
} catch (error) {
logger.error(`Error creating WebSocket connection for ${symbol}:`, error);
this.handleReconnection(subscription);
throw error;
}
}
private handleStreamMessage(symbol: string, message: WebSocketMessage<StreamEventData>): void {
const symbolCallbacks = this.messageCallbacks.get(symbol);
if (!symbolCallbacks) return;
// Extract stream type from the stream name
const streamParts = message.stream.split('@');
if (streamParts.length < 2) return;
let streamType = streamParts[1] as StreamEventType;
// Handle special cases where the stream name has additional parts (e.g., markPrice@1s)
if (streamParts.length > 2) {
streamType = streamParts[1].split('@')[0] as StreamEventType;
}
const handlers = symbolCallbacks.get(streamType);
if (handlers) {
handlers.forEach(handler => {
try {
handler(message.data);
} catch (error) {
logger.error(`Error in message handler for ${symbol} ${streamType}:`, error);
}
});
}
}
public onStreamData(symbol: string, streamType: StreamEventType, handler: MessageHandler): void {
const symbolCallbacks = this.messageCallbacks.get(symbol);
if (!symbolCallbacks) {
logger.error(`No callbacks registered for symbol ${symbol}`);
return;
}
const handlers = symbolCallbacks.get(streamType) || [];
handlers.push(handler);
symbolCallbacks.set(streamType, handlers);
}
private handleReconnection(subscription: StreamSubscription): void {
const { symbol, reconnectAttempts } = subscription;
if (reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
logger.error(`Max reconnection attempts reached for ${symbol}`);
return;
}
subscription.reconnectAttempts++;
const delay = this.RECONNECT_DELAY * Math.pow(2, reconnectAttempts - 1); // Exponential backoff
logger.info(`Attempting to reconnect ${symbol} in ${delay}ms (attempt ${reconnectAttempts})`);
subscription.reconnectTimeout = setTimeout(() => {
this.connectWebSocket(subscription);
}, delay);
}
private setupPingInterval(symbol: string, ws: WebSocket): void {
const interval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping((error?: Error) => {
if (error) {
logger.error(`Error sending ping for ${symbol}:`, error);
}
});
}
}, config.WS_PING_INTERVAL);
this.pingIntervals.set(symbol, interval);
}
private cleanup(symbol: string): void {
const interval = this.pingIntervals.get(symbol);
if (interval) {
clearInterval(interval);
this.pingIntervals.delete(symbol);
}
const subscription = this.subscriptions.get(symbol);
if (subscription?.reconnectTimeout) {
clearTimeout(subscription.reconnectTimeout);
}
this.connections.delete(symbol);
}
public unsubscribe(symbol: string): void {
const ws = this.connections.get(symbol);
if (ws) {
ws.close();
}
this.cleanup(symbol);
this.subscriptions.delete(symbol);
this.messageCallbacks.delete(symbol);
}
public close(): void {
this.connections.forEach((ws, symbol) => {
ws.close();
this.cleanup(symbol);
});
this.subscriptions.clear();
this.messageCallbacks.clear();
}
public getConnectionState(symbol: string): WSReadyState | undefined {
const ws = this.connections.get(symbol);
return ws?.readyState;
}
public isSubscribed(symbol: string, streamType: StreamEventType): boolean {
const subscription = this.subscriptions.get(symbol);
return subscription?.streams.includes(streamType) || false;
}
}
```
--------------------------------------------------------------------------------
/src/connectors/binance-rest.ts:
--------------------------------------------------------------------------------
```typescript
import axios, { AxiosInstance } from 'axios';
import { config } from '../config.js';
import { logger } from '../utils/logger.js';
import { APIError } from '../types/api-types.js';
export class BinanceRestConnector {
private readonly axiosInstance: AxiosInstance;
private readonly retryDelay = config.HTTP_RETRY_DELAY;
private readonly maxRetries = config.HTTP_MAX_RETRIES;
constructor() {
this.axiosInstance = axios.create({
timeout: config.HTTP_TIMEOUT,
headers: {
'Content-Type': 'application/json'
}
});
logger.info('BinanceRestConnector initialized');
logger.info(`Futures REST URL: ${config.FUTURES_REST_URL}`);
}
private async executeWithRetry<T>(operation: () => Promise<T>, retries = 0): Promise<T> {
try {
return await operation();
} catch (error) {
if (retries >= this.maxRetries) {
throw error;
}
const delay = this.retryDelay * Math.pow(2, retries);
logger.warn(`Request failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.executeWithRetry(operation, retries + 1);
}
}
public async getMarketData(symbol: string, type: 'spot' | 'futures'): Promise<any> {
try {
logger.info(`Getting ${type} market data for ${symbol}`);
if (type === 'spot') {
const data = await this.executeWithRetry(() =>
this.axiosInstance.get(`${config.SPOT_REST_URL}/ticker/24hr`, {
params: { symbol: symbol.toUpperCase() }
}).then(response => response.data)
);
logger.info('Successfully fetched spot market data');
return data;
}
// For futures, fetch all relevant data in parallel
logger.info('Fetching futures data from multiple endpoints...');
try {
const [
marketData,
openInterest,
fundingData,
liquidations
] = await Promise.all([
// Basic market data
this.executeWithRetry(() =>
this.axiosInstance.get(`${config.FUTURES_REST_URL}/ticker/24hr`, {
params: { symbol: symbol.toUpperCase() }
}).then(response => {
logger.info('Successfully fetched futures ticker data');
return response.data;
})
),
// Open interest
this.executeWithRetry(() =>
this.axiosInstance.get(`${config.FUTURES_REST_URL}/openInterest`, {
params: { symbol: symbol.toUpperCase() }
}).then(response => {
logger.info('Successfully fetched open interest data');
return response.data;
})
),
// Premium index (funding rate)
this.executeWithRetry(() =>
this.axiosInstance.get(`${config.FUTURES_REST_URL}/premiumIndex`, {
params: {
symbol: symbol.toUpperCase()
}
}).then(response => {
logger.info('Successfully fetched funding rate data');
return response.data;
})
),
// Recent liquidations
this.executeWithRetry(() =>
this.axiosInstance.get(`${config.FUTURES_REST_URL}/forceOrders`, {
params: {
symbol: symbol.toUpperCase(),
startTime: Date.now() - 24 * 60 * 60 * 1000,
limit: 100
}
}).then(response => {
logger.info('Successfully fetched liquidations data');
return response.data;
})
)
]);
logger.info('Successfully fetched all futures data, combining responses...');
// Combine all futures data with correct field mappings
const combinedData = {
...marketData,
openInterest: openInterest.openInterest,
fundingRate: fundingData.lastFundingRate,
markPrice: fundingData.markPrice,
nextFundingTime: fundingData.nextFundingTime,
liquidations24h: liquidations.length,
liquidationVolume24h: liquidations.reduce((sum: number, order: any) =>
sum + parseFloat(order.executedQty), 0
)
};
logger.info('Successfully combined futures data');
return combinedData;
} catch (error) {
logger.error('Error in futures data Promise.all:', error);
throw error;
}
} catch (error) {
logger.error('Error fetching market data:', error);
throw new APIError('Failed to fetch market data', error as Error);
}
}
public async getFuturesOpenInterest(symbol: string): Promise<any> {
try {
logger.info(`Getting futures open interest for ${symbol}`);
const response = await this.executeWithRetry(() =>
this.axiosInstance.get(`${config.FUTURES_REST_URL}/openInterest`, {
params: { symbol: symbol.toUpperCase() }
})
);
logger.info('Successfully fetched open interest data');
return response.data;
} catch (error) {
logger.error('Error fetching open interest:', error);
throw new APIError('Failed to fetch open interest data', error as Error);
}
}
public async getFuturesFundingRate(symbol: string): Promise<any> {
try {
logger.info(`Getting futures funding rate for ${symbol}`);
const response = await this.executeWithRetry(() =>
this.axiosInstance.get(`${config.FUTURES_REST_URL}/premiumIndex`, {
params: {
symbol: symbol.toUpperCase()
}
})
);
logger.info('Successfully fetched funding rate data');
return response.data;
} catch (error) {
logger.error('Error fetching funding rate:', error);
throw new APIError('Failed to fetch funding rate data', error as Error);
}
}
public async getFuturesLiquidations(symbol: string): Promise<any> {
try {
logger.info(`Getting futures liquidations for ${symbol}`);
const response = await this.executeWithRetry(() =>
this.axiosInstance.get(`${config.FUTURES_REST_URL}/forceOrders`, {
params: {
symbol: symbol.toUpperCase(),
startTime: Date.now() - 24 * 60 * 60 * 1000,
limit: 1000
}
})
);
logger.info('Successfully fetched liquidations data');
return response.data;
} catch (error) {
logger.error('Error fetching liquidations:', error);
throw new APIError('Failed to fetch liquidations data', error as Error);
}
}
public async getKlines(
symbol: string,
type: 'spot' | 'futures',
interval: string,
limit?: number
): Promise<any> {
try {
logger.info(`Getting ${type} klines for ${symbol}`);
const baseUrl = type === 'spot' ? config.SPOT_REST_URL : config.FUTURES_REST_URL;
const response = await this.executeWithRetry(() =>
this.axiosInstance.get(`${baseUrl}/klines`, {
params: {
symbol: symbol.toUpperCase(),
interval,
limit: limit || 500
}
})
);
logger.info('Successfully fetched klines data');
return response.data;
} catch (error) {
logger.error('Error fetching klines:', error);
throw new APIError('Failed to fetch klines data', error as Error);
}
}
public async getExchangeInfo(type: 'spot' | 'futures'): Promise<any> {
try {
logger.info(`Getting ${type} exchange info`);
const baseUrl = type === 'spot' ? config.SPOT_REST_URL : config.FUTURES_REST_URL;
const response = await this.executeWithRetry(() =>
this.axiosInstance.get(`${baseUrl}/exchangeInfo`)
);
logger.info('Successfully fetched exchange info');
return response.data;
} catch (error) {
logger.error('Error fetching exchange info:', error);
throw new APIError('Failed to fetch exchange info', error as Error);
}
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { BinanceWebSocketManager } from './connectors/binance-ws.js';
import { BinanceRestConnector } from './connectors/binance-rest.js';
import { config } from './config.js';
import { logger } from './utils/logger.js';
import {
MarketDataParams,
KlineParams,
StreamParams,
FuturesDataParams,
APIError,
isMarketDataParams,
isKlineParams,
isStreamParams,
isFuturesDataParams
} from './types/api-types.js';
import { StreamEventData } from './types/ws-stream.js';
const wsManager = new BinanceWebSocketManager();
const restConnector = new BinanceRestConnector();
const server = new Server(
{
name: config.NAME,
version: config.VERSION,
description: 'Binance market data provider with WebSocket support'
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_market_data",
description: "Get comprehensive market data for a trading pair",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description: "Trading pair symbol (e.g., BTCUSDT)"
},
type: {
type: "string",
enum: ["spot", "futures"],
description: "Market type"
}
},
required: ["symbol", "type"]
}
},
{
name: "test_futures_endpoints",
description: "Test individual futures endpoints",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description: "Trading pair symbol (e.g., BTCUSDT)"
}
},
required: ["symbol"]
}
},
{
name: "get_futures_open_interest",
description: "Get current open interest for a futures trading pair",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description: "Trading pair symbol (e.g., BTCUSDT)"
}
},
required: ["symbol"]
}
},
{
name: "get_futures_funding_rate",
description: "Get current funding rate for a futures trading pair",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description: "Trading pair symbol (e.g., BTCUSDT)"
}
},
required: ["symbol"]
}
},
{
name: "get_klines",
description: "Get historical candlestick data",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description: "Trading pair symbol (e.g., BTCUSDT)"
},
type: {
type: "string",
enum: ["spot", "futures"],
description: "Market type"
},
interval: {
type: "string",
enum: ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1M"],
description: "Kline/candlestick chart interval"
},
limit: {
type: "number",
description: "Number of klines to retrieve (default 500, max 1000)"
}
},
required: ["symbol", "type", "interval"]
}
},
{
name: "subscribe_market_data",
description: "Subscribe to real-time market data updates",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description: "Trading pair symbol (e.g., BTCUSDT)"
},
type: {
type: "string",
enum: ["spot", "futures"],
description: "Market type"
},
streams: {
type: "array",
items: {
type: "string",
enum: ["ticker", "trade", "kline", "depth", "forceOrder", "markPrice", "openInterest"]
},
description: "List of data streams to subscribe to"
}
},
required: ["symbol", "type", "streams"]
}
}
]
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
switch (request.params.name) {
case "get_market_data": {
if (!isMarketDataParams(request.params.arguments)) {
throw new Error('Invalid market data parameters');
}
const { symbol, type } = request.params.arguments;
const data = await restConnector.getMarketData(symbol, type);
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}]
};
}
case "test_futures_endpoints": {
if (!isFuturesDataParams(request.params.arguments)) {
throw new Error('Invalid futures data parameters');
}
const { symbol } = request.params.arguments;
// Test each endpoint individually
const openInterest = await restConnector.getFuturesOpenInterest(symbol);
const fundingRate = await restConnector.getFuturesFundingRate(symbol);
const liquidations = await restConnector.getFuturesLiquidations(symbol);
// Return all test results
return {
content: [{
type: "text",
text: JSON.stringify({
openInterest,
fundingRate,
liquidations
}, null, 2)
}]
};
}
case "get_futures_open_interest": {
if (!isFuturesDataParams(request.params.arguments)) {
throw new Error('Invalid futures data parameters');
}
const { symbol } = request.params.arguments;
const data = await restConnector.getFuturesOpenInterest(symbol);
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}]
};
}
case "get_futures_funding_rate": {
if (!isFuturesDataParams(request.params.arguments)) {
throw new Error('Invalid futures data parameters');
}
const { symbol } = request.params.arguments;
const data = await restConnector.getFuturesFundingRate(symbol);
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}]
};
}
case "get_klines": {
if (!isKlineParams(request.params.arguments)) {
throw new Error('Invalid kline parameters');
}
const { symbol, type, interval, limit } = request.params.arguments;
const data = await restConnector.getKlines(symbol, type, interval, limit);
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}]
};
}
case "subscribe_market_data": {
if (!isStreamParams(request.params.arguments)) {
throw new Error('Invalid stream parameters');
}
const { symbol, type, streams } = request.params.arguments;
wsManager.subscribe(symbol, type, streams);
// Set up message handler
wsManager.onStreamData(symbol, streams[0], (data: StreamEventData) => {
// Handle real-time data updates
logger.info(`Received WebSocket data for ${symbol}:`, data);
});
return {
content: [{
type: "text",
text: `Successfully subscribed to ${streams.join(", ")} for ${symbol}`
}]
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error) {
const apiError = error as APIError;
logger.error('Error handling tool request:', apiError);
throw apiError;
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info('Binance MCP server started successfully');
}
// Handle cleanup on shutdown
process.on('SIGINT', () => {
logger.info('Shutting down server...');
wsManager.close();
process.exit(0);
});
main().catch((error) => {
logger.error('Failed to start server:', error);
process.exit(1);
});
```