fix: Fix GET request parameter passing for all handlers
- Fixed API parameter passing for GET requests (was passing config as third parameter instead of fourth) - Branch filtering now works correctly with filterText parameter for Bitbucket Server - All GET requests with query parameters now functioning properly across all handlers - Updated CHANGELOG.md to reflect the fixes
This commit is contained in:
parent
c72336b460
commit
75c4192815
12 changed files with 1948 additions and 1828 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.0] - 2025-01-06
|
||||
|
||||
### Added
|
||||
- **Enhanced merge commit details in `get_pull_request`**:
|
||||
- Added `merge_commit_hash` field for both Cloud and Server
|
||||
- Added `merged_by` field showing who performed the merge
|
||||
- Added `merged_at` timestamp for when the merge occurred
|
||||
- Added `merge_commit_message` with the merge commit message
|
||||
- For Bitbucket Server: Fetches merge details from activities API when PR is merged
|
||||
- For Bitbucket Cloud: Extracts merge information from existing response fields
|
||||
|
||||
### Changed
|
||||
- **Major code refactoring for better maintainability**:
|
||||
- Split monolithic `index.ts` into modular architecture
|
||||
- Created separate handler classes for different tool categories:
|
||||
- `PullRequestHandlers` for PR lifecycle operations
|
||||
- `BranchHandlers` for branch management
|
||||
- `ReviewHandlers` for code review tools
|
||||
- Extracted types into dedicated files (`types/bitbucket.ts`, `types/guards.ts`)
|
||||
- Created utility modules (`utils/api-client.ts`, `utils/formatters.ts`)
|
||||
- Centralized tool definitions in `tools/definitions.ts`
|
||||
- Improved error handling and API client abstraction
|
||||
- Better separation of concerns between Cloud and Server implementations
|
||||
|
||||
### Fixed
|
||||
- Improved handling of merge commit information retrieval failures
|
||||
- Fixed API parameter passing for GET requests across all handlers (was passing config as third parameter instead of fourth)
|
||||
- Updated Bitbucket Server branch listing to use `/rest/api/latest/` endpoint with proper parameters
|
||||
- Branch filtering now works correctly with the `filterText` parameter for Bitbucket Server
|
||||
|
||||
## [0.2.0] - 2025-06-04
|
||||
|
||||
### Added
|
||||
|
|
|
@ -174,6 +174,11 @@ Returns detailed information about the pull request including:
|
|||
- Source and destination branches
|
||||
- Approval status
|
||||
- Links to web UI and diff
|
||||
- **Merge commit details** (when PR is merged):
|
||||
- `merge_commit_hash`: The hash of the merge commit
|
||||
- `merged_by`: Who performed the merge
|
||||
- `merged_at`: When the merge occurred
|
||||
- `merge_commit_message`: The merge commit message
|
||||
- And more...
|
||||
|
||||
### List Pull Requests
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@nexus2520/bitbucket-mcp-server",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"description": "MCP server for Bitbucket API integration - supports both Cloud and Server",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
|
182
src/handlers/branch-handlers.ts
Normal file
182
src/handlers/branch-handlers.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { BitbucketApiClient } from '../utils/api-client.js';
|
||||
import {
|
||||
isListBranchesArgs,
|
||||
isDeleteBranchArgs
|
||||
} from '../types/guards.js';
|
||||
|
||||
export class BranchHandlers {
|
||||
constructor(
|
||||
private apiClient: BitbucketApiClient,
|
||||
private baseUrl: string
|
||||
) {}
|
||||
|
||||
async handleListBranches(args: any) {
|
||||
if (!isListBranchesArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for list_branches'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, filter, limit = 25, start = 0 } = args;
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
let params: any = {};
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API - using latest version for better filtering support
|
||||
apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`;
|
||||
params = {
|
||||
limit,
|
||||
start,
|
||||
details: true,
|
||||
orderBy: 'MODIFICATION'
|
||||
};
|
||||
if (filter) {
|
||||
params.filterText = filter;
|
||||
}
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/refs/branches`;
|
||||
params = {
|
||||
pagelen: limit,
|
||||
page: Math.floor(start / limit) + 1,
|
||||
};
|
||||
if (filter) {
|
||||
params.q = `name ~ "${filter}"`;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
|
||||
|
||||
// Format the response
|
||||
let branches: any[] = [];
|
||||
let totalCount = 0;
|
||||
let nextPageStart = null;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server response
|
||||
branches = (response.values || []).map((branch: any) => ({
|
||||
name: branch.displayId,
|
||||
id: branch.id,
|
||||
latest_commit: branch.latestCommit,
|
||||
is_default: branch.isDefault || false
|
||||
}));
|
||||
totalCount = response.size || 0;
|
||||
if (!response.isLastPage && response.nextPageStart !== undefined) {
|
||||
nextPageStart = response.nextPageStart;
|
||||
}
|
||||
} else {
|
||||
// Bitbucket Cloud response
|
||||
branches = (response.values || []).map((branch: any) => ({
|
||||
name: branch.name,
|
||||
target: branch.target.hash,
|
||||
is_default: branch.name === 'main' || branch.name === 'master'
|
||||
}));
|
||||
totalCount = response.size || 0;
|
||||
if (response.next) {
|
||||
nextPageStart = start + limit;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
branches,
|
||||
total_count: totalCount,
|
||||
start,
|
||||
limit,
|
||||
has_more: nextPageStart !== null,
|
||||
next_start: nextPageStart
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `listing branches in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleDeleteBranch(args: any) {
|
||||
if (!isDeleteBranchArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for delete_branch'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, branch_name, force } = args;
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// First, we need to get the branch details to find the latest commit
|
||||
const branchesPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`;
|
||||
const branchesResponse = await this.apiClient.makeRequest<any>('get', branchesPath, undefined, {
|
||||
params: {
|
||||
filterText: branch_name,
|
||||
limit: 100
|
||||
}
|
||||
});
|
||||
|
||||
// Find the exact branch
|
||||
const branch = branchesResponse.values?.find((b: any) => b.displayId === branch_name);
|
||||
if (!branch) {
|
||||
throw new Error(`Branch '${branch_name}' not found`);
|
||||
}
|
||||
|
||||
// Now delete using branch-utils endpoint with correct format
|
||||
apiPath = `/rest/branch-utils/latest/projects/${workspace}/repos/${repository}/branches`;
|
||||
|
||||
try {
|
||||
await this.apiClient.makeRequest<any>('delete', apiPath, {
|
||||
name: branch_name,
|
||||
endPoint: branch.latestCommit
|
||||
});
|
||||
} catch (deleteError: any) {
|
||||
// If the error is about empty response but status is 204 (No Content), it's successful
|
||||
if (deleteError.originalError?.response?.status === 204 ||
|
||||
deleteError.message?.includes('No content to map')) {
|
||||
// Branch was deleted successfully
|
||||
} else {
|
||||
throw deleteError;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/refs/branches/${branch_name}`;
|
||||
try {
|
||||
await this.apiClient.makeRequest<any>('delete', apiPath);
|
||||
} catch (deleteError: any) {
|
||||
// If the error is about empty response but status is 204 (No Content), it's successful
|
||||
if (deleteError.originalError?.response?.status === 204 ||
|
||||
deleteError.message?.includes('No content to map')) {
|
||||
// Branch was deleted successfully
|
||||
} else {
|
||||
throw deleteError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: `Branch '${branch_name}' deleted successfully`,
|
||||
branch: branch_name,
|
||||
repository: `${workspace}/${repository}`
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `deleting branch '${branch_name}' in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
}
|
486
src/handlers/pull-request-handlers.ts
Normal file
486
src/handlers/pull-request-handlers.ts
Normal file
|
@ -0,0 +1,486 @@
|
|||
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { BitbucketApiClient } from '../utils/api-client.js';
|
||||
import { formatServerResponse, formatCloudResponse } from '../utils/formatters.js';
|
||||
import {
|
||||
BitbucketServerPullRequest,
|
||||
BitbucketCloudPullRequest,
|
||||
BitbucketServerActivity,
|
||||
MergeInfo
|
||||
} from '../types/bitbucket.js';
|
||||
import {
|
||||
isGetPullRequestArgs,
|
||||
isListPullRequestsArgs,
|
||||
isCreatePullRequestArgs,
|
||||
isUpdatePullRequestArgs,
|
||||
isAddCommentArgs,
|
||||
isMergePullRequestArgs
|
||||
} from '../types/guards.js';
|
||||
|
||||
export class PullRequestHandlers {
|
||||
constructor(
|
||||
private apiClient: BitbucketApiClient,
|
||||
private baseUrl: string,
|
||||
private username: string
|
||||
) {}
|
||||
|
||||
async handleGetPullRequest(args: any) {
|
||||
if (!isGetPullRequestArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for get_pull_request'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, pull_request_id } = args;
|
||||
|
||||
try {
|
||||
// Different API paths for Server vs Cloud
|
||||
const apiPath = this.apiClient.getIsServer()
|
||||
? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
|
||||
: `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
|
||||
|
||||
const pr = await this.apiClient.makeRequest<any>('get', apiPath);
|
||||
|
||||
let mergeInfo: MergeInfo = {};
|
||||
|
||||
// For Bitbucket Server, fetch additional merge information if PR is merged
|
||||
if (this.apiClient.getIsServer() && pr.state === 'MERGED') {
|
||||
try {
|
||||
// Try to get activities to find merge information
|
||||
const activitiesPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/activities`;
|
||||
const activitiesResponse = await this.apiClient.makeRequest<any>('get', activitiesPath, undefined, {
|
||||
params: { limit: 100 }
|
||||
});
|
||||
|
||||
const activities = activitiesResponse.values || [];
|
||||
const mergeActivity = activities.find((a: BitbucketServerActivity) => a.action === 'MERGED');
|
||||
|
||||
if (mergeActivity) {
|
||||
mergeInfo.mergeCommitHash = mergeActivity.commit?.id || null;
|
||||
mergeInfo.mergedBy = mergeActivity.user?.displayName || null;
|
||||
mergeInfo.mergedAt = new Date(mergeActivity.createdDate).toISOString();
|
||||
|
||||
// Try to get commit message if we have the hash
|
||||
if (mergeActivity.commit?.id) {
|
||||
try {
|
||||
const commitPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/commits/${mergeActivity.commit.id}`;
|
||||
const commitResponse = await this.apiClient.makeRequest<any>('get', commitPath);
|
||||
mergeInfo.mergeCommitMessage = commitResponse.message || null;
|
||||
} catch (commitError) {
|
||||
// If we can't get the commit message, continue without it
|
||||
console.error('Failed to fetch merge commit message:', commitError);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (activitiesError) {
|
||||
// If we can't get activities, continue without merge info
|
||||
console.error('Failed to fetch PR activities:', activitiesError);
|
||||
}
|
||||
}
|
||||
|
||||
// Format the response based on server type
|
||||
const formattedResponse = this.apiClient.getIsServer()
|
||||
? formatServerResponse(pr as BitbucketServerPullRequest, mergeInfo, this.baseUrl)
|
||||
: formatCloudResponse(pr as BitbucketCloudPullRequest);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(formattedResponse, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `getting pull request ${pull_request_id} in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleListPullRequests(args: any) {
|
||||
if (!isListPullRequestsArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for list_pull_requests'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, state = 'OPEN', author, limit = 25, start = 0 } = args;
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
let params: any = {};
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API
|
||||
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
|
||||
params = {
|
||||
state: state === 'ALL' ? undefined : state,
|
||||
limit,
|
||||
start,
|
||||
};
|
||||
if (author) {
|
||||
params['role.1'] = 'AUTHOR';
|
||||
params['username.1'] = author;
|
||||
}
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
|
||||
params = {
|
||||
state: state === 'ALL' ? undefined : state,
|
||||
pagelen: limit,
|
||||
page: Math.floor(start / limit) + 1,
|
||||
};
|
||||
if (author) {
|
||||
params['q'] = `author.username="${author}"`;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
|
||||
|
||||
// Format the response
|
||||
let pullRequests: any[] = [];
|
||||
let totalCount = 0;
|
||||
let nextPageStart = null;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
pullRequests = (response.values || []).map((pr: BitbucketServerPullRequest) =>
|
||||
formatServerResponse(pr, undefined, this.baseUrl)
|
||||
);
|
||||
totalCount = response.size || 0;
|
||||
if (!response.isLastPage && response.nextPageStart !== undefined) {
|
||||
nextPageStart = response.nextPageStart;
|
||||
}
|
||||
} else {
|
||||
pullRequests = (response.values || []).map((pr: BitbucketCloudPullRequest) =>
|
||||
formatCloudResponse(pr)
|
||||
);
|
||||
totalCount = response.size || 0;
|
||||
if (response.next) {
|
||||
nextPageStart = start + limit;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
pull_requests: pullRequests,
|
||||
total_count: totalCount,
|
||||
start,
|
||||
limit,
|
||||
has_more: nextPageStart !== null,
|
||||
next_start: nextPageStart,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `listing pull requests in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleCreatePullRequest(args: any) {
|
||||
if (!isCreatePullRequestArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for create_pull_request'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, title, source_branch, destination_branch, description, reviewers, close_source_branch } = args;
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
let requestBody: any;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API
|
||||
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
|
||||
requestBody = {
|
||||
title,
|
||||
description: description || '',
|
||||
fromRef: {
|
||||
id: `refs/heads/${source_branch}`,
|
||||
repository: {
|
||||
slug: repository,
|
||||
project: {
|
||||
key: workspace
|
||||
}
|
||||
}
|
||||
},
|
||||
toRef: {
|
||||
id: `refs/heads/${destination_branch}`,
|
||||
repository: {
|
||||
slug: repository,
|
||||
project: {
|
||||
key: workspace
|
||||
}
|
||||
}
|
||||
},
|
||||
reviewers: reviewers?.map(r => ({ user: { name: r } })) || []
|
||||
};
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
|
||||
requestBody = {
|
||||
title,
|
||||
description: description || '',
|
||||
source: {
|
||||
branch: {
|
||||
name: source_branch
|
||||
}
|
||||
},
|
||||
destination: {
|
||||
branch: {
|
||||
name: destination_branch
|
||||
}
|
||||
},
|
||||
close_source_branch: close_source_branch || false,
|
||||
reviewers: reviewers?.map(r => ({ username: r })) || []
|
||||
};
|
||||
}
|
||||
|
||||
const pr = await this.apiClient.makeRequest<any>('post', apiPath, requestBody);
|
||||
|
||||
const formattedResponse = this.apiClient.getIsServer()
|
||||
? formatServerResponse(pr as BitbucketServerPullRequest, undefined, this.baseUrl)
|
||||
: formatCloudResponse(pr as BitbucketCloudPullRequest);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Pull request created successfully',
|
||||
pull_request: formattedResponse
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `creating pull request in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleUpdatePullRequest(args: any) {
|
||||
if (!isUpdatePullRequestArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for update_pull_request'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, pull_request_id, title, description, destination_branch, reviewers } = args;
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
let requestBody: any = {};
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API
|
||||
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`;
|
||||
|
||||
// First get the current PR to get version number
|
||||
const currentPr = await this.apiClient.makeRequest<any>('get', apiPath);
|
||||
|
||||
requestBody.version = currentPr.version;
|
||||
if (title !== undefined) requestBody.title = title;
|
||||
if (description !== undefined) requestBody.description = description;
|
||||
if (destination_branch !== undefined) {
|
||||
requestBody.toRef = {
|
||||
id: `refs/heads/${destination_branch}`,
|
||||
repository: {
|
||||
slug: repository,
|
||||
project: {
|
||||
key: workspace
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (reviewers !== undefined) {
|
||||
requestBody.reviewers = reviewers.map(r => ({ user: { name: r } }));
|
||||
}
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
|
||||
|
||||
if (title !== undefined) requestBody.title = title;
|
||||
if (description !== undefined) requestBody.description = description;
|
||||
if (destination_branch !== undefined) {
|
||||
requestBody.destination = {
|
||||
branch: {
|
||||
name: destination_branch
|
||||
}
|
||||
};
|
||||
}
|
||||
if (reviewers !== undefined) {
|
||||
requestBody.reviewers = reviewers.map(r => ({ username: r }));
|
||||
}
|
||||
}
|
||||
|
||||
const pr = await this.apiClient.makeRequest<any>('put', apiPath, requestBody);
|
||||
|
||||
const formattedResponse = this.apiClient.getIsServer()
|
||||
? formatServerResponse(pr as BitbucketServerPullRequest, undefined, this.baseUrl)
|
||||
: formatCloudResponse(pr as BitbucketCloudPullRequest);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Pull request updated successfully',
|
||||
pull_request: formattedResponse
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `updating pull request ${pull_request_id} in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleAddComment(args: any) {
|
||||
if (!isAddCommentArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for add_comment'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, pull_request_id, comment_text, parent_comment_id, file_path, line_number, line_type } = args;
|
||||
|
||||
const isInlineComment = file_path !== undefined && line_number !== undefined;
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
let requestBody: any;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API
|
||||
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/comments`;
|
||||
requestBody = {
|
||||
text: comment_text
|
||||
};
|
||||
|
||||
if (parent_comment_id !== undefined) {
|
||||
requestBody.parent = { id: parent_comment_id };
|
||||
}
|
||||
|
||||
if (isInlineComment) {
|
||||
requestBody.anchor = {
|
||||
line: line_number,
|
||||
lineType: line_type || 'CONTEXT',
|
||||
fileType: line_type === 'REMOVED' ? 'FROM' : 'TO',
|
||||
path: file_path,
|
||||
diffType: 'EFFECTIVE'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/comments`;
|
||||
requestBody = {
|
||||
content: {
|
||||
raw: comment_text
|
||||
}
|
||||
};
|
||||
|
||||
if (parent_comment_id !== undefined) {
|
||||
requestBody.parent = { id: parent_comment_id };
|
||||
}
|
||||
|
||||
if (isInlineComment) {
|
||||
requestBody.inline = {
|
||||
to: line_number,
|
||||
path: file_path
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const comment = await this.apiClient.makeRequest<any>('post', apiPath, requestBody);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: isInlineComment ? 'Inline comment added successfully' : 'Comment added successfully',
|
||||
comment: {
|
||||
id: comment.id,
|
||||
text: this.apiClient.getIsServer() ? comment.text : comment.content.raw,
|
||||
author: this.apiClient.getIsServer() ? comment.author.displayName : comment.user.display_name,
|
||||
created_on: this.apiClient.getIsServer() ? new Date(comment.createdDate).toLocaleString() : comment.created_on,
|
||||
file_path: isInlineComment ? file_path : undefined,
|
||||
line_number: isInlineComment ? line_number : undefined,
|
||||
line_type: isInlineComment ? (line_type || 'CONTEXT') : undefined
|
||||
}
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `adding ${isInlineComment ? 'inline ' : ''}comment to pull request ${pull_request_id} in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleMergePullRequest(args: any) {
|
||||
if (!isMergePullRequestArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for merge_pull_request'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, pull_request_id, merge_strategy, close_source_branch, commit_message } = args;
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
let requestBody: any = {};
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API
|
||||
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/merge`;
|
||||
|
||||
// Get current PR version
|
||||
const prPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`;
|
||||
const currentPr = await this.apiClient.makeRequest<any>('get', prPath);
|
||||
|
||||
requestBody.version = currentPr.version;
|
||||
if (commit_message) {
|
||||
requestBody.message = commit_message;
|
||||
}
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/merge`;
|
||||
|
||||
if (merge_strategy) {
|
||||
requestBody.merge_strategy = merge_strategy;
|
||||
}
|
||||
if (close_source_branch !== undefined) {
|
||||
requestBody.close_source_branch = close_source_branch;
|
||||
}
|
||||
if (commit_message) {
|
||||
requestBody.message = commit_message;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.apiClient.makeRequest<any>('post', apiPath, requestBody);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Pull request merged successfully',
|
||||
merge_commit: this.apiClient.getIsServer() ? result.properties?.mergeCommit : result.merge_commit?.hash,
|
||||
pull_request_id
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `merging pull request ${pull_request_id} in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
}
|
236
src/handlers/review-handlers.ts
Normal file
236
src/handlers/review-handlers.ts
Normal file
|
@ -0,0 +1,236 @@
|
|||
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { BitbucketApiClient } from '../utils/api-client.js';
|
||||
import {
|
||||
isGetPullRequestDiffArgs,
|
||||
isApprovePullRequestArgs,
|
||||
isRequestChangesArgs
|
||||
} from '../types/guards.js';
|
||||
|
||||
export class ReviewHandlers {
|
||||
constructor(
|
||||
private apiClient: BitbucketApiClient,
|
||||
private username: string
|
||||
) {}
|
||||
|
||||
async handleGetPullRequestDiff(args: any) {
|
||||
if (!isGetPullRequestDiffArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for get_pull_request_diff'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, pull_request_id, context_lines = 3 } = args;
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
let config: any = {};
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API
|
||||
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/diff`;
|
||||
config.params = { contextLines: context_lines };
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/diff`;
|
||||
config.params = { context: context_lines };
|
||||
}
|
||||
|
||||
// For diff, we want the raw text response
|
||||
config.headers = { 'Accept': 'text/plain' };
|
||||
|
||||
const diff = await this.apiClient.makeRequest<string>('get', apiPath, undefined, config);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Pull request diff retrieved successfully',
|
||||
pull_request_id,
|
||||
diff: diff
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `getting diff for pull request ${pull_request_id} in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleApprovePullRequest(args: any) {
|
||||
if (!isApprovePullRequestArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for approve_pull_request'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, pull_request_id } = args;
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API - use participants endpoint
|
||||
// Convert email format: @ to _ for the API
|
||||
const username = this.username.replace('@', '_');
|
||||
apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`;
|
||||
await this.apiClient.makeRequest<any>('put', apiPath, { status: 'APPROVED' });
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/approve`;
|
||||
await this.apiClient.makeRequest<any>('post', apiPath);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Pull request approved successfully',
|
||||
pull_request_id,
|
||||
approved_by: this.username
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `approving pull request ${pull_request_id} in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleUnapprovePullRequest(args: any) {
|
||||
if (!isApprovePullRequestArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for unapprove_pull_request'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, pull_request_id } = args;
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API - use participants endpoint
|
||||
const username = this.username.replace('@', '_');
|
||||
apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`;
|
||||
await this.apiClient.makeRequest<any>('put', apiPath, { status: 'UNAPPROVED' });
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/approve`;
|
||||
await this.apiClient.makeRequest<any>('delete', apiPath);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Pull request approval removed successfully',
|
||||
pull_request_id,
|
||||
unapproved_by: this.username
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `removing approval from pull request ${pull_request_id} in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRequestChanges(args: any) {
|
||||
if (!isRequestChangesArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for request_changes'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, pull_request_id, comment } = args;
|
||||
|
||||
try {
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API - use needs-work status
|
||||
const username = this.username.replace('@', '_');
|
||||
const apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`;
|
||||
await this.apiClient.makeRequest<any>('put', apiPath, { status: 'NEEDS_WORK' });
|
||||
|
||||
// Add comment if provided
|
||||
if (comment) {
|
||||
const commentPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/comments`;
|
||||
await this.apiClient.makeRequest<any>('post', commentPath, { text: comment });
|
||||
}
|
||||
} else {
|
||||
// Bitbucket Cloud API - use request-changes status
|
||||
const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/request-changes`;
|
||||
await this.apiClient.makeRequest<any>('post', apiPath);
|
||||
|
||||
// Add comment if provided
|
||||
if (comment) {
|
||||
const commentPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/comments`;
|
||||
await this.apiClient.makeRequest<any>('post', commentPath, {
|
||||
content: { raw: comment }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Changes requested on pull request',
|
||||
pull_request_id,
|
||||
requested_by: this.username,
|
||||
comment: comment || 'No comment provided'
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `requesting changes on pull request ${pull_request_id} in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRemoveRequestedChanges(args: any) {
|
||||
if (!isApprovePullRequestArgs(args)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid arguments for remove_requested_changes'
|
||||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, pull_request_id } = args;
|
||||
|
||||
try {
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API - remove needs-work status
|
||||
const username = this.username.replace('@', '_');
|
||||
const apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`;
|
||||
await this.apiClient.makeRequest<any>('put', apiPath, { status: 'UNAPPROVED' });
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/request-changes`;
|
||||
await this.apiClient.makeRequest<any>('delete', apiPath);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: 'Change request removed from pull request',
|
||||
pull_request_id,
|
||||
removed_by: this.username
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return this.apiClient.handleApiError(error, `removing change request from pull request ${pull_request_id} in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
}
|
1874
src/index.ts
1874
src/index.ts
File diff suppressed because it is too large
Load diff
393
src/tools/definitions.ts
Normal file
393
src/tools/definitions.ts
Normal file
|
@ -0,0 +1,393 @@
|
|||
export const toolDefinitions = [
|
||||
{
|
||||
name: 'get_pull_request',
|
||||
description: 'Get details of a Bitbucket pull request including merge commit information',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
pull_request_id: {
|
||||
type: 'number',
|
||||
description: 'Pull request ID',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'pull_request_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_pull_requests',
|
||||
description: 'List pull requests for a repository with optional filters',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
description: 'Filter by PR state: OPEN, MERGED, DECLINED, ALL (default: OPEN)',
|
||||
enum: ['OPEN', 'MERGED', 'DECLINED', 'ALL'],
|
||||
},
|
||||
author: {
|
||||
type: 'string',
|
||||
description: 'Filter by author username',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of PRs to return (default: 25)',
|
||||
},
|
||||
start: {
|
||||
type: 'number',
|
||||
description: 'Start index for pagination (default: 0)',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'create_pull_request',
|
||||
description: 'Create a new pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Title of the pull request',
|
||||
},
|
||||
source_branch: {
|
||||
type: 'string',
|
||||
description: 'Source branch name',
|
||||
},
|
||||
destination_branch: {
|
||||
type: 'string',
|
||||
description: 'Destination branch name (e.g., "main", "master")',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Description of the pull request (optional)',
|
||||
},
|
||||
reviewers: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of reviewer usernames/emails (optional)',
|
||||
},
|
||||
close_source_branch: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to close source branch after merge (optional, default: false)',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'title', 'source_branch', 'destination_branch'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'update_pull_request',
|
||||
description: 'Update an existing pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
pull_request_id: {
|
||||
type: 'number',
|
||||
description: 'Pull request ID',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'New title (optional)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'New description (optional)',
|
||||
},
|
||||
destination_branch: {
|
||||
type: 'string',
|
||||
description: 'New destination branch (optional)',
|
||||
},
|
||||
reviewers: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'New list of reviewer usernames/emails (optional)',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'pull_request_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_comment',
|
||||
description: 'Add a comment to a pull request (general or inline on specific code)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
pull_request_id: {
|
||||
type: 'number',
|
||||
description: 'Pull request ID',
|
||||
},
|
||||
comment_text: {
|
||||
type: 'string',
|
||||
description: 'Comment text',
|
||||
},
|
||||
parent_comment_id: {
|
||||
type: 'number',
|
||||
description: 'Parent comment ID for replies (optional)',
|
||||
},
|
||||
file_path: {
|
||||
type: 'string',
|
||||
description: 'File path for inline comment (optional, e.g., "src/main.js")',
|
||||
},
|
||||
line_number: {
|
||||
type: 'number',
|
||||
description: 'Line number for inline comment (optional, required with file_path)',
|
||||
},
|
||||
line_type: {
|
||||
type: 'string',
|
||||
description: 'Type of line for inline comment: ADDED, REMOVED, or CONTEXT (optional, default: CONTEXT)',
|
||||
enum: ['ADDED', 'REMOVED', 'CONTEXT'],
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'pull_request_id', 'comment_text'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'merge_pull_request',
|
||||
description: 'Merge a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
pull_request_id: {
|
||||
type: 'number',
|
||||
description: 'Pull request ID',
|
||||
},
|
||||
merge_strategy: {
|
||||
type: 'string',
|
||||
description: 'Merge strategy: merge-commit, squash, fast-forward (optional)',
|
||||
enum: ['merge-commit', 'squash', 'fast-forward'],
|
||||
},
|
||||
close_source_branch: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to close source branch after merge (optional)',
|
||||
},
|
||||
commit_message: {
|
||||
type: 'string',
|
||||
description: 'Custom merge commit message (optional)',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'pull_request_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_branches',
|
||||
description: 'List branches in a repository',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
description: 'Filter branches by name pattern (optional)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of branches to return (default: 25)',
|
||||
},
|
||||
start: {
|
||||
type: 'number',
|
||||
description: 'Start index for pagination (default: 0)',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete_branch',
|
||||
description: 'Delete a branch',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
branch_name: {
|
||||
type: 'string',
|
||||
description: 'Branch name to delete',
|
||||
},
|
||||
force: {
|
||||
type: 'boolean',
|
||||
description: 'Force delete even if branch is not merged (optional, default: false)',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'branch_name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_pull_request_diff',
|
||||
description: 'Get the diff/changes for a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
pull_request_id: {
|
||||
type: 'number',
|
||||
description: 'Pull request ID',
|
||||
},
|
||||
context_lines: {
|
||||
type: 'number',
|
||||
description: 'Number of context lines around changes (optional, default: 3)',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'pull_request_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'approve_pull_request',
|
||||
description: 'Approve a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
pull_request_id: {
|
||||
type: 'number',
|
||||
description: 'Pull request ID',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'pull_request_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'unapprove_pull_request',
|
||||
description: 'Remove approval from a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
pull_request_id: {
|
||||
type: 'number',
|
||||
description: 'Pull request ID',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'pull_request_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'request_changes',
|
||||
description: 'Request changes on a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
pull_request_id: {
|
||||
type: 'number',
|
||||
description: 'Pull request ID',
|
||||
},
|
||||
comment: {
|
||||
type: 'string',
|
||||
description: 'Comment explaining requested changes (optional)',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'pull_request_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'remove_requested_changes',
|
||||
description: 'Remove change request from a pull request',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository slug (e.g., "my-repo")',
|
||||
},
|
||||
pull_request_id: {
|
||||
type: 'number',
|
||||
description: 'Pull request ID',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'pull_request_id'],
|
||||
},
|
||||
},
|
||||
];
|
165
src/types/bitbucket.ts
Normal file
165
src/types/bitbucket.ts
Normal file
|
@ -0,0 +1,165 @@
|
|||
// Bitbucket Server API response types
|
||||
export interface BitbucketServerPullRequest {
|
||||
id: number;
|
||||
version: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
state: string;
|
||||
open: boolean;
|
||||
closed: boolean;
|
||||
createdDate: number;
|
||||
updatedDate: number;
|
||||
fromRef: {
|
||||
id: string;
|
||||
displayId: string;
|
||||
latestCommit: string;
|
||||
repository: {
|
||||
slug: string;
|
||||
name: string;
|
||||
project: {
|
||||
key: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
toRef: {
|
||||
id: string;
|
||||
displayId: string;
|
||||
latestCommit: string;
|
||||
repository: {
|
||||
slug: string;
|
||||
name: string;
|
||||
project: {
|
||||
key: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
locked: boolean;
|
||||
author: {
|
||||
user: {
|
||||
name: string;
|
||||
emailAddress: string;
|
||||
displayName: string;
|
||||
};
|
||||
role: string;
|
||||
approved: boolean;
|
||||
status: string;
|
||||
};
|
||||
reviewers: Array<{
|
||||
user: {
|
||||
name: string;
|
||||
emailAddress: string;
|
||||
displayName: string;
|
||||
};
|
||||
role: string;
|
||||
approved: boolean;
|
||||
status: string;
|
||||
}>;
|
||||
participants: Array<{
|
||||
user: {
|
||||
name: string;
|
||||
emailAddress: string;
|
||||
displayName: string;
|
||||
};
|
||||
role: string;
|
||||
approved: boolean;
|
||||
status: string;
|
||||
}>;
|
||||
links: {
|
||||
self: Array<{
|
||||
href: string;
|
||||
}>;
|
||||
};
|
||||
properties?: {
|
||||
mergeCommit?: {
|
||||
id: string;
|
||||
displayId: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Bitbucket Server Activity types
|
||||
export interface BitbucketServerActivity {
|
||||
id: number;
|
||||
createdDate: number;
|
||||
user: {
|
||||
name: string;
|
||||
emailAddress: string;
|
||||
displayName: string;
|
||||
};
|
||||
action: string;
|
||||
comment?: any;
|
||||
commit?: {
|
||||
id: string;
|
||||
displayId: string;
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Bitbucket Cloud API response types
|
||||
export interface BitbucketCloudPullRequest {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
state: string;
|
||||
author: {
|
||||
display_name: string;
|
||||
account_id: string;
|
||||
};
|
||||
source: {
|
||||
branch: {
|
||||
name: string;
|
||||
};
|
||||
repository: {
|
||||
full_name: string;
|
||||
};
|
||||
};
|
||||
destination: {
|
||||
branch: {
|
||||
name: string;
|
||||
};
|
||||
repository: {
|
||||
full_name: string;
|
||||
};
|
||||
};
|
||||
reviewers: Array<{
|
||||
display_name: string;
|
||||
account_id: string;
|
||||
}>;
|
||||
participants: Array<{
|
||||
user: {
|
||||
display_name: string;
|
||||
account_id: string;
|
||||
};
|
||||
role: string;
|
||||
approved: boolean;
|
||||
}>;
|
||||
created_on: string;
|
||||
updated_on: string;
|
||||
links: {
|
||||
html: {
|
||||
href: string;
|
||||
};
|
||||
self: {
|
||||
href: string;
|
||||
};
|
||||
diff: {
|
||||
href: string;
|
||||
};
|
||||
};
|
||||
merge_commit?: {
|
||||
hash: string;
|
||||
};
|
||||
close_source_branch: boolean;
|
||||
closed_by?: {
|
||||
display_name: string;
|
||||
account_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Merge info type for enhanced PR details
|
||||
export interface MergeInfo {
|
||||
mergeCommitHash?: string;
|
||||
mergedBy?: string;
|
||||
mergedAt?: string;
|
||||
mergeCommitMessage?: string;
|
||||
}
|
189
src/types/guards.ts
Normal file
189
src/types/guards.ts
Normal file
|
@ -0,0 +1,189 @@
|
|||
// Type guards for tool arguments
|
||||
export const isGetPullRequestArgs = (
|
||||
args: any
|
||||
): args is { workspace: string; repository: string; pull_request_id: number } =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
typeof args.pull_request_id === 'number';
|
||||
|
||||
export const isListPullRequestsArgs = (
|
||||
args: any
|
||||
): args is {
|
||||
workspace: string;
|
||||
repository: string;
|
||||
state?: string;
|
||||
author?: string;
|
||||
limit?: number;
|
||||
start?: number;
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
(args.state === undefined || typeof args.state === 'string') &&
|
||||
(args.author === undefined || typeof args.author === 'string') &&
|
||||
(args.limit === undefined || typeof args.limit === 'number') &&
|
||||
(args.start === undefined || typeof args.start === 'number');
|
||||
|
||||
export const isCreatePullRequestArgs = (
|
||||
args: any
|
||||
): args is {
|
||||
workspace: string;
|
||||
repository: string;
|
||||
title: string;
|
||||
source_branch: string;
|
||||
destination_branch: string;
|
||||
description?: string;
|
||||
reviewers?: string[];
|
||||
close_source_branch?: boolean;
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
typeof args.title === 'string' &&
|
||||
typeof args.source_branch === 'string' &&
|
||||
typeof args.destination_branch === 'string' &&
|
||||
(args.description === undefined || typeof args.description === 'string') &&
|
||||
(args.reviewers === undefined || Array.isArray(args.reviewers)) &&
|
||||
(args.close_source_branch === undefined || typeof args.close_source_branch === 'boolean');
|
||||
|
||||
export const isUpdatePullRequestArgs = (
|
||||
args: any
|
||||
): args is {
|
||||
workspace: string;
|
||||
repository: string;
|
||||
pull_request_id: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
destination_branch?: string;
|
||||
reviewers?: string[];
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
typeof args.pull_request_id === 'number' &&
|
||||
(args.title === undefined || typeof args.title === 'string') &&
|
||||
(args.description === undefined || typeof args.description === 'string') &&
|
||||
(args.destination_branch === undefined || typeof args.destination_branch === 'string') &&
|
||||
(args.reviewers === undefined || Array.isArray(args.reviewers));
|
||||
|
||||
export const isAddCommentArgs = (
|
||||
args: any
|
||||
): args is {
|
||||
workspace: string;
|
||||
repository: string;
|
||||
pull_request_id: number;
|
||||
comment_text: string;
|
||||
parent_comment_id?: number;
|
||||
file_path?: string;
|
||||
line_number?: number;
|
||||
line_type?: 'ADDED' | 'REMOVED' | 'CONTEXT';
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
typeof args.pull_request_id === 'number' &&
|
||||
typeof args.comment_text === 'string' &&
|
||||
(args.parent_comment_id === undefined || typeof args.parent_comment_id === 'number') &&
|
||||
(args.file_path === undefined || typeof args.file_path === 'string') &&
|
||||
(args.line_number === undefined || typeof args.line_number === 'number') &&
|
||||
(args.line_type === undefined || ['ADDED', 'REMOVED', 'CONTEXT'].includes(args.line_type));
|
||||
|
||||
export const isMergePullRequestArgs = (
|
||||
args: any
|
||||
): args is {
|
||||
workspace: string;
|
||||
repository: string;
|
||||
pull_request_id: number;
|
||||
merge_strategy?: string;
|
||||
close_source_branch?: boolean;
|
||||
commit_message?: string;
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
typeof args.pull_request_id === 'number' &&
|
||||
(args.merge_strategy === undefined || typeof args.merge_strategy === 'string') &&
|
||||
(args.close_source_branch === undefined || typeof args.close_source_branch === 'boolean') &&
|
||||
(args.commit_message === undefined || typeof args.commit_message === 'string');
|
||||
|
||||
export const isDeleteBranchArgs = (
|
||||
args: any
|
||||
): args is {
|
||||
workspace: string;
|
||||
repository: string;
|
||||
branch_name: string;
|
||||
force?: boolean;
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
typeof args.branch_name === 'string' &&
|
||||
(args.force === undefined || typeof args.force === 'boolean');
|
||||
|
||||
export const isListBranchesArgs = (
|
||||
args: any
|
||||
): args is {
|
||||
workspace: string;
|
||||
repository: string;
|
||||
filter?: string;
|
||||
limit?: number;
|
||||
start?: number;
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
(args.filter === undefined || typeof args.filter === 'string') &&
|
||||
(args.limit === undefined || typeof args.limit === 'number') &&
|
||||
(args.start === undefined || typeof args.start === 'number');
|
||||
|
||||
export const isGetPullRequestDiffArgs = (
|
||||
args: any
|
||||
): args is {
|
||||
workspace: string;
|
||||
repository: string;
|
||||
pull_request_id: number;
|
||||
context_lines?: number;
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
typeof args.pull_request_id === 'number' &&
|
||||
(args.context_lines === undefined || typeof args.context_lines === 'number');
|
||||
|
||||
export const isApprovePullRequestArgs = (
|
||||
args: any
|
||||
): args is {
|
||||
workspace: string;
|
||||
repository: string;
|
||||
pull_request_id: number;
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
typeof args.pull_request_id === 'number';
|
||||
|
||||
export const isRequestChangesArgs = (
|
||||
args: any
|
||||
): args is {
|
||||
workspace: string;
|
||||
repository: string;
|
||||
pull_request_id: number;
|
||||
comment?: string;
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.workspace === 'string' &&
|
||||
typeof args.repository === 'string' &&
|
||||
typeof args.pull_request_id === 'number' &&
|
||||
(args.comment === undefined || typeof args.comment === 'string');
|
138
src/utils/api-client.ts
Normal file
138
src/utils/api-client.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||
|
||||
export interface ApiError {
|
||||
status?: number;
|
||||
message: string;
|
||||
isAxiosError: boolean;
|
||||
originalError?: AxiosError;
|
||||
}
|
||||
|
||||
export class BitbucketApiClient {
|
||||
private axiosInstance: AxiosInstance;
|
||||
private isServer: boolean;
|
||||
|
||||
constructor(
|
||||
baseURL: string,
|
||||
username: string,
|
||||
password?: string,
|
||||
token?: string
|
||||
) {
|
||||
this.isServer = !!token;
|
||||
|
||||
const axiosConfig: any = {
|
||||
baseURL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
// Use token auth for Bitbucket Server, basic auth for Cloud
|
||||
if (token) {
|
||||
// Bitbucket Server uses Bearer token
|
||||
axiosConfig.headers['Authorization'] = `Bearer ${token}`;
|
||||
} else {
|
||||
// Bitbucket Cloud uses basic auth with app password
|
||||
axiosConfig.auth = {
|
||||
username,
|
||||
password,
|
||||
};
|
||||
}
|
||||
|
||||
this.axiosInstance = axios.create(axiosConfig);
|
||||
}
|
||||
|
||||
async makeRequest<T>(
|
||||
method: 'get' | 'post' | 'put' | 'delete',
|
||||
path: string,
|
||||
data?: any,
|
||||
config?: any
|
||||
): Promise<T> {
|
||||
try {
|
||||
let response;
|
||||
if (method === 'get') {
|
||||
// For GET, config is the second parameter
|
||||
response = await this.axiosInstance[method](path, config || {});
|
||||
} else if (method === 'delete') {
|
||||
// For DELETE, we might need to pass data in config
|
||||
if (data) {
|
||||
response = await this.axiosInstance[method](path, { ...config, data });
|
||||
} else {
|
||||
response = await this.axiosInstance[method](path, config || {});
|
||||
}
|
||||
} else {
|
||||
// For POST and PUT, data is second, config is third
|
||||
response = await this.axiosInstance[method](path, data, config);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status;
|
||||
const message = error.response?.data?.errors?.[0]?.message ||
|
||||
error.response?.data?.error?.message ||
|
||||
error.response?.data?.message ||
|
||||
error.message;
|
||||
|
||||
throw {
|
||||
status,
|
||||
message,
|
||||
isAxiosError: true,
|
||||
originalError: error
|
||||
} as ApiError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
handleApiError(error: any, context: string) {
|
||||
if (error.isAxiosError) {
|
||||
const { status, message } = error as ApiError;
|
||||
|
||||
if (status === 404) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Not found: ${context}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
} else if (status === 401) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Authentication failed. Please check your ${this.isServer ? 'BITBUCKET_TOKEN' : 'BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD'}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
} else if (status === 403) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Permission denied: ${context}. Ensure your credentials have the necessary permissions.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Bitbucket API error: ${message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
getIsServer(): boolean {
|
||||
return this.isServer;
|
||||
}
|
||||
}
|
76
src/utils/formatters.ts
Normal file
76
src/utils/formatters.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { BitbucketServerPullRequest, BitbucketCloudPullRequest, MergeInfo } from '../types/bitbucket.js';
|
||||
|
||||
export function formatServerResponse(
|
||||
pr: BitbucketServerPullRequest,
|
||||
mergeInfo?: MergeInfo,
|
||||
baseUrl?: string
|
||||
): any {
|
||||
const webUrl = `${baseUrl}/projects/${pr.toRef.repository.project.key}/repos/${pr.toRef.repository.slug}/pull-requests/${pr.id}`;
|
||||
|
||||
return {
|
||||
id: pr.id,
|
||||
title: pr.title,
|
||||
description: pr.description || 'No description provided',
|
||||
state: pr.state,
|
||||
is_open: pr.open,
|
||||
is_closed: pr.closed,
|
||||
author: pr.author.user.displayName,
|
||||
author_username: pr.author.user.name,
|
||||
author_email: pr.author.user.emailAddress,
|
||||
source_branch: pr.fromRef.displayId,
|
||||
destination_branch: pr.toRef.displayId,
|
||||
source_commit: pr.fromRef.latestCommit,
|
||||
destination_commit: pr.toRef.latestCommit,
|
||||
reviewers: pr.reviewers.map(r => ({
|
||||
name: r.user.displayName,
|
||||
approved: r.approved,
|
||||
status: r.status,
|
||||
})),
|
||||
participants: pr.participants.map(p => ({
|
||||
name: p.user.displayName,
|
||||
role: p.role,
|
||||
approved: p.approved,
|
||||
status: p.status,
|
||||
})),
|
||||
created_on: new Date(pr.createdDate).toLocaleString(),
|
||||
updated_on: new Date(pr.updatedDate).toLocaleString(),
|
||||
web_url: webUrl,
|
||||
api_url: pr.links.self[0]?.href || '',
|
||||
is_locked: pr.locked,
|
||||
// Add merge commit details
|
||||
is_merged: pr.state === 'MERGED',
|
||||
merge_commit_hash: mergeInfo?.mergeCommitHash || pr.properties?.mergeCommit?.id || null,
|
||||
merged_by: mergeInfo?.mergedBy || null,
|
||||
merged_at: mergeInfo?.mergedAt || null,
|
||||
merge_commit_message: mergeInfo?.mergeCommitMessage || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatCloudResponse(pr: BitbucketCloudPullRequest): any {
|
||||
return {
|
||||
id: pr.id,
|
||||
title: pr.title,
|
||||
description: pr.description || 'No description provided',
|
||||
state: pr.state,
|
||||
author: pr.author.display_name,
|
||||
source_branch: pr.source.branch.name,
|
||||
destination_branch: pr.destination.branch.name,
|
||||
reviewers: pr.reviewers.map(r => r.display_name),
|
||||
participants: pr.participants.map(p => ({
|
||||
name: p.user.display_name,
|
||||
role: p.role,
|
||||
approved: p.approved,
|
||||
})),
|
||||
created_on: new Date(pr.created_on).toLocaleString(),
|
||||
updated_on: new Date(pr.updated_on).toLocaleString(),
|
||||
web_url: pr.links.html.href,
|
||||
api_url: pr.links.self.href,
|
||||
diff_url: pr.links.diff.href,
|
||||
is_merged: pr.state === 'MERGED',
|
||||
merge_commit_hash: pr.merge_commit?.hash || null,
|
||||
merged_by: pr.closed_by?.display_name || null,
|
||||
merged_at: pr.state === 'MERGED' ? pr.updated_on : null,
|
||||
merge_commit_message: null, // Would need additional API call to get this
|
||||
close_source_branch: pr.close_source_branch,
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue