From fa75326bba8d50029a05e2d76d36c6ad4a915d63 Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Fri, 16 Jan 2026 08:33:09 -0600 Subject: [PATCH] =?UTF-8?q?Migraci=C3=B3n=20desde=20trading-platform/apps/?= =?UTF-8?q?mcp-binance-connector=20-=20Est=C3=A1ndar=20multi-repo=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- .env.example | 52 ++++ .gitignore | 31 +++ Dockerfile | 57 ++++ README.md | 345 ++++++++++++++++++++++++ package.json | 54 ++++ src/config.ts | 159 +++++++++++ src/index.ts | 332 +++++++++++++++++++++++ src/middleware/risk-check.ts | 209 +++++++++++++++ src/services/binance-client.ts | 471 +++++++++++++++++++++++++++++++++ src/tools/account.ts | 265 +++++++++++++++++++ src/tools/index.ts | 288 ++++++++++++++++++++ src/tools/market.ts | 392 +++++++++++++++++++++++++++ src/tools/orders.ts | 334 +++++++++++++++++++++++ src/utils/logger.ts | 67 +++++ tsconfig.json | 23 ++ 15 files changed, 3079 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 package.json create mode 100644 src/config.ts create mode 100644 src/index.ts create mode 100644 src/middleware/risk-check.ts create mode 100644 src/services/binance-client.ts create mode 100644 src/tools/account.ts create mode 100644 src/tools/index.ts create mode 100644 src/tools/market.ts create mode 100644 src/tools/orders.ts create mode 100644 src/utils/logger.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bfdd06b --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1805b1 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..db07492 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d44034 --- /dev/null +++ b/README.md @@ -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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..e3a0ea7 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..0cee051 --- /dev/null +++ b/src/config.ts @@ -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, +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e0df808 --- /dev/null +++ b/src/index.ts @@ -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; diff --git a/src/middleware/risk-check.ts b/src/middleware/risk-check.ts new file mode 100644 index 0000000..7bf810b --- /dev/null +++ b/src/middleware/risk-check.ts @@ -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 { + 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, +}; diff --git a/src/services/binance-client.ts b/src/services/binance-client.ts new file mode 100644 index 0000000..56d20ca --- /dev/null +++ b/src/services/binance-client.ts @@ -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 { + try { + await this.spotClient.fetchTime(); + return true; + } catch { + return false; + } + } + + /** + * Load markets if not already loaded + */ + private async ensureMarketsLoaded(): Promise { + if (!this.marketsLoaded) { + await this.spotClient.loadMarkets(); + this.marketsLoaded = true; + } + } + + // ========================================== + // Market Data Methods + // ========================================== + + /** + * Get ticker for a symbol + */ + async getTicker(symbol: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; +} diff --git a/src/tools/account.ts b/src/tools/account.ts new file mode 100644 index 0000000..444305f --- /dev/null +++ b/src/tools/account.ts @@ -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; + +export interface BinanceGetAccountResult { + success: boolean; + data?: BinanceAccount & { totalUsdtEstimate?: number }; + error?: string; +} + +export async function binance_get_account( + _params: BinanceGetAccountInput +): Promise { + 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; + +export interface BinanceGetOpenOrdersResult { + success: boolean; + data?: { + orders: BinanceOrder[]; + count: number; + }; + error?: string; +} + +export async function binance_get_open_orders( + params: BinanceGetOpenOrdersInput +): Promise { + 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}` }], + }; +} diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..4a838e8 --- /dev/null +++ b/src/tools/index.ts @@ -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'; +} diff --git a/src/tools/market.ts b/src/tools/market.ts new file mode 100644 index 0000000..5df5344 --- /dev/null +++ b/src/tools/market.ts @@ -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; + +export interface BinanceGetTickerResult { + success: boolean; + data?: BinanceTicker; + error?: string; +} + +export async function binance_get_ticker( + params: BinanceGetTickerInput +): Promise { + 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; + +export interface BinanceGetOrderbookResult { + success: boolean; + data?: BinanceOrderBook; + error?: string; +} + +export async function binance_get_orderbook( + params: BinanceGetOrderbookInput +): Promise { + 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; + +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 { + 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); +} diff --git a/src/tools/orders.ts b/src/tools/orders.ts new file mode 100644 index 0000000..7cb7eae --- /dev/null +++ b/src/tools/orders.ts @@ -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; + +export interface BinanceCreateOrderResult { + success: boolean; + data?: { + order: BinanceOrder; + riskWarnings?: string[]; + }; + error?: string; + riskCheckFailed?: boolean; +} + +export async function binance_create_order( + params: BinanceCreateOrderInput +): Promise { + 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; + +export interface BinanceCancelOrderResult { + success: boolean; + data?: { + cancelledOrder: BinanceOrder; + }; + error?: string; +} + +export async function binance_cancel_order( + params: BinanceCancelOrderInput +): Promise { + 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}` }], + }; +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..30f0436 --- /dev/null +++ b/src/utils/logger.ts @@ -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; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ad10886 --- /dev/null +++ b/tsconfig.json @@ -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"] +}