# 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:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Binance MCP Server
2 |
3 | A Model Context Protocol (MCP) server implementation for Binance market data with WebSocket support.
4 |
5 | ## Features
6 |
7 | - Real-time market data streaming via WebSocket
8 | - Support for both spot and futures markets
9 | - Automatic reconnection with exponential backoff
10 | - Type-safe message handling
11 | - Comprehensive error handling
12 |
13 | ## Installation
14 |
15 | ```bash
16 | npm install
17 | ```
18 |
19 | ## Usage
20 |
21 | ### Starting the Server
22 |
23 | ```bash
24 | npm start
25 | ```
26 |
27 | ### WebSocket Stream Types
28 |
29 | The following stream types are supported:
30 |
31 | - `trade`: Real-time trade data
32 | - `ticker`: 24-hour rolling window price change statistics
33 | - `bookTicker`: Best bid/ask price and quantity
34 | - `kline`: Candlestick data
35 | - `markPrice`: Mark price and funding rate (futures only)
36 | - `fundingRate`: Funding rate data (futures only)
37 |
38 | ### Example Usage in Claude Desktop
39 |
40 | ```typescript
41 | // Subscribe to trade and ticker streams for BTC/USDT
42 | await server.subscribe('BTCUSDT', 'spot', ['trade', 'ticker']);
43 |
44 | // Handle incoming data
45 | server.onStreamData('BTCUSDT', 'trade', (data) => {
46 | console.log('New trade:', data);
47 | });
48 | ```
49 |
50 | ## Development
51 |
52 | ### Running Tests
53 |
54 | ```bash
55 | npm test
56 | ```
57 |
58 | ### Building
59 |
60 | ```bash
61 | npm run build
62 | ```
63 |
64 | ## License
65 |
66 | Private
67 |
```
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["jest", "node"],
5 | "isolatedModules": false
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["node_modules"]
9 | }
10 |
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | moduleNameMapper: {
6 | '^@/(.*)$': '<rootDir>/src/$1'
7 | },
8 | testMatch: ['**/__tests__/**/*.test.ts'],
9 | transform: {
10 | '^.+\\.tsx?$': ['ts-jest', {
11 | tsconfig: 'tsconfig.test.json'
12 | }]
13 | },
14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
15 | };
```
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | import winston from 'winston';
2 |
3 | export const logger = winston.createLogger({
4 | level: 'info',
5 | format: winston.format.combine(
6 | winston.format.timestamp(),
7 | winston.format.json()
8 | ),
9 | transports: [
10 | new winston.transports.File({ filename: 'error.log', level: 'error' }),
11 | new winston.transports.File({ filename: 'combined.log' }),
12 | new winston.transports.Console({
13 | format: winston.format.combine(
14 | winston.format.colorize(),
15 | winston.format.simple()
16 | )
17 | })
18 | ]
19 | });
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "NodeNext", // Changed from ES2022
5 | "moduleResolution": "NodeNext",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "resolveJsonModule": true,
13 | "declaration": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "@/*": ["src/*"]
17 | }
18 | },
19 | "include": ["src/**/*"],
20 | "exclude": ["node_modules", "**/*.test.ts", "build"]
21 | }
```
--------------------------------------------------------------------------------
/src/types/market-data.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface MarketData {
2 | symbol: string;
3 | exchange: string;
4 | type: 'spot' | 'futures';
5 | price: number;
6 | timestamp: number;
7 | volume24h: number;
8 | volumeDelta24h?: number;
9 | priceChange24h: number;
10 | priceChange1h?: number;
11 | price24hHigh: number;
12 | price24hLow: number;
13 | tradeCount24h: number;
14 | bidAskSpread?: number;
15 | openInterest?: number;
16 | fundingRate?: number;
17 | liquidations24h?: number;
18 | }
19 |
20 | export interface OrderBookData {
21 | symbol: string;
22 | type: 'spot' | 'futures';
23 | bids: [number, number][];
24 | asks: [number, number][];
25 | timestamp: number;
26 | lastUpdateId: number;
27 | }
28 |
29 | export interface TradeData {
30 | symbol: string;
31 | type: 'spot' | 'futures';
32 | price: number;
33 | quantity: number;
34 | timestamp: number;
35 | isBuyerMaker: boolean;
36 | tradeId: number;
37 | }
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
1 | export const config = {
2 |
3 | // Server config
4 | NAME: 'binance-market-data',
5 | VERSION: '1.0.0',
6 |
7 | // REST endpoints
8 | SPOT_REST_URL: 'https://api.binance.com/api/v3',
9 | FUTURES_REST_URL: 'https://fapi.binance.com/fapi/v1',
10 |
11 | // WebSocket endpoints
12 | SPOT_WS_URL: 'wss://stream.binance.com:9443/ws',
13 | FUTURES_WS_URL: 'wss://fstream.binance.com/ws',
14 |
15 | // API credentials
16 | API_KEY: process.env.BINANCE_API_KEY || '',
17 | API_SECRET: process.env.BINANCE_API_SECRET || '',
18 |
19 | // Constants
20 | DEFAULT_ORDER_BOOK_LIMIT: 100,
21 | DEFAULT_TRADE_LIMIT: 1000,
22 |
23 | // Rate limits
24 | SPOT_RATE_LIMIT: 1200,
25 | FUTURES_RATE_LIMIT: 1200,
26 |
27 | // WebSocket configurations
28 | WS_PING_INTERVAL: 3 * 60 * 1000, // 3 minutes
29 | WS_RECONNECT_DELAY: 5000, // 5 seconds
30 | WS_CONNECTION_TIMEOUT: 10000, // 10 seconds
31 | WS_MAX_RECONNECT_ATTEMPTS: 5,
32 |
33 | // HTTP configurations
34 | HTTP_TIMEOUT: 10000,
35 | HTTP_MAX_RETRIES: 3,
36 | HTTP_RETRY_DELAY: 1000,
37 |
38 | ERRORS: {
39 | RATE_LIMIT_EXCEEDED: 'Rate limit exceeded',
40 | INVALID_SYMBOL: 'Invalid trading pair symbol',
41 | WS_CONNECTION_ERROR: 'WebSocket connection error',
42 | WS_SUBSCRIPTION_ERROR: 'WebSocket subscription error'
43 | }
44 | } as const;
45 |
46 | export type Config = typeof config;
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "binance-mcp-server",
3 | "version": "0.1.0",
4 | "description": "Binance market data provider with WebSocket support",
5 | "private": true,
6 | "type": "module",
7 | "bin": {
8 | "binance-mcp-server": "./build/index.js"
9 | },
10 | "files": [
11 | "build"
12 | ],
13 | "scripts": {
14 | "build": "rimraf build && tsc -p tsconfig.json",
15 | "postbuild": "chmod +x build/index.js",
16 | "prepare": "npm run build",
17 | "watch": "tsc --watch -p tsconfig.json",
18 | "inspector": "npx @modelcontextprotocol/inspector build/index.js",
19 | "start": "node build/index.js",
20 | "test": "jest --config=jest.config.js",
21 | "test:watch": "jest --watch --config=jest.config.js",
22 | "test:coverage": "jest --coverage --config=jest.config.js",
23 | "type-check": "tsc --noEmit -p tsconfig.test.json",
24 | "lint": "eslint 'src/**/*.{js,ts}'"
25 | },
26 | "dependencies": {
27 | "@modelcontextprotocol/sdk": "0.6.0",
28 | "@types/ws": "^8.5.10",
29 | "axios": "^1.6.7",
30 | "ws": "^8.16.0",
31 | "winston": "^3.11.0"
32 | },
33 | "devDependencies": {
34 | "@jest/globals": "^29.7.0",
35 | "@types/jest": "^29.5.12",
36 | "@types/node": "^20.11.24",
37 | "@typescript-eslint/eslint-plugin": "^7.1.0",
38 | "@typescript-eslint/parser": "^7.1.0",
39 | "eslint": "^8.57.0",
40 | "jest": "^29.7.0",
41 | "rimraf": "^5.0.5",
42 | "ts-jest": "^29.1.2",
43 | "typescript": "^5.3.3"
44 | }
45 | }
```
--------------------------------------------------------------------------------
/src/types/api-types.ts:
--------------------------------------------------------------------------------
```typescript
1 | export interface MarketDataParams {
2 | symbol: string;
3 | type: 'spot' | 'futures';
4 | }
5 |
6 | export interface KlineParams {
7 | symbol: string;
8 | type: 'spot' | 'futures';
9 | interval: string;
10 | limit?: number;
11 | }
12 |
13 | export interface StreamParams {
14 | symbol: string;
15 | type: 'spot' | 'futures';
16 | streams: string[];
17 | }
18 |
19 | export interface FuturesDataParams {
20 | symbol: string;
21 | }
22 |
23 | export class APIError extends Error {
24 | constructor(message: string, public readonly cause?: Error) {
25 | super(message);
26 | this.name = 'APIError';
27 | }
28 | }
29 |
30 | // Type guards
31 | export function isMarketDataParams(params: any): params is MarketDataParams {
32 | return (
33 | typeof params === 'object' &&
34 | typeof params.symbol === 'string' &&
35 | (params.type === 'spot' || params.type === 'futures')
36 | );
37 | }
38 |
39 | export function isKlineParams(params: any): params is KlineParams {
40 | return (
41 | typeof params === 'object' &&
42 | typeof params.symbol === 'string' &&
43 | (params.type === 'spot' || params.type === 'futures') &&
44 | typeof params.interval === 'string' &&
45 | (params.limit === undefined || typeof params.limit === 'number')
46 | );
47 | }
48 |
49 | export function isStreamParams(params: any): params is StreamParams {
50 | return (
51 | typeof params === 'object' &&
52 | typeof params.symbol === 'string' &&
53 | (params.type === 'spot' || params.type === 'futures') &&
54 | Array.isArray(params.streams)
55 | );
56 | }
57 |
58 | export function isFuturesDataParams(params: any): params is FuturesDataParams {
59 | return (
60 | typeof params === 'object' &&
61 | typeof params.symbol === 'string'
62 | );
63 | }
```
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # Colors for output
4 | RED='\033[0;31m'
5 | GREEN='\033[0;32m'
6 | NC='\033[0m' # No Color
7 |
8 | # Check if we're in the correct directory (has package.json)
9 | if [ ! -f "package.json" ]; then
10 | echo -e "${RED}Error: package.json not found. Are you in the correct directory?${NC}"
11 | exit 1
12 | fi
13 |
14 | # Function to create directory if it doesn't exist
15 | create_dir() {
16 | if [ ! -d "$1" ]; then
17 | mkdir -p "$1"
18 | echo -e "${GREEN}Created directory: $1${NC}"
19 | else
20 | echo "Directory already exists: $1"
21 | fi
22 | }
23 |
24 | # Function to create file if it doesn't exist
25 | create_file() {
26 | if [ ! -f "$1" ]; then
27 | touch "$1"
28 | # Add basic export statement to TypeScript files
29 | if [[ $1 == *.ts ]]; then
30 | echo "// $1" > "$1"
31 | echo "export {}" >> "$1"
32 | fi
33 | echo -e "${GREEN}Created file: $1${NC}"
34 | else
35 | echo "File already exists: $1"
36 | fi
37 | }
38 |
39 | # Create directory structure
40 | create_dir "src/connectors"
41 | create_dir "src/types"
42 | create_dir "src/utils"
43 |
44 | # Create files
45 | create_file "src/connectors/binance-rest.ts"
46 | create_file "src/connectors/binance-ws.ts"
47 | create_file "src/types/market-data.ts"
48 | create_file "src/types/api-types.ts"
49 | create_file "src/utils/logger.ts"
50 | create_file "src/config.ts"
51 |
52 | echo -e "\n${GREEN}Directory structure created successfully!${NC}"
53 | echo -e "Next steps:"
54 | echo "1. Implement the TypeScript interfaces in types/"
55 | echo "2. Set up the configuration in config.ts"
56 | echo "3. Implement the connectors in connectors/"
57 | echo "4. Set up logging in utils/logger.ts"
58 | echo "5. Update main server implementation in index.ts"
59 |
60 | # Print final directory tree
61 | echo -e "\nFinal directory structure:"
62 | tree
```
--------------------------------------------------------------------------------
/src/types/ws-stream.ts:
--------------------------------------------------------------------------------
```typescript
1 | export type StreamEventType =
2 | | 'trade'
3 | | 'ticker'
4 | | 'bookTicker'
5 | | 'kline'
6 | | 'depth'
7 | | 'forceOrder' // Liquidation orders
8 | | 'markPrice' // Mark price and funding rate
9 | | 'openInterest'; // Open interest updates
10 |
11 | export interface WebSocketMessage<T> {
12 | stream: string;
13 | data: T;
14 | timestamp: number;
15 | }
16 |
17 | export type StreamEventData =
18 | | TradeData
19 | | TickerData
20 | | BookTickerData
21 | | KlineData
22 | | DepthData
23 | | ForceOrderData
24 | | MarkPriceData
25 | | OpenInterestData;
26 |
27 | // Existing interfaces
28 | export interface TradeData {
29 | e: 'trade';
30 | E: number;
31 | s: string;
32 | t: number;
33 | p: string;
34 | q: string;
35 | b: number;
36 | a: number;
37 | T: number;
38 | m: boolean;
39 | }
40 |
41 | export interface TickerData {
42 | e: '24hrTicker';
43 | E: number;
44 | s: string;
45 | p: string;
46 | P: string;
47 | w: string;
48 | c: string;
49 | Q: string;
50 | o: string;
51 | h: string;
52 | l: string;
53 | v: string;
54 | q: string;
55 | }
56 |
57 | export interface BookTickerData {
58 | e: 'bookTicker';
59 | u: number;
60 | s: string;
61 | b: string;
62 | B: string;
63 | a: string;
64 | A: string;
65 | }
66 |
67 | export interface KlineData {
68 | e: 'kline';
69 | E: number;
70 | s: string;
71 | k: {
72 | t: number;
73 | T: number;
74 | s: string;
75 | i: string;
76 | f: number;
77 | L: number;
78 | o: string;
79 | c: string;
80 | h: string;
81 | l: string;
82 | v: string;
83 | n: number;
84 | x: boolean;
85 | q: string;
86 | V: string;
87 | Q: string;
88 | };
89 | }
90 |
91 | export interface DepthData {
92 | e: 'depthUpdate';
93 | E: number;
94 | s: string;
95 | U: number;
96 | u: number;
97 | b: [string, string][];
98 | a: [string, string][];
99 | }
100 |
101 | // New Futures-specific interfaces
102 | export interface ForceOrderData {
103 | e: 'forceOrder';
104 | E: number; // Event time
105 | o: {
106 | s: string; // Symbol
107 | S: 'SELL' | 'BUY'; // Side
108 | o: 'LIMIT'; // Order type
109 | f: 'IOC'; // Time in force
110 | q: string; // Original quantity
111 | p: string; // Price
112 | ap: string; // Average price
113 | X: 'FILLED'; // Order status
114 | l: string; // Last filled quantity
115 | z: string; // Cumulative filled quantity
116 | T: number; // Trade time
117 | };
118 | }
119 |
120 | export interface MarkPriceData {
121 | e: 'markPriceUpdate';
122 | E: number; // Event time
123 | s: string; // Symbol
124 | p: string; // Mark price
125 | i: string; // Index price
126 | P: string; // Estimated settle price
127 | r: string; // Funding rate
128 | T: number; // Next funding time
129 | }
130 |
131 | export interface OpenInterestData {
132 | e: 'openInterest';
133 | E: number; // Event time
134 | s: string; // Symbol
135 | o: string; // Open interest
136 | T: number; // Transaction time
137 | }
```
--------------------------------------------------------------------------------
/src/__tests__/binance-ws.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { BinanceWebSocketManager } from '../connectors/binance-ws';
2 | import WebSocket from 'ws';
3 | import { StreamEventData, TradeData } from '../types/ws-stream';
4 |
5 | jest.mock('ws');
6 |
7 | type MockWebSocket = jest.Mocked<WebSocket> & {
8 | readyState: number;
9 | };
10 |
11 | describe('BinanceWebSocketManager', () => {
12 | let wsManager: BinanceWebSocketManager;
13 | let mockWs: MockWebSocket;
14 |
15 | beforeEach(() => {
16 | wsManager = new BinanceWebSocketManager();
17 | mockWs = {
18 | readyState: WebSocket.CONNECTING,
19 | on: jest.fn(),
20 | once: jest.fn(),
21 | emit: jest.fn(),
22 | close: jest.fn(),
23 | ping: jest.fn(),
24 | send: jest.fn(),
25 | terminate: jest.fn(),
26 | removeListener: jest.fn(),
27 | removeAllListeners: jest.fn(),
28 | setMaxListeners: jest.fn(),
29 | getMaxListeners: jest.fn(),
30 | listeners: jest.fn(),
31 | rawListeners: jest.fn(),
32 | listenerCount: jest.fn(),
33 | eventNames: jest.fn(),
34 | addListener: jest.fn(),
35 | off: jest.fn(),
36 | prependListener: jest.fn(),
37 | prependOnceListener: jest.fn(),
38 | } as unknown as MockWebSocket;
39 |
40 | (WebSocket as unknown as jest.Mock).mockImplementation(() => mockWs);
41 | });
42 |
43 | afterEach(() => {
44 | wsManager.close();
45 | jest.clearAllMocks();
46 | });
47 |
48 | it('should successfully subscribe to stream', () => {
49 | const symbol = 'BTCUSDT';
50 | const streams = ['trade', 'ticker'] as const;
51 |
52 | wsManager.subscribe(symbol, 'spot', streams);
53 |
54 | expect(WebSocket).toHaveBeenCalledWith(
55 | expect.stringContaining(`btcusdt@trade/btcusdt@ticker`)
56 | );
57 | });
58 |
59 | it('should handle incoming messages correctly', (done) => {
60 | const symbol = 'BTCUSDT';
61 | const mockData = {
62 | stream: 'btcusdt@trade',
63 | data: {
64 | e: 'trade',
65 | E: 123456789,
66 | s: 'BTCUSDT',
67 | p: '50000.00',
68 | q: '1.0'
69 | } as TradeData
70 | };
71 |
72 | wsManager.subscribe(symbol, 'spot', ['trade']);
73 | wsManager.onStreamData(symbol, 'trade', (data: StreamEventData) => {
74 | expect(data).toEqual(mockData.data);
75 | done();
76 | });
77 |
78 | // Simulate receiving a message
79 | mockWs.emit('message', JSON.stringify(mockData));
80 | });
81 |
82 | it('should handle reconnection on connection close', () => {
83 | const symbol = 'BTCUSDT';
84 | wsManager.subscribe(symbol, 'spot', ['trade']);
85 |
86 | // Simulate connection close
87 | mockWs.emit('close');
88 |
89 | // Verify that a new connection attempt is made
90 | expect(WebSocket).toHaveBeenCalledTimes(2);
91 | });
92 |
93 | it('should clean up resources on unsubscribe', () => {
94 | const symbol = 'BTCUSDT';
95 | wsManager.subscribe(symbol, 'spot', ['trade']);
96 | wsManager.unsubscribe(symbol);
97 |
98 | expect(mockWs.close).toHaveBeenCalled();
99 | });
100 |
101 | it('should handle multiple stream subscriptions', () => {
102 | const symbol = 'BTCUSDT';
103 | const streams = ['trade', 'ticker', 'bookTicker'] as const;
104 |
105 | wsManager.subscribe(symbol, 'spot', streams);
106 |
107 | expect(WebSocket).toHaveBeenCalledWith(
108 | expect.stringContaining('btcusdt@trade/btcusdt@ticker/btcusdt@bookTicker')
109 | );
110 | });
111 |
112 | it('should properly maintain connection state', () => {
113 | const symbol = 'BTCUSDT';
114 | wsManager.subscribe(symbol, 'spot', ['trade']);
115 |
116 | // Update mockWs.readyState which is now properly typed
117 | mockWs.readyState = WebSocket.OPEN;
118 | mockWs.emit('open');
119 |
120 | expect(wsManager.getConnectionState(symbol)).toBe(WebSocket.OPEN);
121 | });
122 | });
```
--------------------------------------------------------------------------------
/src/connectors/binance-ws.ts:
--------------------------------------------------------------------------------
```typescript
1 | import WebSocket from 'ws';
2 | import { config } from '../config.js';
3 | import { logger } from '../utils/logger.js';
4 | import {
5 | WebSocketMessage,
6 | StreamEventType,
7 | StreamEventData,
8 | TradeData,
9 | TickerData,
10 | BookTickerData,
11 | KlineData,
12 | ForceOrderData,
13 | MarkPriceData,
14 | OpenInterestData
15 | } from '../types/ws-stream.js';
16 |
17 | type WSReadyState = number;
18 |
19 | interface StreamSubscription {
20 | symbol: string;
21 | type: 'spot' | 'futures';
22 | streams: StreamEventType[];
23 | reconnectAttempts: number;
24 | reconnectTimeout?: NodeJS.Timeout;
25 | }
26 |
27 | type MessageHandler = (data: StreamEventData) => void;
28 |
29 | export class BinanceWebSocketManager {
30 | private connections: Map<string, WebSocket>;
31 | private pingIntervals: Map<string, NodeJS.Timeout>;
32 | private messageCallbacks: Map<string, Map<StreamEventType, MessageHandler[]>>;
33 | private subscriptions: Map<string, StreamSubscription>;
34 | private readonly MAX_RECONNECT_ATTEMPTS = 5;
35 | private readonly RECONNECT_DELAY = config.WS_RECONNECT_DELAY || 5000;
36 |
37 | constructor() {
38 | this.connections = new Map();
39 | this.pingIntervals = new Map();
40 | this.messageCallbacks = new Map();
41 | this.subscriptions = new Map();
42 | }
43 |
44 | public subscribe(symbol: string, type: 'spot' | 'futures', streams: StreamEventType[]): void {
45 | const subscription: StreamSubscription = {
46 | symbol,
47 | type,
48 | streams,
49 | reconnectAttempts: 0
50 | };
51 |
52 | if (!this.messageCallbacks.has(symbol)) {
53 | this.messageCallbacks.set(symbol, new Map());
54 | }
55 |
56 | const symbolCallbacks = this.messageCallbacks.get(symbol)!;
57 | streams.forEach(stream => {
58 | if (!symbolCallbacks.has(stream)) {
59 | symbolCallbacks.set(stream, []);
60 | }
61 | });
62 |
63 | this.subscriptions.set(symbol, subscription);
64 | this.connectWebSocket(subscription);
65 | }
66 |
67 | private connectWebSocket(subscription: StreamSubscription): void {
68 | const { symbol, type, streams } = subscription;
69 | const wsUrl = type === 'spot' ? config.SPOT_WS_URL : config.FUTURES_WS_URL;
70 |
71 | // Handle special futures streams
72 | const streamNames = streams.map(stream => {
73 | if (type === 'futures') {
74 | switch (stream) {
75 | case 'forceOrder':
76 | return `${symbol.toLowerCase()}@forceOrder`;
77 | case 'markPrice':
78 | return `${symbol.toLowerCase()}@markPrice@1s`; // 1s update frequency
79 | case 'openInterest':
80 | return `${symbol.toLowerCase()}@openInterest@1s`;
81 | default:
82 | return `${symbol.toLowerCase()}@${stream}`;
83 | }
84 | }
85 | return `${symbol.toLowerCase()}@${stream}`;
86 | });
87 |
88 | try {
89 | const ws = new WebSocket(`${wsUrl}/${streamNames.join('/')}`);
90 |
91 | ws.on('open', () => {
92 | logger.info(`WebSocket connected for ${symbol} ${streams.join(', ')}`);
93 | subscription.reconnectAttempts = 0;
94 | this.setupPingInterval(symbol, ws);
95 | });
96 |
97 | ws.on('message', (data: WebSocket.Data) => {
98 | try {
99 | const message = JSON.parse(data.toString()) as WebSocketMessage<StreamEventData>;
100 | this.handleStreamMessage(symbol, message);
101 | } catch (error) {
102 | logger.error('Error parsing WebSocket message:', error);
103 | }
104 | });
105 |
106 | ws.on('error', (error: Error) => {
107 | logger.error(`WebSocket error for ${symbol}:`, error);
108 | });
109 |
110 | ws.on('close', () => {
111 | logger.info(`WebSocket closed for ${symbol}`);
112 | this.cleanup(symbol);
113 | this.handleReconnection(subscription);
114 | });
115 |
116 | ws.on('pong', () => {
117 | logger.debug(`Received pong from ${symbol} WebSocket`);
118 | });
119 |
120 | this.connections.set(symbol, ws);
121 | } catch (error) {
122 | logger.error(`Error creating WebSocket connection for ${symbol}:`, error);
123 | this.handleReconnection(subscription);
124 | throw error;
125 | }
126 | }
127 |
128 | private handleStreamMessage(symbol: string, message: WebSocketMessage<StreamEventData>): void {
129 | const symbolCallbacks = this.messageCallbacks.get(symbol);
130 | if (!symbolCallbacks) return;
131 |
132 | // Extract stream type from the stream name
133 | const streamParts = message.stream.split('@');
134 | if (streamParts.length < 2) return;
135 |
136 | let streamType = streamParts[1] as StreamEventType;
137 | // Handle special cases where the stream name has additional parts (e.g., markPrice@1s)
138 | if (streamParts.length > 2) {
139 | streamType = streamParts[1].split('@')[0] as StreamEventType;
140 | }
141 |
142 | const handlers = symbolCallbacks.get(streamType);
143 |
144 | if (handlers) {
145 | handlers.forEach(handler => {
146 | try {
147 | handler(message.data);
148 | } catch (error) {
149 | logger.error(`Error in message handler for ${symbol} ${streamType}:`, error);
150 | }
151 | });
152 | }
153 | }
154 |
155 | public onStreamData(symbol: string, streamType: StreamEventType, handler: MessageHandler): void {
156 | const symbolCallbacks = this.messageCallbacks.get(symbol);
157 | if (!symbolCallbacks) {
158 | logger.error(`No callbacks registered for symbol ${symbol}`);
159 | return;
160 | }
161 |
162 | const handlers = symbolCallbacks.get(streamType) || [];
163 | handlers.push(handler);
164 | symbolCallbacks.set(streamType, handlers);
165 | }
166 |
167 | private handleReconnection(subscription: StreamSubscription): void {
168 | const { symbol, reconnectAttempts } = subscription;
169 |
170 | if (reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
171 | logger.error(`Max reconnection attempts reached for ${symbol}`);
172 | return;
173 | }
174 |
175 | subscription.reconnectAttempts++;
176 | const delay = this.RECONNECT_DELAY * Math.pow(2, reconnectAttempts - 1); // Exponential backoff
177 |
178 | logger.info(`Attempting to reconnect ${symbol} in ${delay}ms (attempt ${reconnectAttempts})`);
179 |
180 | subscription.reconnectTimeout = setTimeout(() => {
181 | this.connectWebSocket(subscription);
182 | }, delay);
183 | }
184 |
185 | private setupPingInterval(symbol: string, ws: WebSocket): void {
186 | const interval = setInterval(() => {
187 | if (ws.readyState === WebSocket.OPEN) {
188 | ws.ping((error?: Error) => {
189 | if (error) {
190 | logger.error(`Error sending ping for ${symbol}:`, error);
191 | }
192 | });
193 | }
194 | }, config.WS_PING_INTERVAL);
195 | this.pingIntervals.set(symbol, interval);
196 | }
197 |
198 | private cleanup(symbol: string): void {
199 | const interval = this.pingIntervals.get(symbol);
200 | if (interval) {
201 | clearInterval(interval);
202 | this.pingIntervals.delete(symbol);
203 | }
204 |
205 | const subscription = this.subscriptions.get(symbol);
206 | if (subscription?.reconnectTimeout) {
207 | clearTimeout(subscription.reconnectTimeout);
208 | }
209 |
210 | this.connections.delete(symbol);
211 | }
212 |
213 | public unsubscribe(symbol: string): void {
214 | const ws = this.connections.get(symbol);
215 | if (ws) {
216 | ws.close();
217 | }
218 | this.cleanup(symbol);
219 | this.subscriptions.delete(symbol);
220 | this.messageCallbacks.delete(symbol);
221 | }
222 |
223 | public close(): void {
224 | this.connections.forEach((ws, symbol) => {
225 | ws.close();
226 | this.cleanup(symbol);
227 | });
228 | this.subscriptions.clear();
229 | this.messageCallbacks.clear();
230 | }
231 |
232 | public getConnectionState(symbol: string): WSReadyState | undefined {
233 | const ws = this.connections.get(symbol);
234 | return ws?.readyState;
235 | }
236 |
237 | public isSubscribed(symbol: string, streamType: StreamEventType): boolean {
238 | const subscription = this.subscriptions.get(symbol);
239 | return subscription?.streams.includes(streamType) || false;
240 | }
241 | }
```
--------------------------------------------------------------------------------
/src/connectors/binance-rest.ts:
--------------------------------------------------------------------------------
```typescript
1 | import axios, { AxiosInstance } from 'axios';
2 | import { config } from '../config.js';
3 | import { logger } from '../utils/logger.js';
4 | import { APIError } from '../types/api-types.js';
5 |
6 | export class BinanceRestConnector {
7 | private readonly axiosInstance: AxiosInstance;
8 | private readonly retryDelay = config.HTTP_RETRY_DELAY;
9 | private readonly maxRetries = config.HTTP_MAX_RETRIES;
10 |
11 | constructor() {
12 | this.axiosInstance = axios.create({
13 | timeout: config.HTTP_TIMEOUT,
14 | headers: {
15 | 'Content-Type': 'application/json'
16 | }
17 | });
18 | logger.info('BinanceRestConnector initialized');
19 | logger.info(`Futures REST URL: ${config.FUTURES_REST_URL}`);
20 | }
21 |
22 | private async executeWithRetry<T>(operation: () => Promise<T>, retries = 0): Promise<T> {
23 | try {
24 | return await operation();
25 | } catch (error) {
26 | if (retries >= this.maxRetries) {
27 | throw error;
28 | }
29 |
30 | const delay = this.retryDelay * Math.pow(2, retries);
31 | logger.warn(`Request failed, retrying in ${delay}ms...`);
32 |
33 | await new Promise(resolve => setTimeout(resolve, delay));
34 | return this.executeWithRetry(operation, retries + 1);
35 | }
36 | }
37 |
38 | public async getMarketData(symbol: string, type: 'spot' | 'futures'): Promise<any> {
39 | try {
40 | logger.info(`Getting ${type} market data for ${symbol}`);
41 |
42 | if (type === 'spot') {
43 | const data = await this.executeWithRetry(() =>
44 | this.axiosInstance.get(`${config.SPOT_REST_URL}/ticker/24hr`, {
45 | params: { symbol: symbol.toUpperCase() }
46 | }).then(response => response.data)
47 | );
48 | logger.info('Successfully fetched spot market data');
49 | return data;
50 | }
51 |
52 | // For futures, fetch all relevant data in parallel
53 | logger.info('Fetching futures data from multiple endpoints...');
54 |
55 | try {
56 | const [
57 | marketData,
58 | openInterest,
59 | fundingData,
60 | liquidations
61 | ] = await Promise.all([
62 | // Basic market data
63 | this.executeWithRetry(() =>
64 | this.axiosInstance.get(`${config.FUTURES_REST_URL}/ticker/24hr`, {
65 | params: { symbol: symbol.toUpperCase() }
66 | }).then(response => {
67 | logger.info('Successfully fetched futures ticker data');
68 | return response.data;
69 | })
70 | ),
71 | // Open interest
72 | this.executeWithRetry(() =>
73 | this.axiosInstance.get(`${config.FUTURES_REST_URL}/openInterest`, {
74 | params: { symbol: symbol.toUpperCase() }
75 | }).then(response => {
76 | logger.info('Successfully fetched open interest data');
77 | return response.data;
78 | })
79 | ),
80 | // Premium index (funding rate)
81 | this.executeWithRetry(() =>
82 | this.axiosInstance.get(`${config.FUTURES_REST_URL}/premiumIndex`, {
83 | params: {
84 | symbol: symbol.toUpperCase()
85 | }
86 | }).then(response => {
87 | logger.info('Successfully fetched funding rate data');
88 | return response.data;
89 | })
90 | ),
91 | // Recent liquidations
92 | this.executeWithRetry(() =>
93 | this.axiosInstance.get(`${config.FUTURES_REST_URL}/forceOrders`, {
94 | params: {
95 | symbol: symbol.toUpperCase(),
96 | startTime: Date.now() - 24 * 60 * 60 * 1000,
97 | limit: 100
98 | }
99 | }).then(response => {
100 | logger.info('Successfully fetched liquidations data');
101 | return response.data;
102 | })
103 | )
104 | ]);
105 |
106 | logger.info('Successfully fetched all futures data, combining responses...');
107 |
108 | // Combine all futures data with correct field mappings
109 | const combinedData = {
110 | ...marketData,
111 | openInterest: openInterest.openInterest,
112 | fundingRate: fundingData.lastFundingRate,
113 | markPrice: fundingData.markPrice,
114 | nextFundingTime: fundingData.nextFundingTime,
115 | liquidations24h: liquidations.length,
116 | liquidationVolume24h: liquidations.reduce((sum: number, order: any) =>
117 | sum + parseFloat(order.executedQty), 0
118 | )
119 | };
120 |
121 | logger.info('Successfully combined futures data');
122 | return combinedData;
123 |
124 | } catch (error) {
125 | logger.error('Error in futures data Promise.all:', error);
126 | throw error;
127 | }
128 |
129 | } catch (error) {
130 | logger.error('Error fetching market data:', error);
131 | throw new APIError('Failed to fetch market data', error as Error);
132 | }
133 | }
134 |
135 | public async getFuturesOpenInterest(symbol: string): Promise<any> {
136 | try {
137 | logger.info(`Getting futures open interest for ${symbol}`);
138 | const response = await this.executeWithRetry(() =>
139 | this.axiosInstance.get(`${config.FUTURES_REST_URL}/openInterest`, {
140 | params: { symbol: symbol.toUpperCase() }
141 | })
142 | );
143 | logger.info('Successfully fetched open interest data');
144 | return response.data;
145 | } catch (error) {
146 | logger.error('Error fetching open interest:', error);
147 | throw new APIError('Failed to fetch open interest data', error as Error);
148 | }
149 | }
150 |
151 | public async getFuturesFundingRate(symbol: string): Promise<any> {
152 | try {
153 | logger.info(`Getting futures funding rate for ${symbol}`);
154 | const response = await this.executeWithRetry(() =>
155 | this.axiosInstance.get(`${config.FUTURES_REST_URL}/premiumIndex`, {
156 | params: {
157 | symbol: symbol.toUpperCase()
158 | }
159 | })
160 | );
161 | logger.info('Successfully fetched funding rate data');
162 | return response.data;
163 | } catch (error) {
164 | logger.error('Error fetching funding rate:', error);
165 | throw new APIError('Failed to fetch funding rate data', error as Error);
166 | }
167 | }
168 |
169 | public async getFuturesLiquidations(symbol: string): Promise<any> {
170 | try {
171 | logger.info(`Getting futures liquidations for ${symbol}`);
172 | const response = await this.executeWithRetry(() =>
173 | this.axiosInstance.get(`${config.FUTURES_REST_URL}/forceOrders`, {
174 | params: {
175 | symbol: symbol.toUpperCase(),
176 | startTime: Date.now() - 24 * 60 * 60 * 1000,
177 | limit: 1000
178 | }
179 | })
180 | );
181 | logger.info('Successfully fetched liquidations data');
182 | return response.data;
183 | } catch (error) {
184 | logger.error('Error fetching liquidations:', error);
185 | throw new APIError('Failed to fetch liquidations data', error as Error);
186 | }
187 | }
188 |
189 | public async getKlines(
190 | symbol: string,
191 | type: 'spot' | 'futures',
192 | interval: string,
193 | limit?: number
194 | ): Promise<any> {
195 | try {
196 | logger.info(`Getting ${type} klines for ${symbol}`);
197 | const baseUrl = type === 'spot' ? config.SPOT_REST_URL : config.FUTURES_REST_URL;
198 | const response = await this.executeWithRetry(() =>
199 | this.axiosInstance.get(`${baseUrl}/klines`, {
200 | params: {
201 | symbol: symbol.toUpperCase(),
202 | interval,
203 | limit: limit || 500
204 | }
205 | })
206 | );
207 | logger.info('Successfully fetched klines data');
208 | return response.data;
209 | } catch (error) {
210 | logger.error('Error fetching klines:', error);
211 | throw new APIError('Failed to fetch klines data', error as Error);
212 | }
213 | }
214 |
215 | public async getExchangeInfo(type: 'spot' | 'futures'): Promise<any> {
216 | try {
217 | logger.info(`Getting ${type} exchange info`);
218 | const baseUrl = type === 'spot' ? config.SPOT_REST_URL : config.FUTURES_REST_URL;
219 | const response = await this.executeWithRetry(() =>
220 | this.axiosInstance.get(`${baseUrl}/exchangeInfo`)
221 | );
222 | logger.info('Successfully fetched exchange info');
223 | return response.data;
224 | } catch (error) {
225 | logger.error('Error fetching exchange info:', error);
226 | throw new APIError('Failed to fetch exchange info', error as Error);
227 | }
228 | }
229 | }
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5 | import {
6 | CallToolRequestSchema,
7 | ListToolsRequestSchema,
8 | } from "@modelcontextprotocol/sdk/types.js";
9 |
10 | import { BinanceWebSocketManager } from './connectors/binance-ws.js';
11 | import { BinanceRestConnector } from './connectors/binance-rest.js';
12 | import { config } from './config.js';
13 | import { logger } from './utils/logger.js';
14 | import {
15 | MarketDataParams,
16 | KlineParams,
17 | StreamParams,
18 | FuturesDataParams,
19 | APIError,
20 | isMarketDataParams,
21 | isKlineParams,
22 | isStreamParams,
23 | isFuturesDataParams
24 | } from './types/api-types.js';
25 | import { StreamEventData } from './types/ws-stream.js';
26 |
27 | const wsManager = new BinanceWebSocketManager();
28 | const restConnector = new BinanceRestConnector();
29 |
30 | const server = new Server(
31 | {
32 | name: config.NAME,
33 | version: config.VERSION,
34 | description: 'Binance market data provider with WebSocket support'
35 | },
36 | {
37 | capabilities: {
38 | tools: {},
39 | },
40 | }
41 | );
42 |
43 | // List available tools
44 | server.setRequestHandler(ListToolsRequestSchema, async () => {
45 | return {
46 | tools: [
47 | {
48 | name: "get_market_data",
49 | description: "Get comprehensive market data for a trading pair",
50 | inputSchema: {
51 | type: "object",
52 | properties: {
53 | symbol: {
54 | type: "string",
55 | description: "Trading pair symbol (e.g., BTCUSDT)"
56 | },
57 | type: {
58 | type: "string",
59 | enum: ["spot", "futures"],
60 | description: "Market type"
61 | }
62 | },
63 | required: ["symbol", "type"]
64 | }
65 | },
66 | {
67 | name: "test_futures_endpoints",
68 | description: "Test individual futures endpoints",
69 | inputSchema: {
70 | type: "object",
71 | properties: {
72 | symbol: {
73 | type: "string",
74 | description: "Trading pair symbol (e.g., BTCUSDT)"
75 | }
76 | },
77 | required: ["symbol"]
78 | }
79 | },
80 | {
81 | name: "get_futures_open_interest",
82 | description: "Get current open interest for a futures trading pair",
83 | inputSchema: {
84 | type: "object",
85 | properties: {
86 | symbol: {
87 | type: "string",
88 | description: "Trading pair symbol (e.g., BTCUSDT)"
89 | }
90 | },
91 | required: ["symbol"]
92 | }
93 | },
94 | {
95 | name: "get_futures_funding_rate",
96 | description: "Get current funding rate for a futures trading pair",
97 | inputSchema: {
98 | type: "object",
99 | properties: {
100 | symbol: {
101 | type: "string",
102 | description: "Trading pair symbol (e.g., BTCUSDT)"
103 | }
104 | },
105 | required: ["symbol"]
106 | }
107 | },
108 | {
109 | name: "get_klines",
110 | description: "Get historical candlestick data",
111 | inputSchema: {
112 | type: "object",
113 | properties: {
114 | symbol: {
115 | type: "string",
116 | description: "Trading pair symbol (e.g., BTCUSDT)"
117 | },
118 | type: {
119 | type: "string",
120 | enum: ["spot", "futures"],
121 | description: "Market type"
122 | },
123 | interval: {
124 | type: "string",
125 | enum: ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1M"],
126 | description: "Kline/candlestick chart interval"
127 | },
128 | limit: {
129 | type: "number",
130 | description: "Number of klines to retrieve (default 500, max 1000)"
131 | }
132 | },
133 | required: ["symbol", "type", "interval"]
134 | }
135 | },
136 | {
137 | name: "subscribe_market_data",
138 | description: "Subscribe to real-time market data updates",
139 | inputSchema: {
140 | type: "object",
141 | properties: {
142 | symbol: {
143 | type: "string",
144 | description: "Trading pair symbol (e.g., BTCUSDT)"
145 | },
146 | type: {
147 | type: "string",
148 | enum: ["spot", "futures"],
149 | description: "Market type"
150 | },
151 | streams: {
152 | type: "array",
153 | items: {
154 | type: "string",
155 | enum: ["ticker", "trade", "kline", "depth", "forceOrder", "markPrice", "openInterest"]
156 | },
157 | description: "List of data streams to subscribe to"
158 | }
159 | },
160 | required: ["symbol", "type", "streams"]
161 | }
162 | }
163 | ]
164 | };
165 | });
166 |
167 | // Handle tool calls
168 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
169 | try {
170 | switch (request.params.name) {
171 | case "get_market_data": {
172 | if (!isMarketDataParams(request.params.arguments)) {
173 | throw new Error('Invalid market data parameters');
174 | }
175 | const { symbol, type } = request.params.arguments;
176 | const data = await restConnector.getMarketData(symbol, type);
177 | return {
178 | content: [{
179 | type: "text",
180 | text: JSON.stringify(data, null, 2)
181 | }]
182 | };
183 | }
184 |
185 | case "test_futures_endpoints": {
186 | if (!isFuturesDataParams(request.params.arguments)) {
187 | throw new Error('Invalid futures data parameters');
188 | }
189 | const { symbol } = request.params.arguments;
190 |
191 | // Test each endpoint individually
192 | const openInterest = await restConnector.getFuturesOpenInterest(symbol);
193 | const fundingRate = await restConnector.getFuturesFundingRate(symbol);
194 | const liquidations = await restConnector.getFuturesLiquidations(symbol);
195 |
196 | // Return all test results
197 | return {
198 | content: [{
199 | type: "text",
200 | text: JSON.stringify({
201 | openInterest,
202 | fundingRate,
203 | liquidations
204 | }, null, 2)
205 | }]
206 | };
207 | }
208 |
209 | case "get_futures_open_interest": {
210 | if (!isFuturesDataParams(request.params.arguments)) {
211 | throw new Error('Invalid futures data parameters');
212 | }
213 | const { symbol } = request.params.arguments;
214 | const data = await restConnector.getFuturesOpenInterest(symbol);
215 | return {
216 | content: [{
217 | type: "text",
218 | text: JSON.stringify(data, null, 2)
219 | }]
220 | };
221 | }
222 |
223 | case "get_futures_funding_rate": {
224 | if (!isFuturesDataParams(request.params.arguments)) {
225 | throw new Error('Invalid futures data parameters');
226 | }
227 | const { symbol } = request.params.arguments;
228 | const data = await restConnector.getFuturesFundingRate(symbol);
229 | return {
230 | content: [{
231 | type: "text",
232 | text: JSON.stringify(data, null, 2)
233 | }]
234 | };
235 | }
236 |
237 | case "get_klines": {
238 | if (!isKlineParams(request.params.arguments)) {
239 | throw new Error('Invalid kline parameters');
240 | }
241 | const { symbol, type, interval, limit } = request.params.arguments;
242 | const data = await restConnector.getKlines(symbol, type, interval, limit);
243 | return {
244 | content: [{
245 | type: "text",
246 | text: JSON.stringify(data, null, 2)
247 | }]
248 | };
249 | }
250 |
251 | case "subscribe_market_data": {
252 | if (!isStreamParams(request.params.arguments)) {
253 | throw new Error('Invalid stream parameters');
254 | }
255 | const { symbol, type, streams } = request.params.arguments;
256 | wsManager.subscribe(symbol, type, streams);
257 |
258 | // Set up message handler
259 | wsManager.onStreamData(symbol, streams[0], (data: StreamEventData) => {
260 | // Handle real-time data updates
261 | logger.info(`Received WebSocket data for ${symbol}:`, data);
262 | });
263 |
264 | return {
265 | content: [{
266 | type: "text",
267 | text: `Successfully subscribed to ${streams.join(", ")} for ${symbol}`
268 | }]
269 | };
270 | }
271 |
272 | default:
273 | throw new Error(`Unknown tool: ${request.params.name}`);
274 | }
275 | } catch (error) {
276 | const apiError = error as APIError;
277 | logger.error('Error handling tool request:', apiError);
278 | throw apiError;
279 | }
280 | });
281 |
282 | // Start the server
283 | async function main() {
284 | const transport = new StdioServerTransport();
285 | await server.connect(transport);
286 | logger.info('Binance MCP server started successfully');
287 | }
288 |
289 | // Handle cleanup on shutdown
290 | process.on('SIGINT', () => {
291 | logger.info('Shutting down server...');
292 | wsManager.close();
293 | process.exit(0);
294 | });
295 |
296 | main().catch((error) => {
297 | logger.error('Failed to start server:', error);
298 | process.exit(1);
299 | });
```