Skip to main content
Glama

Bybit MCP Server

by sammcj
BaseTool.ts13.3 kB
import { Tool, TextContent, CallToolResult } from "@modelcontextprotocol/sdk/types.js" import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { z } from "zod" import { RestClientV5, APIResponseV3WithTime } from "bybit-api" import { getEnvConfig } from "../env.js" // Error categories for better error handling export enum ErrorCategory { VALIDATION = "VALIDATION", API_ERROR = "API_ERROR", RATE_LIMIT = "RATE_LIMIT", NETWORK = "NETWORK", AUTHENTICATION = "AUTHENTICATION", PERMISSION = "PERMISSION", INTERNAL = "INTERNAL" } // Structured error interface export interface ToolError { category: ErrorCategory code?: string | number message: string details?: any timestamp: string tool: string } // Standard error codes export const ERROR_CODES = { INVALID_INPUT: "INVALID_INPUT", MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD", INVALID_SYMBOL: "INVALID_SYMBOL", INVALID_CATEGORY: "INVALID_CATEGORY", API_KEY_REQUIRED: "API_KEY_REQUIRED", RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED", BYBIT_API_ERROR: "BYBIT_API_ERROR", NETWORK_ERROR: "NETWORK_ERROR", TIMEOUT: "TIMEOUT", UNKNOWN_ERROR: "UNKNOWN_ERROR" } as const // Rate limit configuration (as per Bybit docs) const RATE_LIMIT = { maxRequestsPerSecond: 10, maxRequestsPerMinute: 120, retryAfter: 2000, // ms maxRetries: 3 } interface QueuedRequest { execute: () => Promise<any> resolve: (value: any) => void reject: (error: any) => void } export abstract class BaseToolImplementation { abstract name: string abstract toolDefinition: Tool abstract toolCall(request: z.infer<typeof CallToolRequestSchema>): Promise<CallToolResult> protected client: RestClientV5 protected isDevMode: boolean protected isTestMode: boolean = false private requestQueue: QueuedRequest[] = [] private processingQueue = false private requestCount = 0 private lastRequestTime = 0 private requestHistory: number[] = [] // Timestamps of requests within the last minute private initialized = false private activeTimeouts: NodeJS.Timeout[] = [] constructor(mockClient?: RestClientV5) { if (mockClient) { // Use provided mock client for testing this.client = mockClient this.isDevMode = true this.isTestMode = true } else { // Normal production/development initialization const config = getEnvConfig() this.isDevMode = !config.apiKey || !config.apiSecret if (this.isDevMode) { this.client = new RestClientV5({ testnet: true, }) } else { this.client = new RestClientV5({ key: config.apiKey, secret: config.apiSecret, testnet: config.useTestnet, recv_window: 5000, // 5 second receive window }) } } } protected ensureInitialized() { if (!this.initialized) { if (this.isDevMode) { this.logWarning("Running in development mode with limited functionality") } this.initialized = true } } /** * Enqueues a request with rate limiting and retry logic */ protected async executeRequest<T>( operation: () => Promise<APIResponseV3WithTime<T>>, retryCount = 0 ): Promise<T> { this.ensureInitialized() return new Promise((resolve, reject) => { this.requestQueue.push({ execute: async () => { try { // Check rate limits if (!this.canMakeRequest() && !this.isTestMode) { const waitTime = this.getWaitTime() this.logInfo(`Rate limit reached. Waiting ${waitTime}ms`) await new Promise(resolve => setTimeout(resolve, waitTime)) } // Execute request with timeout let response: APIResponseV3WithTime<T> if (this.isTestMode) { // In test mode, don't create timeout promises to avoid open handles response = await operation() as APIResponseV3WithTime<T> } else { // In production mode, use timeout for real API calls response = await Promise.race([ operation(), new Promise<never>((_, reject) => setTimeout(() => reject(new Error("Request timeout")), 10000) ) ]) as APIResponseV3WithTime<T> } // Update rate limit tracking this.updateRequestHistory() // Handle Bybit API errors if (response.retCode !== 0) { throw this.createBybitError(response.retCode, response.retMsg) } return response.result } catch (error) { // Retry logic for specific errors if ( retryCount < RATE_LIMIT.maxRetries && this.shouldRetry(error) ) { this.logWarning(`Retrying request (attempt ${retryCount + 1})`) if (!this.isTestMode) { await new Promise(resolve => setTimeout(resolve, RATE_LIMIT.retryAfter) ) } return this.executeRequest(operation, retryCount + 1) } throw error } }, resolve, reject }) if (!this.processingQueue) { this.processQueue() } }) } private async processQueue() { if (this.requestQueue.length === 0) { this.processingQueue = false return } this.processingQueue = true const request = this.requestQueue.shift() if (request) { try { const result = await request.execute() request.resolve(result) } catch (error) { request.reject(error) } } // Process next request setImmediate(() => this.processQueue()) } private canMakeRequest(): boolean { const now = Date.now() // Clean up old requests this.requestHistory = this.requestHistory.filter( time => now - time < 60000 ) return ( this.requestHistory.length < RATE_LIMIT.maxRequestsPerMinute && now - this.lastRequestTime >= (1000 / RATE_LIMIT.maxRequestsPerSecond) ) } private getWaitTime(): number { const now = Date.now() const timeToWaitForSecondLimit = Math.max( 0, this.lastRequestTime + (1000 / RATE_LIMIT.maxRequestsPerSecond) - now ) if (this.requestHistory.length >= RATE_LIMIT.maxRequestsPerMinute) { const timeToWaitForMinuteLimit = Math.max( 0, this.requestHistory[0] + 60000 - now ) return Math.max(timeToWaitForSecondLimit, timeToWaitForMinuteLimit) } return timeToWaitForSecondLimit } private updateRequestHistory() { const now = Date.now() this.requestHistory.push(now) this.lastRequestTime = now } private shouldRetry(error: any): boolean { // Retry on network errors or specific Bybit error codes return ( error.name === "NetworkError" || error.code === 10002 || // Rate limit error.code === 10006 || // System busy error.code === -1 // Unknown error ) } private createBybitError(code: number, message: string): Error { const errorMap: Record<number, string> = { 10001: "Parameter error", 10002: "Rate limit exceeded", 10003: "Invalid API key", 10004: "Invalid sign", 10005: "Permission denied", 10006: "System busy", 10009: "Order not found", 10010: "Insufficient balance", } const errorMessage = errorMap[code] || message const error = new Error(`Bybit API Error ${code}: ${errorMessage}`) ; (error as any).code = code ; (error as any).bybitCode = code ; (error as any).category = this.categoriseBybitError(code) return error } /** * Creates a standardised ToolError object */ protected createToolError( category: ErrorCategory, message: string, code?: string | number, details?: any ): ToolError { return { category, code, message, details, timestamp: new Date().toISOString(), tool: this.name } } /** * Creates a validation error for invalid input */ protected createValidationError(message: string, details?: any): ToolError { return this.createToolError( ErrorCategory.VALIDATION, message, ERROR_CODES.INVALID_INPUT, details ) } /** * Creates an API error from Bybit response */ protected createApiError(code: number, message: string): ToolError { const category = this.categoriseBybitError(code) return this.createToolError( category, `Bybit API Error ${code}: ${message}`, code ) } /** * Categorises Bybit API errors */ private categoriseBybitError(code: number): ErrorCategory { switch (code) { case 10002: return ErrorCategory.RATE_LIMIT case 10003: case 10004: return ErrorCategory.AUTHENTICATION case 10005: return ErrorCategory.PERMISSION case 10001: return ErrorCategory.VALIDATION default: return ErrorCategory.API_ERROR } } /** * Handles errors and returns MCP-compliant CallToolResult */ protected handleError(error: any): CallToolResult { let toolError: ToolError if (error instanceof Error) { // Check if it's a Bybit API error (has bybitCode property) if ((error as any).bybitCode) { toolError = this.createApiError((error as any).bybitCode, error.message) } // Check if it's a validation error (from Zod) else if (error.message.includes("Invalid input")) { toolError = this.createValidationError(error.message) } // Check for specific error patterns else if (error.message.includes("API credentials required") || error.message.includes("development mode")) { toolError = this.createToolError( ErrorCategory.AUTHENTICATION, error.message, ERROR_CODES.API_KEY_REQUIRED ) } else if (error.message.includes("Rate limit")) { toolError = this.createToolError( ErrorCategory.RATE_LIMIT, error.message, ERROR_CODES.RATE_LIMIT_EXCEEDED ) } else if (error.message.includes("timeout") || error.message.includes("Request timeout")) { toolError = this.createToolError( ErrorCategory.NETWORK, error.message, ERROR_CODES.TIMEOUT ) } else if (error.name === "NetworkError") { toolError = this.createToolError( ErrorCategory.NETWORK, error.message, ERROR_CODES.NETWORK_ERROR ) } else { toolError = this.createToolError( ErrorCategory.INTERNAL, error.message, ERROR_CODES.UNKNOWN_ERROR ) } } else { toolError = this.createToolError( ErrorCategory.INTERNAL, String(error), ERROR_CODES.UNKNOWN_ERROR ) } // Log the error console.error(JSON.stringify({ jsonrpc: "2.0", method: "notify", params: { level: "error", message: `${this.name} tool error: ${toolError.message}` } })) // Create MCP-compliant error response const content: TextContent = { type: "text", text: JSON.stringify(toolError, null, 2), annotations: { audience: ["assistant", "user"], priority: 1 } } return { content: [content], isError: true } } // Reference ID counter for generating unique IDs private static referenceIdCounter = 0 /** * Generate a unique reference ID */ protected generateReferenceId(): string { BaseToolImplementation.referenceIdCounter += 1 return `REF${String(BaseToolImplementation.referenceIdCounter).padStart(3, '0')}` } /** * Add reference ID metadata to response if requested */ protected addReferenceMetadata(data: any, includeReferenceId: boolean, toolName: string, endpoint?: string): any { if (!includeReferenceId) { return data } return { ...data, _referenceId: this.generateReferenceId(), _timestamp: new Date().toISOString(), _toolName: toolName, _endpoint: endpoint } } protected formatResponse(data: any): CallToolResult { this.ensureInitialized() const content: TextContent = { type: "text", text: JSON.stringify(data, null, 2), annotations: { audience: ["assistant", "user"], priority: 1 } } return { content: [content] } } protected logInfo(message: string) { console.info(JSON.stringify({ jsonrpc: "2.0", method: "notify", params: { level: "info", message: `${this.name}: ${message}` } })) } protected logWarning(message: string) { console.warn(JSON.stringify({ jsonrpc: "2.0", method: "notify", params: { level: "warning", message: `${this.name}: ${message}` } })) } /** * Cleanup method for tests to clear any remaining timeouts */ public cleanup() { this.activeTimeouts.forEach(timeout => clearTimeout(timeout)) this.activeTimeouts = [] this.requestQueue = [] this.processingQueue = false } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sammcj/bybit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server