feat: add list_branch_commits and list_pr_commits tools

- Add list_branch_commits tool with advanced filtering options:
  - Filter by date range (since/until)
  - Filter by author email/username
  - Include/exclude merge commits
  - Search in commit messages
  - Pagination support

- Add list_pr_commits tool to list all commits in a pull request
  - Returns PR title along with commit list
  - Pagination support
  - Detailed commit information

- Fix author filtering for Bitbucket Server with client-side filtering
- Add TypeScript interfaces for commit types
- Add formatter functions for consistent commit representation
- Update documentation with comprehensive usage examples
- Bump version to 0.10.0
This commit is contained in:
pdogra1299 2025-07-07 11:58:53 +05:30
parent e21f2dcfe8
commit e5da36e515
10 changed files with 732 additions and 8 deletions

View file

@ -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.10.0] - 2025-07-03
### Added
- **New `list_branch_commits` tool for retrieving commit history**:
- List all commits in a specific branch with detailed information
- Advanced filtering options:
- `since` and `until` parameters for date range filtering (ISO date strings)
- `author` parameter to filter by author email/username
- `include_merge_commits` parameter to include/exclude merge commits (default: true)
- `search` parameter to search in commit messages
- Returns branch head information and paginated commit list
- Each commit includes hash, message, author details, date, parents, and merge status
- Supports both Bitbucket Server and Cloud APIs with appropriate parameter mapping
- Useful for reviewing commit history, tracking changes, and analyzing branch activity
- **New `list_pr_commits` tool for pull request commits**:
- List all commits that are part of a specific pull request
- Returns PR title and paginated commit list
- Simpler than branch commits - focused specifically on PR changes
- Each commit includes same detailed information as branch commits
- Supports pagination with `limit` and `start` parameters
- Useful for reviewing all changes in a PR before merging
### Changed
- Added new TypeScript interfaces for commit types:
- `BitbucketServerCommit` and `BitbucketCloudCommit` for API responses
- `FormattedCommit` for consistent commit representation
- Added formatter functions `formatServerCommit` and `formatCloudCommit` for unified output
- Enhanced type guards with `isListBranchCommitsArgs` and `isListPrCommitsArgs`
## [0.9.1] - 2025-01-27
### Fixed

180
README.md
View file

