Bybit MCP Server

import { jest, describe, beforeEach, it, expect } from '@jest/globals' import GetTicker from '../tools/GetTicker.js' import GetOrderbook from '../tools/GetOrderbook.js' import GetPositions from '../tools/GetPositions.js' import GetWalletBalance from '../tools/GetWalletBalance.js' import GetInstrumentInfo from '../tools/GetInstrumentInfo.js' import GetKline from '../tools/GetKline.js' import GetMarketInfo from '../tools/GetMarketInfo.js' import GetOrderHistory from '../tools/GetOrderHistory.js' import GetTrades from '../tools/GetTrades.js' import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { z } from "zod" type ToolCallRequest = z.infer<typeof CallToolRequestSchema> // Mock the Bybit API client jest.mock('bybit-api', () => ({ RestClientV5: jest.fn().mockImplementation(() => ({ getTickers: jest.fn(), getOrderbook: jest.fn(), getPositions: jest.fn(), getWalletBalance: jest.fn(), getInstruments: jest.fn(), getKline: jest.fn(), getMarkets: jest.fn(), getHistoricOrders: jest.fn(), getTrades: jest.fn(), })), APIResponseV3WithTime: jest.fn(), })) // Mock crypto.randomUUID with a properly formatted UUID const mockRandomUUID = jest.fn(() => '123e4567-e89b-12d3-a456-426614174000') global.crypto = { ...global.crypto, randomUUID: mockRandomUUID, } as Crypto describe('Bybit MCP Tools', () => { const mockSuccessResponse = { retCode: 0, retMsg: 'OK', result: { list: [], }, time: Date.now(), } const mockErrorResponse = { retCode: 10002, retMsg: 'Rate limit exceeded', result: null, time: Date.now(), } beforeEach(() => { jest.clearAllMocks() }) describe('GetTicker', () => { let getTicker: GetTicker beforeEach(() => { getTicker = new GetTicker() }) it('should validate input parameters', async () => { const invalidRequest: ToolCallRequest = { params: { name: 'get_ticker', arguments: { symbol: '123!@#', // Invalid symbol }, }, method: 'tools/call' as const, } const result = await getTicker.toolCall(invalidRequest) expect(result.content[0].type).toBe('error') expect(result.content[0].text as string).toContain('Invalid input') }) it('should handle successful API response', async () => { const request: ToolCallRequest = { params: { name: 'get_ticker', arguments: { symbol: 'BTCUSDT', category: 'spot', }, }, method: 'tools/call' as const, }; (getTicker as any).client.getTickers.mockResolvedValueOnce(mockSuccessResponse) const result = await getTicker.toolCall(request) expect(result.content[0].type).toBe('text') expect(JSON.parse(result.content[0].text as string)).toHaveProperty('symbol', 'BTCUSDT') }) it('should handle API errors', async () => { const request: ToolCallRequest = { params: { name: 'get_ticker', arguments: { symbol: 'BTCUSDT', }, }, method: 'tools/call' as const, }; (getTicker as any).client.getTickers.mockResolvedValueOnce(mockErrorResponse) const result = await getTicker.toolCall(request) expect(result.content[0].type).toBe('error') expect(result.content[0].text as string).toContain('Rate limit exceeded') }) }) describe('GetOrderbook', () => { let getOrderbook: GetOrderbook beforeEach(() => { getOrderbook = new GetOrderbook() }) it('should validate input parameters', async () => { const invalidRequest: ToolCallRequest = { params: { name: 'get_orderbook', arguments: { symbol: '', // Empty symbol }, }, method: 'tools/call' as const, } const result = await getOrderbook.toolCall(invalidRequest) expect(result.content[0].type).toBe('error') }) it('should handle successful API response', async () => { const request: ToolCallRequest = { params: { name: 'get_orderbook', arguments: { symbol: 'BTCUSDT', category: 'spot', }, }, method: 'tools/call' as const, }; (getOrderbook as any).client.getOrderbook.mockResolvedValueOnce(mockSuccessResponse) const result = await getOrderbook.toolCall(request) expect(result.content[0].type).toBe('text') }) }) describe('GetPositions', () => { let getPositions: GetPositions beforeEach(() => { getPositions = new GetPositions() }) it('should validate input parameters', async () => { const invalidRequest: ToolCallRequest = { params: { name: 'get_positions', arguments: { category: 'invalid', // Invalid category }, }, method: 'tools/call' as const, } const result = await getPositions.toolCall(invalidRequest) expect(result.content[0].type).toBe('error') }) it('should handle successful API response', async () => { const request: ToolCallRequest = { params: { name: 'get_positions', arguments: { category: 'linear', }, }, method: 'tools/call' as const, }; (getPositions as any).client.getPositions.mockResolvedValueOnce(mockSuccessResponse) const result = await getPositions.toolCall(request) expect(result.content[0].type).toBe('text') }) }) describe('GetWalletBalance', () => { let getWalletBalance: GetWalletBalance beforeEach(() => { getWalletBalance = new GetWalletBalance() }) it('should validate input parameters', async () => { const invalidRequest: ToolCallRequest = { params: { name: 'get_wallet_balance', arguments: { accountType: 'invalid', // Invalid account type }, }, method: 'tools/call' as const, } const result = await getWalletBalance.toolCall(invalidRequest) expect(result.content[0].type).toBe('error') }) it('should handle successful API response', async () => { const request: ToolCallRequest = { params: { name: 'get_wallet_balance', arguments: { accountType: 'UNIFIED', }, }, method: 'tools/call' as const, }; (getWalletBalance as any).client.getWalletBalance.mockResolvedValueOnce(mockSuccessResponse) const result = await getWalletBalance.toolCall(request) expect(result.content[0].type).toBe('text') }) }) describe('Rate Limiting', () => { let getTicker: GetTicker beforeEach(() => { getTicker = new GetTicker() }) it('should handle rate limiting', async () => { const request: ToolCallRequest = { params: { name: 'get_ticker', arguments: { symbol: 'BTCUSDT', }, }, method: 'tools/call' as const, } // Mock multiple rapid requests const promises = Array(15).fill(null).map(() => getTicker.toolCall(request)) const results = await Promise.all(promises) // Verify that some requests were rate limited const errors = results.filter(r => (r.content[0].type as string) === 'error') expect(errors.length).toBeGreaterThan(0) }) }) // Add similar test blocks for remaining tools describe('GetInstrumentInfo', () => { let getInstrumentInfo: GetInstrumentInfo beforeEach(() => { getInstrumentInfo = new GetInstrumentInfo() }) it('should handle successful API response', async () => { const request: ToolCallRequest = { params: { name: 'get_instrument_info', arguments: { category: 'spot', symbol: 'BTCUSDT', }, }, method: 'tools/call' as const, }; (getInstrumentInfo as any).client.getInstruments.mockResolvedValueOnce(mockSuccessResponse) const result = await getInstrumentInfo.toolCall(request) expect(result.content[0].type).toBe('text') }) }) describe('GetKline', () => { let getKline: GetKline beforeEach(() => { getKline = new GetKline() }) it('should handle successful API response', async () => { const request: ToolCallRequest = { params: { name: 'get_kline', arguments: { category: 'spot', symbol: 'BTCUSDT', interval: '1', }, }, method: 'tools/call' as const, }; (getKline as any).client.getKline.mockResolvedValueOnce(mockSuccessResponse) const result = await getKline.toolCall(request) expect(result.content[0].type).toBe('text') }) }) describe('GetMarketInfo', () => { let getMarketInfo: GetMarketInfo beforeEach(() => { getMarketInfo = new GetMarketInfo() }) it('should handle successful API response', async () => { const request: ToolCallRequest = { params: { name: 'get_market_info', arguments: { category: 'spot', }, }, method: 'tools/call' as const, }; (getMarketInfo as any).client.getMarkets.mockResolvedValueOnce(mockSuccessResponse) const result = await getMarketInfo.toolCall(request) expect(result.content[0].type).toBe('text') }) }) describe('GetOrderHistory', () => { let getOrderHistory: GetOrderHistory beforeEach(() => { getOrderHistory = new GetOrderHistory() }) it('should handle successful API response', async () => { const request: ToolCallRequest = { params: { name: 'get_order_history', arguments: { category: 'spot', }, }, method: 'tools/call' as const, }; (getOrderHistory as any).client.getHistoricOrders.mockResolvedValueOnce(mockSuccessResponse) const result = await getOrderHistory.toolCall(request) expect(result.content[0].type).toBe('text') }) }) describe('GetTrades', () => { let getTrades: GetTrades beforeEach(() => { getTrades = new GetTrades() }) it('should handle successful API response', async () => { const request: ToolCallRequest = { params: { name: 'get_trades', arguments: { category: 'spot', symbol: 'BTCUSDT', }, }, method: 'tools/call' as const, }; (getTrades as any).client.getTrades.mockResolvedValueOnce(mockSuccessResponse) const result = await getTrades.toolCall(request) expect(result.content[0].type).toBe('text') }) }) })