Migración desde trading-platform/apps/mcp-binance-connector - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
fa75326bba
52
.env.example
Normal file
52
.env.example
Normal file
@ -0,0 +1,52 @@
|
||||
# MCP Binance Connector Configuration
|
||||
# Copy this file to .env and configure values
|
||||
|
||||
# ==========================================
|
||||
# Server Configuration
|
||||
# ==========================================
|
||||
PORT=3606
|
||||
NODE_ENV=development
|
||||
|
||||
# ==========================================
|
||||
# MCP Authentication
|
||||
# ==========================================
|
||||
MCP_API_KEY=your_mcp_api_key_here
|
||||
|
||||
# ==========================================
|
||||
# Binance API Configuration
|
||||
# ==========================================
|
||||
BINANCE_API_KEY=your_binance_api_key
|
||||
BINANCE_API_SECRET=your_binance_api_secret
|
||||
|
||||
# ==========================================
|
||||
# Network Configuration
|
||||
# ==========================================
|
||||
# Use testnet by default (set to false for production)
|
||||
BINANCE_TESTNET=true
|
||||
BINANCE_FUTURES_TESTNET=true
|
||||
|
||||
# ==========================================
|
||||
# Risk Limits
|
||||
# ==========================================
|
||||
# Maximum value for a single order in USDT
|
||||
MAX_ORDER_VALUE_USDT=1000
|
||||
# Maximum daily trading volume in USDT
|
||||
MAX_DAILY_VOLUME_USDT=10000
|
||||
# Maximum allowed leverage
|
||||
MAX_LEVERAGE=20
|
||||
# Maximum position size as percentage of equity
|
||||
MAX_POSITION_SIZE_PCT=5
|
||||
|
||||
# ==========================================
|
||||
# Request Configuration
|
||||
# ==========================================
|
||||
# Timeout for requests to Binance (ms)
|
||||
REQUEST_TIMEOUT=10000
|
||||
# Maximum retries for failed requests
|
||||
MAX_RETRIES=3
|
||||
|
||||
# ==========================================
|
||||
# Logging
|
||||
# ==========================================
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE=logs/mcp-binance.log
|
||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
|
||||
# Misc
|
||||
*.tsbuildinfo
|
||||
57
Dockerfile
Normal file
57
Dockerfile
Normal file
@ -0,0 +1,57 @@
|
||||
# MCP Binance Connector Dockerfile
|
||||
# Trading Platform
|
||||
# Version: 1.0.0
|
||||
|
||||
# ==========================================
|
||||
# Build Stage
|
||||
# ==========================================
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy source and build
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build
|
||||
|
||||
# ==========================================
|
||||
# Production Stage
|
||||
# ==========================================
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install production dependencies only
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production && npm cache clean --force
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Create non-root user for security
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S mcpuser -u 1001 -G nodejs
|
||||
|
||||
# Create logs directory
|
||||
RUN mkdir -p logs && chown -R mcpuser:nodejs logs
|
||||
|
||||
# Switch to non-root user
|
||||
USER mcpuser
|
||||
|
||||
# Environment configuration
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3606
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3606
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3606/health || exit 1
|
||||
|
||||
# Start application
|
||||
CMD ["node", "dist/index.js"]
|
||||
345
README.md
Normal file
345
README.md
Normal file
@ -0,0 +1,345 @@
|
||||
# MCP Binance Connector
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Date:** 2026-01-04
|
||||
**System:** Trading Platform + NEXUS v3.4 + SIMCO
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
MCP Server that exposes Binance cryptocurrency exchange capabilities as tools for AI agents. This service enables AI agents to:
|
||||
|
||||
- Query market data (prices, order books, candles)
|
||||
- Monitor account balances
|
||||
- View and manage open orders
|
||||
- Execute trades (buy/sell with market, limit, stop orders)
|
||||
|
||||
Uses [CCXT](https://github.com/ccxt/ccxt) library for Binance API integration.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Navigate to the project
|
||||
cd /home/isem/workspace-v1/projects/trading-platform/apps/mcp-binance-connector
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Configure environment
|
||||
cp .env.example .env
|
||||
# Edit .env with your Binance API credentials
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | MCP Server port | 3606 |
|
||||
| `MCP_API_KEY` | API key for MCP authentication | - |
|
||||
| `BINANCE_API_KEY` | Binance API key | - |
|
||||
| `BINANCE_API_SECRET` | Binance API secret | - |
|
||||
| `BINANCE_TESTNET` | Use Binance testnet | true |
|
||||
| `MAX_ORDER_VALUE_USDT` | Max order value limit | 1000 |
|
||||
| `MAX_DAILY_VOLUME_USDT` | Max daily trading volume | 10000 |
|
||||
| `MAX_LEVERAGE` | Max leverage allowed | 20 |
|
||||
| `LOG_LEVEL` | Logging level | info |
|
||||
|
||||
### Example .env
|
||||
|
||||
```env
|
||||
PORT=3606
|
||||
BINANCE_API_KEY=your_api_key_here
|
||||
BINANCE_API_SECRET=your_api_secret_here
|
||||
BINANCE_TESTNET=true
|
||||
MAX_ORDER_VALUE_USDT=1000
|
||||
MAX_DAILY_VOLUME_USDT=10000
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Start Server
|
||||
|
||||
```bash
|
||||
# Development (with hot reload)
|
||||
npm run dev
|
||||
|
||||
# Production
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:3606/health
|
||||
```
|
||||
|
||||
### List Available Tools
|
||||
|
||||
```bash
|
||||
curl http://localhost:3606/tools
|
||||
```
|
||||
|
||||
### Execute a Tool
|
||||
|
||||
```bash
|
||||
# Get BTC price
|
||||
curl -X POST http://localhost:3606/tools/binance_get_ticker \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "BTCUSDT"}}'
|
||||
|
||||
# Get order book
|
||||
curl -X POST http://localhost:3606/tools/binance_get_orderbook \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "ETHUSDT", "limit": 10}}'
|
||||
|
||||
# Get candlestick data
|
||||
curl -X POST http://localhost:3606/tools/binance_get_klines \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "BTCUSDT", "interval": "1h", "limit": 24}}'
|
||||
|
||||
# Get account balance (requires API keys)
|
||||
curl -X POST http://localhost:3606/tools/binance_get_account \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {}}'
|
||||
|
||||
# Create order (requires API keys) - HIGH RISK
|
||||
curl -X POST http://localhost:3606/tools/binance_create_order \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "BTCUSDT", "side": "buy", "type": "market", "amount": 0.001}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP Tools Available
|
||||
|
||||
| Tool | Description | Risk Level |
|
||||
|------|-------------|------------|
|
||||
| `binance_get_ticker` | Get current price and 24h stats | LOW |
|
||||
| `binance_get_orderbook` | Get order book depth | LOW |
|
||||
| `binance_get_klines` | Get OHLCV candles | LOW |
|
||||
| `binance_get_account` | Get account balances | MEDIUM |
|
||||
| `binance_get_open_orders` | List open orders | MEDIUM |
|
||||
| `binance_create_order` | Create buy/sell order | HIGH (*) |
|
||||
| `binance_cancel_order` | Cancel pending order | MEDIUM |
|
||||
|
||||
(*) Tools marked with HIGH risk require explicit confirmation and pass through risk checks.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
mcp-binance-connector/
|
||||
├── README.md # This file
|
||||
├── package.json # Dependencies
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── .env.example # Environment template
|
||||
├── Dockerfile # Container configuration
|
||||
└── src/
|
||||
├── index.ts # Server entry point
|
||||
├── config.ts # Configuration management
|
||||
├── utils/
|
||||
│ └── logger.ts # Winston logger
|
||||
├── services/
|
||||
│ └── binance-client.ts # CCXT wrapper
|
||||
├── middleware/
|
||||
│ └── risk-check.ts # Pre-trade risk validation
|
||||
└── tools/
|
||||
├── index.ts # Tool registry
|
||||
├── market.ts # Market data tools
|
||||
├── account.ts # Account tools
|
||||
└── orders.ts # Order management tools
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Type Check
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker
|
||||
|
||||
### Build Image
|
||||
|
||||
```bash
|
||||
docker build -t mcp-binance-connector:1.0.0 .
|
||||
```
|
||||
|
||||
### Run Container
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mcp-binance-connector \
|
||||
-p 3606:3606 \
|
||||
-e BINANCE_API_KEY=your_key \
|
||||
-e BINANCE_API_SECRET=your_secret \
|
||||
-e BINANCE_TESTNET=true \
|
||||
mcp-binance-connector:1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Claude
|
||||
|
||||
### MCP Configuration
|
||||
|
||||
Add to your Claude/MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"binance": {
|
||||
"url": "http://localhost:3606",
|
||||
"transport": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Agent Prompts
|
||||
|
||||
```
|
||||
"What's the current Bitcoin price?"
|
||||
-> Uses binance_get_ticker({ symbol: "BTCUSDT" })
|
||||
|
||||
"Show me the ETH order book"
|
||||
-> Uses binance_get_orderbook({ symbol: "ETHUSDT" })
|
||||
|
||||
"Get the last 50 hourly candles for BTC"
|
||||
-> Uses binance_get_klines({ symbol: "BTCUSDT", interval: "1h", limit: 50 })
|
||||
|
||||
"Check my Binance balance"
|
||||
-> Uses binance_get_account()
|
||||
|
||||
"Buy 0.01 BTC at market price"
|
||||
-> Uses binance_create_order({ symbol: "BTCUSDT", side: "buy", type: "market", amount: 0.01 })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risk Management
|
||||
|
||||
The connector includes built-in risk checks:
|
||||
|
||||
1. **Maximum Order Value**: Orders exceeding `MAX_ORDER_VALUE_USDT` are rejected
|
||||
2. **Daily Volume Limit**: Trading stops when `MAX_DAILY_VOLUME_USDT` is reached
|
||||
3. **Balance Check**: Buy orders verify sufficient balance
|
||||
4. **Testnet Default**: Testnet is enabled by default for safety
|
||||
5. **High-Risk Confirmation**: Orders require explicit confirmation flag
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime
|
||||
- `express` - HTTP server
|
||||
- `ccxt` - Cryptocurrency exchange library
|
||||
- `zod` - Input validation
|
||||
- `winston` - Logging
|
||||
- `dotenv` - Environment configuration
|
||||
- `@modelcontextprotocol/sdk` - MCP protocol
|
||||
|
||||
### Development
|
||||
- `typescript` - Type safety
|
||||
- `ts-node-dev` - Development server
|
||||
- `jest` - Testing framework
|
||||
- `eslint` - Code linting
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Binance Account** with API keys (optional for public data)
|
||||
2. **Testnet API Keys** for testing (recommended)
|
||||
3. **Node.js** >= 20.0.0
|
||||
|
||||
### Getting Binance API Keys
|
||||
|
||||
1. Log into [Binance](https://www.binance.com)
|
||||
2. Go to API Management
|
||||
3. Create a new API key
|
||||
4. Enable Spot Trading permissions
|
||||
5. (Optional) For testnet: [Binance Testnet](https://testnet.binance.vision/)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot connect to Binance
|
||||
|
||||
```bash
|
||||
# Check connectivity
|
||||
curl https://api.binance.com/api/v3/ping
|
||||
|
||||
# If using testnet, check testnet connectivity
|
||||
curl https://testnet.binance.vision/api/v3/ping
|
||||
```
|
||||
|
||||
### Authentication errors
|
||||
|
||||
```bash
|
||||
# Verify API keys are set
|
||||
cat .env | grep BINANCE
|
||||
|
||||
# Check health endpoint for config status
|
||||
curl http://localhost:3606/health
|
||||
```
|
||||
|
||||
### Order rejected by risk check
|
||||
|
||||
The order may exceed configured limits. Check:
|
||||
- `MAX_ORDER_VALUE_USDT` - single order limit
|
||||
- `MAX_DAILY_VOLUME_USDT` - daily trading limit
|
||||
- Available balance for buy orders
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [MCP Protocol](https://modelcontextprotocol.io)
|
||||
- [CCXT Documentation](https://docs.ccxt.com)
|
||||
- [Binance API](https://binance-docs.github.io/apidocs/)
|
||||
- Architecture: `/docs/01-arquitectura/MCP-BINANCE-CONNECTOR-SPEC.md`
|
||||
- MT4 Connector: `/apps/mcp-mt4-connector/` (reference implementation)
|
||||
|
||||
---
|
||||
|
||||
**Maintained by:** @PERFIL_MCP_DEVELOPER
|
||||
**Project:** Trading Platform
|
||||
54
package.json
Normal file
54
package.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "mcp-binance-connector",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP Server for Binance trading operations via CCXT",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node-dev --respawn src/index.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"lint:fix": "eslint src/**/*.ts --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"health-check": "curl -s http://localhost:${PORT:-3606}/health || echo 'Server not running'"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"anthropic",
|
||||
"claude",
|
||||
"binance",
|
||||
"crypto",
|
||||
"trading",
|
||||
"ccxt"
|
||||
],
|
||||
"author": "Trading Platform Trading Platform",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"ccxt": "^4.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"winston": "^3.11.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
||||
"@typescript-eslint/parser": "^6.13.0",
|
||||
"eslint": "^8.54.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
159
src/config.ts
Normal file
159
src/config.ts
Normal file
@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Configuration Module
|
||||
*
|
||||
* Manages environment variables and creates Binance clients via CCXT.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author Trading Platform Trading Platform
|
||||
*/
|
||||
|
||||
import ccxt from 'ccxt';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
// ==========================================
|
||||
// Configuration Interface
|
||||
// ==========================================
|
||||
|
||||
export interface BinanceConfig {
|
||||
apiKey: string;
|
||||
apiSecret: string;
|
||||
testnet: boolean;
|
||||
futuresTestnet: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
export interface RiskConfig {
|
||||
maxOrderValueUsdt: number;
|
||||
maxDailyVolumeUsdt: number;
|
||||
maxLeverage: number;
|
||||
maxPositionSizePct: number;
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
port: number;
|
||||
nodeEnv: string;
|
||||
mcpApiKey: string;
|
||||
logLevel: string;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Configuration Loading
|
||||
// ==========================================
|
||||
|
||||
export const binanceConfig: BinanceConfig = {
|
||||
apiKey: process.env.BINANCE_API_KEY || '',
|
||||
apiSecret: process.env.BINANCE_API_SECRET || '',
|
||||
testnet: process.env.BINANCE_TESTNET === 'true',
|
||||
futuresTestnet: process.env.BINANCE_FUTURES_TESTNET === 'true',
|
||||
timeout: parseInt(process.env.REQUEST_TIMEOUT || '10000', 10),
|
||||
};
|
||||
|
||||
export const riskConfig: RiskConfig = {
|
||||
maxOrderValueUsdt: parseFloat(process.env.MAX_ORDER_VALUE_USDT || '1000'),
|
||||
maxDailyVolumeUsdt: parseFloat(process.env.MAX_DAILY_VOLUME_USDT || '10000'),
|
||||
maxLeverage: parseInt(process.env.MAX_LEVERAGE || '20', 10),
|
||||
maxPositionSizePct: parseFloat(process.env.MAX_POSITION_SIZE_PCT || '5'),
|
||||
};
|
||||
|
||||
export const serverConfig: ServerConfig = {
|
||||
port: parseInt(process.env.PORT || '3606', 10),
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
mcpApiKey: process.env.MCP_API_KEY || '',
|
||||
logLevel: process.env.LOG_LEVEL || 'info',
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// Binance Client Factory
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Create a Binance Spot client
|
||||
*/
|
||||
export function createBinanceSpotClient(): ccxt.binance {
|
||||
const isTestnet = binanceConfig.testnet;
|
||||
|
||||
const client = new ccxt.binance({
|
||||
apiKey: binanceConfig.apiKey,
|
||||
secret: binanceConfig.apiSecret,
|
||||
sandbox: isTestnet,
|
||||
options: {
|
||||
defaultType: 'spot',
|
||||
adjustForTimeDifference: true,
|
||||
},
|
||||
enableRateLimit: true,
|
||||
rateLimit: 100,
|
||||
timeout: binanceConfig.timeout,
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Binance Futures client
|
||||
*/
|
||||
export function createBinanceFuturesClient(): ccxt.binance {
|
||||
const isTestnet = binanceConfig.futuresTestnet;
|
||||
|
||||
const client = new ccxt.binance({
|
||||
apiKey: binanceConfig.apiKey,
|
||||
secret: binanceConfig.apiSecret,
|
||||
sandbox: isTestnet,
|
||||
options: {
|
||||
defaultType: 'future',
|
||||
adjustForTimeDifference: true,
|
||||
},
|
||||
enableRateLimit: true,
|
||||
rateLimit: 100,
|
||||
timeout: binanceConfig.timeout,
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Configuration Validation
|
||||
// ==========================================
|
||||
|
||||
export function validateConfig(): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Binance API keys are optional for public endpoints
|
||||
// but required for account/trading operations
|
||||
if (!binanceConfig.apiKey && serverConfig.nodeEnv === 'production') {
|
||||
errors.push('BINANCE_API_KEY is required in production');
|
||||
}
|
||||
|
||||
if (!binanceConfig.apiSecret && serverConfig.nodeEnv === 'production') {
|
||||
errors.push('BINANCE_API_SECRET is required in production');
|
||||
}
|
||||
|
||||
// Validate risk limits
|
||||
if (riskConfig.maxOrderValueUsdt <= 0) {
|
||||
errors.push('MAX_ORDER_VALUE_USDT must be positive');
|
||||
}
|
||||
|
||||
if (riskConfig.maxLeverage < 1 || riskConfig.maxLeverage > 125) {
|
||||
errors.push('MAX_LEVERAGE must be between 1 and 125');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Exports
|
||||
// ==========================================
|
||||
|
||||
export default {
|
||||
binance: binanceConfig,
|
||||
risk: riskConfig,
|
||||
server: serverConfig,
|
||||
createBinanceSpotClient,
|
||||
createBinanceFuturesClient,
|
||||
validateConfig,
|
||||
};
|
||||
332
src/index.ts
Normal file
332
src/index.ts
Normal file
@ -0,0 +1,332 @@
|
||||
/**
|
||||
* MCP Server: Binance Connector
|
||||
*
|
||||
* Exposes Binance trading capabilities as MCP tools for AI agents.
|
||||
* Uses CCXT library to communicate with Binance API.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author Trading Platform Trading Platform
|
||||
*/
|
||||
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import dotenv from 'dotenv';
|
||||
import { mcpToolSchemas, toolHandlers, getAllToolDefinitions, toolRequiresConfirmation, getToolRiskLevel } from './tools';
|
||||
import { getBinanceClient } from './services/binance-client';
|
||||
import { serverConfig, binanceConfig, validateConfig } from './config';
|
||||
import { logger } from './utils/logger';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = serverConfig.port;
|
||||
const SERVICE_NAME = 'mcp-binance-connector';
|
||||
const VERSION = '1.0.0';
|
||||
|
||||
// ==========================================
|
||||
// Middleware
|
||||
// ==========================================
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Request logging
|
||||
app.use((req: Request, _res: Response, next: NextFunction) => {
|
||||
logger.info(`${req.method} ${req.path}`, {
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// MCP API Key authentication (optional, for protected endpoints)
|
||||
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||
const mcpKey = req.headers['x-mcp-api-key'];
|
||||
|
||||
// Skip auth if MCP_API_KEY is not configured
|
||||
if (!serverConfig.mcpApiKey) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mcpKey !== serverConfig.mcpApiKey) {
|
||||
res.status(401).json({ error: 'Invalid MCP API key' });
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// Health & Status Endpoints
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Health check endpoint
|
||||
*/
|
||||
app.get('/health', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
const binanceConnected = await client.isConnected();
|
||||
const binanceConfigured = client.isConfigured();
|
||||
|
||||
res.json({
|
||||
status: binanceConnected ? 'healthy' : 'degraded',
|
||||
service: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
timestamp: new Date().toISOString(),
|
||||
testnet: binanceConfig.testnet,
|
||||
dependencies: {
|
||||
binance: binanceConnected ? 'connected' : 'disconnected',
|
||||
binanceApiConfigured: binanceConfigured,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.json({
|
||||
status: 'unhealthy',
|
||||
service: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
timestamp: new Date().toISOString(),
|
||||
testnet: binanceConfig.testnet,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* List available MCP tools
|
||||
*/
|
||||
app.get('/tools', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
tools: mcpToolSchemas.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
riskLevel: (tool as { riskLevel?: string }).riskLevel,
|
||||
requiresConfirmation: (tool as { requiresConfirmation?: boolean }).requiresConfirmation,
|
||||
})),
|
||||
count: mcpToolSchemas.length,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get specific tool schema
|
||||
*/
|
||||
app.get('/tools/:toolName', (req: Request, res: Response) => {
|
||||
const { toolName } = req.params;
|
||||
const tool = mcpToolSchemas.find((t) => t.name === toolName);
|
||||
|
||||
if (!tool) {
|
||||
res.status(404).json({
|
||||
error: `Tool '${toolName}' not found`,
|
||||
availableTools: mcpToolSchemas.map((t) => t.name),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(tool);
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// MCP Tool Execution Endpoints
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Execute an MCP tool
|
||||
* POST /tools/:toolName
|
||||
* Body: { parameters: {...} }
|
||||
*/
|
||||
app.post('/tools/:toolName', authMiddleware, async (req: Request, res: Response) => {
|
||||
const { toolName } = req.params;
|
||||
const { parameters = {} } = req.body;
|
||||
|
||||
// Validate tool exists
|
||||
const handler = toolHandlers[toolName];
|
||||
if (!handler) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Tool '${toolName}' not found`,
|
||||
availableTools: Object.keys(toolHandlers),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Executing tool: ${toolName}`, {
|
||||
parameters,
|
||||
riskLevel: getToolRiskLevel(toolName),
|
||||
requiresConfirmation: toolRequiresConfirmation(toolName),
|
||||
});
|
||||
|
||||
const result = await handler(parameters);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
tool: toolName,
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Tool execution failed: ${toolName}`, { error, parameters });
|
||||
|
||||
// Handle Zod validation errors
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: (error as { issues: unknown[] }).issues,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// MCP Protocol Endpoints (Standard)
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* MCP Initialize
|
||||
* Returns server capabilities
|
||||
*/
|
||||
app.post('/mcp/initialize', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MCP List Tools
|
||||
* Returns all available tools in MCP format
|
||||
*/
|
||||
app.post('/mcp/tools/list', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
tools: getAllToolDefinitions(),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MCP Call Tool
|
||||
* Execute a tool with parameters
|
||||
*/
|
||||
app.post('/mcp/tools/call', authMiddleware, async (req: Request, res: Response) => {
|
||||
const { name, arguments: args = {} } = req.body;
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
code: 'invalid_request',
|
||||
message: 'Tool name is required',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = toolHandlers[name];
|
||||
if (!handler) {
|
||||
res.status(404).json({
|
||||
error: {
|
||||
code: 'unknown_tool',
|
||||
message: `Tool '${name}' not found`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(args);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
// Handle Zod validation errors
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
code: 'invalid_params',
|
||||
message: 'Invalid tool parameters',
|
||||
data: (error as { issues: unknown[] }).issues,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
code: 'internal_error',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Error Handler
|
||||
// ==========================================
|
||||
|
||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
logger.error('Unhandled error', { error: err });
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Start Server
|
||||
// ==========================================
|
||||
|
||||
// Validate configuration before starting
|
||||
const configValidation = validateConfig();
|
||||
if (!configValidation.valid) {
|
||||
logger.warn('Configuration warnings', { errors: configValidation.errors });
|
||||
}
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log('');
|
||||
console.log('================================================================');
|
||||
console.log(' MCP Binance Connector - Trading Platform Trading Platform ');
|
||||
console.log('================================================================');
|
||||
console.log(` Service: ${SERVICE_NAME}`);
|
||||
console.log(` Version: ${VERSION}`);
|
||||
console.log(` Port: ${PORT}`);
|
||||
console.log(` Environment: ${serverConfig.nodeEnv}`);
|
||||
console.log(` Testnet Mode: ${binanceConfig.testnet ? 'ENABLED' : 'DISABLED'}`);
|
||||
console.log(` API Configured: ${binanceConfig.apiKey ? 'Yes' : 'No'}`);
|
||||
console.log('----------------------------------------------------------------');
|
||||
console.log(' Endpoints:');
|
||||
console.log(` - Health: http://localhost:${PORT}/health`);
|
||||
console.log(` - Tools: http://localhost:${PORT}/tools`);
|
||||
console.log('----------------------------------------------------------------');
|
||||
console.log(' MCP Tools Available:');
|
||||
mcpToolSchemas.forEach((tool) => {
|
||||
const risk = (tool as { riskLevel?: string }).riskLevel || 'N/A';
|
||||
const confirm = (tool as { requiresConfirmation?: boolean }).requiresConfirmation ? ' (!)' : '';
|
||||
console.log(` - ${tool.name} [${risk}]${confirm}`);
|
||||
});
|
||||
console.log('================================================================');
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Graceful Shutdown
|
||||
// ==========================================
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('Received SIGTERM, shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('Received SIGINT, shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
export default app;
|
||||
209
src/middleware/risk-check.ts
Normal file
209
src/middleware/risk-check.ts
Normal file
@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Risk Check Middleware
|
||||
*
|
||||
* Pre-trade risk validation to ensure orders comply with risk limits.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author Trading Platform Trading Platform
|
||||
*/
|
||||
|
||||
import { riskConfig } from '../config';
|
||||
import { getBinanceClient } from '../services/binance-client';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// ==========================================
|
||||
// Types
|
||||
// ==========================================
|
||||
|
||||
export interface RiskCheckParams {
|
||||
symbol: string;
|
||||
side: 'buy' | 'sell';
|
||||
amount: number;
|
||||
price?: number;
|
||||
}
|
||||
|
||||
export interface RiskCheckResult {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
warnings?: string[];
|
||||
orderValue?: number;
|
||||
}
|
||||
|
||||
// Daily volume tracking (in-memory, resets on restart)
|
||||
let dailyVolume = 0;
|
||||
let lastVolumeResetDate = new Date().toDateString();
|
||||
|
||||
// ==========================================
|
||||
// Risk Check Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Reset daily volume counter at midnight
|
||||
*/
|
||||
function checkAndResetDailyVolume(): void {
|
||||
const today = new Date().toDateString();
|
||||
if (today !== lastVolumeResetDate) {
|
||||
dailyVolume = 0;
|
||||
lastVolumeResetDate = today;
|
||||
logger.info('Daily volume counter reset');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the quote asset from a symbol (e.g., USDT from BTCUSDT)
|
||||
*/
|
||||
function getQuoteAsset(symbol: string): string {
|
||||
const stablecoins = ['USDT', 'BUSD', 'USDC', 'TUSD', 'DAI'];
|
||||
for (const stable of stablecoins) {
|
||||
if (symbol.endsWith(stable)) {
|
||||
return stable;
|
||||
}
|
||||
}
|
||||
return 'USDT';
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform comprehensive risk check before order execution
|
||||
*/
|
||||
export async function performRiskCheck(params: RiskCheckParams): Promise<RiskCheckResult> {
|
||||
const { symbol, side, amount, price } = params;
|
||||
const warnings: string[] = [];
|
||||
|
||||
try {
|
||||
checkAndResetDailyVolume();
|
||||
|
||||
const client = getBinanceClient();
|
||||
|
||||
// 1. Get current price if not provided
|
||||
let orderPrice = price;
|
||||
if (!orderPrice) {
|
||||
try {
|
||||
orderPrice = await client.getCurrentPrice(symbol);
|
||||
} catch (error) {
|
||||
logger.warn(`Could not fetch current price for ${symbol}, using amount as value estimate`);
|
||||
orderPrice = 1; // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Calculate order value in quote currency (usually USDT)
|
||||
const orderValue = amount * orderPrice;
|
||||
|
||||
// 3. Check maximum order value
|
||||
if (orderValue > riskConfig.maxOrderValueUsdt) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Order value ${orderValue.toFixed(2)} USDT exceeds maximum ${riskConfig.maxOrderValueUsdt} USDT`,
|
||||
orderValue,
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Check daily volume limit
|
||||
if (dailyVolume + orderValue > riskConfig.maxDailyVolumeUsdt) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Daily volume limit reached. Current: ${dailyVolume.toFixed(2)} USDT, Limit: ${riskConfig.maxDailyVolumeUsdt} USDT`,
|
||||
orderValue,
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Check if API keys are configured for trading
|
||||
if (!client.isConfigured()) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Binance API keys are not configured. Cannot execute trades.',
|
||||
orderValue,
|
||||
};
|
||||
}
|
||||
|
||||
// 6. Verify we can connect to Binance
|
||||
const connected = await client.isConnected();
|
||||
if (!connected) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Cannot connect to Binance. Please check your network and API configuration.',
|
||||
orderValue,
|
||||
};
|
||||
}
|
||||
|
||||
// 7. Check balance for buy orders (if we have account access)
|
||||
if (side === 'buy') {
|
||||
try {
|
||||
const account = await client.getAccount();
|
||||
const quoteAsset = getQuoteAsset(symbol);
|
||||
const quoteBalance = account.balances.find(b => b.asset === quoteAsset);
|
||||
const available = quoteBalance?.free ?? 0;
|
||||
|
||||
if (available < orderValue) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Insufficient ${quoteAsset} balance. Required: ${orderValue.toFixed(2)}, Available: ${available.toFixed(2)}`,
|
||||
orderValue,
|
||||
};
|
||||
}
|
||||
|
||||
// Warning if using more than 50% of available balance
|
||||
if (orderValue > available * 0.5) {
|
||||
warnings.push(`This order uses ${((orderValue / available) * 100).toFixed(1)}% of your available ${quoteAsset}`);
|
||||
}
|
||||
} catch (error) {
|
||||
warnings.push('Could not verify account balance');
|
||||
logger.warn('Balance check failed', { error });
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Check for large order warning
|
||||
if (orderValue > riskConfig.maxOrderValueUsdt * 0.5) {
|
||||
warnings.push(`Large order: ${orderValue.toFixed(2)} USDT (${((orderValue / riskConfig.maxOrderValueUsdt) * 100).toFixed(0)}% of max)`);
|
||||
}
|
||||
|
||||
// All checks passed
|
||||
return {
|
||||
allowed: true,
|
||||
orderValue,
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Risk check failed', { error, params });
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Risk check error: ${message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record executed trade volume
|
||||
*/
|
||||
export function recordTradeVolume(orderValue: number): void {
|
||||
checkAndResetDailyVolume();
|
||||
dailyVolume += orderValue;
|
||||
logger.info(`Trade recorded. Daily volume: ${dailyVolume.toFixed(2)} USDT`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current daily volume
|
||||
*/
|
||||
export function getDailyVolume(): number {
|
||||
checkAndResetDailyVolume();
|
||||
return dailyVolume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining daily volume allowance
|
||||
*/
|
||||
export function getRemainingDailyVolume(): number {
|
||||
checkAndResetDailyVolume();
|
||||
return Math.max(0, riskConfig.maxDailyVolumeUsdt - dailyVolume);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Exports
|
||||
// ==========================================
|
||||
|
||||
export default {
|
||||
performRiskCheck,
|
||||
recordTradeVolume,
|
||||
getDailyVolume,
|
||||
getRemainingDailyVolume,
|
||||
};
|
||||
471
src/services/binance-client.ts
Normal file
471
src/services/binance-client.ts
Normal file
@ -0,0 +1,471 @@
|
||||
/**
|
||||
* Binance Client Service
|
||||
*
|
||||
* CCXT wrapper for Binance operations.
|
||||
* Provides a unified interface for both Spot and Futures trading.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author Trading Platform Trading Platform
|
||||
*/
|
||||
|
||||
import ccxt, { Ticker, OrderBook, OHLCV, Balance, Order, Trade } from 'ccxt';
|
||||
import { createBinanceSpotClient, createBinanceFuturesClient, binanceConfig } from '../config';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// ==========================================
|
||||
// Types
|
||||
// ==========================================
|
||||
|
||||
export interface BinanceTicker {
|
||||
symbol: string;
|
||||
price: number;
|
||||
bid: number;
|
||||
ask: number;
|
||||
high24h: number;
|
||||
low24h: number;
|
||||
volume24h: number;
|
||||
change24h: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface BinanceOrderBook {
|
||||
symbol: string;
|
||||
bids: [number, number][];
|
||||
asks: [number, number][];
|
||||
spread: number;
|
||||
spreadPercentage: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface BinanceKline {
|
||||
timestamp: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export interface BinanceAccountBalance {
|
||||
asset: string;
|
||||
free: number;
|
||||
locked: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface BinanceAccount {
|
||||
accountType: string;
|
||||
balances: BinanceAccountBalance[];
|
||||
canTrade: boolean;
|
||||
canWithdraw: boolean;
|
||||
updateTime: number;
|
||||
}
|
||||
|
||||
export interface BinanceOrder {
|
||||
id: string;
|
||||
symbol: string;
|
||||
side: string;
|
||||
type: string;
|
||||
price: number | null;
|
||||
amount: number;
|
||||
filled: number;
|
||||
remaining: number;
|
||||
status: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface CreateOrderParams {
|
||||
symbol: string;
|
||||
side: 'buy' | 'sell';
|
||||
type: 'market' | 'limit' | 'stop_loss' | 'take_profit';
|
||||
amount: number;
|
||||
price?: number;
|
||||
stopPrice?: number;
|
||||
}
|
||||
|
||||
export interface OrderResult {
|
||||
success: boolean;
|
||||
order?: BinanceOrder;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Binance Client Class
|
||||
// ==========================================
|
||||
|
||||
export class BinanceClient {
|
||||
private spotClient: ccxt.binance;
|
||||
private futuresClient: ccxt.binance;
|
||||
private marketsLoaded: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.spotClient = createBinanceSpotClient();
|
||||
this.futuresClient = createBinanceFuturesClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if client is properly configured
|
||||
*/
|
||||
isConfigured(): boolean {
|
||||
return binanceConfig.apiKey !== '' && binanceConfig.apiSecret !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connectivity to Binance
|
||||
*/
|
||||
async isConnected(): Promise<boolean> {
|
||||
try {
|
||||
await this.spotClient.fetchTime();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load markets if not already loaded
|
||||
*/
|
||||
private async ensureMarketsLoaded(): Promise<void> {
|
||||
if (!this.marketsLoaded) {
|
||||
await this.spotClient.loadMarkets();
|
||||
this.marketsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Market Data Methods
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get ticker for a symbol
|
||||
*/
|
||||
async getTicker(symbol: string): Promise<BinanceTicker> {
|
||||
try {
|
||||
await this.ensureMarketsLoaded();
|
||||
const ticker: Ticker = await this.spotClient.fetchTicker(symbol);
|
||||
|
||||
return {
|
||||
symbol: ticker.symbol,
|
||||
price: ticker.last ?? 0,
|
||||
bid: ticker.bid ?? 0,
|
||||
ask: ticker.ask ?? 0,
|
||||
high24h: ticker.high ?? 0,
|
||||
low24h: ticker.low ?? 0,
|
||||
volume24h: ticker.baseVolume ?? 0,
|
||||
change24h: ticker.percentage ?? 0,
|
||||
timestamp: ticker.timestamp ?? Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get ticker for ${symbol}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order book for a symbol
|
||||
*/
|
||||
async getOrderBook(symbol: string, limit: number = 20): Promise<BinanceOrderBook> {
|
||||
try {
|
||||
await this.ensureMarketsLoaded();
|
||||
const orderbook: OrderBook = await this.spotClient.fetchOrderBook(symbol, limit);
|
||||
|
||||
const topBid = orderbook.bids[0]?.[0] ?? 0;
|
||||
const topAsk = orderbook.asks[0]?.[0] ?? 0;
|
||||
const spread = topAsk - topBid;
|
||||
const spreadPercentage = topBid > 0 ? (spread / topBid) * 100 : 0;
|
||||
|
||||
return {
|
||||
symbol,
|
||||
bids: orderbook.bids.slice(0, limit) as [number, number][],
|
||||
asks: orderbook.asks.slice(0, limit) as [number, number][],
|
||||
spread,
|
||||
spreadPercentage,
|
||||
timestamp: orderbook.timestamp ?? Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get order book for ${symbol}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OHLCV (klines/candles) for a symbol
|
||||
*/
|
||||
async getKlines(
|
||||
symbol: string,
|
||||
interval: string = '5m',
|
||||
limit: number = 100
|
||||
): Promise<BinanceKline[]> {
|
||||
try {
|
||||
await this.ensureMarketsLoaded();
|
||||
const ohlcv: OHLCV[] = await this.spotClient.fetchOHLCV(symbol, interval, undefined, limit);
|
||||
|
||||
return ohlcv.map((candle) => ({
|
||||
timestamp: candle[0] as number,
|
||||
open: candle[1] as number,
|
||||
high: candle[2] as number,
|
||||
low: candle[3] as number,
|
||||
close: candle[4] as number,
|
||||
volume: candle[5] as number,
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get klines for ${symbol}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Account Methods
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get account balance
|
||||
*/
|
||||
async getAccount(): Promise<BinanceAccount> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
const balance: Balance = await this.spotClient.fetchBalance();
|
||||
|
||||
// Filter non-zero balances
|
||||
const balances: BinanceAccountBalance[] = Object.entries(balance.total)
|
||||
.filter(([_, amount]) => (amount as number) > 0)
|
||||
.map(([asset, total]) => ({
|
||||
asset,
|
||||
free: (balance.free[asset] as number) ?? 0,
|
||||
locked: (balance.used[asset] as number) ?? 0,
|
||||
total: total as number,
|
||||
}));
|
||||
|
||||
return {
|
||||
accountType: 'SPOT',
|
||||
balances,
|
||||
canTrade: true,
|
||||
canWithdraw: true,
|
||||
updateTime: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to get account info', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get open orders
|
||||
*/
|
||||
async getOpenOrders(symbol?: string): Promise<BinanceOrder[]> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
const orders: Order[] = await this.spotClient.fetchOpenOrders(symbol);
|
||||
|
||||
return orders.map((order) => ({
|
||||
id: order.id,
|
||||
symbol: order.symbol,
|
||||
side: order.side,
|
||||
type: order.type,
|
||||
price: order.price,
|
||||
amount: order.amount,
|
||||
filled: order.filled,
|
||||
remaining: order.remaining,
|
||||
status: order.status,
|
||||
createdAt: order.timestamp ?? Date.now(),
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Failed to get open orders', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trade history
|
||||
*/
|
||||
async getTradeHistory(symbol: string, limit: number = 50): Promise<Trade[]> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
return await this.spotClient.fetchMyTrades(symbol, undefined, limit);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get trade history for ${symbol}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Order Methods
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Create a new order
|
||||
*/
|
||||
async createOrder(params: CreateOrderParams): Promise<OrderResult> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
await this.ensureMarketsLoaded();
|
||||
|
||||
let order: Order;
|
||||
|
||||
switch (params.type) {
|
||||
case 'market':
|
||||
order = await this.spotClient.createMarketOrder(
|
||||
params.symbol,
|
||||
params.side,
|
||||
params.amount
|
||||
);
|
||||
break;
|
||||
|
||||
case 'limit':
|
||||
if (!params.price) {
|
||||
return { success: false, error: 'Price is required for limit orders' };
|
||||
}
|
||||
order = await this.spotClient.createLimitOrder(
|
||||
params.symbol,
|
||||
params.side,
|
||||
params.amount,
|
||||
params.price
|
||||
);
|
||||
break;
|
||||
|
||||
case 'stop_loss':
|
||||
if (!params.stopPrice) {
|
||||
return { success: false, error: 'Stop price is required for stop loss orders' };
|
||||
}
|
||||
order = await this.spotClient.createOrder(
|
||||
params.symbol,
|
||||
'stop_loss',
|
||||
params.side,
|
||||
params.amount,
|
||||
undefined,
|
||||
{ stopPrice: params.stopPrice }
|
||||
);
|
||||
break;
|
||||
|
||||
case 'take_profit':
|
||||
if (!params.stopPrice) {
|
||||
return { success: false, error: 'Stop price is required for take profit orders' };
|
||||
}
|
||||
order = await this.spotClient.createOrder(
|
||||
params.symbol,
|
||||
'take_profit',
|
||||
params.side,
|
||||
params.amount,
|
||||
undefined,
|
||||
{ stopPrice: params.stopPrice }
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
return { success: false, error: `Unsupported order type: ${params.type}` };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
order: {
|
||||
id: order.id,
|
||||
symbol: order.symbol,
|
||||
side: order.side,
|
||||
type: order.type,
|
||||
price: order.price ?? order.average ?? null,
|
||||
amount: order.amount,
|
||||
filled: order.filled,
|
||||
remaining: order.remaining,
|
||||
status: order.status,
|
||||
createdAt: order.timestamp ?? Date.now(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Failed to create order', { error, params });
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an order
|
||||
*/
|
||||
async cancelOrder(orderId: string, symbol: string): Promise<OrderResult> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
const result = await this.spotClient.cancelOrder(orderId, symbol);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
order: {
|
||||
id: result.id,
|
||||
symbol: result.symbol,
|
||||
side: result.side,
|
||||
type: result.type,
|
||||
price: result.price,
|
||||
amount: result.amount,
|
||||
filled: result.filled,
|
||||
remaining: result.remaining,
|
||||
status: 'CANCELLED',
|
||||
createdAt: result.timestamp ?? Date.now(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Failed to cancel order', { error, orderId, symbol });
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all orders for a symbol
|
||||
*/
|
||||
async cancelAllOrders(symbol: string): Promise<{ success: boolean; cancelledCount: number; error?: string }> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
const result = await this.spotClient.cancelAllOrders(symbol);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
cancelledCount: Array.isArray(result) ? result.length : 0,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Failed to cancel all orders', { error, symbol });
|
||||
return { success: false, cancelledCount: 0, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current price for a symbol (helper method)
|
||||
*/
|
||||
async getCurrentPrice(symbol: string): Promise<number> {
|
||||
const ticker = await this.getTicker(symbol);
|
||||
return ticker.price;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Singleton Instance
|
||||
// ==========================================
|
||||
|
||||
let clientInstance: BinanceClient | null = null;
|
||||
|
||||
export function getBinanceClient(): BinanceClient {
|
||||
if (!clientInstance) {
|
||||
clientInstance = new BinanceClient();
|
||||
}
|
||||
return clientInstance;
|
||||
}
|
||||
|
||||
export function resetBinanceClient(): void {
|
||||
clientInstance = null;
|
||||
}
|
||||
265
src/tools/account.ts
Normal file
265
src/tools/account.ts
Normal file
@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Binance Account Tools
|
||||
*
|
||||
* - binance_get_account: Get account balance and status
|
||||
* - binance_get_open_orders: Get all open orders
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author Trading Platform Trading Platform
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getBinanceClient, BinanceAccount, BinanceOrder } from '../services/binance-client';
|
||||
|
||||
// ==========================================
|
||||
// binance_get_account
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_get_account
|
||||
* Get account balance and status
|
||||
*/
|
||||
export const binanceGetAccountSchema = {
|
||||
name: 'binance_get_account',
|
||||
description: 'Get Binance account balance and status. Shows all assets with non-zero balance.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {},
|
||||
required: [] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const BinanceGetAccountInputSchema = z.object({});
|
||||
|
||||
export type BinanceGetAccountInput = z.infer<typeof BinanceGetAccountInputSchema>;
|
||||
|
||||
export interface BinanceGetAccountResult {
|
||||
success: boolean;
|
||||
data?: BinanceAccount & { totalUsdtEstimate?: number };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_get_account(
|
||||
_params: BinanceGetAccountInput
|
||||
): Promise<BinanceGetAccountResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
|
||||
if (!client.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Binance API keys are not configured',
|
||||
};
|
||||
}
|
||||
|
||||
const connected = await client.isConnected();
|
||||
if (!connected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cannot connect to Binance. Please check your network.',
|
||||
};
|
||||
}
|
||||
|
||||
const account = await client.getAccount();
|
||||
|
||||
// Estimate total value in USDT
|
||||
let totalUsdtEstimate = 0;
|
||||
for (const balance of account.balances) {
|
||||
if (balance.asset === 'USDT' || balance.asset === 'BUSD' || balance.asset === 'USDC') {
|
||||
totalUsdtEstimate += balance.total;
|
||||
} else if (balance.total > 0) {
|
||||
try {
|
||||
const price = await client.getCurrentPrice(`${balance.asset}USDT`);
|
||||
totalUsdtEstimate += balance.total * price;
|
||||
} catch {
|
||||
// Skip if no USDT pair exists
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...account,
|
||||
totalUsdtEstimate,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceGetAccount(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceGetAccountInputSchema.parse(params);
|
||||
const result = await binance_get_account(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
|
||||
// Sort balances by total value
|
||||
const sortedBalances = [...d.balances].sort((a, b) => {
|
||||
// USDT first, then by total
|
||||
if (a.asset === 'USDT') return -1;
|
||||
if (b.asset === 'USDT') return 1;
|
||||
return b.total - a.total;
|
||||
});
|
||||
|
||||
let balancesStr = sortedBalances
|
||||
.slice(0, 20) // Top 20 assets
|
||||
.map((b) => {
|
||||
const lockedStr = b.locked > 0 ? ` (Locked: ${b.locked.toFixed(8)})` : '';
|
||||
return ` ${b.asset.padEnd(8)} Free: ${b.free.toFixed(8)}${lockedStr}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const formattedOutput = `
|
||||
Binance Account Information
|
||||
${'='.repeat(35)}
|
||||
Account Type: ${d.accountType}
|
||||
Can Trade: ${d.canTrade ? 'Yes' : 'No'}
|
||||
Can Withdraw: ${d.canWithdraw ? 'Yes' : 'No'}
|
||||
|
||||
Estimated Total Value
|
||||
---------------------
|
||||
~$${d.totalUsdtEstimate?.toFixed(2) ?? 'N/A'} USDT
|
||||
|
||||
Asset Balances (${d.balances.length} with balance)
|
||||
${'='.repeat(35)}
|
||||
${balancesStr}
|
||||
${d.balances.length > 20 ? `\n ... and ${d.balances.length - 20} more assets` : ''}
|
||||
|
||||
Last Update: ${new Date(d.updateTime).toISOString()}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// binance_get_open_orders
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_get_open_orders
|
||||
* Get all open (pending) orders
|
||||
*/
|
||||
export const binanceGetOpenOrdersSchema = {
|
||||
name: 'binance_get_open_orders',
|
||||
description: 'Get all open (pending) orders. Optionally filter by symbol.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Optional: Filter by trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
},
|
||||
required: [] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const BinanceGetOpenOrdersInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()).optional(),
|
||||
});
|
||||
|
||||
export type BinanceGetOpenOrdersInput = z.infer<typeof BinanceGetOpenOrdersInputSchema>;
|
||||
|
||||
export interface BinanceGetOpenOrdersResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
orders: BinanceOrder[];
|
||||
count: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_get_open_orders(
|
||||
params: BinanceGetOpenOrdersInput
|
||||
): Promise<BinanceGetOpenOrdersResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
|
||||
if (!client.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Binance API keys are not configured',
|
||||
};
|
||||
}
|
||||
|
||||
const orders = await client.getOpenOrders(params.symbol);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
orders,
|
||||
count: orders.length,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceGetOpenOrders(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceGetOpenOrdersInputSchema.parse(params);
|
||||
const result = await binance_get_open_orders(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
|
||||
if (d.count === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No open orders${validatedParams.symbol ? ` for ${validatedParams.symbol}` : ''}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
let ordersStr = d.orders
|
||||
.map((o) => {
|
||||
const priceStr = o.price ? `$${o.price.toFixed(8)}` : 'MARKET';
|
||||
const filledPct = o.amount > 0 ? ((o.filled / o.amount) * 100).toFixed(1) : '0';
|
||||
return ` #${o.id}
|
||||
Symbol: ${o.symbol} | ${o.side.toUpperCase()} | ${o.type.toUpperCase()}
|
||||
Price: ${priceStr} | Amount: ${o.amount.toFixed(8)}
|
||||
Filled: ${o.filled.toFixed(8)} (${filledPct}%) | Remaining: ${o.remaining.toFixed(8)}
|
||||
Status: ${o.status} | Created: ${new Date(o.createdAt).toISOString()}`;
|
||||
})
|
||||
.join('\n\n');
|
||||
|
||||
const formattedOutput = `
|
||||
Open Orders${validatedParams.symbol ? ` - ${validatedParams.symbol}` : ''}
|
||||
${'='.repeat(35)}
|
||||
Total Orders: ${d.count}
|
||||
|
||||
${ordersStr}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
288
src/tools/index.ts
Normal file
288
src/tools/index.ts
Normal file
@ -0,0 +1,288 @@
|
||||
/**
|
||||
* MCP Tools Index
|
||||
*
|
||||
* Exports all Binance MCP tools and their schemas for registration
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author Trading Platform Trading Platform
|
||||
*/
|
||||
|
||||
// Import handlers for use in toolHandlers map
|
||||
import { handleBinanceGetTicker, handleBinanceGetOrderbook, handleBinanceGetKlines } from './market';
|
||||
import { handleBinanceGetAccount, handleBinanceGetOpenOrders } from './account';
|
||||
import { handleBinanceCreateOrder, handleBinanceCancelOrder } from './orders';
|
||||
|
||||
// ==========================================
|
||||
// Market Tools Exports
|
||||
// ==========================================
|
||||
|
||||
export {
|
||||
binanceGetTickerSchema,
|
||||
binance_get_ticker,
|
||||
handleBinanceGetTicker,
|
||||
BinanceGetTickerInputSchema,
|
||||
type BinanceGetTickerInput,
|
||||
type BinanceGetTickerResult,
|
||||
binanceGetOrderbookSchema,
|
||||
binance_get_orderbook,
|
||||
handleBinanceGetOrderbook,
|
||||
BinanceGetOrderbookInputSchema,
|
||||
type BinanceGetOrderbookInput,
|
||||
type BinanceGetOrderbookResult,
|
||||
binanceGetKlinesSchema,
|
||||
binance_get_klines,
|
||||
handleBinanceGetKlines,
|
||||
BinanceGetKlinesInputSchema,
|
||||
type BinanceGetKlinesInput,
|
||||
type BinanceGetKlinesResult,
|
||||
} from './market';
|
||||
|
||||
// ==========================================
|
||||
// Account Tools Exports
|
||||
// ==========================================
|
||||
|
||||
export {
|
||||
binanceGetAccountSchema,
|
||||
binance_get_account,
|
||||
handleBinanceGetAccount,
|
||||
BinanceGetAccountInputSchema,
|
||||
type BinanceGetAccountInput,
|
||||
type BinanceGetAccountResult,
|
||||
binanceGetOpenOrdersSchema,
|
||||
binance_get_open_orders,
|
||||
handleBinanceGetOpenOrders,
|
||||
BinanceGetOpenOrdersInputSchema,
|
||||
type BinanceGetOpenOrdersInput,
|
||||
type BinanceGetOpenOrdersResult,
|
||||
} from './account';
|
||||
|
||||
// ==========================================
|
||||
// Order Tools Exports
|
||||
// ==========================================
|
||||
|
||||
export {
|
||||
binanceCreateOrderSchema,
|
||||
binance_create_order,
|
||||
handleBinanceCreateOrder,
|
||||
BinanceCreateOrderInputSchema,
|
||||
type BinanceCreateOrderInput,
|
||||
type BinanceCreateOrderResult,
|
||||
binanceCancelOrderSchema,
|
||||
binance_cancel_order,
|
||||
handleBinanceCancelOrder,
|
||||
BinanceCancelOrderInputSchema,
|
||||
type BinanceCancelOrderInput,
|
||||
type BinanceCancelOrderResult,
|
||||
} from './orders';
|
||||
|
||||
// ==========================================
|
||||
// Tool Registry
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* All available MCP tools with their schemas
|
||||
* Follows MCP protocol format
|
||||
*/
|
||||
export const mcpToolSchemas = [
|
||||
// Market Data Tools (Low Risk)
|
||||
{
|
||||
name: 'binance_get_ticker',
|
||||
description: 'Get the current price and 24-hour statistics for a Binance trading pair',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT, BNBUSDT)',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
{
|
||||
name: 'binance_get_orderbook',
|
||||
description: 'Get the order book (bids and asks) with the specified depth for a trading pair',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Order book depth (5, 10, 20, 50, or 100). Default: 20',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
{
|
||||
name: 'binance_get_klines',
|
||||
description: 'Get historical candlestick (OHLCV) data for technical analysis',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
interval: {
|
||||
type: 'string',
|
||||
description: 'Candle interval: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w. Default: 5m',
|
||||
enum: ['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w'],
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of candles to retrieve (max 500). Default: 100',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
|
||||
// Account Tools (Medium Risk)
|
||||
{
|
||||
name: 'binance_get_account',
|
||||
description: 'Get Binance account balance and status. Shows all assets with non-zero balance.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {},
|
||||
required: [] as string[],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
{
|
||||
name: 'binance_get_open_orders',
|
||||
description: 'Get all open (pending) orders. Optionally filter by symbol.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Optional: Filter by trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
},
|
||||
required: [] as string[],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
|
||||
// Order Tools (High Risk)
|
||||
{
|
||||
name: 'binance_create_order',
|
||||
description: 'Create a new buy or sell order on Binance. HIGH RISK - Ensure you validate with the user before executing.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT)',
|
||||
},
|
||||
side: {
|
||||
type: 'string',
|
||||
enum: ['buy', 'sell'],
|
||||
description: 'Order direction: buy or sell',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['market', 'limit', 'stop_loss', 'take_profit'],
|
||||
description: 'Order type. Default: market',
|
||||
},
|
||||
amount: {
|
||||
type: 'number',
|
||||
description: 'Amount of the base asset to buy/sell',
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
description: 'Price per unit (required for limit orders)',
|
||||
},
|
||||
stopPrice: {
|
||||
type: 'number',
|
||||
description: 'Stop price (required for stop_loss and take_profit orders)',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'side', 'amount'] as string[],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
requiresConfirmation: true,
|
||||
},
|
||||
{
|
||||
name: 'binance_cancel_order',
|
||||
description: 'Cancel a pending order by order ID and symbol',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
orderId: {
|
||||
type: 'string',
|
||||
description: 'Order ID to cancel',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'orderId'] as string[],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Tool handler routing map
|
||||
* Maps tool names to their handler functions
|
||||
*/
|
||||
export const toolHandlers: Record<
|
||||
string,
|
||||
(params: unknown) => Promise<{ content: Array<{ type: string; text: string }> }>
|
||||
> = {
|
||||
// Market tools
|
||||
binance_get_ticker: handleBinanceGetTicker,
|
||||
binance_get_orderbook: handleBinanceGetOrderbook,
|
||||
binance_get_klines: handleBinanceGetKlines,
|
||||
|
||||
// Account tools
|
||||
binance_get_account: handleBinanceGetAccount,
|
||||
binance_get_open_orders: handleBinanceGetOpenOrders,
|
||||
|
||||
// Order tools
|
||||
binance_create_order: handleBinanceCreateOrder,
|
||||
binance_cancel_order: handleBinanceCancelOrder,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all tool definitions for MCP protocol
|
||||
*/
|
||||
export function getAllToolDefinitions() {
|
||||
return mcpToolSchemas.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool by name
|
||||
*/
|
||||
export function getToolByName(name: string) {
|
||||
return mcpToolSchemas.find((tool) => tool.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool requires confirmation
|
||||
*/
|
||||
export function toolRequiresConfirmation(name: string): boolean {
|
||||
const tool = mcpToolSchemas.find((t) => t.name === name);
|
||||
return (tool as { requiresConfirmation?: boolean })?.requiresConfirmation === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool risk level
|
||||
*/
|
||||
export function getToolRiskLevel(name: string): string {
|
||||
const tool = mcpToolSchemas.find((t) => t.name === name);
|
||||
return (tool as { riskLevel?: string })?.riskLevel ?? 'UNKNOWN';
|
||||
}
|
||||
392
src/tools/market.ts
Normal file
392
src/tools/market.ts
Normal file
@ -0,0 +1,392 @@
|
||||
/**
|
||||
* Binance Market Data Tools
|
||||
*
|
||||
* - binance_get_ticker: Get current price and 24h stats
|
||||
* - binance_get_orderbook: Get order book depth
|
||||
* - binance_get_klines: Get OHLCV candles
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author Trading Platform Trading Platform
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getBinanceClient, BinanceTicker, BinanceOrderBook, BinanceKline } from '../services/binance-client';
|
||||
|
||||
// ==========================================
|
||||
// binance_get_ticker
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_get_ticker
|
||||
* Get current price and 24h statistics for a trading pair
|
||||
*/
|
||||
export const binanceGetTickerSchema = {
|
||||
name: 'binance_get_ticker',
|
||||
description: 'Get the current price and 24-hour statistics for a Binance trading pair',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT, BNBUSDT)',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const BinanceGetTickerInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
||||
});
|
||||
|
||||
export type BinanceGetTickerInput = z.infer<typeof BinanceGetTickerInputSchema>;
|
||||
|
||||
export interface BinanceGetTickerResult {
|
||||
success: boolean;
|
||||
data?: BinanceTicker;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_get_ticker(
|
||||
params: BinanceGetTickerInput
|
||||
): Promise<BinanceGetTickerResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
const ticker = await client.getTicker(params.symbol);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: ticker,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceGetTicker(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceGetTickerInputSchema.parse(params);
|
||||
const result = await binance_get_ticker(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
const changeSymbol = d.change24h >= 0 ? '+' : '';
|
||||
|
||||
const formattedOutput = `
|
||||
Binance Ticker: ${d.symbol}
|
||||
${'='.repeat(35)}
|
||||
Current Price: $${d.price.toFixed(getPriceDecimals(d.symbol))}
|
||||
Bid: $${d.bid.toFixed(getPriceDecimals(d.symbol))}
|
||||
Ask: $${d.ask.toFixed(getPriceDecimals(d.symbol))}
|
||||
|
||||
24h Statistics
|
||||
--------------
|
||||
High: $${d.high24h.toFixed(getPriceDecimals(d.symbol))}
|
||||
Low: $${d.low24h.toFixed(getPriceDecimals(d.symbol))}
|
||||
Volume: ${formatVolume(d.volume24h)}
|
||||
Change: ${changeSymbol}${d.change24h.toFixed(2)}%
|
||||
|
||||
Last Update: ${new Date(d.timestamp).toISOString()}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// binance_get_orderbook
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_get_orderbook
|
||||
* Get order book (bids and asks) with specified depth
|
||||
*/
|
||||
export const binanceGetOrderbookSchema = {
|
||||
name: 'binance_get_orderbook',
|
||||
description: 'Get the order book (bids and asks) with the specified depth for a trading pair',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Order book depth (5, 10, 20, 50, or 100). Default: 20',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const BinanceGetOrderbookInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
||||
limit: z.number().int().min(5).max(100).default(20),
|
||||
});
|
||||
|
||||
export type BinanceGetOrderbookInput = z.infer<typeof BinanceGetOrderbookInputSchema>;
|
||||
|
||||
export interface BinanceGetOrderbookResult {
|
||||
success: boolean;
|
||||
data?: BinanceOrderBook;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_get_orderbook(
|
||||
params: BinanceGetOrderbookInput
|
||||
): Promise<BinanceGetOrderbookResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
const orderbook = await client.getOrderBook(params.symbol, params.limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: orderbook,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceGetOrderbook(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceGetOrderbookInputSchema.parse(params);
|
||||
const result = await binance_get_orderbook(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
const decimals = getPriceDecimals(d.symbol);
|
||||
|
||||
// Format top 10 levels
|
||||
const topBids = d.bids.slice(0, 10);
|
||||
const topAsks = d.asks.slice(0, 10);
|
||||
|
||||
let bidsStr = topBids
|
||||
.map(([price, qty]) => ` $${price.toFixed(decimals)} | ${qty.toFixed(6)}`)
|
||||
.join('\n');
|
||||
|
||||
let asksStr = topAsks
|
||||
.map(([price, qty]) => ` $${price.toFixed(decimals)} | ${qty.toFixed(6)}`)
|
||||
.join('\n');
|
||||
|
||||
const formattedOutput = `
|
||||
Order Book: ${d.symbol}
|
||||
${'='.repeat(35)}
|
||||
Spread: $${d.spread.toFixed(decimals)} (${d.spreadPercentage.toFixed(4)}%)
|
||||
|
||||
Top ${topAsks.length} Asks (Sell Orders)
|
||||
${'-'.repeat(25)}
|
||||
${asksStr}
|
||||
|
||||
Top ${topBids.length} Bids (Buy Orders)
|
||||
${'-'.repeat(25)}
|
||||
${bidsStr}
|
||||
|
||||
Timestamp: ${new Date(d.timestamp).toISOString()}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// binance_get_klines
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_get_klines
|
||||
* Get historical OHLCV candles for technical analysis
|
||||
*/
|
||||
export const binanceGetKlinesSchema = {
|
||||
name: 'binance_get_klines',
|
||||
description: 'Get historical candlestick (OHLCV) data for technical analysis',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
interval: {
|
||||
type: 'string',
|
||||
description: 'Candle interval: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w. Default: 5m',
|
||||
enum: ['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w'],
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of candles to retrieve (max 500). Default: 100',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const BinanceGetKlinesInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
||||
interval: z.enum(['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w']).default('5m'),
|
||||
limit: z.number().int().min(1).max(500).default(100),
|
||||
});
|
||||
|
||||
export type BinanceGetKlinesInput = z.infer<typeof BinanceGetKlinesInputSchema>;
|
||||
|
||||
export interface BinanceGetKlinesResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
symbol: string;
|
||||
interval: string;
|
||||
candles: BinanceKline[];
|
||||
count: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_get_klines(
|
||||
params: BinanceGetKlinesInput
|
||||
): Promise<BinanceGetKlinesResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
const klines = await client.getKlines(params.symbol, params.interval, params.limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
symbol: params.symbol,
|
||||
interval: params.interval,
|
||||
candles: klines,
|
||||
count: klines.length,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceGetKlines(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceGetKlinesInputSchema.parse(params);
|
||||
const result = await binance_get_klines(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
const decimals = getPriceDecimals(d.symbol);
|
||||
|
||||
// Get last 5 candles for display
|
||||
const recentCandles = d.candles.slice(-5);
|
||||
|
||||
let candlesStr = recentCandles
|
||||
.map((c) => {
|
||||
const time = new Date(c.timestamp).toISOString().slice(0, 16).replace('T', ' ');
|
||||
const direction = c.close >= c.open ? 'UP' : 'DOWN';
|
||||
return ` ${time} | O:${c.open.toFixed(decimals)} H:${c.high.toFixed(decimals)} L:${c.low.toFixed(decimals)} C:${c.close.toFixed(decimals)} | V:${formatVolume(c.volume)} | ${direction}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
// Calculate basic stats
|
||||
const closes = d.candles.map((c) => c.close);
|
||||
const high = Math.max(...d.candles.map((c) => c.high));
|
||||
const low = Math.min(...d.candles.map((c) => c.low));
|
||||
const avgVolume = d.candles.reduce((sum, c) => sum + c.volume, 0) / d.candles.length;
|
||||
|
||||
const formattedOutput = `
|
||||
Klines: ${d.symbol} (${d.interval})
|
||||
${'='.repeat(45)}
|
||||
Retrieved: ${d.count} candles
|
||||
|
||||
Period Statistics
|
||||
-----------------
|
||||
Highest High: $${high.toFixed(decimals)}
|
||||
Lowest Low: $${low.toFixed(decimals)}
|
||||
Avg Volume: ${formatVolume(avgVolume)}
|
||||
|
||||
Recent Candles (last 5)
|
||||
-----------------------
|
||||
${candlesStr}
|
||||
|
||||
First Candle: ${new Date(d.candles[0].timestamp).toISOString()}
|
||||
Last Candle: ${new Date(d.candles[d.candles.length - 1].timestamp).toISOString()}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Helper Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get appropriate decimal places for price display
|
||||
*/
|
||||
function getPriceDecimals(symbol: string): number {
|
||||
const upper = symbol.toUpperCase();
|
||||
|
||||
// Stablecoins and fiat pairs
|
||||
if (upper.includes('USD') && !upper.startsWith('BTC') && !upper.startsWith('ETH')) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
// BTC pairs
|
||||
if (upper === 'BTCUSDT' || upper === 'BTCBUSD') {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// ETH pairs
|
||||
if (upper === 'ETHUSDT' || upper === 'ETHBUSD') {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Small value coins
|
||||
if (upper.includes('SHIB') || upper.includes('DOGE') || upper.includes('PEPE')) {
|
||||
return 8;
|
||||
}
|
||||
|
||||
// Default
|
||||
return 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format large volume numbers
|
||||
*/
|
||||
function formatVolume(volume: number): string {
|
||||
if (volume >= 1_000_000_000) {
|
||||
return `${(volume / 1_000_000_000).toFixed(2)}B`;
|
||||
}
|
||||
if (volume >= 1_000_000) {
|
||||
return `${(volume / 1_000_000).toFixed(2)}M`;
|
||||
}
|
||||
if (volume >= 1_000) {
|
||||
return `${(volume / 1_000).toFixed(2)}K`;
|
||||
}
|
||||
return volume.toFixed(4);
|
||||
}
|
||||
334
src/tools/orders.ts
Normal file
334
src/tools/orders.ts
Normal file
@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Binance Order Management Tools
|
||||
*
|
||||
* - binance_create_order: Create a new order (HIGH RISK)
|
||||
* - binance_cancel_order: Cancel a pending order
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author Trading Platform Trading Platform
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getBinanceClient, BinanceOrder, CreateOrderParams } from '../services/binance-client';
|
||||
import { performRiskCheck, recordTradeVolume } from '../middleware/risk-check';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// ==========================================
|
||||
// binance_create_order
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_create_order
|
||||
* Create a new buy or sell order
|
||||
* HIGH RISK - Requires confirmation
|
||||
*/
|
||||
export const binanceCreateOrderSchema = {
|
||||
name: 'binance_create_order',
|
||||
description: 'Create a new buy or sell order on Binance. HIGH RISK - Ensure you validate with the user before executing.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT)',
|
||||
},
|
||||
side: {
|
||||
type: 'string',
|
||||
enum: ['buy', 'sell'],
|
||||
description: 'Order direction: buy or sell',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['market', 'limit', 'stop_loss', 'take_profit'],
|
||||
description: 'Order type. Default: market',
|
||||
},
|
||||
amount: {
|
||||
type: 'number',
|
||||
description: 'Amount of the base asset to buy/sell',
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
description: 'Price per unit (required for limit orders)',
|
||||
},
|
||||
stopPrice: {
|
||||
type: 'number',
|
||||
description: 'Stop price (required for stop_loss and take_profit orders)',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'side', 'amount'] as string[],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
requiresConfirmation: true,
|
||||
};
|
||||
|
||||
export const BinanceCreateOrderInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
||||
side: z.enum(['buy', 'sell']),
|
||||
type: z.enum(['market', 'limit', 'stop_loss', 'take_profit']).default('market'),
|
||||
amount: z.number().positive(),
|
||||
price: z.number().positive().optional(),
|
||||
stopPrice: z.number().positive().optional(),
|
||||
});
|
||||
|
||||
export type BinanceCreateOrderInput = z.infer<typeof BinanceCreateOrderInputSchema>;
|
||||
|
||||
export interface BinanceCreateOrderResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
order: BinanceOrder;
|
||||
riskWarnings?: string[];
|
||||
};
|
||||
error?: string;
|
||||
riskCheckFailed?: boolean;
|
||||
}
|
||||
|
||||
export async function binance_create_order(
|
||||
params: BinanceCreateOrderInput
|
||||
): Promise<BinanceCreateOrderResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
|
||||
if (!client.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Binance API keys are not configured',
|
||||
};
|
||||
}
|
||||
|
||||
// 1. Perform risk check
|
||||
const riskCheck = await performRiskCheck({
|
||||
symbol: params.symbol,
|
||||
side: params.side,
|
||||
amount: params.amount,
|
||||
price: params.price,
|
||||
});
|
||||
|
||||
if (!riskCheck.allowed) {
|
||||
logger.warn('Order rejected by risk check', {
|
||||
params,
|
||||
reason: riskCheck.reason,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: riskCheck.reason,
|
||||
riskCheckFailed: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Validate order parameters
|
||||
if (params.type === 'limit' && !params.price) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Price is required for limit orders',
|
||||
};
|
||||
}
|
||||
|
||||
if ((params.type === 'stop_loss' || params.type === 'take_profit') && !params.stopPrice) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Stop price is required for ${params.type} orders`,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Create the order
|
||||
const orderParams: CreateOrderParams = {
|
||||
symbol: params.symbol,
|
||||
side: params.side,
|
||||
type: params.type,
|
||||
amount: params.amount,
|
||||
price: params.price,
|
||||
stopPrice: params.stopPrice,
|
||||
};
|
||||
|
||||
const result = await client.createOrder(orderParams);
|
||||
|
||||
if (result.success && result.order) {
|
||||
// Record trade volume for daily limit tracking
|
||||
if (riskCheck.orderValue) {
|
||||
recordTradeVolume(riskCheck.orderValue);
|
||||
}
|
||||
|
||||
logger.info('Order created successfully', {
|
||||
orderId: result.order.id,
|
||||
symbol: params.symbol,
|
||||
side: params.side,
|
||||
amount: params.amount,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
order: result.order,
|
||||
riskWarnings: riskCheck.warnings,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || 'Failed to create order',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Order creation failed', { error, params });
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceCreateOrder(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceCreateOrderInputSchema.parse(params);
|
||||
const result = await binance_create_order(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const o = result.data.order;
|
||||
const priceStr = o.price ? `$${o.price.toFixed(8)}` : 'MARKET';
|
||||
|
||||
let warningsStr = '';
|
||||
if (result.data.riskWarnings && result.data.riskWarnings.length > 0) {
|
||||
warningsStr = `\n\nWarnings:\n${result.data.riskWarnings.map((w) => ` - ${w}`).join('\n')}`;
|
||||
}
|
||||
|
||||
const formattedOutput = `
|
||||
Order Created Successfully
|
||||
${'='.repeat(35)}
|
||||
Order ID: ${o.id}
|
||||
Symbol: ${o.symbol}
|
||||
Side: ${o.side.toUpperCase()}
|
||||
Type: ${o.type.toUpperCase()}
|
||||
Price: ${priceStr}
|
||||
Amount: ${o.amount.toFixed(8)}
|
||||
Filled: ${o.filled.toFixed(8)}
|
||||
Status: ${o.status}
|
||||
Created: ${new Date(o.createdAt).toISOString()}${warningsStr}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
const errorPrefix = result.riskCheckFailed ? 'Risk Check Failed: ' : 'Error: ';
|
||||
return {
|
||||
content: [{ type: 'text', text: `${errorPrefix}${result.error}` }],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// binance_cancel_order
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_cancel_order
|
||||
* Cancel a pending order
|
||||
*/
|
||||
export const binanceCancelOrderSchema = {
|
||||
name: 'binance_cancel_order',
|
||||
description: 'Cancel a pending order by order ID and symbol',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
orderId: {
|
||||
type: 'string',
|
||||
description: 'Order ID to cancel',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'orderId'] as string[],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
};
|
||||
|
||||
export const BinanceCancelOrderInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
||||
orderId: z.string().min(1),
|
||||
});
|
||||
|
||||
export type BinanceCancelOrderInput = z.infer<typeof BinanceCancelOrderInputSchema>;
|
||||
|
||||
export interface BinanceCancelOrderResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
cancelledOrder: BinanceOrder;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_cancel_order(
|
||||
params: BinanceCancelOrderInput
|
||||
): Promise<BinanceCancelOrderResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
|
||||
if (!client.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Binance API keys are not configured',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await client.cancelOrder(params.orderId, params.symbol);
|
||||
|
||||
if (result.success && result.order) {
|
||||
logger.info('Order cancelled successfully', {
|
||||
orderId: params.orderId,
|
||||
symbol: params.symbol,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
cancelledOrder: result.order,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || 'Failed to cancel order',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Order cancellation failed', { error, params });
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceCancelOrder(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceCancelOrderInputSchema.parse(params);
|
||||
const result = await binance_cancel_order(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const o = result.data.cancelledOrder;
|
||||
|
||||
const formattedOutput = `
|
||||
Order Cancelled Successfully
|
||||
${'='.repeat(35)}
|
||||
Order ID: ${o.id}
|
||||
Symbol: ${o.symbol}
|
||||
Side: ${o.side.toUpperCase()}
|
||||
Type: ${o.type.toUpperCase()}
|
||||
Original Amount: ${o.amount.toFixed(8)}
|
||||
Filled Before Cancel: ${o.filled.toFixed(8)}
|
||||
Status: ${o.status}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
67
src/utils/logger.ts
Normal file
67
src/utils/logger.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Logger Utility
|
||||
*
|
||||
* Winston-based logging for the MCP Binance Connector.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author Trading Platform Trading Platform
|
||||
*/
|
||||
|
||||
import winston from 'winston';
|
||||
import { serverConfig } from '../config';
|
||||
|
||||
const { combine, timestamp, printf, colorize, errors } = winston.format;
|
||||
|
||||
// Custom log format
|
||||
const logFormat = printf(({ level, message, timestamp, ...metadata }) => {
|
||||
let msg = `${timestamp} [${level}]: ${message}`;
|
||||
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
msg += ` ${JSON.stringify(metadata)}`;
|
||||
}
|
||||
|
||||
return msg;
|
||||
});
|
||||
|
||||
// Create logger instance
|
||||
export const logger = winston.createLogger({
|
||||
level: serverConfig.logLevel,
|
||||
format: combine(
|
||||
errors({ stack: true }),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
logFormat
|
||||
),
|
||||
defaultMeta: { service: 'mcp-binance-connector' },
|
||||
transports: [
|
||||
// Console transport
|
||||
new winston.transports.Console({
|
||||
format: combine(
|
||||
colorize(),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
logFormat
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Add file transport in production
|
||||
if (serverConfig.nodeEnv === 'production') {
|
||||
logger.add(
|
||||
new winston.transports.File({
|
||||
filename: process.env.LOG_FILE || 'logs/mcp-binance.log',
|
||||
maxsize: 10 * 1024 * 1024, // 10MB
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
|
||||
logger.add(
|
||||
new winston.transports.File({
|
||||
filename: 'logs/mcp-binance-error.log',
|
||||
level: 'error',
|
||||
maxsize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default logger;
|
||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user