Skip to main content
Glama

Tradovate MCP Server

tools.ts36.6 kB
import * as logger from "./logger.js"; import { tradovateRequest } from './auth.js'; import { contractsCache, positionsCache, ordersCache, accountsCache, fetchPositions } from './data.js'; import { query } from './connect.js'; import { TradovateSocket } from './socket.js'; /** * Handle get_contract_details tool */ export async function handleGetContractDetails(request: any) { const symbol = String(request.params.arguments?.symbol); if (!symbol) { throw new Error("Symbol is required"); } try { // Find contract by symbol const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } return { content: [{ type: "text", text: `Contract details for ${symbol}:\n${JSON.stringify(contract, null, 2)}` }] }; } catch (error) { logger.error(`Error getting contract details for ${symbol}:`, error); // Fallback to cached data const cachedContract = Object.values(contractsCache).find(c => c.name === symbol); if (!cachedContract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } return { content: [{ type: "text", text: `Contract details for ${symbol} (cached):\n${JSON.stringify(cachedContract, null, 2)}` }] }; } } /** * Handle list_positions tool */ export async function handleListPositions(request: any) { const accountId = String(request.params.arguments?.accountId || ""); try { // Get positions from API let endpoint = 'position/list'; if (accountId) { endpoint += `?accountId=${accountId}`; } const positions = await tradovateRequest('GET', endpoint); if (!positions || positions.length === 0) { return { content: [{ type: "text", text: `No positions found${accountId ? ` for account ${accountId}` : ''}` }] }; } return { content: [{ type: "text", text: `Positions${accountId ? ` for account ${accountId}` : ''}:\n${JSON.stringify(positions, null, 2)}` }] }; } catch (error) { // Log error but attempt to retry once more before giving up logger.error("Error listing positions, retrying:", error); try { // Retry API call let endpoint = 'position/list'; if (accountId) { endpoint += `?accountId=${accountId}`; } const positions = await tradovateRequest('GET', endpoint); if (!positions || positions.length === 0) { return { content: [{ type: "text", text: `No positions found${accountId ? ` for account ${accountId}` : ''}` }] }; } return { content: [{ type: "text", text: `Positions${accountId ? ` for account ${accountId}` : ''}:\n${JSON.stringify(positions, null, 2)}` }] }; } catch (retryError) { logger.error("Error listing positions after retry:", retryError); return { content: [{ type: "text", text: `Error fetching positions: ${retryError instanceof Error ? retryError.message : String(retryError)}` }] }; } } } /** * Handle place_order tool */ export async function handlePlaceOrder(request: any) { const symbol = String(request.params.arguments?.symbol); const action = String(request.params.arguments?.action); const orderType = String(request.params.arguments?.orderType); const quantity = Number(request.params.arguments?.quantity); const price = request.params.arguments?.price ? Number(request.params.arguments.price) : undefined; const stopPrice = request.params.arguments?.stopPrice ? Number(request.params.arguments.stopPrice) : undefined; logger.info(`Placing order for ${symbol} with action ${action}, orderType ${orderType}, quantity ${quantity}, price ${price}, stopPrice ${stopPrice}`); if (!symbol || !action || !orderType || !quantity) { throw new Error("Symbol, action, orderType, and quantity are required"); } // Validate order type and required parameters - moved up before any API calls if ((orderType === "Limit" || orderType === "StopLimit") && price === undefined) { throw new Error("Price is required for Limit and StopLimit orders"); } if ((orderType === "Stop" || orderType === "StopLimit") && stopPrice === undefined) { throw new Error("Stop price is required for Stop and StopLimit orders"); } try { // Find contract by symbol const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } // Get account ID const accounts = await tradovateRequest('GET', 'account/list'); if (!accounts || accounts.length === 0) { throw new Error("No accounts found"); } const accountId = accounts[0].id; // Use the first account // Prepare order data const orderData = { accountSpec: process.env.TRADOVATE_USERNAME, accountId: accountId, action, symbol, orderQty: quantity, orderType, price, stopPrice, isAutomated: true }; // Place order via API const newOrder = await tradovateRequest('POST', 'order/placeorder', orderData); // Update orders cache ordersCache[newOrder.id.toString()] = newOrder; return { content: [{ type: "text", text: `Order placed successfully:\n${JSON.stringify(newOrder, null, 2)}` }] }; } catch (error) { logger.error("Error placing order:", error); // Fallback to cached data for simulation try { // Find contract by symbol const cachedContract = Object.values(contractsCache).find(c => c.name === symbol); if (!cachedContract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } // Get first account from cache const cachedAccount = Object.values(accountsCache)[0]; if (!cachedAccount) { return { content: [{ type: "text", text: `No accounts found in cache` }] }; } // Create simulated order const newOrderId = String(Object.keys(ordersCache).length + 1); const simulatedOrder = { id: parseInt(newOrderId), accountId: cachedAccount.id, contractId: cachedContract.id, timestamp: new Date().toISOString(), action, ordStatus: "Working", orderQty: quantity, orderType, price, stopPrice }; // Add to cache ordersCache[newOrderId] = simulatedOrder; return { content: [{ type: "text", text: `Order placed successfully (simulated):\n${JSON.stringify(simulatedOrder, null, 2)}` }] }; } catch (fallbackError) { logger.error("Error in fallback order placement:", fallbackError); return { content: [{ type: "text", text: `Failed to place order: ${error instanceof Error ? error.message : String(error)}` }] }; } } } /** * Handle modify_order tool */ export async function handleModifyOrder(request: any) { const orderId = String(request.params.arguments?.orderId); const price = request.params.arguments?.price !== undefined ? Number(request.params.arguments.price) : undefined; const stopPrice = request.params.arguments?.stopPrice !== undefined ? Number(request.params.arguments.stopPrice) : undefined; const quantity = request.params.arguments?.quantity !== undefined ? Number(request.params.arguments.quantity) : undefined; if (!orderId) { throw new Error("Order ID is required"); } try { // Find order by ID const order = await tradovateRequest('GET', `order/find?id=${orderId}`); if (!order) { return { content: [{ type: "text", text: `Order not found with ID: ${orderId}` }] }; } // Prepare modification data const modifyData: any = { orderId: parseInt(orderId) }; if (price !== undefined) modifyData.price = price; if (stopPrice !== undefined) modifyData.stopPrice = stopPrice; if (quantity !== undefined) modifyData.orderQty = quantity; // Modify order via API const updatedOrder = await tradovateRequest('POST', 'order/modifyOrder', modifyData); // Update orders cache ordersCache[orderId] = updatedOrder; return { content: [{ type: "text", text: `Order modified successfully:\n${JSON.stringify(updatedOrder, null, 2)}` }] }; } catch (error) { logger.error(`Error modifying order ${orderId}:`, error); // Fallback to cached data for simulation const cachedOrder = ordersCache[orderId]; if (!cachedOrder) { return { content: [{ type: "text", text: `Order not found with ID: ${orderId}` }] }; } // Update order in cache if (price !== undefined) cachedOrder.price = price; if (stopPrice !== undefined) cachedOrder.stopPrice = stopPrice; if (quantity !== undefined) cachedOrder.orderQty = quantity; return { content: [{ type: "text", text: `Order modified successfully (simulated):\n${JSON.stringify(cachedOrder, null, 2)}` }] }; } } /** * Handle cancel_order tool */ export async function handleCancelOrder(request: any) { const orderId = String(request.params.arguments?.orderId); if (!orderId) { throw new Error("Order ID is required"); } try { // Find order by ID const order = await tradovateRequest('GET', `order/find?id=${orderId}`); if (!order) { return { content: [{ type: "text", text: `Order not found with ID: ${orderId}` }] }; } /** { "orderId": 0, "clOrdId": "string", "activationTime": "2019-08-24T14:15:22Z", "customTag50": "string", "isAutomated": true } */ const body = { orderId: parseInt(orderId), clOrdId: orderId, isAutomated: true } // Cancel order via API const canceledOrder = await tradovateRequest('POST', 'order/cancelorder', body); // Update orders cache ordersCache[orderId] = canceledOrder; return { content: [{ type: "text", text: `Order canceled successfully:\n${JSON.stringify(canceledOrder, null, 2)}` }] }; } catch (error) { logger.error(`Error canceling order ${orderId}:`, error); // Fallback to cached data for simulation const cachedOrder = ordersCache[orderId]; if (!cachedOrder) { return { content: [{ type: "text", text: `Order not found with ID: ${orderId}` }] }; } // Update order status in cache cachedOrder.ordStatus = "Canceled"; return { content: [{ type: "text", text: `Order canceled successfully (simulated):\n${JSON.stringify(cachedOrder, null, 2)}` }] }; } } /** * Handle liquidate_position tool */ export async function handleLiquidatePosition(request: any) { const symbol = String(request.params.arguments?.symbol); if (!symbol) { throw new Error("Symbol is required"); } try { // Find contract by symbol const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } // Find position by contract ID const positions = await tradovateRequest('GET', 'position/list'); const position = positions.find((p: any) => p.contractId === contract.id); if (!position) { return { content: [{ type: "text", text: `No position found for symbol: ${symbol}` }] }; } /* { "accountId": 0, "contractId": 0, "admin": true, "customTag50": "string" } */ // Liquidate position via API const liquidationResult = await tradovateRequest('POST', 'order/liquidateposition', { accountId: position.accountId, contractId: position.contractId, admin: false, customTag50: "MCPserver" }); return { content: [{ type: "text", text: `Position liquidated successfully:\n${JSON.stringify(liquidationResult, null, 2)}` }] }; } catch (error) { logger.error(`Error liquidating position for ${symbol}, retrying:`, error); // Retry once before giving up try { // Find contract by symbol (retry) const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } // Find position by contract ID (retry) const positions = await tradovateRequest('GET', 'position/list'); const position = positions.find((p: any) => p.contractId === contract.id); if (!position) { return { content: [{ type: "text", text: `No position found for symbol: ${symbol}` }] }; } // Liquidate position via API (retry) const liquidationResult = await tradovateRequest('POST', 'order/liquidateposition', { accountId: position.accountId, contractId: position.contractId, admin: false, customTag50: "MCPserver" }); return { content: [{ type: "text", text: `Position liquidated successfully:\n${JSON.stringify(liquidationResult, null, 2)}` }] }; } catch (retryError) { logger.error(`Error liquidating position for ${symbol} after retry:`, retryError); return { content: [{ type: "text", text: `Failed to liquidate position for ${symbol}: ${retryError instanceof Error ? retryError.message : String(retryError)}` }] }; } } } /** * Handle get_account_summary tool */ export async function handleGetAccountSummary(request: any) { const accountId = String(request.params.arguments?.accountId || ""); try { // Get accounts let accounts; if (accountId) { accounts = [await tradovateRequest('GET', `account/find?id=${accountId}`)]; if (!accounts[0]) { return { content: [{ type: "text", text: `Account not found with ID: ${accountId}` }] }; } } else { accounts = await tradovateRequest('GET', 'account/list'); if (!accounts || accounts.length === 0) { return { content: [{ type: "text", text: `No accounts found` }] }; } } const account = accounts[0]; const actualAccountId = account.id; // Get cash balance const cashBalance = await tradovateRequest('POST', 'cashBalance/getCashBalanceSnapshot', { accountId: actualAccountId }); // Get positions const positions = await tradovateRequest('GET', `position/list?accountId=${actualAccountId}`); // Calculate summary const totalRealizedPnl = positions.reduce((sum: number, pos: any) => sum + pos.realizedPnl, 0); const totalOpenPnl = positions.reduce((sum: number, pos: any) => sum + pos.openPnl, 0); const summary = { account, balance: cashBalance.cashBalance, openPnl: totalOpenPnl, totalEquity: cashBalance.cashBalance + totalOpenPnl, marginUsed: cashBalance.initialMargin, availableMargin: cashBalance.cashBalance - cashBalance.initialMargin + totalOpenPnl, positionCount: positions.length }; return { content: [{ type: "text", text: `Account summary for ${account.name}:\n${JSON.stringify(summary, null, 2)}` }] }; } catch (error) { logger.error("Error getting account summary, retrying:", error); // Retry the API call once before giving up try { // Get accounts (retry) let accounts; if (accountId) { accounts = [await tradovateRequest('GET', `account/find?id=${accountId}`)]; if (!accounts[0]) { return { content: [{ type: "text", text: `Account not found with ID: ${accountId}` }] }; } } else { accounts = await tradovateRequest('GET', 'account/list'); if (!accounts || accounts.length === 0) { return { content: [{ type: "text", text: `No accounts found` }] }; } } const account = accounts[0]; const actualAccountId = account.id; // Get cash balance (retry) const cashBalance = await tradovateRequest('POST', 'cashBalance/getCashBalanceSnapshot', { accountId: actualAccountId }); // Get positions (retry) const positions = await tradovateRequest('GET', `position/list?accountId=${actualAccountId}`); // Calculate summary const totalRealizedPnl = positions.reduce((sum: number, pos: any) => sum + pos.realizedPnl, 0); const totalOpenPnl = positions.reduce((sum: number, pos: any) => sum + pos.openPnl, 0); const summary = { account, balance: cashBalance.cashBalance, openPnl: totalOpenPnl, totalEquity: cashBalance.cashBalance + totalOpenPnl, marginUsed: cashBalance.initialMargin, availableMargin: cashBalance.cashBalance - cashBalance.initialMargin + totalOpenPnl, positionCount: positions.length }; return { content: [{ type: "text", text: `Account summary for ${account.name}:\n${JSON.stringify(summary, null, 2)}` }] }; } catch (retryError) { logger.error("Error getting account summary after retry:", retryError); return { content: [{ type: "text", text: `Error getting account summary: ${retryError instanceof Error ? retryError.message : String(retryError)}` }] }; } } } /** * Handle get_market_data tool */ export async function handleGetMarketData(request: any) { const symbol = String(request.params.arguments?.symbol); const dataType = String(request.params.arguments?.dataType); const chartTimeframe = String(request.params.arguments?.chartTimeframe || "1min"); if (!symbol || !dataType) { throw new Error("Symbol and dataType are required"); } try { // Find contract by symbol const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } let marketData: any; let dataSource = "real-time"; // default source type // Check if we have the new socket connections available if (global.marketDataSocket && global.marketDataSocket.isConnected()) { logger.info(`Using market data socket for ${dataType} data on ${symbol}`); try { switch (dataType) { case "Quote": // Create a promise that will resolve when we get the quote data marketData = await new Promise((resolve, reject) => { let timeoutId: NodeJS.Timeout; // Set a timeout to prevent hanging if we don't get a response timeoutId = setTimeout(() => { reject(new Error('Quote data request timed out')); }, 10000); // Subscribe to quote data global.marketDataSocket.subscribe({ url: 'md/subscribequote', body: { symbol }, subscription: (data) => { clearTimeout(timeoutId); resolve(data); // Note: This would normally keep receiving updates, but we just want the first one } }).catch(error => { clearTimeout(timeoutId); reject(error); }); }); break; case "DOM": // Create a promise that will resolve when we get the DOM data marketData = await new Promise((resolve, reject) => { let timeoutId: NodeJS.Timeout; // Set a timeout to prevent hanging if we don't get a response timeoutId = setTimeout(() => { reject(new Error('DOM data request timed out')); }, 10000); // Subscribe to DOM data global.marketDataSocket.subscribe({ url: 'md/subscribedom', body: { symbol }, subscription: (data) => { clearTimeout(timeoutId); resolve(data); // Note: This would normally keep receiving updates, but we just want the first one } }).catch(error => { clearTimeout(timeoutId); reject(error); }); }); break; case "Chart": // Convert timeframe to chart parameters let chartUnits; let chartLength; switch (chartTimeframe) { case "1min": chartUnits = "m"; chartLength = 1; break; case "5min": chartUnits = "m"; chartLength = 5; break; case "15min": chartUnits = "m"; chartLength = 15; break; case "30min": chartUnits = "m"; chartLength = 30; break; case "1hour": chartUnits = "h"; chartLength = 1; break; case "4hour": chartUnits = "h"; chartLength = 4; break; case "1day": chartUnits = "d"; chartLength = 1; break; default: chartUnits = "m"; chartLength = 1; } // Create a promise that will resolve when we get the chart data marketData = await new Promise((resolve, reject) => { let timeoutId: NodeJS.Timeout; // Set a timeout to prevent hanging if we don't get a response timeoutId = setTimeout(() => { reject(new Error('Chart data request timed out')); }, 10000); // Get chart data using the chart subscription global.marketDataSocket.subscribe({ url: 'md/getchart', body: { symbol, chartDescription: `${chartLength}${chartUnits}`, timeRange: 3600 // 1 hour of data }, subscription: (data) => { clearTimeout(timeoutId); resolve(data); } }).catch(error => { clearTimeout(timeoutId); reject(error); }); }); break; default: throw new Error(`Unsupported data type: ${dataType}`); } } catch (socketError) { logger.error(`Error using market data socket for ${dataType} data on ${symbol}:`, socketError); logger.info(`Falling back to legacy approach for ${dataType} data on ${symbol}`); // Just return the legacy result directly instead of trying to parse it return await handleGetMarketDataLegacy(request); } // If we successfully got market data using the socket, return it return { content: [{ type: "text", text: `Market data for ${symbol} (${dataType}) [${dataSource}]:\n${JSON.stringify(marketData, null, 2)}` }] }; } else { // Fallback to legacy approach logger.warn(`Market data socket not available, using legacy approach for ${dataType} data on ${symbol}`); // Just return the legacy result directly return await handleGetMarketDataLegacy(request); } } catch (error) { logger.error(`Error getting market data for ${symbol}:`, error); return await handleGetMarketDataLegacy(request); } } /** * Legacy implementation of market data handling */ async function handleGetMarketDataLegacy(request: any) { const symbol = String(request.params.arguments?.symbol); const dataType = String(request.params.arguments?.dataType); const chartTimeframe = String(request.params.arguments?.chartTimeframe || "1min"); if (!symbol || !dataType) { throw new Error("Symbol and dataType are required"); } try { // Find contract by symbol const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } let marketData: any; switch (dataType) { case "Quote": // Get quote data using market data API try { if (global.tradovateWs && global.tradovateWs.readyState === WebSocket.OPEN) { const endpoint = `md/getQuote?contractId=${contract.id}`; marketData = await query(global.tradovateWs, endpoint); logger.info(`Market Data: ${JSON.stringify(marketData)}`); } else { throw new Error('Legacy WebSocket not connected'); } } catch (wsError) { logger.error(`WebSocket query failed: ${wsError}`); logger.info('Falling back to mock data for Quote'); // Fallback to mock data marketData = { symbol, bid: 5275.25, ask: 5275.50, last: 5275.25, volume: 1250000, timestamp: new Date().toISOString() }; } break; case "DOM": // Get DOM data using market data API marketData = await tradovateRequest('GET', `md/getDOM?contractId=${contract.id}`, undefined, true); break; case "Chart": // Convert timeframe to chart parameters let chartUnits; let chartLength; switch (chartTimeframe) { case "1min": chartUnits = "m"; chartLength = 1; break; case "5min": chartUnits = "m"; chartLength = 5; break; case "15min": chartUnits = "m"; chartLength = 15; break; case "30min": chartUnits = "m"; chartLength = 30; break; case "1hour": chartUnits = "h"; chartLength = 1; break; case "4hour": chartUnits = "h"; chartLength = 4; break; case "1day": chartUnits = "d"; chartLength = 1; break; default: chartUnits = "m"; chartLength = 1; } // Get chart data using market data API marketData = await tradovateRequest( 'GET', `md/getChart?contractId=${contract.id}&chartDescription=${chartLength}${chartUnits}&timeRange=3600`, undefined, true ); break; default: throw new Error(`Unsupported data type: ${dataType}`); } return { content: [{ type: "text", text: `Market data for ${symbol} (${dataType}):\n${JSON.stringify(marketData, null, 2)}` }] }; } catch (error) { logger.error(`Error getting market data for ${symbol}:`, error); // Fallback to mock data const mockContract = Object.values(contractsCache).find(c => c.name === symbol); if (!mockContract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } let mockMarketData: any; switch (dataType) { case "Quote": mockMarketData = { symbol, bid: 5275.25, ask: 5275.50, last: 5275.25, volume: 1250000, timestamp: new Date().toISOString() }; break; case "DOM": mockMarketData = { symbol, bids: [ { price: 5275.25, size: 250 }, { price: 5275.00, size: 175 }, { price: 5274.75, size: 320 }, { price: 5274.50, size: 450 }, { price: 5274.25, size: 280 } ], asks: [ { price: 5275.50, size: 180 }, { price: 5275.75, size: 220 }, { price: 5276.00, size: 350 }, { price: 5276.25, size: 275 }, { price: 5276.50, size: 400 } ], timestamp: new Date().toISOString() }; break; case "Chart": // Generate mock chart data for the requested timeframe const now = new Date(); const bars = []; for (let i = 0; i < 10; i++) { const barTime = new Date(now); switch (chartTimeframe) { case "1min": barTime.setMinutes(now.getMinutes() - i); break; case "5min": barTime.setMinutes(now.getMinutes() - i * 5); break; case "15min": barTime.setMinutes(now.getMinutes() - i * 15); break; case "30min": barTime.setMinutes(now.getMinutes() - i * 30); break; case "1hour": barTime.setHours(now.getHours() - i); break; case "4hour": barTime.setHours(now.getHours() - i * 4); break; case "1day": barTime.setDate(now.getDate() - i); break; } const basePrice = 5275.00; const open = basePrice - i * 0.25; const high = open + Math.random() * 1.5; const low = open - Math.random() * 1.5; const close = (open + high + low) / 3; const volume = Math.floor(Math.random() * 10000) + 5000; bars.push({ timestamp: barTime.toISOString(), open, high, low, close, volume }); } mockMarketData = { symbol, timeframe: chartTimeframe, bars: bars.reverse() }; break; default: throw new Error(`Unsupported data type: ${dataType}`); } return { content: [{ type: "text", text: `Market data for ${symbol} (${dataType}) [MOCK DATA]:\n${JSON.stringify(mockMarketData, null, 2)}` }] }; } } /** * Handle list_orders tool */ export async function handleListOrders(request: any) { const accountId = String(request.params.arguments?.accountId || ""); try { // Get orders from API let endpoint = 'order/list'; if (accountId) { endpoint += `?accountId=${accountId}`; } const orders = await tradovateRequest('GET', endpoint); if (!orders || orders.length === 0) { return { content: [{ type: "text", text: `No orders found${accountId ? ` for account ${accountId}` : ''}` }] }; } return { content: [{ type: "text", text: `Orders${accountId ? ` for account ${accountId}` : ''}:\n${JSON.stringify(orders, null, 2)}` }] }; } catch (error) { // Log error but attempt to retry once more before giving up logger.error("Error listing orders, retrying:", error); try { // Retry API call let endpoint = 'order/list'; if (accountId) { endpoint += `?accountId=${accountId}`; } const orders = await tradovateRequest('GET', endpoint); if (!orders || orders.length === 0) { return { content: [{ type: "text", text: `No orders found${accountId ? ` for account ${accountId}` : ''}` }] }; } return { content: [{ type: "text", text: `Orders${accountId ? ` for account ${accountId}` : ''}:\n${JSON.stringify(orders, null, 2)}` }] }; } catch (retryError) { logger.error("Error listing orders after retry:", retryError); // Fallback to cached data const cachedOrders = Object.values(ordersCache); const filteredOrders = accountId ? cachedOrders.filter(order => order.accountId === parseInt(accountId)) : cachedOrders; if (filteredOrders.length === 0) { return { content: [{ type: "text", text: `No orders found${accountId ? ` for account ${accountId}` : ''} (cached)` }] }; } return { content: [{ type: "text", text: `Orders${accountId ? ` for account ${accountId}` : ''} (cached):\n${JSON.stringify(filteredOrders, null, 2)}` }] }; } } } /** * Handle list_products tool */ export async function handleListProducts(request: any) { const contractId = request.params.arguments?.contractId; try { // Fetch products from API const productList = await tradovateRequest('GET', 'product/list'); if (!productList || !Array.isArray(productList) || productList.length === 0) { return { content: [{ type: "text", text: "No products found" }] }; } // If contractId is provided, filter the list if (contractId) { const product = productList.find(p => p.id === Number(contractId)); if (!product) { return { content: [{ type: "text", text: `Product not found with contractId: ${contractId}` }] }; } return { content: [{ type: "text", text: `Product for contractId ${contractId}:\n${JSON.stringify(product, null, 2)}` }] }; } // Return the full list return { content: [{ type: "text", text: `Available products:\n${JSON.stringify(productList, null, 2)}` }] }; } catch (error) { logger.error("Error fetching products:", error); return { content: [{ type: "text", text: `Error fetching products: ${error instanceof Error ? error.message : String(error)}` }] }; } } /** * Handle list_exchanges tool */ export async function handleListExchanges(request: any) { try { // Fetch exchanges from API const exchangeList = await tradovateRequest('GET', 'exchange/list'); if (!exchangeList || !Array.isArray(exchangeList) || exchangeList.length === 0) { return { content: [{ type: "text", text: "No exchanges found" }] }; } // Return the full list return { content: [{ type: "text", text: `Available exchanges:\n${JSON.stringify(exchangeList, null, 2)}` }] }; } catch (error) { logger.error("Error fetching exchanges:", error); return { content: [{ type: "text", text: `Error fetching exchanges: ${error instanceof Error ? error.message : String(error)}` }] }; } } /** * Handle find_product tool */ export async function handleFindProduct(request: any) { const name = String(request.params.arguments?.name); if (!name) { throw new Error("Product name is required"); } try { // Find product by name const product = await tradovateRequest('GET', `product/find?name=${name}`); if (!product) { return { content: [{ type: "text", text: `Product not found with name: ${name}` }] }; } return { content: [{ type: "text", text: `Product details for ${name}:\n${JSON.stringify(product, null, 2)}` }] }; } catch (error) { logger.error(`Error finding product with name ${name}:`, error); return { content: [{ type: "text", text: `Error finding product: ${error instanceof Error ? error.message : String(error)}` }] }; } }

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/alexanimal/tradovate-mcp-server'

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