Skip to main content
Glama

TradeStation MCP Server

by maven81g
index.js16.4 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 from 'axios'; import dotenv from 'dotenv'; dotenv.config(); // TradeStation API configuration const TS_API_BASE = 'https://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.TS_CLIENT_ID; const TS_CLIENT_SECRET = process.env.TS_CLIENT_SECRET; const TS_REDIRECT_URI = process.env.TS_REDIRECT_URI; // 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(' - TS_CLIENT_ID'); console.error(' - TS_CLIENT_SECRET'); console.error(' - TS_REDIRECT_URI'); process.exit(1); } // Token storage (in-memory for demonstration) const tokenStore = new Map(); /* // Define schemas for the requests const accountsSchema = z.object({}); const accountBalancesSchema = z.object({ accountId: z.string().describe('Account ID') }); const positionsSchema = z.object({ accountId: z.string().describe('Account ID') }); const ordersSchema = z.object({ accountId: z.string().describe('Account ID'), status: z.enum(['open', 'filled', 'canceled', 'rejected', 'all']) .default('all') .describe('Filter orders by status') }); const placeOrderSchema = z.object({ accountId: z.string().describe('Account ID'), symbol: z.string().describe('Trading symbol'), quantity: z.number().describe('Order quantity'), orderType: z.enum(['market', 'limit', 'stop', 'stop_limit']).describe('Order type'), side: z.enum(['buy', 'sell', 'sell_short', 'buy_to_cover']).describe('Order side'), limitPrice: z.number().optional().describe('Limit price (required for limit and stop_limit orders)'), stopPrice: z.number().optional().describe('Stop price (required for stop and stop_limit orders)'), timeInForce: z.enum(['day', 'gtc', 'gtd', 'ioc', 'fok']) .default('day') .describe('Time in force') }); const cancelOrderSchema = z.object({ accountId: z.string().describe('Account ID'), orderId: z.string().describe('Order ID to cancel') }); */ const marketDataSchema = z.object({ symbols: z.string().describe('Comma-separated list of symbols') }); const barChartSchema = z.object({ 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 = z.object({ criteria: z.string().describe('Search criteria'), type: z.enum(['stock', 'option', 'future', 'forex', 'all']) .default('all') .describe('Symbol type filter') }); const optionChainSchema = z.object({ symbol: z.string().describe('Underlying symbol'), strike: z.number().optional().describe('Strike price filter (optional)'), expiration: z.string().optional().describe('Expiration date in YYYY-MM-DD format (optional)') }); // Token refresh helper async function refreshToken(refreshToken) { try { const response = await axios.post(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: response.data.refresh_token || refreshToken, expiresAt: Date.now() + (response.data.expires_in * 1000) }; } catch (error) { console.error('Error refreshing token:', error.response?.data || error.message); throw new Error('Failed to refresh token'); } } // Helper for making authenticated API requests async function makeAuthenticatedRequest(userId, endpoint, method = 'GET', data = null) { const userTokens = tokenStore.get(userId); if (!userTokens) { throw new Error('User not authenticated'); } // 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(userId, newTokens); } try { const options = { method, url: `${TS_API_BASE}${endpoint}`, headers: { 'Authorization': `Bearer ${tokenStore.get(userId).accessToken}`, 'Content-Type': 'application/json' } }; if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { options.data = data; } const response = await axios(options); return response.data; } catch (error) { console.error('API request error:', error.response?.data || error.message); throw error; } } // Initialize MCP Server const server = new McpServer({ name: "TradeStation", version: "1.0.0", }, { capabilities: { tools: {} } }); /* // Define available tools server.tool( "accounts", "Get user account information", accountsSchema, async ({ arguments: args }) => { try { const userId = args.userId; // You might need to pass this differently const accounts = await makeAuthenticatedRequest(userId, '/brokerage/accounts'); return { content: [ { type: "text", text: JSON.stringify(accounts, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to fetch accounts: ${error.message}` } ], isError: true }; } } ); server.tool( "accountBalances", "Get account balances", accountBalancesSchema, async ({ arguments: args }) => { try { const { accountId } = args; const userId = args.userId; // You might need to pass this differently const balances = await makeAuthenticatedRequest( userId, `/brokerage/accounts/${accountId}/balances` ); return { content: [ { type: "text", text: JSON.stringify(balances, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to fetch account balances: ${error.message}` } ], isError: true }; } } ); server.tool( "positions", "Get account positions", positionsSchema, async ({ arguments: args }) => { try { const { accountId } = args; const userId = args.userId; // You might need to pass this differently const positions = await makeAuthenticatedRequest( userId, `/brokerage/accounts/${accountId}/positions` ); return { content: [ { type: "text", text: JSON.stringify(positions, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to fetch positions: ${error.message}` } ], isError: true }; } } ); server.tool( "orders", "Get account orders", ordersSchema, async ({ arguments: args }) => { try { const { accountId, status } = args; const userId = args.userId; // You might need to pass this differently let endpoint = `/brokerage/accounts/${accountId}/orders`; if (status && status !== 'all') { endpoint += `?status=${status}`; } const orders = await makeAuthenticatedRequest(userId, endpoint); return { content: [ { type: "text", text: JSON.stringify(orders, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to fetch orders: ${error.message}` } ], isError: true }; } } ); server.tool( "placeOrder", "Place a new order", placeOrderSchema, async ({ arguments: args }) => { try { const { accountId, symbol, quantity, orderType, side, limitPrice, stopPrice, timeInForce } = args; const userId = args.userId; // You might need to pass this differently // Build order payload const orderData = { AccountID: accountId, Symbol: symbol, Quantity: quantity, OrderType: orderType.toUpperCase(), TradeAction: side.toUpperCase(), TimeInForce: { Duration: timeInForce.toUpperCase() }, Route: "Intelligent" }; // Add price fields based on order type if (orderType === 'limit' || orderType === 'stop_limit') { orderData.LimitPrice = limitPrice; } if (orderType === 'stop' || orderType === 'stop_limit') { orderData.StopPrice = stopPrice; } const result = await makeAuthenticatedRequest( userId, '/brokerage/orders', 'POST', orderData ); return { content: [ { type: "text", text: JSON.stringify({ orderId: result.OrderID, status: result.Status }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to place order: ${error.message}` } ], isError: true }; } } ); server.tool( "cancelOrder", "Cancel an existing order", cancelOrderSchema, async ({ arguments: args }) => { try { const { accountId, orderId } = args; const userId = args.userId; // You might need to pass this differently await makeAuthenticatedRequest( userId, `/brokerage/accounts/${accountId}/orders/${orderId}`, 'DELETE' ); return { content: [ { type: "text", text: JSON.stringify({ success: true, message: 'Order canceled successfully' }, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ success: false, message: `Failed to cancel order: ${error.message}` }, null, 2) } ], isError: true }; } } ); */ server.tool( "marketData", "Get quotes for symbols", marketDataSchema, async ({ arguments: args }) => { try { const { symbols } = args; const userId = args.userId; // You might need to pass this differently const quotes = await makeAuthenticatedRequest( userId, `/marketdata/quotes?symbols=${encodeURIComponent(symbols)}` ); return { content: [ { type: "text", text: JSON.stringify(quotes, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to fetch market data: ${error.message}` } ], isError: true }; } } ); server.tool( "barChart", "Get historical price bars/candles", barChartSchema, async ({ arguments: args }) => { try { const { symbol, interval, unit, beginTime, endTime, barsBack } = args; const userId = args.userId; // You might need to pass this differently 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(userId, endpoint); return { content: [ { type: "text", text: JSON.stringify(bars, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to fetch bar chart data: ${error.message}` } ], isError: true }; } } ); server.tool( "searchSymbols", "Search for symbols", searchSymbolsSchema, async ({ arguments: args }) => { try { const { criteria, type } = args; const userId = args.userId; // You might need to pass this differently let endpoint = `/marketdata/symbols/search/${encodeURIComponent(criteria)}`; if (type && type !== 'all') { endpoint += `?type=${type}`; } const symbols = await makeAuthenticatedRequest(userId, endpoint); return { content: [ { type: "text", text: JSON.stringify(symbols, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to search symbols: ${error.message}` } ], isError: true }; } } ); server.tool( "optionChain", "Get option chain for a symbol", optionChainSchema, async ({ arguments: args }) => { try { const { symbol, strike, expiration } = args; const userId = args.userId; // You might need to pass this differently let endpoint = `/marketdata/options/chains/${encodeURIComponent(symbol)}`; const queryParams = []; if (strike) { queryParams.push(`strike=${strike}`); } if (expiration) { queryParams.push(`expiration=${encodeURIComponent(expiration)}`); } if (queryParams.length > 0) { endpoint += `?${queryParams.join('&')}`; } const options = await makeAuthenticatedRequest(userId, endpoint); return { content: [ { type: "text", text: JSON.stringify(options, null, 2) } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to fetch option chain: ${error.message}` } ], isError: true }; } } ); // Start the server async function startServer() { // For stdio transport const transport = new StdioServerTransport(); await server.connect(transport); // Alternative: For HTTP transport (if you want to expose it as an HTTP service) // const app = express(); // const port = process.env.PORT || 3000; // app.use(cors()); // app.use(express.json()); // // Authentication routes (like before) // app.get('/auth', (req, res) => { // const userId = uuidv4(); // const state = Buffer.from(JSON.stringify({ userId })).toString('base64'); // const authUrl = `${TS_AUTH_URL}?` + // new URLSearchParams({ // response_type: 'code', // client_id: TS_CLIENT_ID, // redirect_uri: TS_REDIRECT_URI, // audience: 'https://api.tradestation.com', // state, // scope: 'openid offline_access profile MarketData ReadAccount Trade' // }).toString(); // res.redirect(authUrl); // }); // app.get('/callback', async (req, res) => { // // ... callback handling logic ... // }); // app.listen(port, () => { // console.log(`TradeStation MCP Server listening on port ${port}`); // }); } 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