@ -16,12 +16,14 @@ An MCP (Model Context Protocol) server that provides tools for interacting with
- `update_pull_request` - Update PR details (title, description, reviewers, destination branch)
- `add_comment` - Add comments to pull requests (supports replies)
- `merge_pull_request` - Merge pull requests with various strategies
- `list_pr_commits` - List all commits that are part of a pull request
- `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
- `list_branch_commits` - List commits in a branch with advanced filtering
#### File and Directory Tools
- `list_directory_content` - List files and directories in a repository path
@ -596,6 +598,184 @@ This tool is particularly useful for:
- Understanding PR review status
- Identifying stale branches
### List Branch Commits
Get all commits in a specific branch with advanced filtering options:
```typescript
// Basic usage - get recent commits
{
"tool": "list_branch_commits",
"arguments": {
"workspace": "PROJ",
"repository": "my-repo",
"branch_name": "feature/new-feature",
"limit": 50 // Optional (default: 25)
}
}
// Filter by date range
{
"tool": "list_branch_commits",
"arguments": {
"workspace": "PROJ",
"repository": "my-repo",
"branch_name": "main",
"since": "2025-01-01T00:00:00Z", // ISO date string
"until": "2025-01-15T23:59:59Z" // ISO date string
}
}
// Filter by author
{
"tool": "list_branch_commits",
"arguments": {
"workspace": "PROJ",
"repository": "my-repo",
"branch_name": "develop",
"author": "john.doe@company.com", // Email or username
"limit": 100
}
}
// Exclude merge commits
{
"tool": "list_branch_commits",
"arguments": {
"workspace": "PROJ",
"repository": "my-repo",
"branch_name": "release/v2.0",
"include_merge_commits": false
}
}
// Search in commit messages
{
"tool": "list_branch_commits",
"arguments": {
"workspace": "PROJ",
"repository": "my-repo",
"branch_name": "main",
"search": "bugfix", // Search in commit messages
"limit": 50
}
}
// Combine multiple filters
{
"tool": "list_branch_commits",
"arguments": {
"workspace": "PROJ",
"repository": "my-repo",
"branch_name": "develop",
"author": "jane.smith@company.com",
"since": "2025-01-01T00:00:00Z",
"include_merge_commits": false,
"search": "feature",
"limit": 100,
"start": 0 // For pagination
}
}
```
**Filter Parameters:**
- `since`: ISO date string - only show commits after this date
- `until`: ISO date string - only show commits before this date
- `author`: Filter by author email/username
- `include_merge_commits`: Boolean to include/exclude merge commits (default: true)
- `search`: Search for text in commit messages
Returns detailed commit information:
```json
{
"branch_name": "feature/new-feature",
"branch_head": "abc123def456", // Latest commit hash
"commits": [
{
"hash": "abc123def456",
"abbreviated_hash": "abc123d",
"message": "Add new feature implementation",
"author": {
"name": "John Doe",
"email": "john.doe@example.com"
},
"date": "2025-01-03T10:30:00Z",
"parents": ["parent1hash", "parent2hash"],
"is_merge_commit": false
}
// ... more commits
],
"total_count": 150,
"start": 0,
"limit": 25,
"has_more": true,
"next_start": 25,
"filters_applied": {
"author": "john.doe@example.com",
"since": "2025-01-01",
"include_merge_commits": false
}
}
```
This tool is particularly useful for:
- Reviewing commit history before releases
- Finding commits by specific authors
- Tracking changes within date ranges
- Searching for specific features or fixes
- Analyzing branch activity patterns
### List PR Commits
Get all commits that are part of a pull request:
```typescript
{
"tool": "list_pr_commits",
"arguments": {
"workspace": "PROJ",
"repository": "my-repo",
"pull_request_id": 123,
"limit": 50, // Optional (default: 25)
"start": 0 // Optional: for pagination
}
}
```
Returns commit information for the PR:
```json
{
"pull_request_id": 123,
"pull_request_title": "Add awesome feature",
"commits": [
{
"hash": "def456ghi789",
"abbreviated_hash": "def456g",
"message": "Initial implementation",
"author": {
"name": "Jane Smith",
"email": "jane.smith@example.com"
},
"date": "2025-01-02T14:20:00Z",
"parents": ["parent1hash"],
"is_merge_commit": false
}
// ... more commits
],
"total_count": 5,
"start": 0,
"limit": 25,
"has_more": false
}
```
This tool is particularly useful for:
- Reviewing all changes in a PR before merging
- Understanding the development history of a PR
- Checking commit messages for quality
- Verifying authorship of changes
- Analyzing PR complexity by commit count
### Get Pull Request Diff
Get the diff/changes for a pull request with optional filtering capabilities:

View file

@ -1,6 +1,6 @@
{
"name": "@nexus2520/bitbucket-mcp-server",
"version": "0.9.1",
"version": "0.10.0",
"description": "MCP server for Bitbucket API integration - supports both Cloud and Server",
"type": "module",
"main": "./build/index.js",

View file

@ -3,9 +3,17 @@ import { BitbucketApiClient } from '../utils/api-client.js';
import {
isListBranchesArgs,
isDeleteBranchArgs,
isGetBranchArgs
isGetBranchArgs,
isListBranchCommitsArgs
} from '../types/guards.js';
import { BitbucketServerBranch, BitbucketCloudBranch } from '../types/bitbucket.js';
import {
BitbucketServerBranch,
BitbucketCloudBranch,
BitbucketServerCommit,
BitbucketCloudCommit,
FormattedCommit
} from '../types/bitbucket.js';
import { formatServerCommit, formatCloudCommit } from '../utils/formatters.js';
export class BranchHandlers {
constructor(
@ -392,4 +400,188 @@ export class BranchHandlers {
return this.apiClient.handleApiError(error, `getting branch '${branch_name}' in ${workspace}/${repository}`);
}
}
async handleListBranchCommits(args: any) {
if (!isListBranchCommitsArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid arguments for list_branch_commits'
);
}
const {
workspace,
repository,
branch_name,
limit = 25,
start = 0,
since,
until,
author,
include_merge_commits = true,
search
} = args;
try {
let apiPath: string;
let params: any = {};
let commits: FormattedCommit[] = [];
let totalCount = 0;
let nextPageStart: number | null = null;
if (this.apiClient.getIsServer()) {
// Bitbucket Server API
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/commits`;
params = {
until: `refs/heads/${branch_name}`,
limit,
start,
withCounts: true
};
// Add filters
if (since) {
params.since = since;
}
if (!include_merge_commits) {
params.merges = 'exclude';
}
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
// Format commits
commits = (response.values || []).map((commit: BitbucketServerCommit) => formatServerCommit(commit));
// Apply client-side filters for Server API
if (author) {
// Filter by author email or name
commits = commits.filter(c =>
c.author.email === author ||
c.author.name === author ||
c.author.email.toLowerCase() === author.toLowerCase() ||
c.author.name.toLowerCase() === author.toLowerCase()
);
}
// Filter by date if 'until' is provided (Server API doesn't support 'until' param directly)
if (until) {
const untilDate = new Date(until).getTime();
commits = commits.filter(c => new Date(c.date).getTime() <= untilDate);
}
// Filter by message search if provided
if (search) {
const searchLower = search.toLowerCase();
commits = commits.filter(c => c.message.toLowerCase().includes(searchLower));
}
// If we applied client-side filters, update the total count
if (author || until || search) {
totalCount = commits.length;
// Can't determine if there are more results when filtering client-side
nextPageStart = null;
} else {
totalCount = response.size || commits.length;
if (!response.isLastPage && response.nextPageStart !== undefined) {
nextPageStart = response.nextPageStart;
}
}
} else {
// Bitbucket Cloud API
apiPath = `/repositories/${workspace}/${repository}/commits/${encodeURIComponent(branch_name)}`;
params = {
pagelen: limit,
page: Math.floor(start / limit) + 1
};
// Build query string for filters
const queryParts: string[] = [];
if (author) {
queryParts.push(`author.raw ~ "${author}"`);
}
if (!include_merge_commits) {
// Cloud API doesn't have direct merge exclusion, we'll filter client-side
}
if (queryParts.length > 0) {
params.q = queryParts.join(' AND ');
}
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
// Format commits
let cloudCommits = (response.values || []).map((commit: BitbucketCloudCommit) => formatCloudCommit(commit));
// Apply client-side filters
if (!include_merge_commits) {
cloudCommits = cloudCommits.filter((c: FormattedCommit) => !c.is_merge_commit);
}
if (since) {
const sinceDate = new Date(since).getTime();
cloudCommits = cloudCommits.filter((c: FormattedCommit) => new Date(c.date).getTime() >= sinceDate);
}
if (until) {
const untilDate = new Date(until).getTime();
cloudCommits = cloudCommits.filter((c: FormattedCommit) => new Date(c.date).getTime() <= untilDate);
}
if (search) {
const searchLower = search.toLowerCase();
cloudCommits = cloudCommits.filter((c: FormattedCommit) => c.message.toLowerCase().includes(searchLower));
}
commits = cloudCommits;
totalCount = response.size || commits.length;
if (response.next) {
nextPageStart = start + limit;
}
}
// Get branch head info
let branchHead: string | null = null;
try {
if (this.apiClient.getIsServer()) {
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: 1 }
});
const branch = branchesResponse.values?.find((b: any) => b.displayId === branch_name);
branchHead = branch?.latestCommit || null;
} else {
const branchPath = `/repositories/${workspace}/${repository}/refs/branches/${encodeURIComponent(branch_name)}`;
const branch = await this.apiClient.makeRequest<any>('get', branchPath);
branchHead = branch.target?.hash || null;
}
} catch (e) {
// Ignore error, branch head is optional
}
// Build filters applied summary
const filtersApplied: any = {};
if (author) filtersApplied.author = author;
if (since) filtersApplied.since = since;
if (until) filtersApplied.until = until;
if (include_merge_commits !== undefined) filtersApplied.include_merge_commits = include_merge_commits;
if (search) filtersApplied.search = search;
return {
content: [
{
type: 'text',
text: JSON.stringify({
branch_name,
branch_head: branchHead,
commits,
total_count: totalCount,
start,
limit,
has_more: nextPageStart !== null,
next_start: nextPageStart,
filters_applied: filtersApplied
}, null, 2),
},
],
};
} catch (error) {
return this.apiClient.handleApiError(error, `listing commits for branch '${branch_name}' in ${workspace}/${repository}`);
}
}
}

