Add code
This commit is contained in:
612
src/bitbucket-client.ts
Normal file
612
src/bitbucket-client.ts
Normal file
@@ -0,0 +1,612 @@
|
||||
/**
|
||||
* Bitbucket API client with token configuration integration.
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||
import { TokenConfigLoader, TokenConfig } from './config.js';
|
||||
|
||||
export interface BitbucketClientOptions {
|
||||
timeout?: number;
|
||||
retryCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitbucket API client wrapper with authentication and error handling.
|
||||
*/
|
||||
export class BitbucketClient {
|
||||
private client!: AxiosInstance;
|
||||
private tokenSource: string | null = null;
|
||||
private initialized: boolean = false;
|
||||
|
||||
constructor(options: BitbucketClientOptions = {}) {
|
||||
// Initialize synchronously to avoid race conditions
|
||||
this.initializeClient(options);
|
||||
}
|
||||
|
||||
private async initializeClient(options: BitbucketClientOptions): Promise<void> {
|
||||
try {
|
||||
const config = await this.loadTokenConfig();
|
||||
this.tokenSource = config.source;
|
||||
|
||||
this.client = 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
|
||||
});
|
||||
|
||||
// Request interceptor for token refresh and logging
|
||||
this.client.interceptors.request.use(config => {
|
||||
config.headers['User-Agent'] = `Bitbucket-MCP-Server/${this.getVersion()}`;
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response interceptor for rate limiting and errors
|
||||
this.client.interceptors.response.use(
|
||||
response => response,
|
||||
async (error: AxiosError) => {
|
||||
await this.handleResponseError(error);
|
||||
}
|
||||
);
|
||||
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to load token configuration:', error);
|
||||
throw new Error('Unable to initialize Bitbucket client - check token configuration');
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
// Wait for initialization to complete
|
||||
await new Promise(resolve => {
|
||||
const checkInit = () => {
|
||||
if (this.initialized) {
|
||||
resolve(void 0);
|
||||
} else {
|
||||
setTimeout(checkInit, 10);
|
||||
}
|
||||
};
|
||||
checkInit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async loadTokenConfig(): Promise<TokenConfig> {
|
||||
return TokenConfigLoader.load();
|
||||
}
|
||||
|
||||
private getVersion(): string {
|
||||
try {
|
||||
// Use synchronous fs for version lookup - no async needed
|
||||
const fs = require('fs');
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
private async handleResponseError(error: AxiosError): Promise<void> {
|
||||
const status = error.response?.status;
|
||||
|
||||
if (status === 401) {
|
||||
// Authentication error - provide detailed logging
|
||||
const data = error.response?.data;
|
||||
console.error('🔐 Bitbucket API Authentication Error (401):');
|
||||
console.error(` Token source: ${this.tokenSource || 'unknown'}`);
|
||||
console.error(` Request URL: ${error.config?.url}`);
|
||||
console.error(` Request method: ${error.config?.method?.toUpperCase()}`);
|
||||
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
console.error(' Response data:', JSON.stringify(data, null, 2));
|
||||
} else if (data) {
|
||||
console.error(` Response data: ${data}`);
|
||||
}
|
||||
|
||||
// Check if token might be expired or malformed
|
||||
const authHeader = error.config?.headers?.Authorization;
|
||||
if (typeof authHeader === 'string') {
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
const tokenLength = token.length;
|
||||
const isJWT = token.includes('.');
|
||||
console.error(` Token info: length=${tokenLength}, appears to be JWT=${isJWT}`);
|
||||
}
|
||||
|
||||
console.error(' Possible causes:');
|
||||
console.error(' - Token expired');
|
||||
console.error(' - Token lacks required permissions (needs repository read access)');
|
||||
console.error(' - Token is malformed or corrupted');
|
||||
console.error(' - Repository/workspace access denied');
|
||||
} else if (status === 403) {
|
||||
// Forbidden - similar to auth but different permissions
|
||||
console.error('🚫 Bitbucket API Forbidden Error (403):');
|
||||
console.error(` Token source: ${this.tokenSource || 'unknown'}`);
|
||||
console.error(` Request URL: ${error.config?.url}`);
|
||||
console.error(' Possible causes:');
|
||||
console.error(' - Token lacks permission to access this repository');
|
||||
console.error(' - Repository is private and token has insufficient scope');
|
||||
} else if (status === 429) {
|
||||
// Rate limited - wait and retry
|
||||
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);
|
||||
} else if (status && status >= 400 && status < 500) {
|
||||
// Other client error - log but don't retry
|
||||
const data = error.response?.data;
|
||||
let message: string;
|
||||
if (typeof data === 'object' && data !== null && 'message' in data) {
|
||||
message = String(data.message);
|
||||
} else {
|
||||
message = `Client error ${status}`;
|
||||
}
|
||||
console.error(`Bitbucket API ${status}: ${message}`);
|
||||
} else if (status && status >= 500) {
|
||||
// Server error - could implement exponential backoff retry
|
||||
console.warn(`Server error ${status}, will retry...`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the authentication token by making a test API call.
|
||||
*/
|
||||
async validateToken(): Promise<{ valid: boolean; message: string }> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
// Make a simple API call to test authentication
|
||||
const response = await this.client.get('/user');
|
||||
return { valid: true, message: 'Token is valid' };
|
||||
} catch (error: any) {
|
||||
const status = error.response?.status;
|
||||
if (status === 401) {
|
||||
return { valid: false, message: 'Token is invalid or expired' };
|
||||
} else if (status === 403) {
|
||||
return { valid: false, message: 'Token lacks required permissions' };
|
||||
} else {
|
||||
return { valid: false, message: `Token validation failed: ${this.formatError(error)}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List pull requests in a repository.
|
||||
*/
|
||||
async listPullRequests(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
options?: {
|
||||
state?: 'open' | 'closed' | 'all';
|
||||
author?: string;
|
||||
reviewer?: string;
|
||||
since?: string;
|
||||
}
|
||||
): Promise<any[]> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const params: Record<string, any> = {};
|
||||
|
||||
if (options?.state) params.state = options.state;
|
||||
if (options?.author) params.author = options.author;
|
||||
if (options?.reviewer) params.reviewer = options.reviewer;
|
||||
if (options?.since) params.since = options.since;
|
||||
|
||||
try {
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests`,
|
||||
{ params }
|
||||
);
|
||||
|
||||
return response.data.values || [];
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list pull requests: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific pull request.
|
||||
*/
|
||||
async getPullRequest(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request activities (events/actions on the PR).
|
||||
*/
|
||||
async getPullRequestActivities(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number,
|
||||
options?: {
|
||||
limit?: number;
|
||||
start?: number;
|
||||
}
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
if (options?.limit) params.limit = options.limit;
|
||||
if (options?.start) params.start = options.start;
|
||||
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/activity`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request activities: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request changes (files modified in the PR).
|
||||
*/
|
||||
async getPullRequestChanges(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number,
|
||||
options?: {
|
||||
limit?: number;
|
||||
start?: number;
|
||||
}
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
if (options?.limit) params.limit = options.limit;
|
||||
if (options?.start) params.start = options.start;
|
||||
|
||||
const prResponse = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`
|
||||
);
|
||||
const sourceHash = prResponse.data.source?.commit?.hash;
|
||||
const destHash = prResponse.data.destination?.commit?.hash;
|
||||
|
||||
if (sourceHash && destHash) {
|
||||
const changesPath = `/repositories/${workspace}/${repoSlug}/diffstat/${sourceHash}..${destHash}`;
|
||||
const response = await this.client.get(changesPath, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
throw new Error('Could not determine source and destination commits for PR');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request changes: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request comments.
|
||||
*/
|
||||
async getPullRequestComments(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number,
|
||||
options?: {
|
||||
limit?: number;
|
||||
start?: number;
|
||||
}
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
if (options?.limit) params.limit = options.limit;
|
||||
if (options?.start) params.start = options.start;
|
||||
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request comments: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific pull request comment.
|
||||
*/
|
||||
async getPullRequestComment(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number,
|
||||
commentId: number
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments/${commentId}`
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request comment: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commits included in a pull request.
|
||||
*/
|
||||
async getPullRequestCommits(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number,
|
||||
options?: {
|
||||
limit?: number;
|
||||
start?: number;
|
||||
}
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
if (options?.limit) params.limit = options.limit;
|
||||
if (options?.start) params.start = options.start;
|
||||
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/commits`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request commits: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diff for a pull request.
|
||||
*/
|
||||
async getPullRequestDiff(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number,
|
||||
options?: {
|
||||
context?: number;
|
||||
path?: string;
|
||||
whitespace?: 'ignore-all' | 'ignore-changing' | 'ignore-eol' | 'show-all';
|
||||
}
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
if (options?.context) params.context = options.context;
|
||||
if (options?.whitespace) params.whitespace = options.whitespace;
|
||||
|
||||
if (options?.path) {
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/diff/${options.path}`,
|
||||
{ params, responseType: 'text' }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
const prResponse = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`
|
||||
);
|
||||
const sourceHash = prResponse.data.source?.commit?.hash;
|
||||
const destHash = prResponse.data.destination?.commit?.hash;
|
||||
|
||||
if (sourceHash && destHash) {
|
||||
const diffPath = `/repositories/${workspace}/${repoSlug}/diff/${sourceHash}..${destHash}`;
|
||||
const response = await this.client.get(diffPath, { params, responseType: 'text' });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
throw new Error('Could not determine source and destination commits for PR');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request diff: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw patch for a pull request.
|
||||
*/
|
||||
async getPullRequestPatch(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const prResponse = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`
|
||||
);
|
||||
const sourceHash = prResponse.data.source?.commit?.hash;
|
||||
const destHash = prResponse.data.destination?.commit?.hash;
|
||||
|
||||
if (sourceHash && destHash) {
|
||||
const patchPath = `/repositories/${workspace}/${repoSlug}/diff/${sourceHash}..${destHash}`;
|
||||
const response = await this.client.get(patchPath, { responseType: 'text' });
|
||||
return { patch: response.data };
|
||||
}
|
||||
|
||||
throw new Error('Could not determine source and destination commits for PR');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request patch: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get participants of a pull request.
|
||||
*/
|
||||
async getPullRequestParticipants(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`
|
||||
);
|
||||
return response.data.participants || [];
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request participants: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reviewers of a pull request.
|
||||
*/
|
||||
async getPullRequestReviewers(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`
|
||||
);
|
||||
return response.data.reviewers || [];
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request reviewers: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status of a pull request (merged, open, declined).
|
||||
*/
|
||||
async getPullRequestStatus(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`
|
||||
);
|
||||
const pr = response.data;
|
||||
return {
|
||||
id: pr.id,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
status: pr.status,
|
||||
author: pr.author,
|
||||
source_branch: pr.source?.branch?.name,
|
||||
destination_branch: pr.destination?.branch?.name,
|
||||
created_on: pr.created_on,
|
||||
updated_on: pr.updated_on,
|
||||
closed_on: pr.closed_on,
|
||||
merge_commit: pr.merge_commit
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request status: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks associated with a pull request.
|
||||
*/
|
||||
async getPullRequestTasks(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/tasks`
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request tasks: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task count for a pull request.
|
||||
*/
|
||||
async getPullRequestTaskCount(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/tasks`
|
||||
);
|
||||
const tasks = response.data;
|
||||
return { count: tasks.values?.length || 0 };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get pull request task count: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full raw pull request with all details.
|
||||
*/
|
||||
async getFullPullRequest(
|
||||
workspace: string,
|
||||
repoSlug: string,
|
||||
prId: number
|
||||
): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const response = await this.client.get(
|
||||
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`,
|
||||
{
|
||||
params: {
|
||||
fields: '+*'
|
||||
}
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get full pull request: ${this.formatError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private formatError(error: any): string {
|
||||
if (error?.response?.status) {
|
||||
return `HTTP ${error.response.status}: ${error.message}`;
|
||||
}
|
||||
return error?.message || 'Unknown error';
|
||||
}
|
||||
}
|
||||
|
||||
export default BitbucketClient;
|
||||
175
src/config.ts
Normal file
175
src/config.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Configuration with priority order:
|
||||
* 1. Environment variables
|
||||
* 2. .env file in project root
|
||||
* 3. Interactive prompt (development fallback)
|
||||
*/
|
||||
|
||||
export interface TokenConfig {
|
||||
email: string;
|
||||
token: string;
|
||||
source: 'env' | 'dotenv' | 'prompt';
|
||||
}
|
||||
|
||||
export interface DefaultConfig {
|
||||
workspace: string | null;
|
||||
repo: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token configuration loader with priority order.
|
||||
* Loads from environment variable, .env file, or prompts user.
|
||||
*/
|
||||
export class TokenConfigLoader {
|
||||
private static readonly ENV_FILE = '.env';
|
||||
private static readonly EMAIL_ENV = 'BITBUCKET_MCP_EMAIL';
|
||||
private static readonly TOKEN_ENV = 'BITBUCKET_MCP_TOKEN';
|
||||
|
||||
/**
|
||||
* Load configuration from configured sources with priority order.
|
||||
* @returns TokenConfig with loaded email, token and source
|
||||
*/
|
||||
public static async load(): Promise<TokenConfig> {
|
||||
// Priority 1: Environment variables (highest precedence)
|
||||
const envEmail = process.env[this.EMAIL_ENV];
|
||||
const envToken = process.env[this.TOKEN_ENV];
|
||||
if (envEmail && envToken && this.isValidToken(envToken)) {
|
||||
return { email: envEmail, token: envToken, source: 'env' };
|
||||
}
|
||||
|
||||
// Debug: Log environment check result
|
||||
console.debug(`[DEBUG] Env var check - email present: ${!!envEmail}, token present: ${!!envToken}, valid: ${!!(envEmail && envToken && this.isValidToken(envToken))}`);
|
||||
|
||||
// Priority 2: .env file in project root
|
||||
const { email: dotenvEmail, token: dotenvToken } = this.loadFromDotEnv();
|
||||
console.debug(`[DEBUG] Dotenv config loaded - email: ${!!dotenvEmail}, token: ${!!dotenvToken}`);
|
||||
if (dotenvEmail && dotenvToken && this.isValidToken(dotenvToken)) {
|
||||
return { email: dotenvEmail, token: dotenvToken, source: 'dotenv' };
|
||||
}
|
||||
|
||||
// Debug: Log dotenv check result
|
||||
console.debug(`[DEBUG] Dotenv check - email present: ${!!dotenvEmail}, token present: ${!!dotenvToken}, valid: ${!!(dotenvEmail && dotenvToken && this.isValidToken(dotenvToken))}`);
|
||||
|
||||
// Priority 3: Interactive prompt (fallback)
|
||||
console.warn(
|
||||
'⚠️ No Bitbucket credentials found. Please set one of:'
|
||||
);
|
||||
console.warn(' 1. Export BITBUCKET_MCP_EMAIL=<your_email> and BITBUCKET_MCP_TOKEN=<your_token>');
|
||||
console.warn(' 2. Add to .env: BITBUCKET_MCP_EMAIL=<your_email> and BITBUCKET_MCP_TOKEN=<your_token>');
|
||||
|
||||
return this.promptForCredentials().then(credentials => {
|
||||
if (credentials.email && this.isValidToken(credentials.token)) {
|
||||
return { ...credentials, source: 'prompt' };
|
||||
}
|
||||
throw new Error(
|
||||
'Invalid or missing Bitbucket credentials. Aborting.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load email and token from .env file in project root directory.
|
||||
*/
|
||||
private static loadFromDotEnv(): { email: string | null; token: string | null } {
|
||||
console.debug('[DEBUG] Attempting to load credentials from .env file');
|
||||
|
||||
try {
|
||||
// Resolve to project root (look for package.json)
|
||||
let currentDir = process.cwd();
|
||||
while (currentDir !== '/') {
|
||||
const pkgPath = path.join(currentDir, 'package.json');
|
||||
if (fs.existsSync(pkgPath)) break;
|
||||
currentDir = path.dirname(currentDir);
|
||||
}
|
||||
|
||||
const envPath = path.join(currentDir, this.ENV_FILE);
|
||||
if (!fs.existsSync(envPath)) return { email: null, token: null };
|
||||
|
||||
const envContent = fs.readFileSync(envPath, 'utf-8');
|
||||
|
||||
const emailMatch = envContent.match(
|
||||
new RegExp(`${this.EMAIL_ENV}\\s*=\\s*([^\\r\\n]+)`, 'i')
|
||||
);
|
||||
const tokenMatch = envContent.match(
|
||||
new RegExp(`${this.TOKEN_ENV}\\s*=\\s*([^\\r\\n]+)`, 'i')
|
||||
);
|
||||
|
||||
return {
|
||||
email: emailMatch ? emailMatch[1].trim() : null,
|
||||
token: tokenMatch ? tokenMatch[1].trim() : null
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to load .env:', error);
|
||||
return { email: null, token: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Bitbucket access token format.
|
||||
*/
|
||||
private static isValidToken(token: string): boolean {
|
||||
if (!token || token.length < 8) return false;
|
||||
const validPattern = /^[A-Za-z0-9=._\-+/]+$/;
|
||||
return validPattern.test(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user for credentials (development fallback).
|
||||
*/
|
||||
private static async promptForCredentials(): Promise<{ email: string; token: string }> {
|
||||
// Skip prompting if running in non-Node.js environment
|
||||
if (typeof require === 'undefined') {
|
||||
return { email: '', token: '' };
|
||||
}
|
||||
|
||||
const readline = await import('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const askQuestion = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question(
|
||||
'Enter your Bitbucket email (or press Enter to skip): ',
|
||||
async (email: string) => {
|
||||
const token = await askQuestion('Enter your Bitbucket access token (or press Enter to skip): ');
|
||||
rl.close();
|
||||
resolve({ email: email || '', token: token || '' });
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultConfigLoader {
|
||||
private static readonly WORKSPACE_ENV = 'DEFAULT_WORKSPACE';
|
||||
private static readonly REPO_ENV = 'DEFAULT_REPO';
|
||||
|
||||
public static load(): DefaultConfig {
|
||||
return {
|
||||
workspace: process.env[this.WORKSPACE_ENV] || null,
|
||||
repo: process.env[this.REPO_ENV] || null
|
||||
};
|
||||
}
|
||||
|
||||
public static getWorkspace(): string | null {
|
||||
return process.env[this.WORKSPACE_ENV] || null;
|
||||
}
|
||||
|
||||
public static getRepo(): string | null {
|
||||
return process.env[this.REPO_ENV] || null;
|
||||
}
|
||||
}
|
||||
|
||||
export default TokenConfigLoader;
|
||||
347
src/index.ts
Normal file
347
src/index.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Bitbucket Pull Request MCP Server
|
||||
*
|
||||
* Configuration:
|
||||
* 1. Export BITBUCKET_MCP_TOKEN=<token> (environment variable)
|
||||
* 2. Add to .env file in project root
|
||||
* 3. Interactive prompt (development fallback)
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
ListToolsRequestSchema,
|
||||
CallToolRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
import { BitbucketRouter } from './router.js';
|
||||
|
||||
const router = new BitbucketRouter();
|
||||
|
||||
/**
|
||||
* MCP Server implementation for Bitbucket Pull Requests.
|
||||
*/
|
||||
class BitbucketMCPServer {
|
||||
private server: Server;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server({
|
||||
name: 'bitbucket-pullrequests',
|
||||
version: '1.0.0',
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
});
|
||||
|
||||
// Handle requests
|
||||
this.setupRequestHandlers();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setup MCP request handlers.
|
||||
*/
|
||||
private setupRequestHandlers(): void {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: 'validate_token',
|
||||
description: 'Validate the Bitbucket authentication token',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_pull_requests',
|
||||
description: 'List pull requests in a Bitbucket repository',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
state: { type: 'string', enum: ['open', 'closed', 'all'] },
|
||||
author: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request',
|
||||
description: 'Get details of a specific pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_activities',
|
||||
description: 'Get activities/events for a pull request (opens, closes, comments, reviews, etc.)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
limit: { type: 'integer', description: 'Max results per page (default 25)' },
|
||||
start: { type: 'integer', description: 'Pagination start index' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_changes',
|
||||
description: 'Get files changed in a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
limit: { type: 'integer', description: 'Max results per page (default 25)' },
|
||||
start: { type: 'integer', description: 'Pagination start index' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_comments',
|
||||
description: 'Get all comments on a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
limit: { type: 'integer', description: 'Max results per page (default 25)' },
|
||||
start: { type: 'integer', description: 'Pagination start index' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_comment',
|
||||
description: 'Get a specific comment on a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
commentId: { type: 'integer', description: 'The comment ID' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId', 'commentId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_commits',
|
||||
description: 'Get commits included in a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
limit: { type: 'integer', description: 'Max results per page (default 25)' },
|
||||
start: { type: 'integer', description: 'Pagination start index' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_diff',
|
||||
description: 'Get the diff of a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
path: { type: 'string', description: 'Specific file path to get diff for' },
|
||||
context: { type: 'integer', description: 'Number of context lines around changes' },
|
||||
whitespace: { type: 'string', enum: ['ignore-all', 'ignore-changing', 'ignore-eol', 'show-all'] },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_patch',
|
||||
description: 'Get the raw patch file for a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_participants',
|
||||
description: 'Get participants of a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_reviewers',
|
||||
description: 'Get reviewers of a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_status',
|
||||
description: 'Get status/state of a pull request (open, merged, declined)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_tasks',
|
||||
description: 'Get tasks associated with a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_task_count',
|
||||
description: 'Get count of tasks on a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_full_pull_request',
|
||||
description: 'Get full detailed information about a pull request with all fields expanded',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: { type: 'string' },
|
||||
repository: { type: 'string' },
|
||||
pullRequestId: { type: 'integer' },
|
||||
},
|
||||
required: ['workspace', 'repository', 'pullRequestId'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: params } = request.params;
|
||||
|
||||
try {
|
||||
console.log(`Executing tool: ${name}`);
|
||||
console.log('Parameters:', JSON.stringify(params));
|
||||
|
||||
const result = await router.executeTool(name, params as any);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Tool execution failed');
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result.data, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Tool error (${name}):`, error);
|
||||
|
||||
throw {
|
||||
name: 'ToolExecutionError',
|
||||
message: typeof error === 'string' ? error : (error as Error).message || 'Unknown error',
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the MCP server.
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
|
||||
console.log('✅ Bitbucket MCP Server started');
|
||||
console.log(' Name: bitbucket-pullrequests v1.0.0');
|
||||
|
||||
// Log token source for debugging
|
||||
if (process.env.BITBUCKET_MCP_TOKEN) {
|
||||
console.log(' Token source: environment variable');
|
||||
} else {
|
||||
console.log(' Token source: .env file or interactive prompt');
|
||||
}
|
||||
|
||||
// Validate token on startup
|
||||
try {
|
||||
console.log('🔍 Validating Bitbucket token...');
|
||||
const router = new BitbucketRouter();
|
||||
const validation = await router.executeTool('validate_token', {});
|
||||
if (validation.success) {
|
||||
console.log('✅ Token validation successful');
|
||||
} else {
|
||||
console.error('❌ Token validation failed:', validation.error);
|
||||
console.error(' Please check your BITBUCKET_MCP_TOKEN configuration');
|
||||
console.error(' Required permissions: Repository read access');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Token validation error:', error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Server startup failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point
|
||||
const server = new BitbucketMCPServer();
|
||||
server.start().catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
489
src/router.ts
Normal file
489
src/router.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
/**
|
||||
* MCP tool router that maps tool calls to Bitbucket API operations.
|
||||
*/
|
||||
|
||||
import { BitbucketClient } from './bitbucket-client.js';
|
||||
import { DefaultConfigLoader } from './config.js';
|
||||
|
||||
interface ToolCallParams {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ToolResult {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Router that maps MCP tool calls to Bitbucket API operations.
|
||||
*/
|
||||
export class BitbucketRouter {
|
||||
private client: BitbucketClient;
|
||||
|
||||
constructor() {
|
||||
this.client = new BitbucketClient();
|
||||
}
|
||||
|
||||
private getDefaultParams(params: ToolCallParams): { workspace: string | undefined; repoSlug: string | undefined } {
|
||||
const defaults = DefaultConfigLoader.load();
|
||||
return {
|
||||
workspace: params.workspace || defaults.workspace || undefined,
|
||||
repoSlug: params.repository || params.repo || defaults.repo || undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the Bitbucket token.
|
||||
*/
|
||||
private async validateToken(): Promise<ToolResult> {
|
||||
try {
|
||||
const result = await this.client.validateToken();
|
||||
return {
|
||||
success: result.valid,
|
||||
data: result
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Token validation failed: ${String(error)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tool call and return MCP-compatible result.
|
||||
*/
|
||||
async executeTool(
|
||||
toolName: string,
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
switch (toolName) {
|
||||
case 'validate_token':
|
||||
return this.validateToken();
|
||||
|
||||
case 'list_pull_requests':
|
||||
return this.listPullRequests(params);
|
||||
|
||||
case 'get_pull_request':
|
||||
return this.getPullRequest(params);
|
||||
|
||||
case 'get_pull_request_activities':
|
||||
return this.getPullRequestActivities(params);
|
||||
|
||||
case 'get_pull_request_changes':
|
||||
return this.getPullRequestChanges(params);
|
||||
|
||||
case 'get_pull_request_comments':
|
||||
return this.getPullRequestComments(params);
|
||||
|
||||
case 'get_pull_request_comment':
|
||||
return this.getPullRequestComment(params);
|
||||
|
||||
case 'get_pull_request_commits':
|
||||
return this.getPullRequestCommits(params);
|
||||
|
||||
case 'get_pull_request_diff':
|
||||
return this.getPullRequestDiff(params);
|
||||
|
||||
case 'get_pull_request_patch':
|
||||
return this.getPullRequestPatch(params);
|
||||
|
||||
case 'get_pull_request_participants':
|
||||
return this.getPullRequestParticipants(params);
|
||||
|
||||
case 'get_pull_request_reviewers':
|
||||
return this.getPullRequestReviewers(params);
|
||||
|
||||
case 'get_pull_request_status':
|
||||
return this.getPullRequestStatus(params);
|
||||
|
||||
case 'get_pull_request_tasks':
|
||||
return this.getPullRequestTasks(params);
|
||||
|
||||
case 'get_pull_request_task_count':
|
||||
return this.getPullRequestTaskCount(params);
|
||||
|
||||
case 'get_full_pull_request':
|
||||
return this.getFullPullRequest(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
error: `Unknown tool: ${toolName}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List pull requests in a repository.
|
||||
*/
|
||||
private async listPullRequests(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
|
||||
if (!workspace || !repoSlug) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Missing required parameters: workspace and repository'
|
||||
};
|
||||
}
|
||||
|
||||
// Default to open PRs if state not specified
|
||||
const options = params.state ? { state: params.state } : undefined;
|
||||
|
||||
const prs = await this.client.listPullRequests(workspace, repoSlug, options);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
pull_requests: prs,
|
||||
count: prs.length
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `List pull requests failed: ${typeof error === 'string' ? error : String(error)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific pull request.
|
||||
*/
|
||||
private async getPullRequest(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Missing required parameters'
|
||||
};
|
||||
}
|
||||
|
||||
const pr = await this.client.getPullRequest(workspace, repoSlug, prId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: pr
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Get pull request failed: ${typeof error === 'string' ? error : String(error)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request activities (events/actions).
|
||||
*/
|
||||
private async getPullRequestActivities(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestActivities(workspace, repoSlug, prId, {
|
||||
limit: params.limit,
|
||||
start: params.start
|
||||
});
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get activities failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request changes (files modified).
|
||||
*/
|
||||
private async getPullRequestChanges(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestChanges(workspace, repoSlug, prId, {
|
||||
limit: params.limit,
|
||||
start: params.start
|
||||
});
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get changes failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request comments.
|
||||
*/
|
||||
private async getPullRequestComments(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestComments(workspace, repoSlug, prId, {
|
||||
limit: params.limit,
|
||||
start: params.start
|
||||
});
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get comments failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific pull request comment.
|
||||
*/
|
||||
private async getPullRequestComment(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
const commentId = parseInt(params.commentId || params.comment_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId) || isNaN(commentId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestComment(workspace, repoSlug, prId, commentId);
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get comment failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commits in a pull request.
|
||||
*/
|
||||
private async getPullRequestCommits(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestCommits(workspace, repoSlug, prId, {
|
||||
limit: params.limit,
|
||||
start: params.start
|
||||
});
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get commits failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request diff.
|
||||
*/
|
||||
private async getPullRequestDiff(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestDiff(workspace, repoSlug, prId, {
|
||||
context: params.context,
|
||||
path: params.path,
|
||||
whitespace: params.whitespace
|
||||
});
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get diff failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request patch.
|
||||
*/
|
||||
private async getPullRequestPatch(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestPatch(workspace, repoSlug, prId);
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get patch failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request participants.
|
||||
*/
|
||||
private async getPullRequestParticipants(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestParticipants(workspace, repoSlug, prId);
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get participants failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request reviewers.
|
||||
*/
|
||||
private async getPullRequestReviewers(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestReviewers(workspace, repoSlug, prId);
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get reviewers failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request status.
|
||||
*/
|
||||
private async getPullRequestStatus(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestStatus(workspace, repoSlug, prId);
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get status failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request tasks.
|
||||
*/
|
||||
private async getPullRequestTasks(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestTasks(workspace, repoSlug, prId);
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get tasks failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull request task count.
|
||||
*/
|
||||
private async getPullRequestTaskCount(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getPullRequestTaskCount(workspace, repoSlug, prId);
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get task count failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full pull request details.
|
||||
*/
|
||||
private async getFullPullRequest(
|
||||
params: ToolCallParams
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
const { workspace, repoSlug } = this.getDefaultParams(params);
|
||||
const prId = parseInt(params.pullRequestId || params.pr_id, 10);
|
||||
|
||||
if (!workspace || !repoSlug || isNaN(prId)) {
|
||||
return { success: false, error: 'Missing required parameters' };
|
||||
}
|
||||
|
||||
const result = await this.client.getFullPullRequest(workspace, repoSlug, prId);
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Get full PR failed: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BitbucketRouter;
|
||||
Reference in New Issue
Block a user