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:
parent
e21f2dcfe8
commit
e5da36e515
10 changed files with 732 additions and 8 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/),
|
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).
|
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
|
## [0.9.1] - 2025-01-27
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
180
README.md
180
README.md
|
@ -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)
|
- `update_pull_request` - Update PR details (title, description, reviewers, destination branch)
|
||||||
- `add_comment` - Add comments to pull requests (supports replies)
|
- `add_comment` - Add comments to pull requests (supports replies)
|
||||||
- `merge_pull_request` - Merge pull requests with various strategies
|
- `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
|
- `delete_branch` - Delete branches after merge
|
||||||
|
|
||||||
#### Branch Management Tools
|
#### Branch Management Tools
|
||||||
- `list_branches` - List branches with filtering and pagination
|
- `list_branches` - List branches with filtering and pagination
|
||||||
- `delete_branch` - Delete branches (with protection checks)
|
- `delete_branch` - Delete branches (with protection checks)
|
||||||
- `get_branch` - Get detailed branch information including associated PRs
|
- `get_branch` - Get detailed branch information including associated PRs
|
||||||
|
- `list_branch_commits` - List commits in a branch with advanced filtering
|
||||||
|
|
||||||
#### File and Directory Tools
|
#### File and Directory Tools
|
||||||
- `list_directory_content` - List files and directories in a repository path
|
- `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
|
- Understanding PR review status
|
||||||
- Identifying stale branches
|
- 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 Pull Request Diff
|
||||||
|
|
||||||
Get the diff/changes for a pull request with optional filtering capabilities:
|
Get the diff/changes for a pull request with optional filtering capabilities:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@nexus2520/bitbucket-mcp-server",
|
"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",
|
"description": "MCP server for Bitbucket API integration - supports both Cloud and Server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|
|
@ -3,9 +3,17 @@ import { BitbucketApiClient } from '../utils/api-client.js';
|
||||||
import {
|
import {
|
||||||
isListBranchesArgs,
|
isListBranchesArgs,
|
||||||
isDeleteBranchArgs,
|
isDeleteBranchArgs,
|
||||||
isGetBranchArgs
|
isGetBranchArgs,
|
||||||
|
isListBranchCommitsArgs
|
||||||
} from '../types/guards.js';
|
} 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 {
|
export class BranchHandlers {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -392,4 +400,188 @@ export class BranchHandlers {
|
||||||
return this.apiClient.handleApiError(error, `getting branch '${branch_name}' in ${workspace}/${repository}`);
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { BitbucketApiClient } from '../utils/api-client.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 { formatSuggestionComment } from '../utils/suggestion-formatter.js';
|
||||||
import { DiffParser } from '../utils/diff-parser.js';
|
import { DiffParser } from '../utils/diff-parser.js';
|
||||||
import {
|
import {
|
||||||
|
@ -13,7 +13,10 @@ import {
|
||||||
FormattedComment,
|
FormattedComment,
|
||||||
FormattedFileChange,
|
FormattedFileChange,
|
||||||
CodeMatch,
|
CodeMatch,
|
||||||
MultipleMatchesError
|
MultipleMatchesError,
|
||||||
|
BitbucketServerCommit,
|
||||||
|
BitbucketCloudCommit,
|
||||||
|
FormattedCommit
|
||||||
} from '../types/bitbucket.js';
|
} from '../types/bitbucket.js';
|
||||||
import {
|
import {
|
||||||
isGetPullRequestArgs,
|
isGetPullRequestArgs,
|
||||||
|
@ -21,7 +24,8 @@ import {
|
||||||
isCreatePullRequestArgs,
|
isCreatePullRequestArgs,
|
||||||
isUpdatePullRequestArgs,
|
isUpdatePullRequestArgs,
|
||||||
isAddCommentArgs,
|
isAddCommentArgs,
|
||||||
isMergePullRequestArgs
|
isMergePullRequestArgs,
|
||||||
|
isListPrCommitsArgs
|
||||||
} from '../types/guards.js';
|
} from '../types/guards.js';
|
||||||
|
|
||||||
export class PullRequestHandlers {
|
export class PullRequestHandlers {
|
||||||
|
@ -1113,4 +1117,93 @@ export class PullRequestHandlers {
|
||||||
private selectBestMatch(matches: CodeMatch[]): CodeMatch {
|
private selectBestMatch(matches: CodeMatch[]): CodeMatch {
|
||||||
return matches.sort((a, b) => b.confidence - a.confidence)[0];
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ class BitbucketMCPServer {
|
||||||
this.server = new Server(
|
this.server = new Server(
|
||||||
{
|
{
|
||||||
name: 'bitbucket-mcp-server',
|
name: 'bitbucket-mcp-server',
|
||||||
version: '0.9.1',
|
version: '0.10.0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {
|
||||||
|
@ -99,6 +99,8 @@ class BitbucketMCPServer {
|
||||||
return this.pullRequestHandlers.handleAddComment(request.params.arguments);
|
return this.pullRequestHandlers.handleAddComment(request.params.arguments);
|
||||||
case 'merge_pull_request':
|
case 'merge_pull_request':
|
||||||
return this.pullRequestHandlers.handleMergePullRequest(request.params.arguments);
|
return this.pullRequestHandlers.handleMergePullRequest(request.params.arguments);
|
||||||
|
case 'list_pr_commits':
|
||||||
|
return this.pullRequestHandlers.handleListPrCommits(request.params.arguments);
|
||||||
|
|
||||||
// Branch tools
|
// Branch tools
|
||||||
case 'list_branches':
|
case 'list_branches':
|
||||||
|
@ -107,6 +109,8 @@ class BitbucketMCPServer {
|
||||||
return this.branchHandlers.handleDeleteBranch(request.params.arguments);
|
return this.branchHandlers.handleDeleteBranch(request.params.arguments);
|
||||||
case 'get_branch':
|
case 'get_branch':
|
||||||
return this.branchHandlers.handleGetBranch(request.params.arguments);
|
return this.branchHandlers.handleGetBranch(request.params.arguments);
|
||||||
|
case 'list_branch_commits':
|
||||||
|
return this.branchHandlers.handleListBranchCommits(request.params.arguments);
|
||||||
|
|
||||||
// Code Review tools
|
// Code Review tools
|
||||||
case 'get_pull_request_diff':
|
case 'get_pull_request_diff':
|
||||||
|
|
|
@ -527,4 +527,84 @@ export const toolDefinitions = [
|
||||||
required: ['workspace', 'repository', 'file_path'],
|
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'],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -384,3 +384,62 @@ export interface MultipleMatchesError {
|
||||||
}>;
|
}>;
|
||||||
suggestion: string;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -261,3 +261,47 @@ export const isGetFileContentArgs = (
|
||||||
(args.start_line === undefined || typeof args.start_line === 'number') &&
|
(args.start_line === undefined || typeof args.start_line === 'number') &&
|
||||||
(args.line_count === undefined || typeof args.line_count === 'number') &&
|
(args.line_count === undefined || typeof args.line_count === 'number') &&
|
||||||
(args.full_content === undefined || typeof args.full_content === 'boolean');
|
(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');
|
||||||
|
|
|
@ -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(
|
export function formatServerResponse(
|
||||||
pr: BitbucketServerPullRequest,
|
pr: BitbucketServerPullRequest,
|
||||||
|
@ -74,3 +81,38 @@ export function formatCloudResponse(pr: BitbucketCloudPullRequest): any {
|
||||||
close_source_branch: pr.close_source_branch,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue