// src/clients/base-client.ts import axios, { AxiosInstance, AxiosError } from 'axios'; import { TokenConfigLoader, TokenConfig } from '../config.js'; export interface ClientOptions { timeout?: number; } export class BaseClient { protected axiosInstance!: AxiosInstance; private tokenSource: string | null = null; private _initialized = false; private _initPromise: Promise | null = null; constructor(options: ClientOptions = {}) { this._initPromise = this._init(options); } private async _init(options: ClientOptions): Promise { try { const config = await TokenConfigLoader.load(); this.tokenSource = config.source; this.axiosInstance = axios.create({ baseURL: 'https://api.bitbucket.org/2.0', headers: { 'Content-Type': 'application/json' }, auth: { username: config.email, password: config.token }, timeout: options.timeout || 30000, maxRedirects: 5, }); this.axiosInstance.interceptors.request.use(cfg => { cfg.headers['User-Agent'] = 'Bitbucket-MCP-Server/1.0.0'; return cfg; }); this.axiosInstance.interceptors.response.use( r => r, (error: AxiosError) => this._handleResponseError(error), ); this._initialized = true; } catch (error) { throw new Error(`Unable to initialize Bitbucket client: ${error}`); } } protected async ensureInitialized(): Promise { if (this._initialized) return; await this._initPromise; } private async _handleResponseError(error: AxiosError): Promise { const status = error.response?.status; if (status === 401) { console.error('🔐 Bitbucket API Authentication Error (401)'); console.error(` Token source: ${this.tokenSource || 'unknown'}`); console.error(` URL: ${error.config?.url}`); } else if (status === 403) { console.error('🚫 Bitbucket API Forbidden (403)'); console.error(` URL: ${error.config?.url}`); } else if (status === 429) { const retryAfter = error.response?.headers['retry-after']; const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000; console.log(`Rate limited. Retrying in ${delay}ms...`); await this._sleep(delay); } throw error; } protected _sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } protected formatError(error: any): string { if (error?.response?.status) { return `HTTP ${error.response.status}: ${error.message}`; } return error?.message || 'Unknown error'; } }