UNPKG

questrade-mcp-server

Version:

MCP server for Questrade API integration

131 lines (112 loc) 4.34 kB
import axios, { AxiosInstance, AxiosResponse } from 'axios'; import { QuestradeConfig, Account, Position, Balance, Quote, Symbol, Order, TokenResponse } from './types.js'; import { TokenManager } from './token-manager.js'; export class QuestradeClient { private http: AxiosInstance; private config: QuestradeConfig; private tokenManager: TokenManager; constructor(config: QuestradeConfig, tokenManager: TokenManager) { this.config = config; this.tokenManager = tokenManager; this.http = axios.create({ baseURL: config.apiUrl, headers: { 'Authorization': `Bearer ${config.accessToken}`, 'Content-Type': 'application/json' } }); } async refreshToken(): Promise<TokenResponse> { try { const data = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: this.config.refreshToken }); const response = await axios.post('https://login.questrade.com/oauth2/token', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); const tokenData = response.data; this.config.accessToken = tokenData.access_token; this.config.refreshToken = tokenData.refresh_token; this.config.apiUrl = tokenData.api_server; this.http.defaults.baseURL = this.config.apiUrl; this.http.defaults.headers['Authorization'] = `Bearer ${this.config.accessToken}`; await this.tokenManager.saveTokens( tokenData.refresh_token, tokenData.access_token, tokenData.api_server ); return tokenData; } catch (error) { if (axios.isAxiosError(error)) { const errorDetails = error.response?.data || error.message; throw new Error(`Token refresh failed: ${JSON.stringify(errorDetails)}`); } throw error; } } async getAccounts(): Promise<Account[]> { const response: AxiosResponse<{ accounts: Account[] }> = await this.http.get('/v1/accounts'); return response.data.accounts; } async getPositions(accountNumber: string): Promise<Position[]> { const response: AxiosResponse<{ positions: Position[] }> = await this.http.get( `/v1/accounts/${accountNumber}/positions` ); return response.data.positions; } async getBalances(accountNumber: string): Promise<Balance[]> { const response: AxiosResponse<{ perCurrencyBalances: Balance[], combinedBalances: Balance[], sodPerCurrencyBalances: Balance[], sodCombinedBalances: Balance[] }> = await this.http.get( `/v1/accounts/${accountNumber}/balances` ); return response.data.combinedBalances; } async getQuotes(symbolIds: number[]): Promise<Quote[]> { const ids = symbolIds.join(','); const response: AxiosResponse<{ quotes: Quote[] }> = await this.http.get( `/v1/markets/quotes/${ids}` ); return response.data.quotes; } async searchSymbols(prefix: string, offset: number = 0): Promise<Symbol[]> { const response: AxiosResponse<{ symbols: Symbol[] }> = await this.http.get( `/v1/symbols/search?prefix=${encodeURIComponent(prefix)}&offset=${offset}` ); return response.data.symbols; } async getSymbol(symbolId: number): Promise<Symbol[]> { const response: AxiosResponse<{ symbols: Symbol[] }> = await this.http.get( `/v1/symbols/${symbolId}` ); return response.data.symbols; } async getOrders(accountNumber: string, startTime?: string, endTime?: string, stateFilter?: string): Promise<Order[]> { let url = `/v1/accounts/${accountNumber}/orders`; const params = new URLSearchParams(); if (startTime) params.append('startTime', startTime); if (endTime) params.append('endTime', endTime); if (stateFilter) params.append('stateFilter', stateFilter); if (params.toString()) { url += `?${params.toString()}`; } const response: AxiosResponse<{ orders: Order[] }> = await this.http.get(url); return response.data.orders; } async getCandles(symbolId: number, startTime: string, endTime: string, interval: string): Promise<any[]> { const response = await this.http.get( `/v1/markets/candles/${symbolId}?startTime=${startTime}&endTime=${endTime}&interval=${interval}` ); return response.data.candles; } }