feat: Add get_branch tool for comprehensive branch information

- Added new tool to get detailed branch information including associated PRs
- Supports both Bitbucket Server and Cloud APIs
- Returns branch details, open PRs, optionally merged PRs, and statistics
- Useful for checking PR status before branch deletion and branch management
- Added TypeScript interfaces for branch responses
- Added comprehensive documentation in README
This commit is contained in:
pdogra1299 2025-06-21 16:20:15 +05:30
parent 75c4192815
commit 5d38645127
6 changed files with 340 additions and 1 deletions

View file

@ -18,6 +18,11 @@ An MCP (Model Context Protocol) server that provides tools for interacting with
- `merge_pull_request` - Merge pull requests with various strategies
- `delete_branch` - Delete branches after merge
#### Branch Management Tools
- `list_branches` - List branches with filtering and pagination
- `delete_branch` - Delete branches (with protection checks)
- `get_branch` - Get detailed branch information including associated PRs
#### Code Review Tools
- `get_pull_request_diff` - Get the diff/changes for a pull request
- `approve_pull_request` - Approve a pull request
@ -334,6 +339,45 @@ Returns a paginated list of branches with:
**Note**: Branch deletion requires appropriate permissions. The branch will be permanently deleted.
### Get Branch
```typescript
{
"tool": "get_branch",
"arguments": {
"workspace": "PROJ",
"repository": "my-repo",
"branch_name": "feature/new-feature",
"include_merged_prs": false // Optional (default: false)
}
}
```
Returns comprehensive branch information including:
- Branch details:
- Name and ID
- Latest commit (hash, message, author, date)
- Default branch indicator
- Open pull requests from this branch:
- PR title and ID
- Destination branch
- Author and reviewers
- Approval status (approved by, changes requested by, pending)
- PR URL
- Merged pull requests (if `include_merged_prs` is true):
- PR title and ID
- Merge date and who merged it
- Statistics:
- Total open PRs count
- Total merged PRs count
- Days since last commit
This tool is particularly useful for:
- Checking if a branch has open PRs before deletion
- Getting an overview of branch activity
- Understanding PR review status
- Identifying stale branches
### Get Pull Request Diff
```typescript

View file

@ -2,8 +2,10 @@ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { BitbucketApiClient } from '../utils/api-client.js';
import {
isListBranchesArgs,
isDeleteBranchArgs
isDeleteBranchArgs,
isGetBranchArgs
} from '../types/guards.js';
import { BitbucketServerBranch, BitbucketCloudBranch } from '../types/bitbucket.js';
export class BranchHandlers {
constructor(
@ -179,4 +181,215 @@ export class BranchHandlers {
return this.apiClient.handleApiError(error, `deleting branch '${branch_name}' in ${workspace}/${repository}`);
}
}
async handleGetBranch(args: any) {
if (!isGetBranchArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid arguments for get_branch'
);
}
const { workspace, repository, branch_name, include_merged_prs = false } = args;
try {
// Step 1: Get branch details
let branchInfo: any;
let branchCommitInfo: any = {};
if (this.apiClient.getIsServer()) {
// Bitbucket Server - get branch details
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,
details: true
}
});
// Find the exact branch
const branch = branchesResponse.values?.find((b: BitbucketServerBranch) => b.displayId === branch_name);
if (!branch) {
throw new Error(`Branch '${branch_name}' not found`);
}
branchInfo = {
name: branch.displayId,
id: branch.id,
latest_commit: {
id: branch.latestCommit,
message: branch.metadata?.['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata']?.message || null,
author: branch.metadata?.['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata']?.author || null,
date: branch.metadata?.['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata']?.authorTimestamp
? new Date(branch.metadata['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata'].authorTimestamp).toISOString()
: null
},
is_default: branch.isDefault || false
};
} else {
// Bitbucket Cloud - get branch details
const branchPath = `/repositories/${workspace}/${repository}/refs/branches/${encodeURIComponent(branch_name)}`;
const branch = await this.apiClient.makeRequest<BitbucketCloudBranch>('get', branchPath);
branchInfo = {
name: branch.name,
id: `refs/heads/${branch.name}`,
latest_commit: {
id: branch.target.hash,
message: branch.target.message,
author: branch.target.author.user?.display_name || branch.target.author.raw,
date: branch.target.date
},
is_default: false // Will check this with default branch info
};
// Check if this is the default branch
try {
const repoPath = `/repositories/${workspace}/${repository}`;
const repoInfo = await this.apiClient.makeRequest<any>('get', repoPath);
branchInfo.is_default = branch.name === repoInfo.mainbranch?.name;
} catch (e) {
// Ignore error, just assume not default
}
}
// Step 2: Get open PRs from this branch
let openPRs: any[] = [];
if (this.apiClient.getIsServer()) {
// Bitbucket Server
const prPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
const prResponse = await this.apiClient.makeRequest<any>('get', prPath, undefined, {
params: {
state: 'OPEN',
direction: 'OUTGOING',
at: `refs/heads/${branch_name}`,
limit: 100
}
});
openPRs = (prResponse.values || []).map((pr: any) => ({
id: pr.id,
title: pr.title,
destination_branch: pr.toRef.displayId,
author: pr.author.user.displayName,
created_on: new Date(pr.createdDate).toISOString(),
reviewers: pr.reviewers.map((r: any) => r.user.displayName),
approval_status: {
approved_by: pr.reviewers.filter((r: any) => r.approved).map((r: any) => r.user.displayName),
changes_requested_by: pr.reviewers.filter((r: any) => r.status === 'NEEDS_WORK').map((r: any) => r.user.displayName),
pending: pr.reviewers.filter((r: any) => !r.approved && r.status !== 'NEEDS_WORK').map((r: any) => r.user.displayName)
},
url: `${this.baseUrl}/projects/${workspace}/repos/${repository}/pull-requests/${pr.id}`
}));
} else {
// Bitbucket Cloud
const prPath = `/repositories/${workspace}/${repository}/pullrequests`;
const prResponse = await this.apiClient.makeRequest<any>('get', prPath, undefined, {
params: {
state: 'OPEN',
q: `source.branch.name="${branch_name}"`,
pagelen: 50
}
});
openPRs = (prResponse.values || []).map((pr: any) => ({
id: pr.id,
title: pr.title,
destination_branch: pr.destination.branch.name,
author: pr.author.display_name,
created_on: pr.created_on,
reviewers: pr.reviewers.map((r: any) => r.display_name),
approval_status: {
approved_by: pr.participants.filter((p: any) => p.approved).map((p: any) => p.user.display_name),
changes_requested_by: [], // Cloud doesn't have explicit "changes requested" status
pending: pr.reviewers.filter((r: any) => !pr.participants.find((p: any) => p.user.account_id === r.account_id && p.approved))
.map((r: any) => r.display_name)
},
url: pr.links.html.href
}));
}
// Step 3: Optionally get merged PRs
let mergedPRs: any[] = [];
if (include_merged_prs) {
if (this.apiClient.getIsServer()) {
// Bitbucket Server
const mergedPrPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
const mergedPrResponse = await this.apiClient.makeRequest<any>('get', mergedPrPath, undefined, {
params: {
state: 'MERGED',
direction: 'OUTGOING',
at: `refs/heads/${branch_name}`,
limit: 25
}
});
mergedPRs = (mergedPrResponse.values || []).map((pr: any) => ({
id: pr.id,
title: pr.title,
merged_at: new Date(pr.updatedDate).toISOString(), // Using updated date as merge date
merged_by: pr.participants.find((p: any) => p.role === 'PARTICIPANT' && p.approved)?.user.displayName || 'Unknown'
}));
} else {
// Bitbucket Cloud
const mergedPrPath = `/repositories/${workspace}/${repository}/pullrequests`;
const mergedPrResponse = await this.apiClient.makeRequest<any>('get', mergedPrPath, undefined, {
params: {
state: 'MERGED',
q: `source.branch.name="${branch_name}"`,
pagelen: 25
}
});
mergedPRs = (mergedPrResponse.values || []).map((pr: any) => ({
id: pr.id,
title: pr.title,
merged_at: pr.updated_on,
merged_by: pr.closed_by?.display_name || 'Unknown'
}));
}
}
// Step 4: Calculate statistics
const daysSinceLastCommit = branchInfo.latest_commit.date
? Math.floor((Date.now() - new Date(branchInfo.latest_commit.date).getTime()) / (1000 * 60 * 60 * 24))
: null;
// Step 5: Format and return combined response
return {
content: [
{
type: 'text',
text: JSON.stringify({
branch: branchInfo,
open_pull_requests: openPRs,
merged_pull_requests: mergedPRs,
statistics: {
total_open_prs: openPRs.length,
total_merged_prs: mergedPRs.length,
days_since_last_commit: daysSinceLastCommit
}
}, null, 2),
},
],
};
} catch (error: any) {
// Handle specific not found error
if (error.message?.includes('not found')) {
return {
content: [
{
type: 'text',
text: `Branch '${branch_name}' not found in ${workspace}/${repository}`,
},
],
isError: true,
};
}
return this.apiClient.handleApiError(error, `getting branch '${branch_name}' in ${workspace}/${repository}`);
}
}
}

View file

@ -102,6 +102,8 @@ class BitbucketMCPServer {
return this.branchHandlers.handleListBranches(request.params.arguments);
case 'delete_branch':
return this.branchHandlers.handleDeleteBranch(request.params.arguments);
case 'get_branch':
return this.branchHandlers.handleGetBranch(request.params.arguments);
// Code Review tools
case 'get_pull_request_diff':

View file

@ -390,4 +390,30 @@ export const toolDefinitions = [
required: ['workspace', 'repository', 'pull_request_id'],
},
},
{
name: 'get_branch',
description: 'Get detailed information about a branch including associated pull requests',
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 get details for',
},
include_merged_prs: {
type: 'boolean',
description: 'Include merged PRs from this branch (default: false)',
},
},
required: ['workspace', 'repository', 'branch_name'],
},
},
];

View file

@ -95,6 +95,26 @@ export interface BitbucketServerActivity {
};
}
// Bitbucket Server Branch types
export interface BitbucketServerBranch {
id: string;
displayId: string;
type: string;
latestCommit: string;
latestChangeset: string;
isDefault: boolean;
metadata?: {
"com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata": {
author: {
name: string;
emailAddress: string;
};
authorTimestamp: number;
message: string;
};
};
}
// Bitbucket Cloud API response types
export interface BitbucketCloudPullRequest {
id: number;
@ -156,6 +176,25 @@ export interface BitbucketCloudPullRequest {
};
}
// Bitbucket Cloud Branch types
export interface BitbucketCloudBranch {
name: string;
target: {
hash: string;
type: string;
message: string;
author: {
raw: string;
user?: {
display_name: string;
account_id: string;
};
};
date: string;
};
type: string;
}
// Merge info type for enhanced PR details
export interface MergeInfo {
mergeCommitHash?: string;

View file

@ -187,3 +187,18 @@ export const isRequestChangesArgs = (
typeof args.repository === 'string' &&
typeof args.pull_request_id === 'number' &&
(args.comment === undefined || typeof args.comment === 'string');
export const isGetBranchArgs = (
args: any
): args is {
workspace: string;
repository: string;
branch_name: string;
include_merged_prs?: boolean;
} =>
typeof args === 'object' &&
args !== null &&
typeof args.workspace === 'string' &&
typeof args.repository === 'string' &&
typeof args.branch_name === 'string' &&
(args.include_merged_prs === undefined || typeof args.include_merged_prs === 'boolean');