Bybit MCP Server
by sammcj
- src
- tools
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"
// 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
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
constructor() {
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()) {
const waitTime = this.getWaitTime()
this.logInfo(`Rate limit reached. Waiting ${waitTime}ms`)
await new Promise(resolve => setTimeout(resolve, waitTime))
}
// Execute request with timeout
const response = await Promise.race([
operation(),
new Promise((_, 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})`)
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 error = new Error(
`Bybit API Error ${code}: ${errorMap[code] || message}`
)
; (error as any).code = code
return error
}
protected handleError(error: any): CallToolResult {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(JSON.stringify({
jsonrpc: "2.0",
method: "notify",
params: {
level: "error",
message: `${this.name} tool error: ${errorMessage}`
}
}))
const content: TextContent = {
type: "text",
text: JSON.stringify({
tool: this.name,
error: errorMessage,
code: error.code,
status: error.status,
timestamp: new Date().toISOString()
}, null, 2),
annotations: {
audience: ["assistant", "user"],
priority: 1
}
}
return {
content: [content],
isError: true
}
}
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}`
}
}))
}
}