[TASK-2026-02-06-ESTANDARIZACION-ESTRUCTURA-PROYECTOS] docs: Document MCP root exception and update structure diagram
- Updated CLAUDE.md v1.0.0 -> v2.0.0 - Corrected structure diagram: removed stale apps/mcp-* entries - Added formal exception note for 8 MCP submodules at root (master branch) - Noted stale duplicate dirs apps/mcp-binance-connector and apps/mcp-mt4-connector Part of: TASK-2026-02-06-ESTANDARIZACION-ESTRUCTURA-PROYECTOS Sprint 4 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a201cdafc3
commit
616aa2c847
16
.gitmodules
vendored
16
.gitmodules
vendored
@ -26,33 +26,33 @@
|
|||||||
|
|
||||||
# MCP Services
|
# MCP Services
|
||||||
[submodule "mcp-auth"]
|
[submodule "mcp-auth"]
|
||||||
path = mcp-auth
|
path = apps/mcp-auth
|
||||||
url = git@gitea-server:rckrdmrd/trading-platform-mcp-auth-v2.git
|
url = git@gitea-server:rckrdmrd/trading-platform-mcp-auth-v2.git
|
||||||
|
|
||||||
[submodule "mcp-binance-connector"]
|
[submodule "mcp-binance-connector"]
|
||||||
path = mcp-binance-connector
|
path = apps/mcp-binance-connector
|
||||||
url = git@gitea-server:rckrdmrd/trading-platform-mcp-binance-connector-v2.git
|
url = git@gitea-server:rckrdmrd/trading-platform-mcp-binance-connector-v2.git
|
||||||
|
|
||||||
[submodule "mcp-investment"]
|
[submodule "mcp-investment"]
|
||||||
path = mcp-investment
|
path = apps/mcp-investment
|
||||||
url = git@gitea-server:rckrdmrd/trading-platform-mcp-investment-v2.git
|
url = git@gitea-server:rckrdmrd/trading-platform-mcp-investment-v2.git
|
||||||
|
|
||||||
[submodule "mcp-mt4-connector"]
|
[submodule "mcp-mt4-connector"]
|
||||||
path = mcp-mt4-connector
|
path = apps/mcp-mt4-connector
|
||||||
url = git@gitea-server:rckrdmrd/trading-platform-mcp-mt4-connector-v2.git
|
url = git@gitea-server:rckrdmrd/trading-platform-mcp-mt4-connector-v2.git
|
||||||
|
|
||||||
[submodule "mcp-predictions"]
|
[submodule "mcp-predictions"]
|
||||||
path = mcp-predictions
|
path = apps/mcp-predictions
|
||||||
url = git@gitea-server:rckrdmrd/trading-platform-mcp-predictions-v2.git
|
url = git@gitea-server:rckrdmrd/trading-platform-mcp-predictions-v2.git
|
||||||
|
|
||||||
[submodule "mcp-products"]
|
[submodule "mcp-products"]
|
||||||
path = mcp-products
|
path = apps/mcp-products
|
||||||
url = git@gitea-server:rckrdmrd/trading-platform-mcp-products-v2.git
|
url = git@gitea-server:rckrdmrd/trading-platform-mcp-products-v2.git
|
||||||
|
|
||||||
[submodule "mcp-vip"]
|
[submodule "mcp-vip"]
|
||||||
path = mcp-vip
|
path = apps/mcp-vip
|
||||||
url = git@gitea-server:rckrdmrd/trading-platform-mcp-vip-v2.git
|
url = git@gitea-server:rckrdmrd/trading-platform-mcp-vip-v2.git
|
||||||
|
|
||||||
[submodule "mcp-wallet"]
|
[submodule "mcp-wallet"]
|
||||||
path = mcp-wallet
|
path = apps/mcp-wallet
|
||||||
url = git@gitea-server:rckrdmrd/trading-platform-mcp-wallet-v2.git
|
url = git@gitea-server:rckrdmrd/trading-platform-mcp-wallet-v2.git
|
||||||
|
|||||||
72
CLAUDE.md
72
CLAUDE.md
@ -4,8 +4,8 @@
|
|||||||
**Sistema:** SIMCO v4.0.0 + NEXUS v4.0
|
**Sistema:** SIMCO v4.0.0 + NEXUS v4.0
|
||||||
**Proyecto:** trading-platform
|
**Proyecto:** trading-platform
|
||||||
**Tipo:** STANDALONE
|
**Tipo:** STANDALONE
|
||||||
**Versión:** 1.0.0
|
**Versión:** 2.0.0
|
||||||
**Actualizado:** 2026-01-27
|
**Actualizado:** 2026-02-06
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -59,33 +59,57 @@ User: trading_user
|
|||||||
Password: trading_dev_2026
|
Password: trading_dev_2026
|
||||||
Port: 5432
|
Port: 5432
|
||||||
Host: localhost
|
Host: localhost
|
||||||
Schemas: auth, education, trading, investment, financial, portfolio, market_data, ml, llm, audit
|
Schemas (11): auth, trading, education, financial, investment, ml, llm, audit, portfolio, market_data, feature_flags
|
||||||
|
Tables: 101 DDL | Enums: 50 | Functions: 36 | Triggers: 46
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Metricas de Coherencia (SSOT: orchestration/inventarios/)
|
||||||
|
|
||||||
|
| Capa | Metrica | Valor |
|
||||||
|
|------|---------|-------|
|
||||||
|
| Database | Schemas | 11 |
|
||||||
|
| Database | Tablas DDL | 101 |
|
||||||
|
| Backend | Modulos | 18 |
|
||||||
|
| Backend | Type Interfaces | 85/101 (84%) |
|
||||||
|
| Backend | Services | 76/101 (75%) |
|
||||||
|
| Backend | Controllers | 62/101 (61%) |
|
||||||
|
| Frontend | Modulos | 14 |
|
||||||
|
| Frontend | Componentes | 225 (185 funcionales) |
|
||||||
|
| Frontend | Paginas | 58 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ESTRUCTURA DEL MONOREPO
|
## ESTRUCTURA DEL MONOREPO
|
||||||
|
|
||||||
```
|
```
|
||||||
trading-platform/
|
trading-platform/
|
||||||
├── apps/
|
├── apps/ ← Componentes principales (submodulos)
|
||||||
│ ├── backend/ → Express.js API
|
│ ├── backend/ → Express.js API
|
||||||
│ ├── frontend/ → React SPA
|
│ ├── frontend/ → React SPA
|
||||||
│ ├── database/ → DDL/Schemas
|
│ ├── database/ → DDL/Schemas
|
||||||
│ ├── data-service/ → Python data aggregation
|
│ ├── data-service/ → Python data aggregation
|
||||||
│ ├── ml-engine/ → Python ML service
|
│ └── ml-engine/ → Python ML service
|
||||||
│ ├── mcp-binance-connector/
|
├── mcp-auth/ → Auth service (port 3095) [submodulo, master]
|
||||||
│ └── mcp-mt4-connector/
|
├── mcp-binance-connector/ → Binance connector (port N/A) [submodulo, master]
|
||||||
├── mcp-auth/ → Auth service (port 3095)
|
├── mcp-investment/ → Investment service (port 3093) [submodulo, master]
|
||||||
├── mcp-wallet/ → Wallet service (port 3090)
|
├── mcp-mt4-connector/ → MT4 connector (port N/A) [submodulo, master]
|
||||||
├── mcp-products/ → Products service (port 3091)
|
├── mcp-predictions/ → ML signals (port 3094) [submodulo, master]
|
||||||
├── mcp-vip/ → VIP service (port 3092)
|
├── mcp-products/ → Products service (port 3091) [submodulo, master]
|
||||||
├── mcp-investment/ → Investment service (port 3093)
|
├── mcp-vip/ → VIP service (port 3092) [submodulo, master]
|
||||||
├── mcp-predictions/ → ML signals (port 3094)
|
├── mcp-wallet/ → Wallet service (port 3090) [submodulo, master]
|
||||||
├── packages/ → Shared packages
|
├── packages/ → Shared packages
|
||||||
└── docker/ → Docker configs
|
├── docker/ → Docker configs
|
||||||
|
├── orchestration/ → Gobernanza SIMCO
|
||||||
|
└── docs/ → Documentacion
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **EXCEPCION ESTRUCTURA (ADR-0004):** Los 8 modulos MCP permanecen en raiz (no en apps/)
|
||||||
|
> porque son submodulos en branch `master` (no `main`). Migrarlos requeriria:
|
||||||
|
> (1) cambiar branch en 8 repos remotos, (2) deinit+re-add submodulos, (3) actualizar
|
||||||
|
> docker-compose y CI. Se acepta como variante valida para este proyecto STANDALONE.
|
||||||
|
> Las copias regulares en apps/mcp-binance-connector/ y apps/mcp-mt4-connector/ son
|
||||||
|
> duplicados obsoletos pendientes de eliminacion.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## MODULOS/EPICS (9)
|
## MODULOS/EPICS (9)
|
||||||
@ -108,17 +132,19 @@ trading-platform/
|
|||||||
|
|
||||||
| Servicio | Puerto |
|
| Servicio | Puerto |
|
||||||
|----------|--------|
|
|----------|--------|
|
||||||
| Backend API | 3080 |
|
| Backend API | 3081 |
|
||||||
| Frontend | 3000 |
|
| Frontend | 3080 |
|
||||||
|
| Backend WebSocket | 3082 |
|
||||||
| ML Engine | 3083 |
|
| ML Engine | 3083 |
|
||||||
| Trading Agents | 3086 |
|
| Data Service | 3084 |
|
||||||
| LLM Agent | 3085 |
|
| LLM Agent | 3085 |
|
||||||
| MCP Auth | 3095 |
|
| Trading Agents | 3086 |
|
||||||
| MCP Wallet | 3090 |
|
| MCP Wallet | 3090 |
|
||||||
| MCP Products | 3091 |
|
| MCP Products | 3091 |
|
||||||
| MCP VIP | 3092 |
|
| MCP VIP | 3092 |
|
||||||
| MCP Investment | 3093 |
|
| MCP Investment | 3093 |
|
||||||
| MCP Predictions | 3094 |
|
| MCP Predictions | 3094 |
|
||||||
|
| MCP Auth | 3095 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
# 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
apps/mcp-binance-connector/.gitignore
vendored
31
apps/mcp-binance-connector/.gitignore
vendored
@ -1,31 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
# 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"]
|
|
||||||
@ -1,345 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,159 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@ -1,332 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
@ -1,209 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@ -1,471 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@ -1,265 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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}` }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,288 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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';
|
|
||||||
}
|
|
||||||
@ -1,392 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
@ -1,334 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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}` }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
# MCP MT4 Connector Configuration
|
|
||||||
# Copy this file to .env and configure values
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Server Configuration
|
|
||||||
# ==========================================
|
|
||||||
PORT=3605
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# MT4 Gateway Connection
|
|
||||||
# ==========================================
|
|
||||||
# Host where mt4-gateway is running
|
|
||||||
MT4_GATEWAY_HOST=localhost
|
|
||||||
# Port of the mt4-gateway service
|
|
||||||
MT4_GATEWAY_PORT=8081
|
|
||||||
# Authentication token for mt4-gateway
|
|
||||||
MT4_GATEWAY_AUTH_TOKEN=your-secret-token-here
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Request Configuration
|
|
||||||
# ==========================================
|
|
||||||
# Timeout for requests to MT4 Gateway (ms)
|
|
||||||
REQUEST_TIMEOUT=10000
|
|
||||||
# Maximum retries for failed requests
|
|
||||||
MAX_RETRIES=3
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Logging
|
|
||||||
# ==========================================
|
|
||||||
LOG_LEVEL=info
|
|
||||||
31
apps/mcp-mt4-connector/.gitignore
vendored
31
apps/mcp-mt4-connector/.gitignore
vendored
@ -1,31 +0,0 @@
|
|||||||
# Dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Build output
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
logs/
|
|
||||||
|
|
||||||
# Test coverage
|
|
||||||
coverage/
|
|
||||||
|
|
||||||
# Temporary files
|
|
||||||
tmp/
|
|
||||||
temp/
|
|
||||||
@ -1,277 +0,0 @@
|
|||||||
# MCP MT4 Connector
|
|
||||||
|
|
||||||
**Version:** 0.1.0
|
|
||||||
**Date:** 2026-01-04
|
|
||||||
**System:** Trading Platform + NEXUS v3.4 + SIMCO
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
MCP Server that exposes MetaTrader 4 (MT4) trading capabilities as tools for AI agents. This service enables AI agents to:
|
|
||||||
- Query account information
|
|
||||||
- Monitor open positions
|
|
||||||
- Execute trades (BUY/SELL)
|
|
||||||
- Manage positions (modify SL/TP, close)
|
|
||||||
- Get real-time price quotes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Navigate to the project
|
|
||||||
cd /home/isem/workspace-v1/projects/trading-platform/apps/mcp-mt4-connector
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Configure environment
|
|
||||||
cp .env.example .env
|
|
||||||
# Edit .env with your MT4 Gateway credentials
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description | Default |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `PORT` | MCP Server port | 3605 |
|
|
||||||
| `MT4_GATEWAY_HOST` | MT4 Gateway hostname | localhost |
|
|
||||||
| `MT4_GATEWAY_PORT` | MT4 Gateway port | 8081 |
|
|
||||||
| `MT4_GATEWAY_AUTH_TOKEN` | Authentication token | secret |
|
|
||||||
| `REQUEST_TIMEOUT` | Request timeout (ms) | 10000 |
|
|
||||||
| `LOG_LEVEL` | Logging level | info |
|
|
||||||
|
|
||||||
### Example .env
|
|
||||||
```env
|
|
||||||
PORT=3605
|
|
||||||
MT4_GATEWAY_HOST=localhost
|
|
||||||
MT4_GATEWAY_PORT=8081
|
|
||||||
MT4_GATEWAY_AUTH_TOKEN=your-secure-token
|
|
||||||
REQUEST_TIMEOUT=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:3605/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### List Available Tools
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:3605/tools
|
|
||||||
```
|
|
||||||
|
|
||||||
### Execute a Tool
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Get account info
|
|
||||||
curl -X POST http://localhost:3605/tools/mt4_get_account \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"parameters": {}}'
|
|
||||||
|
|
||||||
# Get price quote
|
|
||||||
curl -X POST http://localhost:3605/tools/mt4_get_quote \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"parameters": {"symbol": "XAUUSD"}}'
|
|
||||||
|
|
||||||
# Execute trade
|
|
||||||
curl -X POST http://localhost:3605/tools/mt4_execute_trade \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"parameters": {"symbol": "XAUUSD", "action": "buy", "lots": 0.1}}'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MCP Tools Available
|
|
||||||
|
|
||||||
| Tool | Description | Risk |
|
|
||||||
|------|-------------|------|
|
|
||||||
| `mt4_get_account` | Get account balance, equity, margin | Low |
|
|
||||||
| `mt4_get_positions` | List open positions | Low |
|
|
||||||
| `mt4_get_quote` | Get current bid/ask price | Low |
|
|
||||||
| `mt4_execute_trade` | Execute BUY/SELL order | HIGH |
|
|
||||||
| `mt4_close_position` | Close a position | HIGH |
|
|
||||||
| `mt4_modify_position` | Modify SL/TP | Medium |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
mcp-mt4-connector/
|
|
||||||
├── README.md # This file
|
|
||||||
├── package.json # Dependencies
|
|
||||||
├── tsconfig.json # TypeScript configuration
|
|
||||||
├── .env.example # Environment template
|
|
||||||
├── .gitignore # Git ignore rules
|
|
||||||
├── docs/
|
|
||||||
│ ├── ARCHITECTURE.md # Architecture documentation
|
|
||||||
│ └── MCP-TOOLS-SPEC.md # Detailed tool specifications
|
|
||||||
└── src/
|
|
||||||
├── index.ts # Server entry point
|
|
||||||
├── tools/
|
|
||||||
│ ├── index.ts # Tool exports
|
|
||||||
│ ├── account.ts # mt4_get_account
|
|
||||||
│ ├── positions.ts # mt4_get_positions, mt4_close_position
|
|
||||||
│ ├── trading.ts # mt4_execute_trade, mt4_modify_position
|
|
||||||
│ └── quotes.ts # mt4_get_quote
|
|
||||||
└── services/
|
|
||||||
└── mt4-client.ts # MT4 Gateway HTTP client
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Integration with Claude
|
|
||||||
|
|
||||||
### MCP Configuration
|
|
||||||
|
|
||||||
Add to your Claude/MCP configuration:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mt4": {
|
|
||||||
"url": "http://localhost:3605",
|
|
||||||
"transport": "http"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Agent Prompts
|
|
||||||
|
|
||||||
```
|
|
||||||
"Check my MT4 account balance"
|
|
||||||
→ Uses mt4_get_account
|
|
||||||
|
|
||||||
"What's the current gold price?"
|
|
||||||
→ Uses mt4_get_quote({ symbol: "XAUUSD" })
|
|
||||||
|
|
||||||
"Buy 0.1 lots of XAUUSD with stop loss at 2640"
|
|
||||||
→ Uses mt4_execute_trade({ symbol: "XAUUSD", action: "buy", lots: 0.1, stopLoss: 2640 })
|
|
||||||
|
|
||||||
"Close my profitable gold positions"
|
|
||||||
→ Uses mt4_get_positions({ symbol: "XAUUSD" }) + mt4_close_position for each
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### Runtime
|
|
||||||
- `express` - HTTP server
|
|
||||||
- `axios` - HTTP client
|
|
||||||
- `zod` - Input validation
|
|
||||||
- `dotenv` - Environment configuration
|
|
||||||
- `@modelcontextprotocol/sdk` - MCP protocol
|
|
||||||
|
|
||||||
### Development
|
|
||||||
- `typescript` - Type safety
|
|
||||||
- `ts-node-dev` - Development server
|
|
||||||
- `jest` - Testing framework
|
|
||||||
- `eslint` - Code linting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
1. **MT4 Gateway** running on configured host:port
|
|
||||||
2. **MT4 Terminal** connected with EA Bridge active
|
|
||||||
3. **Node.js** >= 18.0.0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Cannot connect to MT4 Gateway
|
|
||||||
```bash
|
|
||||||
# Check if mt4-gateway is running
|
|
||||||
curl http://localhost:8081/status
|
|
||||||
|
|
||||||
# Verify environment variables
|
|
||||||
cat .env | grep MT4
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool execution fails
|
|
||||||
```bash
|
|
||||||
# Check health endpoint for dependency status
|
|
||||||
curl http://localhost:3605/health
|
|
||||||
|
|
||||||
# Check server logs
|
|
||||||
npm run dev # Logs will show in console
|
|
||||||
```
|
|
||||||
|
|
||||||
### Invalid parameters error
|
|
||||||
```bash
|
|
||||||
# Verify tool schema
|
|
||||||
curl http://localhost:3605/tools/mt4_execute_trade
|
|
||||||
|
|
||||||
# Check parameter names match schema
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [MCP Protocol](https://modelcontextprotocol.io)
|
|
||||||
- MT4 Gateway: `apps/mt4-gateway/`
|
|
||||||
- SIMCO-MCP: `orchestration/directivas/simco/SIMCO-MCP.md`
|
|
||||||
- Architecture: `docs/ARCHITECTURE.md`
|
|
||||||
- Tool Specs: `docs/MCP-TOOLS-SPEC.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Maintained by:** @PERFIL_MCP_DEVELOPER
|
|
||||||
**Project:** Trading Platform
|
|
||||||
@ -1,272 +0,0 @@
|
|||||||
# MCP MT4 Connector - Architecture
|
|
||||||
|
|
||||||
**Version:** 0.1.0
|
|
||||||
**Date:** 2026-01-04
|
|
||||||
**System:** Trading Platform
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The MCP MT4 Connector is a Model Context Protocol (MCP) server that exposes MetaTrader 4 trading capabilities as tools that AI agents can use. It acts as a bridge between MCP-compatible AI systems (like Claude) and the MT4 trading terminal.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Diagram
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ AI Agent (Claude) │
|
|
||||||
│ │
|
|
||||||
│ "Execute a buy order for 0.1 lots of XAUUSD with SL at 2640" │
|
|
||||||
└─────────────────────────────────────┬───────────────────────────────────┘
|
|
||||||
│ MCP Protocol
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ MCP MT4 Connector (Port 3605) │
|
|
||||||
│ │
|
|
||||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ Express Server │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ /health - Health check endpoint │ │
|
|
||||||
│ │ /tools - List available tools │ │
|
|
||||||
│ │ /tools/:name - Execute specific tool │ │
|
|
||||||
│ │ /mcp/* - MCP protocol endpoints │ │
|
|
||||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ Tool Handlers │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │
|
|
||||||
│ │ │ account.ts │ │positions.ts │ │ trading.ts │ │ quotes.ts │ │ │
|
|
||||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ │
|
|
||||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ MT4 Client Service │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ - HTTP client wrapper for mt4-gateway │ │
|
|
||||||
│ │ - Request/response handling │ │
|
|
||||||
│ │ - Error management │ │
|
|
||||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────┬───────────────────────────────────┘
|
|
||||||
│ HTTP/REST
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ MT4 Gateway (Port 8081) │
|
|
||||||
│ │
|
|
||||||
│ Python service that communicates with MT4 EA Bridge │
|
|
||||||
└─────────────────────────────────────┬───────────────────────────────────┘
|
|
||||||
│ Local Socket/HTTP
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ MT4 Terminal + EA Bridge │
|
|
||||||
│ │
|
|
||||||
│ Windows MT4 with Expert Advisor running │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Component Details
|
|
||||||
|
|
||||||
### 1. Express Server (`src/index.ts`)
|
|
||||||
|
|
||||||
The main entry point that:
|
|
||||||
- Hosts the MCP server on port 3605
|
|
||||||
- Provides REST endpoints for tool execution
|
|
||||||
- Implements MCP protocol endpoints
|
|
||||||
- Handles health checks and service discovery
|
|
||||||
|
|
||||||
**Endpoints:**
|
|
||||||
| Endpoint | Method | Description |
|
|
||||||
|----------|--------|-------------|
|
|
||||||
| `/health` | GET | Health check with MT4 connection status |
|
|
||||||
| `/tools` | GET | List all available MCP tools |
|
|
||||||
| `/tools/:name` | GET | Get specific tool schema |
|
|
||||||
| `/tools/:name` | POST | Execute tool with parameters |
|
|
||||||
| `/mcp/initialize` | POST | MCP initialization handshake |
|
|
||||||
| `/mcp/tools/list` | POST | MCP tool listing |
|
|
||||||
| `/mcp/tools/call` | POST | MCP tool execution |
|
|
||||||
|
|
||||||
### 2. Tool Handlers (`src/tools/`)
|
|
||||||
|
|
||||||
Individual tool implementations following the MCP tool pattern:
|
|
||||||
|
|
||||||
| File | Tools | Description |
|
|
||||||
|------|-------|-------------|
|
|
||||||
| `account.ts` | `mt4_get_account` | Account information retrieval |
|
|
||||||
| `positions.ts` | `mt4_get_positions`, `mt4_close_position` | Position management |
|
|
||||||
| `trading.ts` | `mt4_execute_trade`, `mt4_modify_position` | Trade execution |
|
|
||||||
| `quotes.ts` | `mt4_get_quote` | Price data retrieval |
|
|
||||||
|
|
||||||
Each tool handler:
|
|
||||||
- Defines Zod validation schemas
|
|
||||||
- Implements the core logic
|
|
||||||
- Formats responses for MCP protocol
|
|
||||||
- Handles errors gracefully
|
|
||||||
|
|
||||||
### 3. MT4 Client Service (`src/services/mt4-client.ts`)
|
|
||||||
|
|
||||||
HTTP client wrapper that:
|
|
||||||
- Manages connection to mt4-gateway
|
|
||||||
- Handles authentication (Bearer token)
|
|
||||||
- Provides typed interfaces for all operations
|
|
||||||
- Manages request timeouts and retries
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
### Example: Execute Trade
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Agent Request
|
|
||||||
POST /mcp/tools/call
|
|
||||||
{
|
|
||||||
"name": "mt4_execute_trade",
|
|
||||||
"arguments": {
|
|
||||||
"symbol": "XAUUSD",
|
|
||||||
"action": "buy",
|
|
||||||
"lots": 0.1,
|
|
||||||
"stopLoss": 2640,
|
|
||||||
"takeProfit": 2680
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
2. Tool Handler (trading.ts)
|
|
||||||
- Validates input with Zod schema
|
|
||||||
- Checks MT4 connection status
|
|
||||||
- Validates SL/TP logic
|
|
||||||
- Calls MT4Client.executeTrade()
|
|
||||||
|
|
||||||
3. MT4 Client Service
|
|
||||||
- Formats request payload
|
|
||||||
- Sends HTTP POST to mt4-gateway
|
|
||||||
- Receives and parses response
|
|
||||||
|
|
||||||
4. MT4 Gateway
|
|
||||||
- Forwards to EA Bridge
|
|
||||||
- EA executes trade on MT4
|
|
||||||
- Returns result
|
|
||||||
|
|
||||||
5. Response to Agent
|
|
||||||
{
|
|
||||||
"content": [{
|
|
||||||
"type": "text",
|
|
||||||
"text": "Trade Executed Successfully\n..."
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| `PORT` | 3605 | MCP server port |
|
|
||||||
| `MT4_GATEWAY_HOST` | localhost | MT4 Gateway host |
|
|
||||||
| `MT4_GATEWAY_PORT` | 8081 | MT4 Gateway port |
|
|
||||||
| `MT4_GATEWAY_AUTH_TOKEN` | secret | Authentication token |
|
|
||||||
| `REQUEST_TIMEOUT` | 10000 | Request timeout (ms) |
|
|
||||||
| `LOG_LEVEL` | info | Logging level |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Error Types
|
|
||||||
|
|
||||||
1. **Connection Errors**
|
|
||||||
- MT4 Gateway unreachable
|
|
||||||
- MT4 Terminal disconnected
|
|
||||||
|
|
||||||
2. **Validation Errors**
|
|
||||||
- Invalid parameters (Zod)
|
|
||||||
- Invalid SL/TP configuration
|
|
||||||
|
|
||||||
3. **Trading Errors**
|
|
||||||
- Insufficient margin
|
|
||||||
- Market closed
|
|
||||||
- Invalid symbol
|
|
||||||
|
|
||||||
### Error Response Format
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Error message description"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
1. **Authentication**
|
|
||||||
- Bearer token for mt4-gateway communication
|
|
||||||
- No external network exposure by default
|
|
||||||
|
|
||||||
2. **Validation**
|
|
||||||
- All inputs validated with Zod schemas
|
|
||||||
- Type-safe throughout the codebase
|
|
||||||
|
|
||||||
3. **Rate Limiting**
|
|
||||||
- Consider adding rate limiting for production
|
|
||||||
- Respect MT4 order frequency limits
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
| Package | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `express` | HTTP server |
|
|
||||||
| `axios` | HTTP client |
|
|
||||||
| `zod` | Input validation |
|
|
||||||
| `dotenv` | Environment configuration |
|
|
||||||
| `@modelcontextprotocol/sdk` | MCP protocol types |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
cp .env.example .env
|
|
||||||
# Edit .env with your configuration
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker (Future)
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
FROM node:20-alpine
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci --only=production
|
|
||||||
COPY dist ./dist
|
|
||||||
EXPOSE 3605
|
|
||||||
CMD ["node", "dist/index.js"]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- MCP Protocol: https://modelcontextprotocol.io
|
|
||||||
- MT4 Bridge Client: `apps/mt4-gateway/src/providers/mt4_bridge_client.py`
|
|
||||||
- Trading Platform: `projects/trading-platform/`
|
|
||||||
- SIMCO-MCP Directive: `orchestration/directivas/simco/SIMCO-MCP.md`
|
|
||||||
@ -1,428 +0,0 @@
|
|||||||
# MCP MT4 Connector - Tools Specification
|
|
||||||
|
|
||||||
**Version:** 0.1.0
|
|
||||||
**Date:** 2026-01-04
|
|
||||||
**Total Tools:** 6
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tool Overview
|
|
||||||
|
|
||||||
| Tool Name | Description | Risk Level |
|
|
||||||
|-----------|-------------|------------|
|
|
||||||
| `mt4_get_account` | Get account information | Low |
|
|
||||||
| `mt4_get_positions` | List open positions | Low |
|
|
||||||
| `mt4_get_quote` | Get current price quote | Low |
|
|
||||||
| `mt4_execute_trade` | Execute market order | HIGH |
|
|
||||||
| `mt4_close_position` | Close a position | HIGH |
|
|
||||||
| `mt4_modify_position` | Modify SL/TP | Medium |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## mt4_get_account
|
|
||||||
|
|
||||||
### Description
|
|
||||||
Retrieves comprehensive account information from the connected MT4 terminal including balance, equity, margin, leverage, and broker details.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
| Name | Type | Required | Description |
|
|
||||||
|------|------|----------|-------------|
|
|
||||||
| - | - | - | No parameters required |
|
|
||||||
|
|
||||||
### Return Value
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"balance": 10000.00,
|
|
||||||
"equity": 10250.50,
|
|
||||||
"margin": 500.00,
|
|
||||||
"freeMargin": 9750.50,
|
|
||||||
"marginLevel": 2050.10,
|
|
||||||
"profit": 250.50,
|
|
||||||
"currency": "USD",
|
|
||||||
"leverage": 100,
|
|
||||||
"name": "Demo Account",
|
|
||||||
"server": "ICMarkets-Demo",
|
|
||||||
"company": "IC Markets"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Usage
|
|
||||||
```typescript
|
|
||||||
// Get account info
|
|
||||||
const result = await mt4_get_account({});
|
|
||||||
|
|
||||||
// Response content:
|
|
||||||
// MT4 Account Information
|
|
||||||
// =======================
|
|
||||||
// Account Name: Demo Account
|
|
||||||
// Server: ICMarkets-Demo
|
|
||||||
// Broker: IC Markets
|
|
||||||
// Leverage: 1:100
|
|
||||||
//
|
|
||||||
// Financial Summary
|
|
||||||
// -----------------
|
|
||||||
// Balance: 10000.00 USD
|
|
||||||
// Equity: 10250.50 USD
|
|
||||||
// Profit/Loss: +250.50 USD
|
|
||||||
```
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
| Code | Message | Solution |
|
|
||||||
|------|---------|----------|
|
|
||||||
| - | MT4 terminal is not connected | Check MT4 Gateway connection |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## mt4_get_positions
|
|
||||||
|
|
||||||
### Description
|
|
||||||
Lists all currently open trading positions from the MT4 terminal. Can optionally filter by symbol.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
| Name | Type | Required | Description |
|
|
||||||
|------|------|----------|-------------|
|
|
||||||
| `symbol` | string | No | Filter positions by symbol (e.g., XAUUSD) |
|
|
||||||
|
|
||||||
### Return Value
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"positions": [
|
|
||||||
{
|
|
||||||
"ticket": 123456,
|
|
||||||
"symbol": "XAUUSD",
|
|
||||||
"type": "buy",
|
|
||||||
"lots": 0.10,
|
|
||||||
"openPrice": 2650.50,
|
|
||||||
"currentPrice": 2655.00,
|
|
||||||
"stopLoss": 2640.00,
|
|
||||||
"takeProfit": 2680.00,
|
|
||||||
"profit": 45.00,
|
|
||||||
"swap": -1.20,
|
|
||||||
"openTime": "2026-01-04T10:30:00Z",
|
|
||||||
"magic": 12345,
|
|
||||||
"comment": "AI Signal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"totalProfit": 45.00,
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Usage
|
|
||||||
```typescript
|
|
||||||
// Get all positions
|
|
||||||
const result = await mt4_get_positions({});
|
|
||||||
|
|
||||||
// Get only XAUUSD positions
|
|
||||||
const goldPositions = await mt4_get_positions({ symbol: "XAUUSD" });
|
|
||||||
```
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
| Code | Message | Solution |
|
|
||||||
|------|---------|----------|
|
|
||||||
| - | MT4 terminal is not connected | Check MT4 Gateway connection |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## mt4_get_quote
|
|
||||||
|
|
||||||
### Description
|
|
||||||
Retrieves the current bid/ask prices for a trading symbol. Also calculates the spread in points/pips.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
| Name | Type | Required | Description |
|
|
||||||
|------|------|----------|-------------|
|
|
||||||
| `symbol` | string | Yes | Trading symbol (e.g., XAUUSD, EURUSD) |
|
|
||||||
|
|
||||||
### Return Value
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"symbol": "XAUUSD",
|
|
||||||
"bid": 2650.50,
|
|
||||||
"ask": 2650.80,
|
|
||||||
"spread": 0.30,
|
|
||||||
"timestamp": "2026-01-04T12:00:00.000Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Usage
|
|
||||||
```typescript
|
|
||||||
// Get gold price
|
|
||||||
const quote = await mt4_get_quote({ symbol: "XAUUSD" });
|
|
||||||
|
|
||||||
// Response content:
|
|
||||||
// Price Quote: XAUUSD
|
|
||||||
// =========================
|
|
||||||
// Bid: 2650.50
|
|
||||||
// Ask: 2650.80
|
|
||||||
// Spread: 0.30 (3.0 pips)
|
|
||||||
// Time: 2026-01-04T12:00:00.000Z
|
|
||||||
```
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
| Code | Message | Solution |
|
|
||||||
|------|---------|----------|
|
|
||||||
| - | No quote data available for {symbol} | Verify symbol is available on broker |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## mt4_execute_trade
|
|
||||||
|
|
||||||
### Description
|
|
||||||
Opens a new trading position with a market order. Supports BUY and SELL orders with optional stop loss and take profit levels.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
| Name | Type | Required | Description |
|
|
||||||
|------|------|----------|-------------|
|
|
||||||
| `symbol` | string | Yes | Trading symbol (e.g., XAUUSD) |
|
|
||||||
| `action` | string | Yes | Trade direction: "buy" or "sell" |
|
|
||||||
| `lots` | number | Yes | Volume in lots (e.g., 0.01, 0.1, 1.0) |
|
|
||||||
| `stopLoss` | number | No | Stop loss price level |
|
|
||||||
| `takeProfit` | number | No | Take profit price level |
|
|
||||||
| `slippage` | number | No | Maximum slippage in points (default: 3) |
|
|
||||||
| `magic` | number | No | Magic number for EA identification (default: 12345) |
|
|
||||||
| `comment` | string | No | Order comment (max 31 chars) |
|
|
||||||
|
|
||||||
### Return Value
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"success": true,
|
|
||||||
"ticket": 123456,
|
|
||||||
"message": "Order placed successfully",
|
|
||||||
"symbol": "XAUUSD",
|
|
||||||
"action": "buy",
|
|
||||||
"lots": 0.1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Usage
|
|
||||||
```typescript
|
|
||||||
// Simple buy order
|
|
||||||
const result = await mt4_execute_trade({
|
|
||||||
symbol: "XAUUSD",
|
|
||||||
action: "buy",
|
|
||||||
lots: 0.1
|
|
||||||
});
|
|
||||||
|
|
||||||
// Buy with risk management
|
|
||||||
const result = await mt4_execute_trade({
|
|
||||||
symbol: "XAUUSD",
|
|
||||||
action: "buy",
|
|
||||||
lots: 0.1,
|
|
||||||
stopLoss: 2640.00,
|
|
||||||
takeProfit: 2680.00,
|
|
||||||
comment: "AI Signal - Gold Long"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sell order
|
|
||||||
const result = await mt4_execute_trade({
|
|
||||||
symbol: "EURUSD",
|
|
||||||
action: "sell",
|
|
||||||
lots: 0.5,
|
|
||||||
stopLoss: 1.1050,
|
|
||||||
takeProfit: 1.0900
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validation Rules
|
|
||||||
- For BUY orders: stopLoss must be below takeProfit
|
|
||||||
- For SELL orders: stopLoss must be above takeProfit
|
|
||||||
- Lots must be positive and reasonable (max 100)
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
| Code | Message | Solution |
|
|
||||||
|------|---------|----------|
|
|
||||||
| - | For BUY orders, stop loss must be below take profit | Fix SL/TP levels |
|
|
||||||
| - | For SELL orders, stop loss must be above take profit | Fix SL/TP levels |
|
|
||||||
| - | Trade execution failed | Check margin, market hours |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## mt4_close_position
|
|
||||||
|
|
||||||
### Description
|
|
||||||
Closes an open position by ticket number. Can optionally close only a partial volume.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
| Name | Type | Required | Description |
|
|
||||||
|------|------|----------|-------------|
|
|
||||||
| `ticket` | number | Yes | Position ticket number to close |
|
|
||||||
| `lots` | number | No | Partial volume to close (default: close all) |
|
|
||||||
| `slippage` | number | No | Maximum slippage in points (default: 3) |
|
|
||||||
|
|
||||||
### Return Value
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"success": true,
|
|
||||||
"ticket": 123456,
|
|
||||||
"message": "Position closed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Usage
|
|
||||||
```typescript
|
|
||||||
// Close entire position
|
|
||||||
const result = await mt4_close_position({
|
|
||||||
ticket: 123456
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close partial position (0.5 of 1.0 lots)
|
|
||||||
const result = await mt4_close_position({
|
|
||||||
ticket: 123456,
|
|
||||||
lots: 0.5
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
| Code | Message | Solution |
|
|
||||||
|------|---------|----------|
|
|
||||||
| - | Position with ticket {x} not found | Verify ticket number |
|
|
||||||
| - | Requested lots exceeds position size | Reduce lots parameter |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## mt4_modify_position
|
|
||||||
|
|
||||||
### Description
|
|
||||||
Modifies the stop loss and/or take profit levels of an existing open position.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
| Name | Type | Required | Description |
|
|
||||||
|------|------|----------|-------------|
|
|
||||||
| `ticket` | number | Yes | Position ticket number to modify |
|
|
||||||
| `stopLoss` | number | No | New stop loss price level |
|
|
||||||
| `takeProfit` | number | No | New take profit price level |
|
|
||||||
|
|
||||||
### Return Value
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"success": true,
|
|
||||||
"ticket": 123456,
|
|
||||||
"message": "Position modified successfully"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Usage
|
|
||||||
```typescript
|
|
||||||
// Set both SL and TP
|
|
||||||
const result = await mt4_modify_position({
|
|
||||||
ticket: 123456,
|
|
||||||
stopLoss: 2640.00,
|
|
||||||
takeProfit: 2680.00
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update only take profit (trailing)
|
|
||||||
const result = await mt4_modify_position({
|
|
||||||
ticket: 123456,
|
|
||||||
takeProfit: 2700.00
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set only stop loss (risk management)
|
|
||||||
const result = await mt4_modify_position({
|
|
||||||
ticket: 123456,
|
|
||||||
stopLoss: 2650.00
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validation Rules
|
|
||||||
- At least one of stopLoss or takeProfit must be provided
|
|
||||||
- For BUY positions: stopLoss must be below takeProfit
|
|
||||||
- For SELL positions: stopLoss must be above takeProfit
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
| Code | Message | Solution |
|
|
||||||
|------|---------|----------|
|
|
||||||
| - | At least one of stopLoss or takeProfit must be provided | Add SL or TP |
|
|
||||||
| - | Position with ticket {x} not found | Verify ticket number |
|
|
||||||
| - | For BUY positions, stop loss must be below take profit | Fix SL/TP levels |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Error Responses
|
|
||||||
|
|
||||||
### Connection Error
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "MT4 terminal is not connected"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validation Error
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Validation error",
|
|
||||||
"details": [
|
|
||||||
{
|
|
||||||
"path": ["symbol"],
|
|
||||||
"message": "Required"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Trading Error
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Trade execution failed: Insufficient margin"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage Examples with AI Agent
|
|
||||||
|
|
||||||
### Scenario 1: Check Account and Open Trade
|
|
||||||
```
|
|
||||||
Agent: "Check my account balance and if equity is above 10000, buy 0.1 lots of XAUUSD"
|
|
||||||
|
|
||||||
1. Call mt4_get_account({})
|
|
||||||
2. Parse response, check equity > 10000
|
|
||||||
3. Call mt4_execute_trade({ symbol: "XAUUSD", action: "buy", lots: 0.1 })
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scenario 2: Risk Management
|
|
||||||
```
|
|
||||||
Agent: "Set stop loss at 2640 and take profit at 2680 for my gold position"
|
|
||||||
|
|
||||||
1. Call mt4_get_positions({ symbol: "XAUUSD" })
|
|
||||||
2. Get ticket number from response
|
|
||||||
3. Call mt4_modify_position({ ticket: 123456, stopLoss: 2640, takeProfit: 2680 })
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scenario 3: Close Profitable Trades
|
|
||||||
```
|
|
||||||
Agent: "Close all profitable gold positions"
|
|
||||||
|
|
||||||
1. Call mt4_get_positions({ symbol: "XAUUSD" })
|
|
||||||
2. Filter positions where profit > 0
|
|
||||||
3. For each: Call mt4_close_position({ ticket: ticketNumber })
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version History
|
|
||||||
|
|
||||||
| Version | Date | Changes |
|
|
||||||
|---------|------|---------|
|
|
||||||
| 0.1.0 | 2026-01-04 | Initial release with 6 core tools |
|
|
||||||
7170
apps/mcp-mt4-connector/package-lock.json
generated
7170
apps/mcp-mt4-connector/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "mcp-mt4-connector",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "MCP Server for MT4 trading operations via EA Bridge",
|
|
||||||
"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:-3605}/health || echo 'Server not running'"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"mcp",
|
|
||||||
"model-context-protocol",
|
|
||||||
"anthropic",
|
|
||||||
"claude",
|
|
||||||
"mt4",
|
|
||||||
"metatrader",
|
|
||||||
"trading",
|
|
||||||
"forex"
|
|
||||||
],
|
|
||||||
"author": "Trading Platform Trading Platform",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
||||||
"axios": "^1.6.0",
|
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"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": ">=18.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,291 +0,0 @@
|
|||||||
/**
|
|
||||||
* MCP Server: MT4 Connector
|
|
||||||
*
|
|
||||||
* Exposes MT4 trading capabilities as MCP tools for AI agents.
|
|
||||||
* Communicates with mt4-gateway service to execute trading operations.
|
|
||||||
*
|
|
||||||
* @version 0.1.0
|
|
||||||
* @author Trading Platform Trading Platform
|
|
||||||
*/
|
|
||||||
|
|
||||||
import express, { Request, Response, NextFunction } from 'express';
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
import { mcpToolSchemas, toolHandlers } from './tools';
|
|
||||||
import { getMT4Client } from './services/mt4-client';
|
|
||||||
|
|
||||||
// Load environment variables
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const PORT = process.env.PORT || 3605;
|
|
||||||
const SERVICE_NAME = 'mcp-mt4-connector';
|
|
||||||
const VERSION = '0.1.0';
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Middleware
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// Request logging
|
|
||||||
app.use((req: Request, _res: Response, next: NextFunction) => {
|
|
||||||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Health & Status Endpoints
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Health check endpoint
|
|
||||||
*/
|
|
||||||
app.get('/health', async (_req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const client = getMT4Client();
|
|
||||||
const mt4Connected = await client.isConnected();
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
status: 'ok',
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
version: VERSION,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
dependencies: {
|
|
||||||
mt4Gateway: mt4Connected ? 'connected' : 'disconnected',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.json({
|
|
||||||
status: 'degraded',
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
version: VERSION,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
dependencies: {
|
|
||||||
mt4Gateway: 'error',
|
|
||||||
},
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List available MCP tools
|
|
||||||
*/
|
|
||||||
app.get('/tools', (_req: Request, res: Response) => {
|
|
||||||
res.json({
|
|
||||||
tools: mcpToolSchemas,
|
|
||||||
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', 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 {
|
|
||||||
console.log(`[${new Date().toISOString()}] Executing tool: ${toolName}`);
|
|
||||||
console.log(`Parameters: ${JSON.stringify(parameters)}`);
|
|
||||||
|
|
||||||
const result = await handler(parameters);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
tool: toolName,
|
|
||||||
result,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[${new Date().toISOString()}] Tool error: ${toolName}`, error);
|
|
||||||
|
|
||||||
// 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: mcpToolSchemas.map(tool => ({
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
inputSchema: tool.inputSchema,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MCP Call Tool
|
|
||||||
* Execute a tool with parameters
|
|
||||||
*/
|
|
||||||
app.post('/mcp/tools/call', 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) => {
|
|
||||||
console.error(`[${new Date().toISOString()}] Unhandled error:`, err);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: err.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Start Server
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log('');
|
|
||||||
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
||||||
console.log('║ MCP MT4 Connector - Trading Platform ║');
|
|
||||||
console.log('╠══════════════════════════════════════════════════════════╣');
|
|
||||||
console.log(`║ Service: ${SERVICE_NAME.padEnd(45)}║`);
|
|
||||||
console.log(`║ Version: ${VERSION.padEnd(45)}║`);
|
|
||||||
console.log(`║ Port: ${String(PORT).padEnd(48)}║`);
|
|
||||||
console.log('╠══════════════════════════════════════════════════════════╣');
|
|
||||||
console.log('║ Endpoints: ║');
|
|
||||||
console.log(`║ - Health: http://localhost:${PORT}/health`.padEnd(63) + '║');
|
|
||||||
console.log(`║ - Tools: http://localhost:${PORT}/tools`.padEnd(63) + '║');
|
|
||||||
console.log('╠══════════════════════════════════════════════════════════╣');
|
|
||||||
console.log('║ MCP Tools Available: ║');
|
|
||||||
mcpToolSchemas.forEach(tool => {
|
|
||||||
console.log(`║ - ${tool.name.padEnd(54)}║`);
|
|
||||||
});
|
|
||||||
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
||||||
console.log('');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Graceful Shutdown
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
console.log('Received SIGTERM, shutting down gracefully...');
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
console.log('Received SIGINT, shutting down gracefully...');
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
@ -1,375 +0,0 @@
|
|||||||
/**
|
|
||||||
* MT4 Client Service
|
|
||||||
*
|
|
||||||
* HTTP client wrapper for communicating with mt4-gateway.
|
|
||||||
* This service mirrors the functionality of mt4_bridge_client.py
|
|
||||||
* but is written in TypeScript for the MCP Server.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Types
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
export interface MT4AccountInfo {
|
|
||||||
balance: number;
|
|
||||||
equity: number;
|
|
||||||
margin: number;
|
|
||||||
freeMargin: number;
|
|
||||||
marginLevel: number | null;
|
|
||||||
profit: number;
|
|
||||||
currency: string;
|
|
||||||
leverage: number;
|
|
||||||
name: string;
|
|
||||||
server: string;
|
|
||||||
company: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MT4Position {
|
|
||||||
ticket: number;
|
|
||||||
symbol: string;
|
|
||||||
type: 'buy' | 'sell';
|
|
||||||
lots: number;
|
|
||||||
openPrice: number;
|
|
||||||
currentPrice: number;
|
|
||||||
stopLoss: number | null;
|
|
||||||
takeProfit: number | null;
|
|
||||||
profit: number;
|
|
||||||
swap: number;
|
|
||||||
openTime: string;
|
|
||||||
magic: number;
|
|
||||||
comment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MT4Tick {
|
|
||||||
symbol: string;
|
|
||||||
bid: number;
|
|
||||||
ask: number;
|
|
||||||
timestamp: string;
|
|
||||||
spread: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TradeResult {
|
|
||||||
success: boolean;
|
|
||||||
ticket?: number;
|
|
||||||
message: string;
|
|
||||||
errorCode?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TradeRequest {
|
|
||||||
action: 'buy' | 'sell';
|
|
||||||
symbol: string;
|
|
||||||
lots: number;
|
|
||||||
stopLoss?: number;
|
|
||||||
takeProfit?: number;
|
|
||||||
price?: number;
|
|
||||||
slippage?: number;
|
|
||||||
magic?: number;
|
|
||||||
comment?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClosePositionRequest {
|
|
||||||
ticket: number;
|
|
||||||
lots?: number;
|
|
||||||
slippage?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModifyPositionRequest {
|
|
||||||
ticket: number;
|
|
||||||
stopLoss?: number;
|
|
||||||
takeProfit?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MT4ClientConfig {
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
authToken: string;
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// MT4 Client Class
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
export class MT4Client {
|
|
||||||
private client: AxiosInstance;
|
|
||||||
private baseUrl: string;
|
|
||||||
|
|
||||||
constructor(config: MT4ClientConfig) {
|
|
||||||
this.baseUrl = `http://${config.host}:${config.port}`;
|
|
||||||
|
|
||||||
this.client = axios.create({
|
|
||||||
baseURL: this.baseUrl,
|
|
||||||
timeout: config.timeout || 10000,
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${config.authToken}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the MT4 terminal is connected
|
|
||||||
*/
|
|
||||||
async isConnected(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const response = await this.client.get('/status');
|
|
||||||
return response.data?.connected ?? false;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get MT4 account information
|
|
||||||
*/
|
|
||||||
async getAccountInfo(): Promise<MT4AccountInfo> {
|
|
||||||
try {
|
|
||||||
const response = await this.client.get('/account');
|
|
||||||
const data = response.data;
|
|
||||||
|
|
||||||
return {
|
|
||||||
balance: data.balance ?? 0,
|
|
||||||
equity: data.equity ?? 0,
|
|
||||||
margin: data.margin ?? 0,
|
|
||||||
freeMargin: data.freeMargin ?? 0,
|
|
||||||
marginLevel: data.marginLevel ?? null,
|
|
||||||
profit: data.profit ?? 0,
|
|
||||||
currency: data.currency ?? 'USD',
|
|
||||||
leverage: data.leverage ?? 100,
|
|
||||||
name: data.name ?? '',
|
|
||||||
server: data.server ?? '',
|
|
||||||
company: data.company ?? '',
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw this.handleError(error, 'Failed to get account info');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current tick (quote) for a symbol
|
|
||||||
*/
|
|
||||||
async getTick(symbol: string): Promise<MT4Tick> {
|
|
||||||
try {
|
|
||||||
const response = await this.client.get(`/tick/${symbol}`);
|
|
||||||
const data = response.data;
|
|
||||||
|
|
||||||
const bid = data.bid ?? 0;
|
|
||||||
const ask = data.ask ?? 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
symbol,
|
|
||||||
bid,
|
|
||||||
ask,
|
|
||||||
timestamp: data.time ?? new Date().toISOString(),
|
|
||||||
spread: Math.round((ask - bid) * 100000) / 100000,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw this.handleError(error, `Failed to get tick for ${symbol}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all open positions
|
|
||||||
*/
|
|
||||||
async getPositions(): Promise<MT4Position[]> {
|
|
||||||
try {
|
|
||||||
const response = await this.client.get('/positions');
|
|
||||||
const data = response.data;
|
|
||||||
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.map((p: Record<string, unknown>) => ({
|
|
||||||
ticket: (p.ticket as number) ?? 0,
|
|
||||||
symbol: (p.symbol as string) ?? '',
|
|
||||||
type: (p.type as 'buy' | 'sell') ?? 'buy',
|
|
||||||
lots: (p.lots as number) ?? 0,
|
|
||||||
openPrice: (p.openPrice as number) ?? 0,
|
|
||||||
currentPrice: (p.currentPrice as number) ?? 0,
|
|
||||||
stopLoss: (p.stopLoss as number | null) ?? null,
|
|
||||||
takeProfit: (p.takeProfit as number | null) ?? null,
|
|
||||||
profit: (p.profit as number) ?? 0,
|
|
||||||
swap: (p.swap as number) ?? 0,
|
|
||||||
openTime: (p.openTime as string) ?? new Date().toISOString(),
|
|
||||||
magic: (p.magic as number) ?? 0,
|
|
||||||
comment: (p.comment as string) ?? '',
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
throw this.handleError(error, 'Failed to get positions');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a specific position by ticket
|
|
||||||
*/
|
|
||||||
async getPosition(ticket: number): Promise<MT4Position | null> {
|
|
||||||
const positions = await this.getPositions();
|
|
||||||
return positions.find(p => p.ticket === ticket) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a trade (buy or sell)
|
|
||||||
*/
|
|
||||||
async executeTrade(request: TradeRequest): Promise<TradeResult> {
|
|
||||||
try {
|
|
||||||
const payload: Record<string, unknown> = {
|
|
||||||
action: request.action,
|
|
||||||
symbol: request.symbol,
|
|
||||||
lots: request.lots,
|
|
||||||
slippage: request.slippage ?? 3,
|
|
||||||
magic: request.magic ?? 12345,
|
|
||||||
comment: request.comment ?? 'MCP-MT4',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (request.stopLoss !== undefined) {
|
|
||||||
payload.stopLoss = request.stopLoss;
|
|
||||||
}
|
|
||||||
if (request.takeProfit !== undefined) {
|
|
||||||
payload.takeProfit = request.takeProfit;
|
|
||||||
}
|
|
||||||
if (request.price !== undefined) {
|
|
||||||
payload.price = request.price;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.client.post('/trade', payload);
|
|
||||||
const data = response.data;
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: data.success ?? false,
|
|
||||||
ticket: data.ticket,
|
|
||||||
message: data.message ?? '',
|
|
||||||
errorCode: data.errorCode,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: this.getErrorMessage(error),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close a position
|
|
||||||
*/
|
|
||||||
async closePosition(request: ClosePositionRequest): Promise<TradeResult> {
|
|
||||||
try {
|
|
||||||
const payload: Record<string, unknown> = {
|
|
||||||
action: 'close',
|
|
||||||
ticket: request.ticket,
|
|
||||||
slippage: request.slippage ?? 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (request.lots !== undefined) {
|
|
||||||
payload.lots = request.lots;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.client.post('/trade', payload);
|
|
||||||
const data = response.data;
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: data.success ?? false,
|
|
||||||
ticket: request.ticket,
|
|
||||||
message: data.message ?? '',
|
|
||||||
errorCode: data.errorCode,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
ticket: request.ticket,
|
|
||||||
message: this.getErrorMessage(error),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modify a position (SL/TP)
|
|
||||||
*/
|
|
||||||
async modifyPosition(request: ModifyPositionRequest): Promise<TradeResult> {
|
|
||||||
try {
|
|
||||||
const payload: Record<string, unknown> = {
|
|
||||||
action: 'modify',
|
|
||||||
ticket: request.ticket,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (request.stopLoss !== undefined) {
|
|
||||||
payload.stopLoss = request.stopLoss;
|
|
||||||
}
|
|
||||||
if (request.takeProfit !== undefined) {
|
|
||||||
payload.takeProfit = request.takeProfit;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.client.post('/trade', payload);
|
|
||||||
const data = response.data;
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: data.success ?? false,
|
|
||||||
ticket: request.ticket,
|
|
||||||
message: data.message ?? '',
|
|
||||||
errorCode: data.errorCode,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
ticket: request.ticket,
|
|
||||||
message: this.getErrorMessage(error),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle axios errors and convert to meaningful messages
|
|
||||||
*/
|
|
||||||
private handleError(error: unknown, context: string): Error {
|
|
||||||
const message = this.getErrorMessage(error);
|
|
||||||
return new Error(`${context}: ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract error message from various error types
|
|
||||||
*/
|
|
||||||
private getErrorMessage(error: unknown): string {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
const axiosError = error as AxiosError;
|
|
||||||
if (axiosError.response) {
|
|
||||||
return `HTTP ${axiosError.response.status}: ${JSON.stringify(axiosError.response.data)}`;
|
|
||||||
}
|
|
||||||
if (axiosError.code === 'ECONNREFUSED') {
|
|
||||||
return 'Connection refused - MT4 Gateway is not running';
|
|
||||||
}
|
|
||||||
if (axiosError.code === 'ETIMEDOUT') {
|
|
||||||
return 'Connection timeout - MT4 Gateway is not responding';
|
|
||||||
}
|
|
||||||
return axiosError.message;
|
|
||||||
}
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
return 'Unknown error';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Singleton Instance
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
let clientInstance: MT4Client | null = null;
|
|
||||||
|
|
||||||
export function getMT4Client(): MT4Client {
|
|
||||||
if (!clientInstance) {
|
|
||||||
const config: MT4ClientConfig = {
|
|
||||||
host: process.env.MT4_GATEWAY_HOST || 'localhost',
|
|
||||||
port: parseInt(process.env.MT4_GATEWAY_PORT || '8081', 10),
|
|
||||||
authToken: process.env.MT4_GATEWAY_AUTH_TOKEN || 'secret',
|
|
||||||
timeout: parseInt(process.env.REQUEST_TIMEOUT || '10000', 10),
|
|
||||||
};
|
|
||||||
clientInstance = new MT4Client(config);
|
|
||||||
}
|
|
||||||
return clientInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetMT4Client(): void {
|
|
||||||
clientInstance = null;
|
|
||||||
}
|
|
||||||
@ -1,143 +0,0 @@
|
|||||||
/**
|
|
||||||
* mt4_get_account - Get MT4 account information
|
|
||||||
*
|
|
||||||
* @description Retrieves comprehensive account information from the connected MT4 terminal
|
|
||||||
* including balance, equity, margin, leverage, and broker details.
|
|
||||||
*
|
|
||||||
* @returns Account information object with balance, equity, margin details
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const result = await mt4_get_account({});
|
|
||||||
* // Returns:
|
|
||||||
* // {
|
|
||||||
* // balance: 10000.00,
|
|
||||||
* // equity: 10250.50,
|
|
||||||
* // margin: 500.00,
|
|
||||||
* // freeMargin: 9750.50,
|
|
||||||
* // marginLevel: 2050.10,
|
|
||||||
* // profit: 250.50,
|
|
||||||
* // currency: "USD",
|
|
||||||
* // leverage: 100,
|
|
||||||
* // name: "Demo Account",
|
|
||||||
* // server: "ICMarkets-Demo",
|
|
||||||
* // company: "IC Markets"
|
|
||||||
* // }
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { getMT4Client, MT4AccountInfo } from '../services/mt4-client';
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Schema Definition
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
export const mt4GetAccountSchema = {
|
|
||||||
name: 'mt4_get_account',
|
|
||||||
description: 'Get MT4 account information including balance, equity, margin, and broker details',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {},
|
|
||||||
required: [] as string[],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Input validation schema (no params required)
|
|
||||||
export const Mt4GetAccountInputSchema = z.object({});
|
|
||||||
|
|
||||||
export type Mt4GetAccountInput = z.infer<typeof Mt4GetAccountInputSchema>;
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Tool Implementation
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
export interface Mt4GetAccountResult {
|
|
||||||
success: boolean;
|
|
||||||
data?: MT4AccountInfo;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mt4_get_account(
|
|
||||||
_params: Mt4GetAccountInput
|
|
||||||
): Promise<Mt4GetAccountResult> {
|
|
||||||
try {
|
|
||||||
const client = getMT4Client();
|
|
||||||
|
|
||||||
// Check connection first
|
|
||||||
const isConnected = await client.isConnected();
|
|
||||||
if (!isConnected) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'MT4 terminal is not connected',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get account info
|
|
||||||
const accountInfo = await client.getAccountInfo();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: accountInfo,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// MCP Tool Handler
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
export async function handleMt4GetAccount(
|
|
||||||
params: unknown
|
|
||||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
||||||
// Validate input
|
|
||||||
const validatedParams = Mt4GetAccountInputSchema.parse(params);
|
|
||||||
|
|
||||||
// Execute tool
|
|
||||||
const result = await mt4_get_account(validatedParams);
|
|
||||||
|
|
||||||
// Format response for MCP
|
|
||||||
if (result.success && result.data) {
|
|
||||||
const formattedOutput = `
|
|
||||||
MT4 Account Information
|
|
||||||
=======================
|
|
||||||
Account Name: ${result.data.name}
|
|
||||||
Server: ${result.data.server}
|
|
||||||
Broker: ${result.data.company}
|
|
||||||
Leverage: 1:${result.data.leverage}
|
|
||||||
|
|
||||||
Financial Summary
|
|
||||||
-----------------
|
|
||||||
Balance: ${result.data.balance.toFixed(2)} ${result.data.currency}
|
|
||||||
Equity: ${result.data.equity.toFixed(2)} ${result.data.currency}
|
|
||||||
Profit/Loss: ${result.data.profit >= 0 ? '+' : ''}${result.data.profit.toFixed(2)} ${result.data.currency}
|
|
||||||
|
|
||||||
Margin Details
|
|
||||||
--------------
|
|
||||||
Used Margin: ${result.data.margin.toFixed(2)} ${result.data.currency}
|
|
||||||
Free Margin: ${result.data.freeMargin.toFixed(2)} ${result.data.currency}
|
|
||||||
Margin Level: ${result.data.marginLevel !== null ? result.data.marginLevel.toFixed(2) + '%' : 'N/A'}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: formattedOutput,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: `Error: ${result.error}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,212 +0,0 @@
|
|||||||
/**
|
|
||||||
* MCP Tools Index
|
|
||||||
*
|
|
||||||
* Exports all MT4 MCP tools and their schemas for registration
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Import handlers for use in toolHandlers map
|
|
||||||
import { handleMt4GetAccount } from './account';
|
|
||||||
import { handleMt4GetPositions, handleMt4ClosePosition } from './positions';
|
|
||||||
import { handleMt4ExecuteTrade, handleMt4ModifyPosition } from './trading';
|
|
||||||
import { handleMt4GetQuote } from './quotes';
|
|
||||||
|
|
||||||
// Account tools
|
|
||||||
export {
|
|
||||||
mt4GetAccountSchema,
|
|
||||||
mt4_get_account,
|
|
||||||
handleMt4GetAccount,
|
|
||||||
Mt4GetAccountInputSchema,
|
|
||||||
type Mt4GetAccountInput,
|
|
||||||
type Mt4GetAccountResult,
|
|
||||||
} from './account';
|
|
||||||
|
|
||||||
// Position tools
|
|
||||||
export {
|
|
||||||
mt4GetPositionsSchema,
|
|
||||||
mt4_get_positions,
|
|
||||||
handleMt4GetPositions,
|
|
||||||
Mt4GetPositionsInputSchema,
|
|
||||||
mt4ClosePositionSchema,
|
|
||||||
mt4_close_position,
|
|
||||||
handleMt4ClosePosition,
|
|
||||||
Mt4ClosePositionInputSchema,
|
|
||||||
type Mt4GetPositionsInput,
|
|
||||||
type Mt4GetPositionsResult,
|
|
||||||
type Mt4ClosePositionInput,
|
|
||||||
type Mt4ClosePositionResult,
|
|
||||||
} from './positions';
|
|
||||||
|
|
||||||
// Trading tools
|
|
||||||
export {
|
|
||||||
mt4ExecuteTradeSchema,
|
|
||||||
mt4_execute_trade,
|
|
||||||
handleMt4ExecuteTrade,
|
|
||||||
Mt4ExecuteTradeInputSchema,
|
|
||||||
mt4ModifyPositionSchema,
|
|
||||||
mt4_modify_position,
|
|
||||||
handleMt4ModifyPosition,
|
|
||||||
Mt4ModifyPositionInputSchema,
|
|
||||||
type Mt4ExecuteTradeInput,
|
|
||||||
type Mt4ExecuteTradeResult,
|
|
||||||
type Mt4ModifyPositionInput,
|
|
||||||
type Mt4ModifyPositionResult,
|
|
||||||
} from './trading';
|
|
||||||
|
|
||||||
// Quote tools
|
|
||||||
export {
|
|
||||||
mt4GetQuoteSchema,
|
|
||||||
mt4_get_quote,
|
|
||||||
handleMt4GetQuote,
|
|
||||||
Mt4GetQuoteInputSchema,
|
|
||||||
type Mt4GetQuoteInput,
|
|
||||||
type Mt4GetQuoteResult,
|
|
||||||
} from './quotes';
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Tool Registry
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All available MCP tools with their schemas
|
|
||||||
*/
|
|
||||||
export const mcpToolSchemas = [
|
|
||||||
{
|
|
||||||
name: 'mt4_get_account',
|
|
||||||
description: 'Get MT4 account information including balance, equity, margin, and broker details',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {},
|
|
||||||
required: [] as string[],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mt4_get_positions',
|
|
||||||
description: 'List all open trading positions from MT4. Optionally filter by symbol.',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {
|
|
||||||
symbol: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Optional: Filter positions by symbol (e.g., XAUUSD, EURUSD)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: [] as string[],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mt4_execute_trade',
|
|
||||||
description: 'Execute a market order (BUY or SELL) on MT4 with optional stop loss and take profit',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {
|
|
||||||
symbol: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Trading symbol (e.g., XAUUSD, EURUSD, GBPUSD)',
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
type: 'string',
|
|
||||||
enum: ['buy', 'sell'],
|
|
||||||
description: 'Trade direction: buy or sell',
|
|
||||||
},
|
|
||||||
lots: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Volume in lots (e.g., 0.01, 0.1, 1.0)',
|
|
||||||
},
|
|
||||||
stopLoss: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Stop loss price level',
|
|
||||||
},
|
|
||||||
takeProfit: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Take profit price level',
|
|
||||||
},
|
|
||||||
slippage: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
|
||||||
},
|
|
||||||
magic: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Magic number for EA identification (default: 12345)',
|
|
||||||
},
|
|
||||||
comment: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Optional: Order comment (max 31 chars)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['symbol', 'action', 'lots'] as string[],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mt4_close_position',
|
|
||||||
description: 'Close an open trading position by ticket number. Can close partially.',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {
|
|
||||||
ticket: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Position ticket number to close',
|
|
||||||
},
|
|
||||||
lots: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Partial volume to close. If not specified, closes entire position.',
|
|
||||||
},
|
|
||||||
slippage: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['ticket'] as string[],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mt4_modify_position',
|
|
||||||
description: 'Modify stop loss and/or take profit of an existing position',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {
|
|
||||||
ticket: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Position ticket number to modify',
|
|
||||||
},
|
|
||||||
stopLoss: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'New stop loss price level (optional)',
|
|
||||||
},
|
|
||||||
takeProfit: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'New take profit price level (optional)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['ticket'] as string[],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mt4_get_quote',
|
|
||||||
description: 'Get current price quote (bid/ask/spread) for a trading symbol',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {
|
|
||||||
symbol: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Trading symbol to get quote for (e.g., XAUUSD, EURUSD, GBPUSD)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['symbol'] as string[],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tool handler routing map
|
|
||||||
*/
|
|
||||||
export const toolHandlers: Record<
|
|
||||||
string,
|
|
||||||
(params: unknown) => Promise<{ content: Array<{ type: string; text: string }> }>
|
|
||||||
> = {
|
|
||||||
mt4_get_account: handleMt4GetAccount,
|
|
||||||
mt4_get_positions: handleMt4GetPositions,
|
|
||||||
mt4_execute_trade: handleMt4ExecuteTrade,
|
|
||||||
mt4_close_position: handleMt4ClosePosition,
|
|
||||||
mt4_modify_position: handleMt4ModifyPosition,
|
|
||||||
mt4_get_quote: handleMt4GetQuote,
|
|
||||||
};
|
|
||||||
@ -1,315 +0,0 @@
|
|||||||
/**
|
|
||||||
* MT4 Position Tools
|
|
||||||
*
|
|
||||||
* - mt4_get_positions: List all open positions
|
|
||||||
* - mt4_close_position: Close a specific position
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { getMT4Client, MT4Position, TradeResult } from '../services/mt4-client';
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// mt4_get_positions
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* mt4_get_positions - List all open positions
|
|
||||||
*
|
|
||||||
* @description Retrieves all currently open positions from MT4 terminal.
|
|
||||||
* Can optionally filter by symbol.
|
|
||||||
*
|
|
||||||
* @param symbol - Optional symbol to filter positions (e.g., "XAUUSD")
|
|
||||||
* @returns Array of open positions with details
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const result = await mt4_get_positions({});
|
|
||||||
* // Returns all positions
|
|
||||||
*
|
|
||||||
* const result = await mt4_get_positions({ symbol: "XAUUSD" });
|
|
||||||
* // Returns only XAUUSD positions
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const mt4GetPositionsSchema = {
|
|
||||||
name: 'mt4_get_positions',
|
|
||||||
description: 'List all open trading positions from MT4. Optionally filter by symbol.',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {
|
|
||||||
symbol: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Optional: Filter positions by symbol (e.g., XAUUSD, EURUSD)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: [] as string[],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Mt4GetPositionsInputSchema = z.object({
|
|
||||||
symbol: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Mt4GetPositionsInput = z.infer<typeof Mt4GetPositionsInputSchema>;
|
|
||||||
|
|
||||||
export interface Mt4GetPositionsResult {
|
|
||||||
success: boolean;
|
|
||||||
data?: {
|
|
||||||
positions: MT4Position[];
|
|
||||||
totalProfit: number;
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mt4_get_positions(
|
|
||||||
params: Mt4GetPositionsInput
|
|
||||||
): Promise<Mt4GetPositionsResult> {
|
|
||||||
try {
|
|
||||||
const client = getMT4Client();
|
|
||||||
|
|
||||||
// Check connection
|
|
||||||
const isConnected = await client.isConnected();
|
|
||||||
if (!isConnected) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'MT4 terminal is not connected',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all positions
|
|
||||||
let positions = await client.getPositions();
|
|
||||||
|
|
||||||
// Filter by symbol if specified
|
|
||||||
if (params.symbol) {
|
|
||||||
positions = positions.filter(
|
|
||||||
p => p.symbol.toUpperCase() === params.symbol!.toUpperCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate total profit
|
|
||||||
const totalProfit = positions.reduce((sum, p) => sum + p.profit, 0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
positions,
|
|
||||||
totalProfit,
|
|
||||||
count: positions.length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleMt4GetPositions(
|
|
||||||
params: unknown
|
|
||||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
||||||
const validatedParams = Mt4GetPositionsInputSchema.parse(params);
|
|
||||||
const result = await mt4_get_positions(validatedParams);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
if (result.data.count === 0) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: params && (params as Mt4GetPositionsInput).symbol
|
|
||||||
? `No open positions found for ${(params as Mt4GetPositionsInput).symbol}`
|
|
||||||
: 'No open positions found',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const positionLines = result.data.positions.map(p => {
|
|
||||||
const direction = p.type.toUpperCase();
|
|
||||||
const profitSign = p.profit >= 0 ? '+' : '';
|
|
||||||
const slInfo = p.stopLoss !== null ? `SL: ${p.stopLoss}` : 'SL: None';
|
|
||||||
const tpInfo = p.takeProfit !== null ? `TP: ${p.takeProfit}` : 'TP: None';
|
|
||||||
|
|
||||||
return `
|
|
||||||
#${p.ticket} | ${p.symbol} | ${direction} ${p.lots} lots
|
|
||||||
Open: ${p.openPrice} | Current: ${p.currentPrice}
|
|
||||||
${slInfo} | ${tpInfo}
|
|
||||||
P/L: ${profitSign}${p.profit.toFixed(2)} | Swap: ${p.swap.toFixed(2)}
|
|
||||||
Opened: ${p.openTime}
|
|
||||||
Magic: ${p.magic} | Comment: ${p.comment || 'None'}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const formattedOutput = `
|
|
||||||
Open Positions (${result.data.count})
|
|
||||||
${'='.repeat(30)}
|
|
||||||
${positionLines.join('\n---\n')}
|
|
||||||
|
|
||||||
Total P/L: ${result.data.totalProfit >= 0 ? '+' : ''}${result.data.totalProfit.toFixed(2)}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: formattedOutput,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: `Error: ${result.error}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// mt4_close_position
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* mt4_close_position - Close a trading position
|
|
||||||
*
|
|
||||||
* @description Closes an open position by ticket number.
|
|
||||||
* Can optionally close partial volume.
|
|
||||||
*
|
|
||||||
* @param ticket - Position ticket number to close
|
|
||||||
* @param lots - Optional: Partial volume to close (default: close all)
|
|
||||||
* @param slippage - Optional: Maximum slippage in points (default: 3)
|
|
||||||
* @returns Trade result with success status
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Close entire position
|
|
||||||
* const result = await mt4_close_position({ ticket: 123456 });
|
|
||||||
*
|
|
||||||
* // Close partial position
|
|
||||||
* const result = await mt4_close_position({ ticket: 123456, lots: 0.5 });
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const mt4ClosePositionSchema = {
|
|
||||||
name: 'mt4_close_position',
|
|
||||||
description: 'Close an open trading position by ticket number. Can close partially.',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {
|
|
||||||
ticket: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Position ticket number to close',
|
|
||||||
},
|
|
||||||
lots: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Partial volume to close. If not specified, closes entire position.',
|
|
||||||
},
|
|
||||||
slippage: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['ticket'] as string[],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Mt4ClosePositionInputSchema = z.object({
|
|
||||||
ticket: z.number().int().positive(),
|
|
||||||
lots: z.number().positive().optional(),
|
|
||||||
slippage: z.number().int().min(0).max(100).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Mt4ClosePositionInput = z.infer<typeof Mt4ClosePositionInputSchema>;
|
|
||||||
|
|
||||||
export interface Mt4ClosePositionResult {
|
|
||||||
success: boolean;
|
|
||||||
data?: TradeResult;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mt4_close_position(
|
|
||||||
params: Mt4ClosePositionInput
|
|
||||||
): Promise<Mt4ClosePositionResult> {
|
|
||||||
try {
|
|
||||||
const client = getMT4Client();
|
|
||||||
|
|
||||||
// Check connection
|
|
||||||
const isConnected = await client.isConnected();
|
|
||||||
if (!isConnected) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'MT4 terminal is not connected',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify position exists
|
|
||||||
const position = await client.getPosition(params.ticket);
|
|
||||||
if (!position) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Position with ticket ${params.ticket} not found`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate lots if specified
|
|
||||||
if (params.lots !== undefined && params.lots > position.lots) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Requested lots (${params.lots}) exceeds position size (${position.lots})`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close position
|
|
||||||
const result = await client.closePosition({
|
|
||||||
ticket: params.ticket,
|
|
||||||
lots: params.lots,
|
|
||||||
slippage: params.slippage,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: result.success,
|
|
||||||
data: result,
|
|
||||||
error: result.success ? undefined : result.message,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleMt4ClosePosition(
|
|
||||||
params: unknown
|
|
||||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
||||||
const validatedParams = Mt4ClosePositionInputSchema.parse(params);
|
|
||||||
const result = await mt4_close_position(validatedParams);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
const formattedOutput = `
|
|
||||||
Position Closed Successfully
|
|
||||||
============================
|
|
||||||
Ticket: ${validatedParams.ticket}
|
|
||||||
${validatedParams.lots ? `Closed Volume: ${validatedParams.lots} lots` : 'Closed: Entire position'}
|
|
||||||
Message: ${result.data.message || 'Position closed'}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: formattedOutput,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: `Error closing position: ${result.error}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
/**
|
|
||||||
* mt4_get_quote - Get current price quote for a symbol
|
|
||||||
*
|
|
||||||
* @description Retrieves the current bid/ask prices for a trading symbol.
|
|
||||||
* Also calculates the spread in points.
|
|
||||||
*
|
|
||||||
* @param symbol - Trading symbol to get quote for (e.g., "XAUUSD")
|
|
||||||
* @returns Current bid, ask, spread, and timestamp
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const result = await mt4_get_quote({ symbol: "XAUUSD" });
|
|
||||||
* // Returns:
|
|
||||||
* // {
|
|
||||||
* // symbol: "XAUUSD",
|
|
||||||
* // bid: 2650.50,
|
|
||||||
* // ask: 2650.80,
|
|
||||||
* // spread: 0.30,
|
|
||||||
* // timestamp: "2026-01-04T12:00:00.000Z"
|
|
||||||
* // }
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { getMT4Client, MT4Tick } from '../services/mt4-client';
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Schema Definition
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
export const mt4GetQuoteSchema = {
|
|
||||||
name: 'mt4_get_quote',
|
|
||||||
description: 'Get current price quote (bid/ask/spread) for a trading symbol',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {
|
|
||||||
symbol: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Trading symbol to get quote for (e.g., XAUUSD, EURUSD, GBPUSD)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['symbol'] as string[],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Mt4GetQuoteInputSchema = z.object({
|
|
||||||
symbol: z.string().min(1).max(20),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Mt4GetQuoteInput = z.infer<typeof Mt4GetQuoteInputSchema>;
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Tool Implementation
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
export interface Mt4GetQuoteResult {
|
|
||||||
success: boolean;
|
|
||||||
data?: MT4Tick;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mt4_get_quote(
|
|
||||||
params: Mt4GetQuoteInput
|
|
||||||
): Promise<Mt4GetQuoteResult> {
|
|
||||||
try {
|
|
||||||
const client = getMT4Client();
|
|
||||||
|
|
||||||
// Check connection
|
|
||||||
const isConnected = await client.isConnected();
|
|
||||||
if (!isConnected) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'MT4 terminal is not connected',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tick data
|
|
||||||
const tick = await client.getTick(params.symbol.toUpperCase());
|
|
||||||
|
|
||||||
// Validate we got valid data
|
|
||||||
if (tick.bid === 0 && tick.ask === 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `No quote data available for ${params.symbol}. Symbol may not be available on this broker.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: tick,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// MCP Tool Handler
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
export async function handleMt4GetQuote(
|
|
||||||
params: unknown
|
|
||||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
||||||
const validatedParams = Mt4GetQuoteInputSchema.parse(params);
|
|
||||||
const result = await mt4_get_quote(validatedParams);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
// Determine decimal places based on symbol
|
|
||||||
const decimals = getDecimalPlaces(result.data.symbol);
|
|
||||||
const spreadPips = calculateSpreadPips(result.data.spread, result.data.symbol);
|
|
||||||
|
|
||||||
const formattedOutput = `
|
|
||||||
Price Quote: ${result.data.symbol}
|
|
||||||
${'='.repeat(25)}
|
|
||||||
Bid: ${result.data.bid.toFixed(decimals)}
|
|
||||||
Ask: ${result.data.ask.toFixed(decimals)}
|
|
||||||
Spread: ${result.data.spread.toFixed(decimals)} (${spreadPips.toFixed(1)} pips)
|
|
||||||
Time: ${result.data.timestamp}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: formattedOutput,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: `Error: ${result.error}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Helper Functions
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine decimal places based on symbol type
|
|
||||||
*/
|
|
||||||
function getDecimalPlaces(symbol: string): number {
|
|
||||||
const upperSymbol = symbol.toUpperCase();
|
|
||||||
|
|
||||||
// JPY pairs have 3 decimals, most forex 5 decimals
|
|
||||||
if (upperSymbol.includes('JPY')) {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gold and metals typically 2 decimals
|
|
||||||
if (upperSymbol.startsWith('XAU') || upperSymbol.startsWith('XAG')) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indices vary, use 2 as default
|
|
||||||
if (
|
|
||||||
upperSymbol.includes('US30') ||
|
|
||||||
upperSymbol.includes('US500') ||
|
|
||||||
upperSymbol.includes('NAS')
|
|
||||||
) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default forex pairs
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate spread in pips based on symbol
|
|
||||||
*/
|
|
||||||
function calculateSpreadPips(spread: number, symbol: string): number {
|
|
||||||
const upperSymbol = symbol.toUpperCase();
|
|
||||||
|
|
||||||
// JPY pairs: 1 pip = 0.01
|
|
||||||
if (upperSymbol.includes('JPY')) {
|
|
||||||
return spread * 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gold: 1 pip = 0.10
|
|
||||||
if (upperSymbol.startsWith('XAU')) {
|
|
||||||
return spread * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default forex: 1 pip = 0.0001
|
|
||||||
return spread * 10000;
|
|
||||||
}
|
|
||||||
@ -1,402 +0,0 @@
|
|||||||
/**
|
|
||||||
* MT4 Trading Tools
|
|
||||||
*
|
|
||||||
* - mt4_execute_trade: Execute BUY/SELL market orders
|
|
||||||
* - mt4_modify_position: Modify SL/TP of existing positions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { getMT4Client, TradeResult } from '../services/mt4-client';
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// mt4_execute_trade
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* mt4_execute_trade - Execute a market order (BUY or SELL)
|
|
||||||
*
|
|
||||||
* @description Opens a new trading position with optional SL/TP levels.
|
|
||||||
* Supports market orders only (pending orders not implemented).
|
|
||||||
*
|
|
||||||
* @param symbol - Trading symbol (e.g., "XAUUSD", "EURUSD")
|
|
||||||
* @param action - Trade direction: "buy" or "sell"
|
|
||||||
* @param lots - Volume in lots (e.g., 0.01, 0.1, 1.0)
|
|
||||||
* @param stopLoss - Optional stop loss price
|
|
||||||
* @param takeProfit - Optional take profit price
|
|
||||||
* @param slippage - Optional max slippage in points (default: 3)
|
|
||||||
* @param magic - Optional magic number for EA identification
|
|
||||||
* @param comment - Optional order comment
|
|
||||||
* @returns Trade result with ticket number
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Simple buy order
|
|
||||||
* const result = await mt4_execute_trade({
|
|
||||||
* symbol: "XAUUSD",
|
|
||||||
* action: "buy",
|
|
||||||
* lots: 0.1
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Buy with SL/TP
|
|
||||||
* const result = await mt4_execute_trade({
|
|
||||||
* symbol: "XAUUSD",
|
|
||||||
* action: "buy",
|
|
||||||
* lots: 0.1,
|
|
||||||
* stopLoss: 2640.00,
|
|
||||||
* takeProfit: 2680.00,
|
|
||||||
* comment: "AI Signal"
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const mt4ExecuteTradeSchema = {
|
|
||||||
name: 'mt4_execute_trade',
|
|
||||||
description: 'Execute a market order (BUY or SELL) on MT4 with optional stop loss and take profit',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {
|
|
||||||
symbol: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Trading symbol (e.g., XAUUSD, EURUSD, GBPUSD)',
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
type: 'string',
|
|
||||||
enum: ['buy', 'sell'],
|
|
||||||
description: 'Trade direction: buy or sell',
|
|
||||||
},
|
|
||||||
lots: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Volume in lots (e.g., 0.01, 0.1, 1.0)',
|
|
||||||
},
|
|
||||||
stopLoss: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Stop loss price level',
|
|
||||||
},
|
|
||||||
takeProfit: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Take profit price level',
|
|
||||||
},
|
|
||||||
slippage: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
|
||||||
},
|
|
||||||
magic: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Optional: Magic number for EA identification (default: 12345)',
|
|
||||||
},
|
|
||||||
comment: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Optional: Order comment (max 31 chars)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['symbol', 'action', 'lots'] as string[],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Mt4ExecuteTradeInputSchema = z.object({
|
|
||||||
symbol: z.string().min(1).max(20),
|
|
||||||
action: z.enum(['buy', 'sell']),
|
|
||||||
lots: z.number().positive().max(100),
|
|
||||||
stopLoss: z.number().positive().optional(),
|
|
||||||
takeProfit: z.number().positive().optional(),
|
|
||||||
slippage: z.number().int().min(0).max(100).optional(),
|
|
||||||
magic: z.number().int().optional(),
|
|
||||||
comment: z.string().max(31).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Mt4ExecuteTradeInput = z.infer<typeof Mt4ExecuteTradeInputSchema>;
|
|
||||||
|
|
||||||
export interface Mt4ExecuteTradeResult {
|
|
||||||
success: boolean;
|
|
||||||
data?: TradeResult & {
|
|
||||||
symbol: string;
|
|
||||||
action: string;
|
|
||||||
lots: number;
|
|
||||||
};
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mt4_execute_trade(
|
|
||||||
params: Mt4ExecuteTradeInput
|
|
||||||
): Promise<Mt4ExecuteTradeResult> {
|
|
||||||
try {
|
|
||||||
const client = getMT4Client();
|
|
||||||
|
|
||||||
// Check connection
|
|
||||||
const isConnected = await client.isConnected();
|
|
||||||
if (!isConnected) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'MT4 terminal is not connected',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate SL/TP logic (basic check)
|
|
||||||
if (params.stopLoss !== undefined && params.takeProfit !== undefined) {
|
|
||||||
if (params.action === 'buy') {
|
|
||||||
// For buy: SL should be below current price, TP above
|
|
||||||
if (params.stopLoss >= params.takeProfit) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'For BUY orders, stop loss must be below take profit',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For sell: SL should be above current price, TP below
|
|
||||||
if (params.stopLoss <= params.takeProfit) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'For SELL orders, stop loss must be above take profit',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute trade
|
|
||||||
const result = await client.executeTrade({
|
|
||||||
symbol: params.symbol.toUpperCase(),
|
|
||||||
action: params.action,
|
|
||||||
lots: params.lots,
|
|
||||||
stopLoss: params.stopLoss,
|
|
||||||
takeProfit: params.takeProfit,
|
|
||||||
slippage: params.slippage,
|
|
||||||
magic: params.magic,
|
|
||||||
comment: params.comment,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
...result,
|
|
||||||
symbol: params.symbol.toUpperCase(),
|
|
||||||
action: params.action,
|
|
||||||
lots: params.lots,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: result.message || 'Trade execution failed',
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleMt4ExecuteTrade(
|
|
||||||
params: unknown
|
|
||||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
||||||
const validatedParams = Mt4ExecuteTradeInputSchema.parse(params);
|
|
||||||
const result = await mt4_execute_trade(validatedParams);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
const formattedOutput = `
|
|
||||||
Trade Executed Successfully
|
|
||||||
===========================
|
|
||||||
Ticket: #${result.data.ticket}
|
|
||||||
Symbol: ${result.data.symbol}
|
|
||||||
Direction: ${result.data.action.toUpperCase()}
|
|
||||||
Volume: ${result.data.lots} lots
|
|
||||||
${validatedParams.stopLoss ? `Stop Loss: ${validatedParams.stopLoss}` : 'Stop Loss: Not set'}
|
|
||||||
${validatedParams.takeProfit ? `Take Profit: ${validatedParams.takeProfit}` : 'Take Profit: Not set'}
|
|
||||||
Message: ${result.data.message || 'Order placed successfully'}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: formattedOutput,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: `Error executing trade: ${result.error}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// mt4_modify_position
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* mt4_modify_position - Modify SL/TP of an existing position
|
|
||||||
*
|
|
||||||
* @description Updates the stop loss and/or take profit levels of an open position.
|
|
||||||
*
|
|
||||||
* @param ticket - Position ticket number to modify
|
|
||||||
* @param stopLoss - New stop loss price (null to remove)
|
|
||||||
* @param takeProfit - New take profit price (null to remove)
|
|
||||||
* @returns Modification result
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Set both SL and TP
|
|
||||||
* const result = await mt4_modify_position({
|
|
||||||
* ticket: 123456,
|
|
||||||
* stopLoss: 2640.00,
|
|
||||||
* takeProfit: 2680.00
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Update only TP
|
|
||||||
* const result = await mt4_modify_position({
|
|
||||||
* ticket: 123456,
|
|
||||||
* takeProfit: 2700.00
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const mt4ModifyPositionSchema = {
|
|
||||||
name: 'mt4_modify_position',
|
|
||||||
description: 'Modify stop loss and/or take profit of an existing position',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: {
|
|
||||||
ticket: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Position ticket number to modify',
|
|
||||||
},
|
|
||||||
stopLoss: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'New stop loss price level (optional)',
|
|
||||||
},
|
|
||||||
takeProfit: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'New take profit price level (optional)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['ticket'] as string[],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Mt4ModifyPositionInputSchema = z.object({
|
|
||||||
ticket: z.number().int().positive(),
|
|
||||||
stopLoss: z.number().positive().optional(),
|
|
||||||
takeProfit: z.number().positive().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Mt4ModifyPositionInput = z.infer<typeof Mt4ModifyPositionInputSchema>;
|
|
||||||
|
|
||||||
export interface Mt4ModifyPositionResult {
|
|
||||||
success: boolean;
|
|
||||||
data?: TradeResult;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mt4_modify_position(
|
|
||||||
params: Mt4ModifyPositionInput
|
|
||||||
): Promise<Mt4ModifyPositionResult> {
|
|
||||||
try {
|
|
||||||
const client = getMT4Client();
|
|
||||||
|
|
||||||
// Check connection
|
|
||||||
const isConnected = await client.isConnected();
|
|
||||||
if (!isConnected) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'MT4 terminal is not connected',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate at least one parameter is provided
|
|
||||||
if (params.stopLoss === undefined && params.takeProfit === undefined) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'At least one of stopLoss or takeProfit must be provided',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify position exists
|
|
||||||
const position = await client.getPosition(params.ticket);
|
|
||||||
if (!position) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Position with ticket ${params.ticket} not found`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate SL/TP based on position type
|
|
||||||
const sl = params.stopLoss;
|
|
||||||
const tp = params.takeProfit;
|
|
||||||
|
|
||||||
if (sl !== undefined && tp !== undefined) {
|
|
||||||
if (position.type === 'buy') {
|
|
||||||
if (sl >= tp) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'For BUY positions, stop loss must be below take profit',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (sl <= tp) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'For SELL positions, stop loss must be above take profit',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modify position
|
|
||||||
const result = await client.modifyPosition({
|
|
||||||
ticket: params.ticket,
|
|
||||||
stopLoss: params.stopLoss,
|
|
||||||
takeProfit: params.takeProfit,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: result.success,
|
|
||||||
data: result,
|
|
||||||
error: result.success ? undefined : result.message,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleMt4ModifyPosition(
|
|
||||||
params: unknown
|
|
||||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
||||||
const validatedParams = Mt4ModifyPositionInputSchema.parse(params);
|
|
||||||
const result = await mt4_modify_position(validatedParams);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
const formattedOutput = `
|
|
||||||
Position Modified Successfully
|
|
||||||
==============================
|
|
||||||
Ticket: #${validatedParams.ticket}
|
|
||||||
${validatedParams.stopLoss !== undefined ? `New Stop Loss: ${validatedParams.stopLoss}` : 'Stop Loss: Unchanged'}
|
|
||||||
${validatedParams.takeProfit !== undefined ? `New Take Profit: ${validatedParams.takeProfit}` : 'Take Profit: Unchanged'}
|
|
||||||
Message: ${result.data.message || 'Position modified successfully'}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: formattedOutput,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: `Error modifying position: ${result.error}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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