import axios, { AxiosInstance, AxiosError } from 'axios'; import { TokenConfigLoader } 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/${this._getVersion()}`; 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}`); console.error(` Method: ${error.config?.method?.toUpperCase()}`); const data = error.response?.data; if (typeof data === 'object' && data !== null) { console.error(' Response:', JSON.stringify(data, null, 2)); } else if (data) { console.error(` Response: ${data}`); } } else if (status === 403) { console.error('Bitbucket API Forbidden (403)'); console.error(` Token source: ${this.tokenSource || 'unknown'}`); console.error(` URL: ${error.config?.url}`); } else if (status === 429) { console.warn(`Rate limited (429). URL: ${error.config?.url}`); } throw error; } private _sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } private _getVersion(): string { try { // eslint-disable-next-line @typescript-eslint/no-require-imports const fs = require('fs'); // eslint-disable-next-line @typescript-eslint/no-require-imports const path = require('path'); const pkgPath = path.join(process.cwd(), 'package.json'); if (fs.existsSync(pkgPath)) { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); return pkg.version || '1.0.0'; } return '1.0.0'; } catch { return '1.0.0'; } } protected formatError(error: unknown): string { if (error && typeof error === 'object' && 'response' in error) { const axiosError = error as { response?: { status?: number }; message?: string }; if (axiosError.response?.status) { return `HTTP ${axiosError.response.status}: ${axiosError.message || 'Unknown error'}`; } } if (error instanceof Error) return error.message; return String(error) || 'Unknown error'; } }