Skip to main content
Glama
index.js31.4 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const axios_1 = __importDefault(require("axios")); const ws_1 = __importDefault(require("ws")); // WebSocket realtime data cache class OKXWebSocketClient { ws = null; subscriptions = new Set(); dataCache = new Map(); connectionAttempts = 0; maxConnectionAttempts = 5; reconnectDelay = 5000; pingInterval = null; isConnecting = false; constructor() { this.connect(); } connect() { if (this.isConnecting) return; this.isConnecting = true; if (this.connectionAttempts >= this.maxConnectionAttempts) { console.error("[WebSocket] Max connection attempts reached. Giving up."); return; } console.error(`[WebSocket] Connecting to OKX (attempt ${this.connectionAttempts + 1}/${this.maxConnectionAttempts})...`); this.ws = new ws_1.default("wss://ws.okx.com:8443/ws/v5/public"); this.ws.on("open", () => { console.error("[WebSocket] Connected to OKX WebSocket"); this.connectionAttempts = 0; this.isConnecting = false; this.resubscribe(); // Set up ping interval to keep connection alive if (this.pingInterval) clearInterval(this.pingInterval); this.pingInterval = setInterval(() => this.ping(), 30000); }); this.ws.on("message", (data) => { try { const message = JSON.parse(data.toString()); // Handle ping response if (message.event === "pong") { return; } // Handle data updates if (message.data && message.arg) { const key = `${message.arg.channel}:${message.arg.instId}`; this.dataCache.set(key, message.data); console.error(`[WebSocket] Received update for ${key}`); } } catch (error) { console.error("[WebSocket] Error parsing message:", error); } }); this.ws.on("error", (error) => { console.error("[WebSocket] Error:", error); }); this.ws.on("close", () => { console.error("[WebSocket] Disconnected"); this.isConnecting = false; if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } this.connectionAttempts++; setTimeout(() => this.connect(), this.reconnectDelay); }); } ping() { if (this.ws && this.ws.readyState === ws_1.default.OPEN) { this.ws.send(JSON.stringify({ op: "ping" })); } } resubscribe() { if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) return; for (const subscription of this.subscriptions) { const [channel, instId] = subscription.split(":"); this.sendSubscription(channel, instId); console.error(`[WebSocket] Resubscribed to ${channel} for ${instId}`); } } sendSubscription(channel, instId) { if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) return; this.ws.send(JSON.stringify({ op: "subscribe", args: [ { channel, instId, }, ], })); } subscribe(channel, instId) { const key = `${channel}:${instId}`; if (this.subscriptions.has(key)) { return; // Already subscribed } console.error(`[WebSocket] Subscribing to ${channel} for ${instId}`); this.subscriptions.add(key); if (this.ws && this.ws.readyState === ws_1.default.OPEN) { this.sendSubscription(channel, instId); } } unsubscribe(channel, instId) { const key = `${channel}:${instId}`; if (!this.subscriptions.has(key)) { return; // Not subscribed } console.error(`[WebSocket] Unsubscribing from ${channel} for ${instId}`); this.subscriptions.delete(key); if (this.ws && this.ws.readyState === ws_1.default.OPEN) { this.ws.send(JSON.stringify({ op: "unsubscribe", args: [ { channel, instId, }, ], })); } } getLatestData(channel, instId) { const key = `${channel}:${instId}`; return this.dataCache.get(key) || null; } isSubscribed(channel, instId) { const key = `${channel}:${instId}`; return this.subscriptions.has(key); } close() { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } if (this.ws) { this.ws.terminate(); this.ws = null; } this.subscriptions.clear(); this.dataCache.clear(); } } class OKXServer { server; axiosInstance; wsClient; constructor() { console.error("[Setup] Initializing OKX MCP server..."); this.server = new index_js_1.Server({ name: "okx-mcp-server", version: "0.2.0", }, { capabilities: { tools: {}, }, }); this.axiosInstance = axios_1.default.create({ baseURL: "https://www.okx.com/api/v5", timeout: 5000, headers: { "Accept": "application/json", "Content-Type": "application/json", }, }); // Initialize WebSocket client this.wsClient = new OKXWebSocketClient(); this.setupToolHandlers(); this.server.onerror = (error) => console.error("[Error]", error); process.on("SIGINT", async () => { await this.cleanup(); process.exit(0); }); } async cleanup() { console.error("[Cleanup] Shutting down..."); this.wsClient.close(); await this.server.close(); } setupToolHandlers() { this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: [ { name: "get_price", description: "Get latest price for an OKX instrument with formatted visualization", inputSchema: { type: "object", properties: { instrument: { type: "string", description: "Instrument ID (e.g. BTC-USDT)", }, format: { type: "string", description: "Output format (json or markdown)", default: "markdown", }, }, required: ["instrument"], }, }, { name: "get_candlesticks", description: "Get candlestick data for an OKX instrument with visualization options", inputSchema: { type: "object", properties: { instrument: { type: "string", description: "Instrument ID (e.g. BTC-USDT)", }, bar: { type: "string", description: "Time interval (e.g. 1m, 5m, 1H, 1D)", default: "1m", }, limit: { type: "number", description: "Number of candlesticks (max 100)", default: 100, }, format: { type: "string", description: "Output format (json, markdown, or table)", default: "markdown", }, }, required: ["instrument"], }, }, { name: "subscribe_ticker", description: "Subscribe to real-time ticker updates for an instrument", inputSchema: { type: "object", properties: { instrument: { type: "string", description: "Instrument ID (e.g. BTC-USDT)", }, }, required: ["instrument"], }, }, { name: "get_live_ticker", description: "Get the latest ticker data from WebSocket subscription", inputSchema: { type: "object", properties: { instrument: { type: "string", description: "Instrument ID (e.g. BTC-USDT)", }, format: { type: "string", description: "Output format (json or markdown)", default: "markdown", }, }, required: ["instrument"], }, }, { name: "unsubscribe_ticker", description: "Unsubscribe from real-time ticker updates for an instrument", inputSchema: { type: "object", properties: { instrument: { type: "string", description: "Instrument ID (e.g. BTC-USDT)", }, }, required: ["instrument"], }, }, ], })); this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { try { const validTools = [ "get_price", "get_candlesticks", "subscribe_ticker", "get_live_ticker", "unsubscribe_ticker", ]; if (!validTools.includes(request.params.name)) { throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } const args = request.params.arguments; if (!args.instrument) { throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, "Missing required parameter: instrument"); } // Default format to markdown if not specified args.format = args.format || "markdown"; // Handle WebSocket subscriptions if (request.params.name === "subscribe_ticker") { console.error(`[WebSocket] Subscribing to ticker for ${args.instrument}`); this.wsClient.subscribe("tickers", args.instrument); return { content: [ { type: "text", text: `Successfully subscribed to real-time ticker updates for ${args.instrument}. Use get_live_ticker to retrieve the latest data.`, }, ], }; } // Handle WebSocket unsubscriptions if (request.params.name === "unsubscribe_ticker") { console.error(`[WebSocket] Unsubscribing from ticker for ${args.instrument}`); this.wsClient.unsubscribe("tickers", args.instrument); return { content: [ { type: "text", text: `Successfully unsubscribed from real-time ticker updates for ${args.instrument}.`, }, ], }; } // Handle WebSocket data retrieval if (request.params.name === "get_live_ticker") { const isSubscribed = this.wsClient.isSubscribed("tickers", args.instrument); if (!isSubscribed) { console.error(`[WebSocket] Auto-subscribing to ticker for ${args.instrument}`); this.wsClient.subscribe("tickers", args.instrument); // Give it a moment to connect and receive data await new Promise((resolve) => setTimeout(resolve, 1000)); } const tickerData = this.wsClient.getLatestData("tickers", args.instrument); if (!tickerData || tickerData.length === 0) { return { content: [ { type: "text", text: `No live data available yet for ${args.instrument}. If you just subscribed, please wait a moment and try again.`, }, ], }; } const ticker = tickerData[0]; if (args.format === "json") { return { content: [ { type: "text", text: JSON.stringify(ticker, null, 2), }, ], }; } else { // Calculate change for markdown formatting const last = parseFloat(ticker.last); const open24h = parseFloat(ticker.open24h); const priceChange = last - open24h; const priceChangePercent = (priceChange / open24h) * 100; const changeSymbol = priceChange >= 0 ? "▲" : "▼"; // Create price range visual const low24h = parseFloat(ticker.low24h); const high24h = parseFloat(ticker.high24h); const range = high24h - low24h; const position = Math.min(Math.max((last - low24h) / range, 0), 1) * 100; const priceBar = `Low ${low24h.toFixed(2)} [${"▮".repeat(Math.floor(position / 5))}|${"▯".repeat(20 - Math.floor(position / 5))}] ${high24h.toFixed(2)} High`; return { content: [ { type: "text", text: `# ${args.instrument} Live Price Data\n\n` + `## Current Price: $${last.toLocaleString()}\n\n` + `**24h Change:** ${changeSymbol} $${Math.abs(priceChange).toLocaleString()} (${priceChangePercent >= 0 ? "+" : ""}${priceChangePercent.toFixed(2)}%)\n\n` + `**Bid:** $${parseFloat(ticker.bidPx).toLocaleString()} | **Ask:** $${parseFloat(ticker.askPx).toLocaleString()}\n\n` + `### 24-Hour Price Range\n\n` + `\`\`\`\n${priceBar}\n\`\`\`\n\n` + `**24h High:** $${parseFloat(ticker.high24h).toLocaleString()}\n` + `**24h Low:** $${parseFloat(ticker.low24h).toLocaleString()}\n\n` + `**24h Volume:** ${parseFloat(ticker.vol24h).toLocaleString()} units\n\n` + `**Last Updated:** ${new Date(parseInt(ticker.ts)).toLocaleString()}\n\n` + `*Data source: Live WebSocket feed*`, }, ], }; } } if (request.params.name === "get_price") { console.error(`[API] Fetching price for instrument: ${args.instrument}`); const response = await this.axiosInstance.get("/market/ticker", { params: { instId: args.instrument }, }); if (response.data.code !== "0") { throw new Error(`OKX API error: ${response.data.msg}`); } if (!response.data.data || response.data.data.length === 0) { throw new Error("No data returned from OKX API"); } const ticker = response.data.data[0]; if (args.format === "json") { // Original JSON format return { content: [ { type: "text", text: JSON.stringify({ instrument: ticker.instId, lastPrice: ticker.last, bid: ticker.bidPx, ask: ticker.askPx, high24h: ticker.high24h, low24h: ticker.low24h, volume24h: ticker.vol24h, timestamp: new Date(parseInt(ticker.ts)).toISOString(), }, null, 2), }, ], }; } else { // Enhanced markdown format with visualization elements const priceChange = parseFloat(ticker.last) - parseFloat(ticker.open24h); const priceChangePercent = (priceChange / parseFloat(ticker.open24h)) * 100; const changeSymbol = priceChange >= 0 ? "▲" : "▼"; const changeColor = priceChange >= 0 ? "green" : "red"; // Create price range visual const low24h = parseFloat(ticker.low24h); const high24h = parseFloat(ticker.high24h); const range = high24h - low24h; const currentPrice = parseFloat(ticker.last); const position = Math.min(Math.max((currentPrice - low24h) / range, 0), 1) * 100; const priceBar = `Low ${low24h.toFixed(2)} [${"▮".repeat(Math.floor(position / 5))}|${"▯".repeat(20 - Math.floor(position / 5))}] ${high24h.toFixed(2)} High`; return { content: [ { type: "text", text: `# ${ticker.instId} Price Summary\n\n` + `## Current Price: $${parseFloat(ticker.last).toLocaleString()}\n\n` + `**24h Change:** ${changeSymbol} $${Math.abs(priceChange).toLocaleString()} (${priceChangePercent >= 0 ? "+" : ""}${priceChangePercent.toFixed(2)}%)\n\n` + `**Bid:** $${parseFloat(ticker.bidPx).toLocaleString()} | **Ask:** $${parseFloat(ticker.askPx).toLocaleString()}\n\n` + `### 24-Hour Price Range\n\n` + `\`\`\`\n${priceBar}\n\`\`\`\n\n` + `**24h High:** $${parseFloat(ticker.high24h).toLocaleString()}\n` + `**24h Low:** $${parseFloat(ticker.low24h).toLocaleString()}\n\n` + `**24h Volume:** ${parseFloat(ticker.vol24h).toLocaleString()} units\n\n` + `**Last Updated:** ${new Date(parseInt(ticker.ts)).toLocaleString()}\n\n` + `*Note: For real-time updates, use the subscribe_ticker and get_live_ticker tools.*`, }, ], }; } } else if (request.params.name === "get_candlesticks") { // get_candlesticks console.error(`[API] Fetching candlesticks for instrument: ${args.instrument}, bar: ${args.bar || "1m"}, limit: ${args.limit || 100}`); const response = await this.axiosInstance.get("/market/candles", { params: { instId: args.instrument, bar: args.bar || "1m", limit: args.limit || 100, }, }); if (response.data.code !== "0") { throw new Error(`OKX API error: ${response.data.msg}`); } if (!response.data.data || response.data.data.length === 0) { throw new Error("No data returned from OKX API"); } // Process the candlestick data const processedData = response.data.data.map(([time, open, high, low, close, vol, volCcy]) => ({ timestamp: new Date(parseInt(time)).toISOString(), date: new Date(parseInt(time)).toLocaleString(), open: parseFloat(open), high: parseFloat(high), low: parseFloat(low), close: parseFloat(close), change: (((parseFloat(close) - parseFloat(open)) / parseFloat(open)) * 100).toFixed(2), volume: parseFloat(vol), volumeCurrency: parseFloat(volCcy), })); // Reverse for chronological order (oldest first) const chronologicalData = [...processedData].reverse(); if (args.format === "json") { // Original JSON format return { content: [ { type: "text", text: JSON.stringify(processedData, null, 2), }, ], }; } else if (args.format === "table") { // Table format (still markdown but formatted as a table) let tableMarkdown = `# ${args.instrument} Candlestick Data (${args.bar || "1m"})\n\n`; tableMarkdown += "| Time | Open | High | Low | Close | Change % | Volume |\n"; tableMarkdown += "|------|------|------|-----|-------|----------|--------|\n"; // Only show last 20 entries if there are too many to avoid huge tables const displayData = chronologicalData.slice(-20); displayData.forEach((candle) => { const changeSymbol = parseFloat(candle.change) >= 0 ? "▲" : "▼"; tableMarkdown += `| ${candle.date} | $${candle.open.toFixed(2)} | $${candle.high.toFixed(2)} | $${candle.low.toFixed(2)} | $${candle.close.toFixed(2)} | ${changeSymbol} ${Math.abs(parseFloat(candle.change)).toFixed(2)}% | ${candle.volume.toLocaleString()} |\n`; }); return { content: [ { type: "text", text: tableMarkdown, }, ], }; } else { // Enhanced markdown format with visualization // Calculate some stats const firstPrice = chronologicalData[0]?.open || 0; const lastPrice = chronologicalData[chronologicalData.length - 1]?.close || 0; const overallChange = (((lastPrice - firstPrice) / firstPrice) * 100).toFixed(2); const highestPrice = Math.max(...chronologicalData.map((c) => c.high)); const lowestPrice = Math.min(...chronologicalData.map((c) => c.low)); // Create a simple ASCII chart const chartHeight = 10; const priceRange = highestPrice - lowestPrice; // Get a subset of data points for the chart (we'll use up to 40 points) const step = Math.max(1, Math.floor(chronologicalData.length / 40)); const chartData = chronologicalData.filter((_, i) => i % step === 0); // Create the ASCII chart let chart = ""; for (let row = 0; row < chartHeight; row++) { const priceAtRow = highestPrice - (row / (chartHeight - 1)) * priceRange; // Price label on y-axis (right aligned) chart += `${priceAtRow.toFixed(2).padStart(8)} |`; // Plot the points for (let i = 0; i < chartData.length; i++) { const candle = chartData[i]; if (candle.high >= priceAtRow && candle.low <= priceAtRow) { // This price level is within this candle's range if ((priceAtRow <= candle.close && priceAtRow >= candle.open) || (priceAtRow >= candle.close && priceAtRow <= candle.open)) { chart += "█"; // Body of the candle } else { chart += "│"; // Wick of the candle } } else { chart += " "; } } chart += "\n"; } // X-axis chart += " " + "‾".repeat(chartData.length) + "\n"; // Create the markdown with stats and chart let markdownText = `# ${args.instrument} Candlestick Analysis (${args.bar || "1m"})\n\n`; markdownText += `## Summary\n\n`; markdownText += `- **Period:** ${chronologicalData[0].date} to ${chronologicalData[chronologicalData.length - 1].date}\n`; markdownText += `- **Starting Price:** $${firstPrice.toLocaleString()}\n`; markdownText += `- **Ending Price:** $${lastPrice.toLocaleString()}\n`; markdownText += `- **Overall Change:** ${overallChange}%\n`; markdownText += `- **Highest Price:** $${highestPrice.toLocaleString()}\n`; markdownText += `- **Lowest Price:** $${lowestPrice.toLocaleString()}\n`; markdownText += `- **Number of Candles:** ${chronologicalData.length}\n\n`; markdownText += `## Price Chart\n\n`; markdownText += "```\n" + chart + "```\n\n"; markdownText += `## Recent Price Action\n\n`; // Add a table of the most recent 5 candles markdownText += "| Time | Open | High | Low | Close | Change % |\n"; markdownText += "|------|------|------|-----|-------|----------|\n"; chronologicalData.slice(-5).forEach((candle) => { const changeSymbol = parseFloat(candle.change) >= 0 ? "▲" : "▼"; markdownText += `| ${candle.date} | $${candle.open.toFixed(2)} | $${candle.high.toFixed(2)} | $${candle.low.toFixed(2)} | $${candle.close.toFixed(2)} | ${changeSymbol} ${Math.abs(parseFloat(candle.change)).toFixed(2)}% |\n`; }); markdownText += `\n*Note: For real-time updates, use the WebSocket subscription tools.*`; return { content: [ { type: "text", text: markdownText, }, ], }; } } // This should never happen due to the check at the beginning throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } catch (error) { if (error instanceof Error) { console.error("[Error] Failed to fetch data:", error); throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to fetch data: ${error.message}`); } throw error; } }); } async run() { const transport = new stdio_js_1.StdioServerTransport(); await this.server.connect(transport); console.error("OKX MCP server running on stdio"); } } const server = new OKXServer(); server.run().catch(console.error);

Latest Blog Posts

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/badger3000/okx-mcp-server'

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