import axios, { AxiosInstance, AxiosError } from "axios"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; import { CredentialManager, UpworkTokens } from "./credential-manager.js"; export class UpworkClient { private static instance: UpworkClient; private client: AxiosInstance; private graphqlEndpoint = "https://api.upwork.com/graphql"; private secretsClient: SecretsManagerClient; private credentialManager: CredentialManager; private currentAccountType: 'freelancer' | 'agency' = 'freelancer'; constructor() { // Initialize AWS Secrets Manager client this.secretsClient = new SecretsManagerClient({ region: "us-east-1" }); // Initialize credential manager for local caching this.credentialManager = new CredentialManager(); // Initialize axios client for general requests this.client = axios.create({ baseURL: "https://www.upwork.com/api", timeout: 30000, headers: { "Content-Type": "application/json", "User-Agent": "ChillMCP-Upwork/2.4.0" }, }); } // Set which account to use (freelancer or agency) public setAccount(account: "freelancer" | "agency") { this.currentAccountType = account; } // Get tokens from AWS Secrets Manager (bypass local cache) private async getTokensFromAWS(): Promise { try { const secretId = this.currentAccountType === "agency" ? "upwork-oauth-tokens-agency" : "upwork-oauth-tokens"; const command = new GetSecretValueCommand({ SecretId: secretId }); const response = await this.secretsClient.send(command); if (!response.SecretString) { throw new Error("No secret string found in AWS Secrets Manager"); } const tokens = JSON.parse(response.SecretString) as UpworkTokens; // Cache the fresh tokens locally for future use await this.credentialManager.saveTokens(tokens, this.currentAccountType); console.log(`[UpworkClient] Fresh tokens retrieved from AWS and cached locally for ${this.currentAccountType}`); return tokens; } catch (error) { throw new Error(`Failed to get tokens from AWS Secrets Manager: ${error instanceof Error ? error.message : String(error)}`); } } // Get tokens with smart caching: try local cache first, fallback to AWS private async getValidTokens(): Promise { // Try to load from local cache first (keychain/file) let tokens = await this.credentialManager.loadTokens(this.currentAccountType); if (tokens) { console.log(`[UpworkClient] Using cached tokens for ${this.currentAccountType}`); return tokens; } // No cached tokens, get fresh from AWS console.log(`[UpworkClient] No cached tokens found, fetching from AWS for ${this.currentAccountType}`); return await this.getTokensFromAWS(); } // Force refresh tokens from AWS (for the refresh tool) public async forceRefreshTokens(accountType: 'freelancer' | 'agency' = 'freelancer'): Promise { try { const originalAccountType = this.currentAccountType; this.currentAccountType = accountType; // Clear local cache first await this.credentialManager.clearTokens(accountType); // Get fresh tokens from AWS await this.getTokensFromAWS(); // Restore original account type this.currentAccountType = originalAccountType; return true; } catch (error) { console.error(`[UpworkClient] Failed to force refresh tokens for ${accountType}:`, error); return false; } } // Get age of cached tokens (for diagnostics) public async getTokenAge(accountType: 'freelancer' | 'agency' = 'freelancer'): Promise { return await this.credentialManager.getTokenAge(accountType); } // GraphQL query method with smart caching and error handling public async graphqlQuery(query: string, variables?: any): Promise { try { const tokens = await this.getValidTokens(); const response = await axios.post( this.graphqlEndpoint, { query, variables }, { headers: { "Authorization": `Bearer ${tokens.access_token}`, "Content-Type": "application/json", "User-Agent": "ChillMCP-Upwork/2.4.0" } } ); if (response.data.errors) { throw new Error(`GraphQL errors: ${JSON.stringify(response.data.errors)}`); } return response.data; } catch (error) { throw this.handleError(error); } } public getClient(): AxiosInstance { return this.client; } public static getInstance(): UpworkClient { if (!UpworkClient.instance) { UpworkClient.instance = new UpworkClient(); } return UpworkClient.instance; } // Legacy REST methods (for backwards compatibility) public async get(endpoint: string, params?: any): Promise { try { const tokens = await this.getValidTokens(); const response = await this.client.get(endpoint, { params, headers: { "Authorization": `Bearer ${tokens.access_token}` } }); return response.data; } catch (error) { throw this.handleError(error); } } public async post(endpoint: string, data?: any): Promise { try { const tokens = await this.getValidTokens(); const response = await this.client.post(endpoint, data, { headers: { "Authorization": `Bearer ${tokens.access_token}` } }); return response.data; } catch (error) { throw this.handleError(error); } } public handleError(error: any): McpError { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; if (axiosError.response) { const status = axiosError.response.status; const data = axiosError.response.data as any; switch (status) { case 401: return new McpError( ErrorCode.InvalidRequest, "❌ Authentication failed - token may have expired. Use 'upwork_refresh_token' tool to get fresh tokens, then retry your request." ); case 403: return new McpError( ErrorCode.InvalidRequest, "Access forbidden. Please check your API permissions." ); case 404: return new McpError( ErrorCode.InvalidRequest, "Resource not found." ); case 429: return new McpError( ErrorCode.InvalidRequest, "Rate limit exceeded. Please try again later." ); default: return new McpError( ErrorCode.InternalError, `Upwork API error: ${data?.message || axiosError.message}` ); } } return new McpError( ErrorCode.InternalError, `Network error: ${axiosError.message}` ); } return new McpError( ErrorCode.InternalError, `Unexpected error: ${error instanceof Error ? error.message : String(error)}` ); } }