diff --git a/src/clients/base-client.ts b/src/clients/base-client.ts new file mode 100644 index 0000000..f176bc9 --- /dev/null +++ b/src/clients/base-client.ts @@ -0,0 +1,77 @@ +// 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'; + } +}