Tradovate MCP Server
by alexanimal
Verified
import axios from 'axios';
import dotenv from 'dotenv';
import * as logger from "./logger.js";
// Load environment variables at module scope
dotenv.config();
// Tradovate API authentication credentials interface
export interface TradovateCredentials {
name: string;
password: string;
appId: string;
appVersion: string;
deviceId: string;
cid: string;
sec: string;
}
// API URLs for different environments
const API_URLS = {
demo: 'https://demo.tradovateapi.com/v1',
live: 'https://live.tradovateapi.com/v1',
md_demo: 'wss://md-demo.tradovateapi.com/v1/websocket',
md_live: 'wss://md.tradovateapi.com/v1/websocket'
};
// Get API environment from env vars - use a function to get fresh values
function getApiEnvironment() {
return process.env.TRADOVATE_API_ENVIRONMENT || 'demo';
}
// Set API URLs based on environment - updated to use the function
export function getTradovateApiUrl() {
const apiEnv = getApiEnvironment();
return API_URLS[apiEnv as keyof typeof API_URLS] || API_URLS.demo;
}
export function getTradovateMdApiUrl() {
const apiEnv = getApiEnvironment();
return apiEnv.includes('live') ? API_URLS.md_live : API_URLS.md_demo;
}
// Keep tokens in memory
let accessToken: string | null = null;
let accessTokenExpiry: number | null = null;
let refreshToken: string | null = null;
// Function to get credentials from environment variables
// This will be called when needed, not at module initialization
export function getCredentials(): TradovateCredentials {
const credentials: TradovateCredentials = {
name: process.env.TRADOVATE_USERNAME || '',
password: process.env.TRADOVATE_PASSWORD || '',
appId: process.env.TRADOVATE_APP_ID || '',
appVersion: process.env.TRADOVATE_APP_VERSION || '1.0.0',
deviceId: process.env.TRADOVATE_DEVICE_ID || '',
cid: process.env.TRADOVATE_CID || '',
sec: process.env.TRADOVATE_SECRET || ''
};
// Debug log for credentials
logger.debug('DEBUG: Credentials retrieved from environment:');
logger.debug('name present:', !!credentials.name);
logger.debug('password present:', !!credentials.password);
logger.debug('appId present:', !!credentials.appId);
logger.debug('deviceId present:', !!credentials.deviceId);
logger.debug('cid present:', !!credentials.cid);
logger.debug('sec present:', !!credentials.sec);
return credentials;
}
/**
* Check if the current access token is valid
*/
export function isAccessTokenValid(): boolean {
if (!accessToken || !accessTokenExpiry) return false;
// Consider token expired 5 minutes before actual expiry
const currentTime = Date.now();
const expiryWithBuffer = accessTokenExpiry - (5 * 60 * 1000);
return currentTime < expiryWithBuffer;
}
/**
* Refresh the access token using the refresh token
*/
export async function refreshAccessToken(): Promise<string> {
if (!refreshToken) {
throw new Error('No refresh token available');
}
const credentials = getCredentials();
try {
const response = await axios.post(`${getTradovateApiUrl()}/auth/renewAccessToken`, {
name: credentials.name,
refreshToken
});
if (response.data && response.data.accessToken) {
accessToken = response.data.accessToken;
// Set expiry time (default to 24 hours if not provided)
if (response.data.expirationTime) {
accessTokenExpiry = response.data.expirationTime;
} else {
accessTokenExpiry = Date.now() + (24 * 60 * 60 * 1000);
}
logger.info('Successfully refreshed access token');
return response.data.accessToken;
} else {
throw new Error('Token refresh response did not contain an access token');
}
} catch (error) {
logger.error('Failed to refresh access token:', error);
// Clear tokens to force a full re-authentication
accessToken = null;
accessTokenExpiry = null;
refreshToken = null;
throw new Error('Failed to refresh access token');
}
}
/**
* Authenticate with Tradovate API and get access token
*/
export async function authenticate(): Promise<string> {
// If we have a valid token, return it
if (isAccessTokenValid() && accessToken) {
return accessToken;
}
// If we have a refresh token, try to use it
if (refreshToken) {
try {
return await refreshAccessToken();
} catch (error) {
logger.warn('Failed to refresh token, will attempt full authentication');
// Continue with full authentication
}
}
// Get fresh credentials
const credentials = getCredentials();
// Perform full authentication
try {
// Validate required credentials
if (!credentials.name || !credentials.password || !credentials.appId ||
!credentials.deviceId || !credentials.cid || !credentials.sec) {
logger.error('DEBUG: Credential validation failed!');
throw new Error('Missing required Tradovate API credentials');
}
const response = await axios.post(`${getTradovateApiUrl()}/auth/accessTokenRequest`, credentials);
if (response.data && response.data.accessToken) {
accessToken = response.data.accessToken;
// Store refresh token if provided
if (response.data.refreshToken) {
refreshToken = response.data.refreshToken;
}
// Set expiry time (default to 24 hours if not provided)
if (response.data.expirationTime) {
accessTokenExpiry = response.data.expirationTime;
} else {
accessTokenExpiry = Date.now() + (24 * 60 * 60 * 1000);
}
logger.info('Successfully authenticated with Tradovate API');
return response.data.accessToken;
} else {
throw new Error('Authentication response did not contain an access token');
}
} catch (error) {
logger.error('Failed to authenticate with Tradovate API:', error);
throw new Error('Authentication with Tradovate API failed');
}
}
/**
* Make an authenticated request to the Tradovate API
*/
export async function tradovateRequest(method: string, endpoint: string, data?: any, isMarketData: boolean = false): Promise<any> {
const token = await authenticate();
const baseUrl = isMarketData ? getTradovateMdApiUrl() : getTradovateApiUrl();
logger.info(`Making request to ${baseUrl}/${endpoint}`);
try {
const response = await axios({
method,
url: `${baseUrl}/${endpoint}`,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
data
});
logger.info(`${baseUrl}/${endpoint}: ${JSON.stringify(response.data)}`);
return response.data;
} catch (error: any) {
// Handle specific API errors
if (error.response) {
const status = error.response.status;
const errorData = error.response.data;
// Handle authentication errors
if (status === 401) {
// Clear tokens to force re-authentication on next request
accessToken = null;
accessTokenExpiry = null;
throw new Error('Authentication failed: ' + (errorData.errorText || 'Unauthorized'));
}
// Handle rate limiting
if (status === 429) {
logger.warn('Rate limit exceeded, retrying after delay');
// Wait for 2 seconds and retry
await new Promise(resolve => setTimeout(resolve, 2000));
return tradovateRequest(method, endpoint, data, isMarketData);
}
// Handle other API errors
throw new Error(`Tradovate API error (${status}): ${errorData.errorText || 'Unknown error'}`);
}
// Handle network errors
logger.error(`Error making request to ${endpoint}:`, error);
throw new Error(`Tradovate API request to ${endpoint} failed: ${error.message}`);
}
}