Files
bitbucket-mcp/src/clients/base-client.ts

103 lines
3.5 KiB
TypeScript

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<void> | null = null;
constructor(options: ClientOptions = {}) {
this._initPromise = this._init(options);
}
private async _init(options: ClientOptions): Promise<void> {
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<void> {
if (this._initialized) return;
await this._initPromise;
}
private async _handleResponseError(error: AxiosError): Promise<never> {
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<void> {
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';
}
}