Tradovate MCP Server

by alexanimal
Verified
#!/usr/bin/env node /** * This is a Tradovate MCP server that implements tools for managing Contract and Order Positions. * It demonstrates core MCP concepts like resources and tools by allowing: * - Listing contracts and positions as resources * - Reading individual contract and position details * - Managing positions via tools (create, modify, close) * - Getting account information and market data */ // First, load environment variables from .env file import dotenv from "dotenv"; dotenv.config(); import * as logger from "./logger.js"; // Then import other dependencies import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { fileURLToPath } from "url"; // Import from abstracted modules import { authenticate, tradovateRequest } from "./auth.js"; import { contractsCache, positionsCache, ordersCache, accountsCache, initializeData } from "./data.js"; import { handleGetContractDetails, handleListPositions, handlePlaceOrder, handleModifyOrder, handleCancelOrder, handleLiquidatePosition, handleGetAccountSummary, handleGetMarketData, handleListOrders } from "./tools.js"; /** * Create the MCP server */ export const server = new Server( { name: "tradovate-mcp-server", version: "0.1.0" }, { capabilities: { resources: { "tradovate://contract/": { name: "Tradovate Contracts", description: "Futures contracts available on Tradovate", }, "tradovate://position/": { name: "Tradovate Positions", description: "Current positions in your Tradovate account", }, }, prompts: {}, tools: { get_contract_details: { description: "Get detailed information about a specific contract by symbol", parameters: { type: "object", properties: { symbol: { type: "string", description: "The contract symbol (e.g., ESZ4, NQZ4)", }, }, required: ["symbol"], }, }, list_positions: { description: "List all positions for an account", parameters: { type: "object", properties: { accountId: { type: "string", description: "The account ID (optional, will use default if not provided)", }, }, }, }, place_order: { description: "Place a new order", parameters: { type: "object", properties: { symbol: { type: "string", description: "The contract symbol (e.g., ESZ4, NQZ4)", }, action: { type: "string", description: "Buy or Sell", enum: ["Buy", "Sell"], }, orderType: { type: "string", description: "Type of order", enum: ["Market", "Limit", "Stop", "StopLimit"], }, quantity: { type: "number", description: "Number of contracts", }, price: { type: "number", description: "Price for Limit and StopLimit orders", }, stopPrice: { type: "number", description: "Stop price for Stop and StopLimit orders", }, }, required: ["symbol", "action", "orderType", "quantity"], }, }, modify_order: { description: "Modify an existing order", parameters: { type: "object", properties: { orderId: { type: "string", description: "The order ID to modify", }, price: { type: "number", description: "New price for Limit and StopLimit orders", }, stopPrice: { type: "number", description: "New stop price for Stop and StopLimit orders", }, quantity: { type: "number", description: "New quantity", }, }, required: ["orderId"], }, }, cancel_order: { description: "Cancel an existing order", parameters: { type: "object", properties: { orderId: { type: "string", description: "The order ID to cancel", }, }, required: ["orderId"], }, }, liquidate_position: { description: "Close an existing position", parameters: { type: "object", properties: { symbol: { type: "string", description: "The contract symbol (e.g., ESZ4, NQZ4)", }, }, required: ["symbol"], }, }, get_account_summary: { description: "Get account summary information", parameters: { type: "object", properties: { accountId: { type: "string", description: "The account ID (optional, will use default if not provided)", }, }, }, }, get_market_data: { description: "Get market data for a specific contract", parameters: { type: "object", properties: { symbol: { type: "string", description: "The contract symbol (e.g., ESZ4, NQZ4)", }, dataType: { type: "string", description: "Type of market data to retrieve", enum: ["Quote", "DOM", "Chart"], }, chartTimeframe: { type: "string", description: "Timeframe for chart data", enum: ["1min", "5min", "15min", "30min", "1hour", "4hour", "1day"], }, }, required: ["symbol", "dataType"], }, }, list_orders: { description: "Get a list of orders, optionally filtered by account ID", parameters: { type: "object", properties: { accountId: { type: "string", description: "Optional account ID to filter orders by" } }, required: [] }, }, }, }, } ); /** * Resource handlers for the MCP server */ server.setRequestHandler(ListResourcesRequestSchema, async () => { // Return list of available resources return { resources: [ { uri: "tradovate://contract/", name: "Tradovate Contracts", description: "Futures contracts available on Tradovate", }, { uri: "tradovate://position/", name: "Tradovate Positions", description: "Current positions in your Tradovate account", }, ], }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; // Parse the URI to get the resource type and ID // Handle both tradovate:// protocol scheme and simple tradovate/ format const match = uri.match(/^(?:tradovate:\/\/|tradovate\/)([^\/]+)(?:\/(.*))?$/); if (!match) { throw new Error(`Invalid resource URI: ${uri}`); } const resourceType = match[1]; const resourceId = match[2] || ''; if (!resourceType) { throw new Error(`Invalid resource URI: ${uri}`); } // Handle different resource types switch (resourceType) { case "contract": { if (resourceId) { // Return specific contract const contract = contractsCache[resourceId]; if (!contract) { throw new Error(`Resource not found: ${uri}`); } return { contents: [ { type: "application/json", text: JSON.stringify(contract), uri: `tradovate://contract/${resourceId}` }, ], }; } else { // Return list of contracts const contracts = Object.values(contractsCache); return { contents: [ { type: "application/json", text: JSON.stringify(contracts), uri: "tradovate://contract/" }, ], }; } } case "position": { if (resourceId) { // Return specific position - fetch directly from API try { const position = await tradovateRequest('GET', `position/find?id=${resourceId}`); if (!position) { throw new Error(`Resource not found: ${uri}`); } return { contents: [ { type: "application/json", text: JSON.stringify(position), uri: `tradovate://position/${resourceId}` }, ], }; } catch (error) { logger.error(`Error fetching position ${resourceId}:`, error); throw new Error(`Resource not found: ${uri}`); } } else { // Return list of positions - fetch directly from API try { const positions = await tradovateRequest('GET', 'position/list'); return { contents: [ { type: "application/json", text: JSON.stringify(positions), uri: "tradovate://position/" }, ], }; } catch (error) { logger.error('Error fetching positions:', error); throw new Error(`Error fetching positions: ${error instanceof Error ? error.message : String(error)}`); } } } default: throw new Error(`Unknown resource type: ${resourceType}`); } }); /** * Tool handlers for the MCP server */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_contract_details", description: "Get detailed information about a specific contract by symbol", inputSchema: { type: "object", properties: { symbol: { type: "string", description: "The contract symbol (e.g., ESZ4, NQZ4)", }, }, required: ["symbol"], } }, { name: "list_positions", description: "List all positions for an account", inputSchema: { type: "object", properties: { accountId: { type: "string", description: "The account ID (optional, will use default if not provided)", }, }, } }, { name: "place_order", description: "Place a new order", inputSchema: { type: "object", properties: { symbol: { type: "string", description: "The contract symbol (e.g., ESZ4, NQZ4)", }, action: { type: "string", description: "Buy or Sell", enum: ["Buy", "Sell"], }, orderType: { type: "string", description: "Type of order", enum: ["Market", "Limit", "Stop", "StopLimit"], }, quantity: { type: "number", description: "Number of contracts", }, price: { type: "number", description: "Price for Limit and StopLimit orders", }, stopPrice: { type: "number", description: "Stop price for Stop and StopLimit orders", }, }, required: ["symbol", "action", "orderType", "quantity"], } }, { name: "modify_order", description: "Modify an existing order", inputSchema: { type: "object", properties: { orderId: { type: "string", description: "The order ID to modify", }, price: { type: "number", description: "New price for Limit and StopLimit orders", }, stopPrice: { type: "number", description: "New stop price for Stop and StopLimit orders", }, quantity: { type: "number", description: "New quantity", }, }, required: ["orderId"], } }, { name: "cancel_order", description: "Cancel an existing order", inputSchema: { type: "object", properties: { orderId: { type: "string", description: "The order ID to cancel", }, }, required: ["orderId"], } }, { name: "liquidate_position", description: "Close an existing position", inputSchema: { type: "object", properties: { symbol: { type: "string", description: "The contract symbol (e.g., ESZ4, NQZ4)", }, }, required: ["symbol"], } }, { name: "get_account_summary", description: "Get account summary information", inputSchema: { type: "object", properties: { accountId: { type: "string", description: "The account ID (optional, will use default if not provided)", }, }, } }, { name: "get_market_data", description: "Get market data for a specific contract", inputSchema: { type: "object", properties: { symbol: { type: "string", description: "The contract symbol (e.g., ESZ4, NQZ4)", }, dataType: { type: "string", description: "Type of market data to retrieve", enum: ["Quote", "DOM", "Chart"], }, chartTimeframe: { type: "string", description: "Timeframe for chart data", enum: ["1min", "5min", "15min", "30min", "1hour", "4hour", "1day"], }, }, required: ["symbol", "dataType"], }, }, { name: "list_orders", description: "Get a list of orders, optionally filtered by account ID", inputSchema: { type: "object", properties: { accountId: { type: "string", description: "Optional account ID to filter orders by" } }, required: [] }, }, ], }; }); /** * Handler for tool calls. * Implements the logic for each tool */ server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "get_contract_details": return await handleGetContractDetails(request); case "list_positions": return await handleListPositions(request); case "place_order": return await handlePlaceOrder(request); case "modify_order": return await handleModifyOrder(request); case "cancel_order": return await handleCancelOrder(request); case "liquidate_position": return await handleLiquidatePosition(request); case "get_account_summary": return await handleGetAccountSummary(request); case "get_market_data": return await handleGetMarketData(request); case "list_orders": return await handleListOrders(request); default: throw new Error(`Unknown tool: ${request.params.name}`); } }); /** * Prompt/template handlers for the MCP server */ server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [] // No templates/prompts available in this server }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { throw new Error(`Prompt not found: ${request.params.id}`); }); /** * Prompt/template handlers for the MCP server */ server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [] // No templates/prompts available in this server }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { throw new Error(`Prompt not found: ${request.params.id}`); }); /** * Initialize the server by authenticating with Tradovate API */ export async function initialize() { try { // Authenticate with Tradovate API await authenticate(); logger.info("Tradovate MCP server initialized successfully"); logger.info("Tradovate MCP server initialized successfully"); // Initialize data await initializeData(); // Set up periodic data refresh (every 5 minutes) setInterval(async () => { try { await initializeData(); } catch (error) { logger.error("Error refreshing data:", error); logger.error("Error refreshing data:", error); } }, 60 * 60 * 1000); } catch (error) { logger.error("Failed to initialize Tradovate MCP server:", error); logger.warn("Server will start with mock data fallback"); logger.error("Failed to initialize Tradovate MCP server:", error); logger.warn("Server will start with mock data fallback"); } } /** * Start the server using stdio transport. * This allows the server to communicate via standard input/output streams. */ export async function main() { await initialize(); const transport = new StdioServerTransport(); await server.connect(transport); } // Only run main if this file is executed directly // Check if we're in a test environment const isTestEnvironment = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined; // Skip the main execution in test environment if (!isTestEnvironment) { // Simple check to see if this file is being run directly const isMainModule = process.argv.length > 1 && process.argv[1].includes('index'); if (isMainModule) { main().catch((error) => { logger.error("Server error:", error); logger.error("Server error:", error); process.exit(1); }); } }