Skip to main content
Glama

TradeStation MCP Server

by maven81g
index.ts21.5 kB
// TradeStation MCP Server // Implementation of Model Context Protocol server for TradeStation APIs import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import axios, { AxiosRequestConfig, AxiosError } from 'axios'; import dotenv from 'dotenv'; dotenv.config(); // TradeStation API configuration const TS_API_BASE = 'https://sim-api.tradestation.com/v3'; const TS_AUTH_URL = 'https://signin.tradestation.com/authorize'; const TS_TOKEN_URL = 'https://signin.tradestation.com/oauth/token'; // Auth credentials (must be set in .env file) const TS_CLIENT_ID = process.env.TRADESTATION_CLIENT_ID; const TS_CLIENT_SECRET = process.env.TRADESTATION_CLIENT_SECRET; const TS_REDIRECT_URI = process.env.TRADESTATION_REDIRECT_URI; const TS_ACCOUNT_ID = process.env.TRADESTATION_ACCOUNT_ID; // Validate required environment variables if (!TS_CLIENT_ID || !TS_CLIENT_SECRET || !TS_REDIRECT_URI) { console.error('ERROR: Missing required environment variables!'); console.error('Please ensure the following are set in your .env file:'); console.error(' - TRADESTATION_CLIENT_ID'); console.error(' - TRADESTATION_CLIENT_SECRET'); console.error(' - TRADESTATION_REDIRECT_URI'); process.exit(1); } // Warn if account ID is not set if (!TS_ACCOUNT_ID) { console.warn('WARNING: TRADESTATION_ACCOUNT_ID not set in .env file'); console.warn('Account-related tools will require accountId parameter'); } // Types interface TokenData { accessToken: string; refreshToken: string; expiresAt: number; } interface RefreshTokenResponse { access_token: string; refresh_token?: string; expires_in: number; } interface ToolArguments { userId?: string; [key: string]: any; } // Token storage (single user) const DEFAULT_USER = "default"; const tokenStore = new Map<string, TokenData>(); // Initialize token from environment variable if available const TS_REFRESH_TOKEN = process.env.TRADESTATION_REFRESH_TOKEN; if (TS_REFRESH_TOKEN) { tokenStore.set(DEFAULT_USER, { accessToken: "", refreshToken: TS_REFRESH_TOKEN, expiresAt: -1 }); } // Define schemas for the requests - including userId in all schemas const marketDataSchema = { symbols: z.string().describe('Comma-separated list of symbols') }; const barChartSchema = { symbol: z.string().describe('Trading symbol'), interval: z.number().describe('Bar interval value (e.g., 1, 5, 15)'), unit: z.enum(['Minute', 'Daily', 'Weekly', 'Monthly']).describe('Bar interval unit'), beginTime: z.string().optional().describe('Begin time in ISO format (optional)'), endTime: z.string().optional().describe('End time in ISO format (optional)'), barsBack: z.number().optional().describe('Number of bars to return (optional)') }; const searchSymbolsSchema = { criteria: z.string().describe('Search criteria'), type: z.enum(['stock', 'option', 'future', 'forex', 'all']) .default('all') .describe('Symbol type filter') }; const optionExpirationsSchema = { underlying: z.string().describe('Underlying symbol (e.g., AAPL, SPY)') }; const optionStrikesSchema = { underlying: z.string().describe('Underlying symbol (e.g., AAPL, SPY)'), expiration: z.string().optional().describe('Expiration date filter in YYYY-MM-DD format (optional)') }; // Account-related schemas const accountsSchema = {}; const balancesSchema = { accountId: z.string().optional().describe('Account ID (optional, uses TRADESTATION_ACCOUNT_ID from env if not provided)') }; const positionsSchema = { accountId: z.string().optional().describe('Account ID (optional, uses TRADESTATION_ACCOUNT_ID from env if not provided)') }; const ordersSchema = { accountId: z.string().optional().describe('Account ID (optional, uses TRADESTATION_ACCOUNT_ID from env if not provided)'), status: z.enum(['Open', 'Filled', 'Canceled', 'Rejected', 'All']) .default('All') .describe('Filter orders by status') }; const orderDetailsSchema = { accountId: z.string().optional().describe('Account ID (optional, uses TRADESTATION_ACCOUNT_ID from env if not provided)'), orderId: z.string().describe('Order ID') }; const executionsSchema = { accountId: z.string().optional().describe('Account ID (optional, uses TRADESTATION_ACCOUNT_ID from env if not provided)'), orderId: z.string().describe('Order ID') }; const symbolDetailsSchema = { symbols: z.string().describe('Single symbol or comma-separated list of symbols') }; const confirmOrderSchema = { accountId: z.string().optional().describe('Account ID (optional, uses TRADESTATION_ACCOUNT_ID from env if not provided)'), symbol: z.string().describe('Symbol to trade (e.g., SPY, SPY 251121C580)'), quantity: z.number().describe('Order quantity'), orderType: z.enum(['Market', 'Limit', 'Stop', 'StopLimit']).describe('Order type'), tradeAction: z.enum(['BUY', 'SELL', 'BUYTOOPEN', 'BUYTOCLOSE', 'SELLTOOPEN', 'SELLTOCLOSE']).describe('Trade action'), limitPrice: z.number().optional().describe('Limit price (required for Limit and StopLimit orders)'), stopPrice: z.number().optional().describe('Stop price (required for Stop and StopLimit orders)'), duration: z.enum(['DAY', 'GTC', 'GTD', 'DYP', 'GCP']).default('DAY').describe('Time in force duration') }; // Token refresh helper async function refreshToken(refreshToken: string): Promise<TokenData> { try { const response = await axios.post<RefreshTokenResponse>(TS_TOKEN_URL, new URLSearchParams({ grant_type: 'refresh_token', client_id: TS_CLIENT_ID!, client_secret: TS_CLIENT_SECRET!, refresh_token: refreshToken }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ); return { accessToken: response.data.access_token, refreshToken: refreshToken, expiresAt: Date.now() + 3600000 // 1 hour from now }; } catch (error: unknown) { if (error instanceof AxiosError) { console.error('Error refreshing token:', error.response?.data || error.message); } else if (error instanceof Error) { console.error('Error refreshing token:', error.message); } else { console.error('Unknown error refreshing token:', error); } throw new Error('Failed to refresh token'); } } // Helper for making authenticated API requests async function makeAuthenticatedRequest( endpoint: string, method: AxiosRequestConfig['method'] = 'GET', data: any = null ): Promise<any> { const userTokens = tokenStore.get(DEFAULT_USER); if (!userTokens) { throw new Error('User not authenticated. Please set TRADESTATION_REFRESH_TOKEN in .env file.'); } // Check if token is expired or about to expire (within 60 seconds) if (userTokens.expiresAt < Date.now() + 60000) { // Refresh the token const newTokens = await refreshToken(userTokens.refreshToken); tokenStore.set(DEFAULT_USER, newTokens); } try { const options: AxiosRequestConfig = { method, url: `${TS_API_BASE}${endpoint}`, headers: { 'Authorization': `Bearer ${tokenStore.get(DEFAULT_USER)?.accessToken}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }, timeout: 60000 }; if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { options.data = data; } const response = await axios(options); return response.data; } catch (error: unknown) { if (error instanceof AxiosError) { const errorMessage = error.response?.data?.Message || error.response?.data?.message || error.message; const statusCode = error.response?.status; console.error(`API request error [${statusCode}]: ${errorMessage}`); console.error('Endpoint:', endpoint); throw new Error(`API Error (${statusCode}): ${errorMessage}`); } else if (error instanceof Error) { console.error('API request error:', error.message); throw error; } else { console.error('Unknown API request error:', error); throw new Error('Unknown API request error'); } } } // Initialize MCP Server const server = new McpServer({ name: "TradeStation", version: "1.0.0", }, { capabilities: { tools: {} } }); // Type for tool handler response interface ToolResponse { content: Array<{ type: string; text: string; }>; isError?: boolean; } // Type for tool handler function type ToolHandler<T> = (params: { arguments: T & ToolArguments }) => Promise<ToolResponse>; // Define available tools server.tool( "marketData", "Get quotes for symbols", marketDataSchema, async (args) => { try { const { symbols } = args; // Fixed: symbols should be in the path, not query parameter const quotes = await makeAuthenticatedRequest( `/marketdata/quotes/${encodeURIComponent(symbols)}` ); return { content: [ { type: "text", text: JSON.stringify(quotes, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch market data: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "barChart", "Get historical price bars/candles", barChartSchema, async (args) => { try { const { symbol, interval, unit, beginTime, endTime, barsBack } = args; let endpoint = `/marketdata/barcharts/${encodeURIComponent(symbol)}?interval=${interval}&unit=${unit}`; if (beginTime) { endpoint += `&begin=${encodeURIComponent(beginTime)}`; } if (endTime) { endpoint += `&end=${encodeURIComponent(endTime)}`; } if (barsBack) { endpoint += `&barsback=${barsBack}`; } const bars = await makeAuthenticatedRequest(endpoint); return { content: [ { type: "text", text: JSON.stringify(bars, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch bar chart data: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "searchSymbols", "Search for symbols (Note: Symbol search not available in TradeStation v3 API - use getSymbolDetails instead with known symbols)", searchSymbolsSchema, async (args) => { try { const { criteria } = args; return { content: [ { type: "text", text: `Symbol search is not available in TradeStation API v3.\n\nAlternatives:\n1. Use getSymbolDetails with known symbols: "${criteria}"\n2. Use marketData to get quotes for known symbols\n3. For options, use getOptionExpirations and getOptionStrikes with the underlying symbol` } ], isError: true }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to search symbols: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "getOptionExpirations", "Get available expiration dates for options on an underlying symbol", optionExpirationsSchema, async (args) => { try { const { underlying } = args; const expirations = await makeAuthenticatedRequest( `/marketdata/options/expirations/${encodeURIComponent(underlying)}` ); return { content: [ { type: "text", text: JSON.stringify(expirations, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch option expirations: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "getOptionStrikes", "Get available strike prices for options on an underlying symbol", optionStrikesSchema, async (args) => { try { const { underlying, expiration } = args; let endpoint = `/marketdata/options/strikes/${encodeURIComponent(underlying)}`; if (expiration) { endpoint += `?expiration=${encodeURIComponent(expiration)}`; } const strikes = await makeAuthenticatedRequest(endpoint); return { content: [ { type: "text", text: JSON.stringify(strikes, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch option strikes: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); // Account Functions server.tool( "getAccounts", "Get list of brokerage accounts", accountsSchema, async (args) => { try { const accounts = await makeAuthenticatedRequest('/brokerage/accounts'); return { content: [ { type: "text", text: JSON.stringify(accounts, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch accounts: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "getBalances", "Get account balances and buying power", balancesSchema, async (args) => { try { const accountId = args.accountId || TS_ACCOUNT_ID; if (!accountId) { throw new Error('Account ID is required. Either provide accountId parameter or set TRADESTATION_ACCOUNT_ID in .env file.'); } const balances = await makeAuthenticatedRequest( `/brokerage/accounts/${encodeURIComponent(accountId)}/balances` ); return { content: [ { type: "text", text: JSON.stringify(balances, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch balances: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "getPositions", "Get current positions with P&L", positionsSchema, async (args) => { try { const accountId = args.accountId || TS_ACCOUNT_ID; if (!accountId) { throw new Error('Account ID is required. Either provide accountId parameter or set TRADESTATION_ACCOUNT_ID in .env file.'); } const positions = await makeAuthenticatedRequest( `/brokerage/accounts/${encodeURIComponent(accountId)}/positions` ); return { content: [ { type: "text", text: JSON.stringify(positions, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch positions: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "getOrders", "Get order history with optional status filter", ordersSchema, async (args) => { try { const accountId = args.accountId || TS_ACCOUNT_ID; const { status } = args; if (!accountId) { throw new Error('Account ID is required. Either provide accountId parameter or set TRADESTATION_ACCOUNT_ID in .env file.'); } let endpoint = `/brokerage/accounts/${encodeURIComponent(accountId)}/orders`; if (status && status !== 'All') { endpoint += `?status=${status}`; } const orders = await makeAuthenticatedRequest(endpoint); return { content: [ { type: "text", text: JSON.stringify(orders, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch orders: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "getOrderDetails", "Get detailed information for a specific order", orderDetailsSchema, async (args) => { try { const accountId = args.accountId || TS_ACCOUNT_ID; const { orderId } = args; if (!accountId) { throw new Error('Account ID is required. Either provide accountId parameter or set TRADESTATION_ACCOUNT_ID in .env file.'); } const orderDetails = await makeAuthenticatedRequest( `/brokerage/accounts/${encodeURIComponent(accountId)}/orders/${encodeURIComponent(orderId)}` ); return { content: [ { type: "text", text: JSON.stringify(orderDetails, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch order details: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "getExecutions", "Get fills/executions for a specific order", executionsSchema, async (args) => { try { const accountId = args.accountId || TS_ACCOUNT_ID; const { orderId } = args; if (!accountId) { throw new Error('Account ID is required. Either provide accountId parameter or set TRADESTATION_ACCOUNT_ID in .env file.'); } const executions = await makeAuthenticatedRequest( `/brokerage/accounts/${encodeURIComponent(accountId)}/orders/${encodeURIComponent(orderId)}/executions` ); return { content: [ { type: "text", text: JSON.stringify(executions, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch executions: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "getSymbolDetails", "Get detailed symbol information", symbolDetailsSchema, async (args) => { try { const { symbols } = args; const symbolDetails = await makeAuthenticatedRequest( `/marketdata/symbols/${encodeURIComponent(symbols)}` ); return { content: [ { type: "text", text: JSON.stringify(symbolDetails, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to fetch symbol details: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); server.tool( "confirmOrder", "Preview order costs and requirements (READ-ONLY - does not execute trades)", confirmOrderSchema, async (args) => { try { const accountId = args.accountId || TS_ACCOUNT_ID; const { symbol, quantity, orderType, tradeAction, limitPrice, stopPrice, duration } = args; if (!accountId) { throw new Error('Account ID is required. Either provide accountId parameter or set TRADESTATION_ACCOUNT_ID in .env file.'); } // Build order confirmation request body const orderData: any = { AccountID: accountId, Symbol: symbol, Quantity: quantity, OrderType: orderType, TradeAction: tradeAction, TimeInForce: { Duration: duration } }; // Add price fields based on order type if (orderType === 'Limit' || orderType === 'StopLimit') { if (!limitPrice) { throw new Error('limitPrice is required for Limit and StopLimit orders'); } orderData.LimitPrice = limitPrice; } if (orderType === 'Stop' || orderType === 'StopLimit') { if (!stopPrice) { throw new Error('stopPrice is required for Stop and StopLimit orders'); } orderData.StopPrice = stopPrice; } const confirmation = await makeAuthenticatedRequest( '/orderexecution/orderconfirm', 'POST', orderData ); return { content: [ { type: "text", text: JSON.stringify(confirmation, null, 2) } ] }; } catch (error: unknown) { return { content: [ { type: "text", text: `Failed to confirm order: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); // Start the server async function startServer(): Promise<void> { // For stdio transport const transport = new StdioServerTransport(); await server.connect(transport); //console.log('TradeStation MCP Server started with stdio transport'); } startServer().catch(console.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/maven81g/tradestation_mcp'

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