index.ts•33.7 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { BybitService } from './bybit-service.js';
import { OrderRequest } from './types.js';
class BybitMCPServer {
private server: Server;
private bybitService: BybitService;
constructor() {
this.server = new Server(
{
name: 'bybit-mcp-node',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.bybitService = new BybitService();
this.setupToolHandlers();
this.setupErrorHandling();
}
private setupErrorHandling(): void {
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'get_secret_key',
description: 'Get secret key from environment variables',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_access_key',
description: 'Get access key from environment variables',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_orderbook',
description: 'Get orderbook data for a specific symbol',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
limit: {
type: 'number',
description: 'Number of orderbook entries to retrieve',
default: 50,
},
},
required: ['category', 'symbol'],
},
},
{
name: 'get_kline',
description: 'Get K-line (candlestick) data',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
interval: {
type: 'string',
description: 'Time interval (1, 3, 5, 15, 30, 60, 120, 240, 360, 720, D, W, M)',
},
start: {
type: 'number',
description: 'Start time in milliseconds',
},
end: {
type: 'number',
description: 'End time in milliseconds',
},
limit: {
type: 'number',
description: 'Number of records to retrieve',
default: 200,
},
},
required: ['category', 'symbol', 'interval'],
},
},
{
name: 'get_tickers',
description: 'Get ticker information',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
},
required: ['category', 'symbol'],
},
},
{
name: 'get_wallet_balance',
description: 'Get wallet balance',
inputSchema: {
type: 'object',
properties: {
accountType: {
type: 'string',
description: 'Account type (UNIFIED, CONTRACT, SPOT)',
},
coin: {
type: 'string',
description: 'Coin symbol',
},
},
required: ['accountType'],
},
},
{
name: 'get_positions',
description: 'Get position information',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
},
required: ['category'],
},
},
{
name: 'place_order',
description: 'Execute order',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
side: {
type: 'string',
description: 'Order direction (Buy, Sell)',
},
orderType: {
type: 'string',
description: 'Order type (Market, Limit)',
},
qty: {
type: 'string',
description: 'Order quantity',
},
price: {
type: 'string',
description: 'Order price (for limit orders)',
},
positionIdx: {
type: 'string',
description: 'Position index (1: Long, 2: Short)',
},
timeInForce: {
type: 'string',
description: 'Time in force (GTC, IOC, FOK, PostOnly)',
},
orderLinkId: {
type: 'string',
description: 'Order link ID',
},
isLeverage: {
type: 'number',
description: 'Use leverage (0: No, 1: Yes)',
},
orderFilter: {
type: 'string',
description: 'Order filter (Order, tpslOrder, StopOrder)',
},
triggerPrice: {
type: 'string',
description: 'Trigger price',
},
triggerBy: {
type: 'string',
description: 'Trigger basis',
},
orderIv: {
type: 'string',
description: 'Order volatility',
},
takeProfit: {
type: 'string',
description: 'Take profit price',
},
stopLoss: {
type: 'string',
description: 'Stop loss price',
},
tpTriggerBy: {
type: 'string',
description: 'Take profit trigger basis',
},
slTriggerBy: {
type: 'string',
description: 'Stop loss trigger basis',
},
tpLimitPrice: {
type: 'string',
description: 'Take profit limit price',
},
slLimitPrice: {
type: 'string',
description: 'Stop loss limit price',
},
tpOrderType: {
type: 'string',
description: 'Take profit order type (Market, Limit)',
},
slOrderType: {
type: 'string',
description: 'Stop loss order type (Market, Limit)',
},
},
required: ['category', 'symbol', 'side', 'orderType', 'qty'],
},
},
{
name: 'cancel_order',
description: 'Cancel order',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
orderId: {
type: 'string',
description: 'Order ID',
},
orderLinkId: {
type: 'string',
description: 'Order link ID',
},
orderFilter: {
type: 'string',
description: 'Order filter',
},
},
required: ['category', 'symbol'],
},
},
{
name: 'get_order_history',
description: 'Get order history',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
orderId: {
type: 'string',
description: 'Order ID',
},
orderLinkId: {
type: 'string',
description: 'Order link ID',
},
orderFilter: {
type: 'string',
description: 'Order filter',
},
orderStatus: {
type: 'string',
description: 'Order status',
},
startTime: {
type: 'number',
description: 'Start time in milliseconds',
},
endTime: {
type: 'number',
description: 'End time in milliseconds',
},
limit: {
type: 'number',
description: 'Number of orders to retrieve',
default: 50,
},
},
required: ['category'],
},
},
{
name: 'get_open_orders',
description: 'Get open orders',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
orderId: {
type: 'string',
description: 'Order ID',
},
orderLinkId: {
type: 'string',
description: 'Order link ID',
},
orderFilter: {
type: 'string',
description: 'Order filter',
},
limit: {
type: 'number',
description: 'Number of orders to retrieve',
default: 50,
},
},
required: ['category'],
},
},
{
name: 'set_trading_stop',
description: 'Set trading stop',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
takeProfit: {
type: 'string',
description: 'Take profit price',
},
stopLoss: {
type: 'string',
description: 'Stop loss price',
},
trailingStop: {
type: 'string',
description: 'Trailing stop',
},
positionIdx: {
type: 'number',
description: 'Position index',
},
},
required: ['category', 'symbol'],
},
},
{
name: 'set_margin_mode',
description: 'Set margin mode',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
tradeMode: {
type: 'number',
description: 'Trading mode (0: Isolated, 1: Cross)',
},
buyLeverage: {
type: 'string',
description: 'Buying leverage',
},
sellLeverage: {
type: 'string',
description: 'Selling leverage',
},
},
required: ['category', 'symbol', 'tradeMode', 'buyLeverage', 'sellLeverage'],
},
},
{
name: 'get_api_key_information',
description: 'Get API key information',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_instruments_info',
description: 'Get exchange information',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
status: {
type: 'string',
description: 'Status',
},
baseCoin: {
type: 'string',
description: 'Base coin',
},
},
required: ['category', 'symbol'],
},
},
{
name: 'validate_order_quantity',
description: 'Validate order quantity against Bybit trading rules and get properly formatted quantity',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., BTCUSDT)',
},
targetAmount: {
type: 'number',
description: 'Target amount in quote currency (e.g., 80 for $80 worth)',
},
currentPrice: {
type: 'number',
description: 'Current price (optional, will fetch if not provided)',
},
},
required: ['category', 'symbol', 'targetAmount'],
},
},
{
name: 'detect_position_mode',
description: 'Detect current position mode (one-way vs hedge) and get recommended positionIdx for orders',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (spot, linear, inverse, etc.)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., ETHUSDT) - optional, will check all positions if not provided',
},
},
required: ['category'],
},
},
{
name: 'set_leverage',
description: 'Set leverage for a specific symbol',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (linear, inverse)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., ETHUSDT)',
},
buyLeverage: {
type: 'string',
description: 'Buy leverage (e.g., "10" for 10x)',
},
sellLeverage: {
type: 'string',
description: 'Sell leverage (e.g., "10" for 10x)',
},
},
required: ['category', 'symbol', 'buyLeverage', 'sellLeverage'],
},
},
{
name: 'calculate_position_size',
description: 'Calculate optimal position size based on risk management principles',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (linear, inverse)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., ETHUSDT)',
},
accountBalance: {
type: 'number',
description: 'Total account balance in USDT',
},
riskPercentage: {
type: 'number',
description: 'Risk percentage per trade (e.g., 2 for 2%)',
},
stopLossPrice: {
type: 'number',
description: 'Stop loss price',
},
currentPrice: {
type: 'number',
description: 'Current price (optional, will fetch if not provided)',
},
leverage: {
type: 'number',
description: 'Leverage to use (optional, defaults to 1x)',
},
},
required: ['category', 'symbol', 'accountBalance', 'riskPercentage', 'stopLossPrice'],
},
},
{
name: 'calculate_trailing_stop',
description: 'Calculate trailing stop loss with breakeven and profit protection',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (linear, inverse)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., ETHUSDT)',
},
entryPrice: {
type: 'number',
description: 'Entry price of the position',
},
currentPrice: {
type: 'number',
description: 'Current market price',
},
side: {
type: 'string',
description: 'Position side (Buy for long, Sell for short)',
},
initialStopLoss: {
type: 'number',
description: 'Initial stop loss price',
},
trailingDistance: {
type: 'number',
description: 'Trailing distance in price units',
},
breakevenTrigger: {
type: 'number',
description: 'Price distance from entry to activate breakeven protection (optional)',
},
profitProtectionTrigger: {
type: 'number',
description: 'Price distance from entry to activate profit protection (optional)',
},
},
required: ['category', 'symbol', 'entryPrice', 'currentPrice', 'side', 'initialStopLoss', 'trailingDistance'],
},
},
{
name: 'place_order_with_trailing_stop',
description: 'Place order with trailing stop loss',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Category (linear, inverse)',
},
symbol: {
type: 'string',
description: 'Symbol (e.g., ETHUSDT)',
},
side: {
type: 'string',
description: 'Order side (Buy, Sell)',
},
orderType: {
type: 'string',
description: 'Order type (Market, Limit)',
},
qty: {
type: 'string',
description: 'Order quantity',
},
price: {
type: 'string',
description: 'Order price (for limit orders)',
},
trailingStop: {
type: 'string',
description: 'Trailing stop distance',
},
activePrice: {
type: 'string',
description: 'Price at which trailing stop activates (optional)',
},
positionIdx: {
type: 'string',
description: 'Position index (optional, auto-detected if not provided)',
},
timeInForce: {
type: 'string',
description: 'Time in force (GTC, IOC, FOK)',
},
orderLinkId: {
type: 'string',
description: 'Order link ID (optional)',
},
},
required: ['category', 'symbol', 'side', 'orderType', 'qty', 'trailingStop'],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
// Type assertion for args
const typedArgs = args as any;
switch (name) {
case 'get_secret_key':
return {
content: [
{
type: 'text',
text: process.env.SECRET_KEY || '',
},
],
};
case 'get_access_key':
return {
content: [
{
type: 'text',
text: process.env.ACCESS_KEY || '',
},
],
};
case 'get_orderbook': {
const result = await this.bybitService.getOrderbook(
typedArgs.category,
typedArgs.symbol,
typedArgs.limit
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_kline': {
const result = await this.bybitService.getKline(
typedArgs.category,
typedArgs.symbol,
typedArgs.interval,
typedArgs.start,
typedArgs.end,
typedArgs.limit
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_tickers': {
const result = await this.bybitService.getTickers(typedArgs.category, typedArgs.symbol);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_wallet_balance': {
const result = await this.bybitService.getWalletBalance(typedArgs.accountType, typedArgs.coin);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_positions': {
const result = await this.bybitService.getPositions(typedArgs.category, typedArgs.symbol);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'place_order': {
const orderRequest: OrderRequest = {
category: typedArgs.category,
symbol: typedArgs.symbol,
side: typedArgs.side,
orderType: typedArgs.orderType,
qty: typedArgs.qty,
price: typedArgs.price,
timeInForce: typedArgs.timeInForce,
orderLinkId: typedArgs.orderLinkId,
isLeverage: typedArgs.isLeverage,
orderFilter: typedArgs.orderFilter,
triggerPrice: typedArgs.triggerPrice,
triggerBy: typedArgs.triggerBy,
orderIv: typedArgs.orderIv,
positionIdx: typedArgs.positionIdx,
takeProfit: typedArgs.takeProfit,
stopLoss: typedArgs.stopLoss,
tpTriggerBy: typedArgs.tpTriggerBy,
slTriggerBy: typedArgs.slTriggerBy,
tpLimitPrice: typedArgs.tpLimitPrice,
slLimitPrice: typedArgs.slLimitPrice,
tpOrderType: typedArgs.tpOrderType,
slOrderType: typedArgs.slOrderType,
};
const result = await this.bybitService.placeOrder(orderRequest);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'cancel_order': {
const result = await this.bybitService.cancelOrder(
typedArgs.category,
typedArgs.symbol,
typedArgs.orderId,
typedArgs.orderLinkId,
typedArgs.orderFilter
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_order_history': {
const result = await this.bybitService.getOrderHistory(
typedArgs.category,
typedArgs.symbol,
typedArgs.orderId,
typedArgs.orderLinkId,
typedArgs.orderFilter,
typedArgs.orderStatus,
typedArgs.startTime,
typedArgs.endTime,
typedArgs.limit
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_open_orders': {
const result = await this.bybitService.getOpenOrders(
typedArgs.category,
typedArgs.symbol,
typedArgs.orderId,
typedArgs.orderLinkId,
typedArgs.orderFilter,
typedArgs.limit
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'set_trading_stop': {
const result = await this.bybitService.setTradingStop(
typedArgs.category,
typedArgs.symbol,
typedArgs.takeProfit,
typedArgs.stopLoss,
typedArgs.trailingStop,
typedArgs.positionIdx
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'set_margin_mode': {
const result = await this.bybitService.setMarginMode(
typedArgs.category,
typedArgs.symbol,
typedArgs.tradeMode,
typedArgs.buyLeverage,
typedArgs.sellLeverage
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_api_key_information': {
const result = await this.bybitService.getApiKeyInformation();
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_instruments_info': {
const result = await this.bybitService.getInstrumentsInfo(
typedArgs.category,
typedArgs.symbol,
typedArgs.status,
typedArgs.baseCoin
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'validate_order_quantity': {
const result = await this.bybitService.validateOrderQuantity(
typedArgs.category,
typedArgs.symbol,
typedArgs.targetAmount,
typedArgs.currentPrice
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'detect_position_mode': {
const result = await this.bybitService.detectPositionMode(
typedArgs.category,
typedArgs.symbol
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'set_leverage': {
const result = await this.bybitService.setLeverage(
typedArgs.category,
typedArgs.symbol,
typedArgs.buyLeverage,
typedArgs.sellLeverage
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'calculate_position_size': {
const result = await this.bybitService.calculatePositionSize(
typedArgs.category,
typedArgs.symbol,
typedArgs.accountBalance,
typedArgs.riskPercentage,
typedArgs.stopLossPrice,
typedArgs.currentPrice,
typedArgs.leverage
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'calculate_trailing_stop': {
const result = await this.bybitService.calculateTrailingStop(
typedArgs.category,
typedArgs.symbol,
typedArgs.entryPrice,
typedArgs.currentPrice,
typedArgs.side,
typedArgs.initialStopLoss,
typedArgs.trailingDistance,
typedArgs.breakevenTrigger,
typedArgs.profitProtectionTrigger
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'place_order_with_trailing_stop': {
const result = await this.bybitService.placeOrderWithTrailingStop(
typedArgs.category,
typedArgs.symbol,
typedArgs.side,
typedArgs.orderType,
typedArgs.qty,
typedArgs.price,
typedArgs.trailingStop,
typedArgs.activePrice,
typedArgs.positionIdx,
typedArgs.timeInForce,
typedArgs.orderLinkId
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${errorMessage}`);
}
});
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Bybit MCP Server (Node.js) running on stdio');
}
}
const server = new BybitMCPServer();
server.run().catch(console.error);