View file

@ -1,6 +1,6 @@
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { BitbucketApiClient } from '../utils/api-client.js';
import { formatServerResponse, formatCloudResponse } from '../utils/formatters.js';
import { formatServerResponse, formatCloudResponse, formatServerCommit, formatCloudCommit } from '../utils/formatters.js';
import { formatSuggestionComment } from '../utils/suggestion-formatter.js';
import { DiffParser } from '../utils/diff-parser.js';
import {
@ -13,7 +13,10 @@ import {
FormattedComment,
FormattedFileChange,
CodeMatch,
MultipleMatchesError
MultipleMatchesError,
BitbucketServerCommit,
BitbucketCloudCommit,
FormattedCommit
} from '../types/bitbucket.js';
import {
isGetPullRequestArgs,
@ -21,7 +24,8 @@ import {
isCreatePullRequestArgs,
isUpdatePullRequestArgs,
isAddCommentArgs,
isMergePullRequestArgs
isMergePullRequestArgs,
isListPrCommitsArgs
} from '../types/guards.js';
export class PullRequestHandlers {
@ -1113,4 +1117,93 @@ export class PullRequestHandlers {
private selectBestMatch(matches: CodeMatch[]): CodeMatch {
return matches.sort((a, b) => b.confidence - a.confidence)[0];
}
async handleListPrCommits(args: any) {
if (!isListPrCommitsArgs(args)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid arguments for list_pr_commits'
);
}
const { workspace, repository, pull_request_id, limit = 25, start = 0 } = args;
try {
// First get the PR details to include in response
const prPath = this.apiClient.getIsServer()
? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
: `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
let prTitle = '';
try {
const pr = await this.apiClient.makeRequest<any>('get', prPath);
prTitle = pr.title;
} catch (e) {
// Ignore error, PR title is optional
}
let apiPath: string;
let params: any = {};
let commits: FormattedCommit[] = [];
let totalCount = 0;
let nextPageStart: number | null = null;
if (this.apiClient.getIsServer()) {
// Bitbucket Server API
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/commits`;
params = {
limit,
start,
withCounts: true
};
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
// Format commits
commits = (response.values || []).map((commit: BitbucketServerCommit) => formatServerCommit(commit));
totalCount = response.size || commits.length;
if (!response.isLastPage && response.nextPageStart !== undefined) {
nextPageStart = response.nextPageStart;
}
} else {
// Bitbucket Cloud API
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/commits`;
params = {
pagelen: limit,
page: Math.floor(start / limit) + 1
};
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
// Format commits
commits = (response.values || []).map((commit: BitbucketCloudCommit) => formatCloudCommit(commit));
totalCount = response.size || commits.length;
if (response.next) {
nextPageStart = start + limit;
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
pull_request_id,
pull_request_title: prTitle,
commits,
total_count: totalCount,
start,
limit,
has_more: nextPageStart !== null,
next_start: nextPageStart
}, null, 2),
},
],
};
} catch (error) {
return this.apiClient.handleApiError(error, `listing commits for pull request ${pull_request_id} in ${workspace}/${repository}`);
}
}
}

View file

@ -40,7 +40,7 @@ class BitbucketMCPServer {
this.server = new Server(
{
name: 'bitbucket-mcp-server',
version: '0.9.1',
version: '0.10.0',
},
{
capabilities: {
@ -99,6 +99,8 @@ class BitbucketMCPServer {
return this.pullRequestHandlers.handleAddComment(request.params.arguments);
case 'merge_pull_request':
return this.pullRequestHandlers.handleMergePullRequest(request.params.arguments);
case 'list_pr_commits':
return this.pullRequestHandlers.handleListPrCommits(request.params.arguments);
// Branch tools
case 'list_branches':
@ -107,6 +109,8 @@ class BitbucketMCPServer {
return this.branchHandlers.handleDeleteBranch(request.params.arguments);
case 'get_branch':
return this.branchHandlers.handleGetBranch(request.params.arguments);
case 'list_branch_commits':
return this.branchHandlers.handleListBranchCommits(request.params.arguments);
// Code Review tools
case 'get_pull_request_diff':

View file

@ -527,4 +527,84 @@ export const toolDefinitions = [
required: ['workspace', 'repository', 'file_path'],
},
},
{
name: 'list_branch_commits',
description: 'List commits in a branch with detailed information and filtering options',
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 commits from',
},
limit: {
type: 'number',
description: 'Maximum number of commits to return (default: 25)',
},
start: {
type: 'number',
description: 'Start index for pagination (default: 0)',
},
since: {
type: 'string',
description: 'ISO date string - only show commits after this date (optional)',
},
until: {
type: 'string',
description: 'ISO date string - only show commits before this date (optional)',
},
author: {
type: 'string',
description: 'Filter by author email/username (optional)',
},
include_merge_commits: {
type: 'boolean',
description: 'Include merge commits in results (default: true)',
},
search: {
type: 'string',
description: 'Search for text in commit messages (optional)',
},
},
required: ['workspace', 'repository', 'branch_name'],
},
},
{
name: 'list_pr_commits',
description: 'List all commits that are part of 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',
},
limit: {
type: 'number',
description: 'Maximum number of commits to return (default: 25)',
},
start: {
type: 'number',
description: 'Start index for pagination (default: 0)',
},
},
required: ['workspace', 'repository', 'pull_request_id'],
},
},
];

View file

@ -384,3 +384,62 @@ export interface MultipleMatchesError {
}>;
suggestion: string;
}
// Commit types
export interface BitbucketServerCommit {
id: string;
displayId: string;
message: string;
author: {
name: string;
emailAddress: string;
};
authorTimestamp: number;
committer?: {
name: string;
emailAddress: string;
};
committerTimestamp?: number;
parents: Array<{
id: string;
displayId: string;
}>;
}
export interface BitbucketCloudCommit {
hash: string;
message: string;
author: {
raw: string;
user?: {
display_name: string;
account_id: string;
};
};
date: string;
parents: Array<{
hash: string;
type: string;
}>;
links?: {
self: {
href: string;
};
html: {
href: string;
};
};
}
export interface FormattedCommit {
hash: string;
abbreviated_hash: string;
message: string;
author: {
name: string;
email: string;
};
date: string;
parents: string[];
is_merge_commit: boolean;
}

View file

@ -261,3 +261,47 @@ export const isGetFileContentArgs = (
(args.start_line === undefined || typeof args.start_line === 'number') &&
(args.line_count === undefined || typeof args.line_count === 'number') &&
(args.full_content === undefined || typeof args.full_content === 'boolean');
export const isListBranchCommitsArgs = (
args: any
): args is {
workspace: string;
repository: string;
branch_name: string;
limit?: number;
start?: number;
since?: string;
until?: string;
author?: string;
include_merge_commits?: boolean;
search?: string;
} =>
typeof args === 'object' &&
args !== null &&
typeof args.workspace === 'string' &&
typeof args.repository === 'string' &&
typeof args.branch_name === 'string' &&
(args.limit === undefined || typeof args.limit === 'number') &&
(args.start === undefined || typeof args.start === 'number') &&
(args.since === undefined || typeof args.since === 'string') &&
(args.until === undefined || typeof args.until === 'string') &&
(args.author === undefined || typeof args.author === 'string') &&
(args.include_merge_commits === undefined || typeof args.include_merge_commits === 'boolean') &&
(args.search === undefined || typeof args.search === 'string');
export const isListPrCommitsArgs = (
args: any
): args is {
workspace: string;
repository: string;
pull_request_id: number;
limit?: number;
start?: number;
} =>
typeof args === 'object' &&
args !== null &&
typeof args.workspace === 'string' &&
typeof args.repository === 'string' &&
typeof args.pull_request_id === 'number' &&
(args.limit === undefined || typeof args.limit === 'number') &&
(args.start === undefined || typeof args.start === 'number');

View file

@ -1,4 +1,11 @@
import { BitbucketServerPullRequest, BitbucketCloudPullRequest, MergeInfo } from '../types/bitbucket.js';
import {
BitbucketServerPullRequest,
BitbucketCloudPullRequest,
MergeInfo,
BitbucketServerCommit,
BitbucketCloudCommit,
FormattedCommit
} from '../types/bitbucket.js';
export function formatServerResponse(
pr: BitbucketServerPullRequest,
@ -74,3 +81,38 @@ export function formatCloudResponse(pr: BitbucketCloudPullRequest): any {
close_source_branch: pr.close_source_branch,
};
}
export function formatServerCommit(commit: BitbucketServerCommit): FormattedCommit {
return {
hash: commit.id,
abbreviated_hash: commit.displayId,
message: commit.message,
author: {
name: commit.author.name,
email: commit.author.emailAddress,
},
date: new Date(commit.authorTimestamp).toISOString(),
parents: commit.parents.map(p => p.id),
is_merge_commit: commit.parents.length > 1,
};
}
export function formatCloudCommit(commit: BitbucketCloudCommit): FormattedCommit {
// Parse the author raw string which is in format "Name <email>"
const authorMatch = commit.author.raw.match(/^(.+?)\s*<(.+?)>$/);
const authorName = authorMatch ? authorMatch[1] : (commit.author.user?.display_name || commit.author.raw);
const authorEmail = authorMatch ? authorMatch[2] : '';
return {
hash: commit.hash,
abbreviated_hash: commit.hash.substring(0, 7),
message: commit.message,
author: {
name: authorName,
email: authorEmail,
},
date: commit.date,
parents: commit.parents.map(p => p.hash),
is_merge_commit: commit.parents.length > 1,
};
}