feat: Add support for nested comment replies in Bitbucket Server (v0.6.1)
- Enhanced FormattedComment interface with replies field for nested threads - Updated comment fetching to recursively process nested structure - Fixed comment counting to include all nested replies - Updated documentation to explain reply handling differences - Maintained backward compatibility with existing tools
This commit is contained in:
parent
c6d5b0a6f5
commit
793355bdaa
6 changed files with 420 additions and 7 deletions
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -5,6 +5,46 @@ 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.6.1] - 2025-01-26
|
||||
|
||||
### Added
|
||||
- Support for nested comment replies in Bitbucket Server
|
||||
- Added `replies` field to `FormattedComment` interface to support nested comment threads
|
||||
- Comments now include nested replies that are still relevant (not orphaned or resolved)
|
||||
- Total and active comment counts now include nested replies
|
||||
|
||||
### Changed
|
||||
- Updated comment fetching logic to handle Bitbucket Server's nested comment structure
|
||||
- Server uses `comments` array inside each comment object for replies
|
||||
- Cloud continues to use `parent` field for reply relationships
|
||||
- Improved comment filtering to exclude orphaned inline comments when code has changed
|
||||
|
||||
### Fixed
|
||||
- Fixed missing comment replies in PR details - replies are now properly included in the response
|
||||
|
||||
## [0.6.0] - 2025-01-26
|
||||
|
||||
### Added
|
||||
- **Enhanced `get_pull_request` with active comments and file changes**:
|
||||
- Fetches and displays active (unresolved) comments that need attention
|
||||
- Shows up to 20 most recent active comments with:
|
||||
- Comment text, author, and creation date
|
||||
- Inline comment details (file path and line number)
|
||||
- Comment state (OPEN/RESOLVED for Server)
|
||||
- Provides comment counts:
|
||||
- `active_comment_count`: Total unresolved comments
|
||||
- `total_comment_count`: Total comments including resolved
|
||||
- Includes file change statistics:
|
||||
- List of all modified files with lines added/removed
|
||||
- File status (added, modified, removed, renamed)
|
||||
- Summary statistics (total files, lines added/removed)
|
||||
- Added new TypeScript interfaces for comments and file changes
|
||||
- Added `FormattedComment` and `FormattedFileChange` types for consistent response format
|
||||
|
||||
### Changed
|
||||
- Modified `handleGetPullRequest` to make parallel API calls for better performance
|
||||
- Enhanced error handling to gracefully continue if comment/file fetching fails
|
||||
|
||||
## [0.5.0] - 2025-01-21
|
||||
|
||||
### Added
|
||||
|
|
43
README.md
43
README.md
|
@ -188,6 +188,25 @@ Returns detailed information about the pull request including:
|
|||
- `merged_by`: Who performed the merge
|
||||
- `merged_at`: When the merge occurred
|
||||
- `merge_commit_message`: The merge commit message
|
||||
- **Active comments with nested replies** (unresolved comments that need attention):
|
||||
- `active_comments`: Array of active comments (up to 20 most recent top-level comments)
|
||||
- Comment text and author
|
||||
- Creation date
|
||||
- Whether it's an inline comment (with file path and line number)
|
||||
- **Nested replies** (for Bitbucket Server):
|
||||
- `replies`: Array of reply comments with same structure
|
||||
- Replies can be nested multiple levels deep
|
||||
- **Parent reference** (for Bitbucket Cloud):
|
||||
- `parent_id`: ID of the parent comment for replies
|
||||
- `active_comment_count`: Total count of unresolved comments (including nested replies)
|
||||
- `total_comment_count`: Total count of all comments (including resolved and replies)
|
||||
- **File changes**:
|
||||
- `file_changes`: Array of all files modified in the PR
|
||||
- File path
|
||||
- Status (added, modified, removed, or renamed)
|
||||
- Old path (for renamed files)
|
||||
- `file_changes_summary`: Summary statistics
|
||||
- Total files changed
|
||||
- And more...
|
||||
|
||||
### List Pull Requests
|
||||
|
@ -252,7 +271,7 @@ Returns a paginated list of pull requests with:
|
|||
|
||||
### Add Comment
|
||||
|
||||
Add general comments or inline comments on specific lines of code:
|
||||
Add general comments, reply to existing comments, or add inline comments on specific lines of code:
|
||||
|
||||
```typescript
|
||||
// General comment
|
||||
|
@ -262,8 +281,19 @@ Add general comments or inline comments on specific lines of code:
|
|||
"workspace": "PROJ",
|
||||
"repository": "my-repo",
|
||||
"pull_request_id": 123,
|
||||
"comment_text": "Great work! Just one small suggestion...",
|
||||
"parent_comment_id": 456 // Optional - for replies
|
||||
"comment_text": "Great work! Just one small suggestion..."
|
||||
}
|
||||
}
|
||||
|
||||
// Reply to an existing comment
|
||||
{
|
||||
"tool": "add_comment",
|
||||
"arguments": {
|
||||
"workspace": "PROJ",
|
||||
"repository": "my-repo",
|
||||
"pull_request_id": 123,
|
||||
"comment_text": "Thanks for the feedback! I've updated the code.",
|
||||
"parent_comment_id": 456 // ID of the comment you're replying to
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,6 +312,13 @@ Add general comments or inline comments on specific lines of code:
|
|||
}
|
||||
```
|
||||
|
||||
**Note on comment replies:**
|
||||
- Use `parent_comment_id` to reply to any comment (general or inline)
|
||||
- In `get_pull_request` responses:
|
||||
- Bitbucket Server shows replies nested in a `replies` array
|
||||
- Bitbucket Cloud shows a `parent_id` field for reply comments
|
||||
- You can reply to replies, creating nested conversations
|
||||
|
||||
**Note on inline comments:**
|
||||
- `file_path`: The path to the file as shown in the diff
|
||||
- `line_number`: The line number as shown in the diff
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@nexus2520/bitbucket-mcp-server",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.1",
|
||||
"description": "MCP server for Bitbucket API integration - supports both Cloud and Server",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
|
|
@ -5,7 +5,11 @@ import {
|
|||
BitbucketServerPullRequest,
|
||||
BitbucketCloudPullRequest,
|
||||
BitbucketServerActivity,
|
||||
MergeInfo
|
||||
MergeInfo,
|
||||
BitbucketCloudComment,
|
||||
BitbucketCloudFileChange,
|
||||
FormattedComment,
|
||||
FormattedFileChange
|
||||
} from '../types/bitbucket.js';
|
||||
import {
|
||||
isGetPullRequestArgs,
|
||||
|
@ -78,16 +82,49 @@ export class PullRequestHandlers {
|
|||
}
|
||||
}
|
||||
|
||||
// Fetch comments and file changes in parallel
|
||||
let comments: FormattedComment[] = [];
|
||||
let activeCommentCount = 0;
|
||||
let totalCommentCount = 0;
|
||||
let fileChanges: FormattedFileChange[] = [];
|
||||
let fileChangesSummary: any = null;
|
||||
|
||||
try {
|
||||
const [commentsResult, fileChangesResult] = await Promise.all([
|
||||
this.fetchPullRequestComments(workspace, repository, pull_request_id),
|
||||
this.fetchPullRequestFileChanges(workspace, repository, pull_request_id)
|
||||
]);
|
||||
|
||||
comments = commentsResult.comments;
|
||||
activeCommentCount = commentsResult.activeCount;
|
||||
totalCommentCount = commentsResult.totalCount;
|
||||
fileChanges = fileChangesResult.fileChanges;
|
||||
fileChangesSummary = fileChangesResult.summary;
|
||||
} catch (error) {
|
||||
// Log error but continue with PR data
|
||||
console.error('Failed to fetch additional PR data:', error);
|
||||
}
|
||||
|
||||
// Format the response based on server type
|
||||
const formattedResponse = this.apiClient.getIsServer()
|
||||
? formatServerResponse(pr as BitbucketServerPullRequest, mergeInfo, this.baseUrl)
|
||||
: formatCloudResponse(pr as BitbucketCloudPullRequest);
|
||||
|
||||
// Add comments and file changes to the response
|
||||
const enhancedResponse = {
|
||||
...formattedResponse,
|
||||
active_comments: comments,
|
||||
active_comment_count: activeCommentCount,
|
||||
total_comment_count: totalCommentCount,
|
||||
file_changes: fileChanges,
|
||||
file_changes_summary: fileChangesSummary
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(formattedResponse, null, 2),
|
||||
text: JSON.stringify(enhancedResponse, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -483,4 +520,213 @@ export class PullRequestHandlers {
|
|||
return this.apiClient.handleApiError(error, `merging pull request ${pull_request_id} in ${workspace}/${repository}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchPullRequestComments(
|
||||
workspace: string,
|
||||
repository: string,
|
||||
pullRequestId: number
|
||||
): Promise<{ comments: FormattedComment[]; activeCount: number; totalCount: number }> {
|
||||
try {
|
||||
let comments: FormattedComment[] = [];
|
||||
let activeCount = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Helper function to process nested comments recursively
|
||||
const processNestedComments = (comment: any, anchor: any): FormattedComment => {
|
||||
const formattedComment: FormattedComment = {
|
||||
id: comment.id,
|
||||
author: comment.author.displayName,
|
||||
text: comment.text,
|
||||
created_on: new Date(comment.createdDate).toISOString(),
|
||||
is_inline: !!anchor,
|
||||
file_path: anchor?.path,
|
||||
line_number: anchor?.line,
|
||||
state: comment.state
|
||||
};
|
||||
|
||||
// Process nested replies
|
||||
if (comment.comments && comment.comments.length > 0) {
|
||||
formattedComment.replies = comment.comments
|
||||
.filter((reply: any) => {
|
||||
// Apply same filters to replies
|
||||
if (reply.state === 'RESOLVED') return false;
|
||||
if (anchor && anchor.orphaned === true) return false;
|
||||
return true;
|
||||
})
|
||||
.map((reply: any) => processNestedComments(reply, anchor));
|
||||
}
|
||||
|
||||
return formattedComment;
|
||||
};
|
||||
|
||||
// Helper to count all comments including nested ones
|
||||
const countAllComments = (comment: any): number => {
|
||||
let count = 1;
|
||||
if (comment.comments && comment.comments.length > 0) {
|
||||
count += comment.comments.reduce((sum: number, reply: any) => sum + countAllComments(reply), 0);
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
// Helper to count active comments including nested ones
|
||||
const countActiveComments = (comment: any, anchor: any): number => {
|
||||
let count = 0;
|
||||
|
||||
// Check if this comment is active
|
||||
if (comment.state !== 'RESOLVED' && (!anchor || anchor.orphaned !== true)) {
|
||||
count = 1;
|
||||
}
|
||||
|
||||
// Count active nested comments
|
||||
if (comment.comments && comment.comments.length > 0) {
|
||||
count += comment.comments.reduce((sum: number, reply: any) => sum + countActiveComments(reply, anchor), 0);
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
// Bitbucket Server API - fetch from activities
|
||||
const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/activities`;
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
|
||||
params: { limit: 1000 }
|
||||
});
|
||||
|
||||
const activities = response.values || [];
|
||||
|
||||
// Filter for comment activities
|
||||
const commentActivities = activities.filter((a: any) =>
|
||||
a.action === 'COMMENTED' && a.comment
|
||||
);
|
||||
|
||||
// Count all comments including nested ones
|
||||
totalCount = commentActivities.reduce((sum: number, activity: any) => {
|
||||
return sum + countAllComments(activity.comment);
|
||||
}, 0);
|
||||
|
||||
// Count active comments including nested ones
|
||||
activeCount = commentActivities.reduce((sum: number, activity: any) => {
|
||||
return sum + countActiveComments(activity.comment, activity.commentAnchor);
|
||||
}, 0);
|
||||
|
||||
// Process top-level comments and their nested replies
|
||||
const processedComments = commentActivities
|
||||
.filter((a: any) => {
|
||||
const c = a.comment;
|
||||
const anchor = a.commentAnchor;
|
||||
|
||||
// Skip resolved comments
|
||||
if (c.state === 'RESOLVED') return false;
|
||||
|
||||
// Skip orphaned inline comments
|
||||
if (anchor && anchor.orphaned === true) return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((a: any) => processNestedComments(a.comment, a.commentAnchor));
|
||||
|
||||
// Limit to 20 top-level comments
|
||||
comments = processedComments.slice(0, 20);
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/comments`;
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
|
||||
params: { pagelen: 100 }
|
||||
});
|
||||
|
||||
const allComments = response.values || [];
|
||||
totalCount = allComments.length;
|
||||
|
||||
// Filter for active comments (not deleted or resolved) and limit to 20
|
||||
const activeComments = allComments
|
||||
.filter((c: BitbucketCloudComment) => !c.deleted && !c.resolved)
|
||||
.slice(0, 20);
|
||||
|
||||
activeCount = allComments.filter((c: BitbucketCloudComment) => !c.deleted && !c.resolved).length;
|
||||
|
||||
comments = activeComments.map((c: BitbucketCloudComment) => ({
|
||||
id: c.id,
|
||||
author: c.user.display_name,
|
||||
text: c.content.raw,
|
||||
created_on: c.created_on,
|
||||
is_inline: !!c.inline,
|
||||
file_path: c.inline?.path,
|
||||
line_number: c.inline?.to
|
||||
}));
|
||||
}
|
||||
|
||||
return { comments, activeCount, totalCount };
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch comments:', error);
|
||||
return { comments: [], activeCount: 0, totalCount: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchPullRequestFileChanges(
|
||||
workspace: string,
|
||||
repository: string,
|
||||
pullRequestId: number
|
||||
): Promise<{ fileChanges: FormattedFileChange[]; summary: any }> {
|
||||
try {
|
||||
let fileChanges: FormattedFileChange[] = [];
|
||||
let totalLinesAdded = 0;
|
||||
let totalLinesRemoved = 0;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API - use changes endpoint
|
||||
const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/changes`;
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
|
||||
params: { limit: 1000 }
|
||||
});
|
||||
|
||||
const changes = response.values || [];
|
||||
|
||||
fileChanges = changes.map((change: any) => {
|
||||
let status: 'added' | 'modified' | 'removed' | 'renamed' = 'modified';
|
||||
if (change.type === 'ADD') status = 'added';
|
||||
else if (change.type === 'DELETE') status = 'removed';
|
||||
else if (change.type === 'MOVE' || change.type === 'RENAME') status = 'renamed';
|
||||
|
||||
return {
|
||||
path: change.path.toString,
|
||||
status,
|
||||
old_path: change.srcPath?.toString
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Bitbucket Cloud API - use diffstat endpoint (has line statistics)
|
||||
const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diffstat`;
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
|
||||
params: { pagelen: 100 }
|
||||
});
|
||||
|
||||
const diffstats = response.values || [];
|
||||
|
||||
fileChanges = diffstats.map((stat: BitbucketCloudFileChange) => {
|
||||
totalLinesAdded += stat.lines_added;
|
||||
totalLinesRemoved += stat.lines_removed;
|
||||
|
||||
return {
|
||||
path: stat.path,
|
||||
status: stat.type,
|
||||
old_path: stat.old?.path
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const summary = {
|
||||
total_files: fileChanges.length
|
||||
};
|
||||
|
||||
return { fileChanges, summary };
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch file changes:', error);
|
||||
return {
|
||||
fileChanges: [],
|
||||
summary: {
|
||||
total_files: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ class BitbucketMCPServer {
|
|||
this.server = new Server(
|
||||
{
|
||||
name: 'bitbucket-mcp-server',
|
||||
version: '0.5.0',
|
||||
version: '0.6.1',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
|
|
|
@ -262,3 +262,93 @@ export interface MergeInfo {
|
|||
mergedAt?: string;
|
||||
mergeCommitMessage?: string;
|
||||
}
|
||||
|
||||
// Comment types
|
||||
export interface BitbucketServerComment {
|
||||
id: number;
|
||||
version: number;
|
||||
text: string;
|
||||
author: {
|
||||
name: string;
|
||||
emailAddress: string;
|
||||
displayName: string;
|
||||
};
|
||||
createdDate: number;
|
||||
updatedDate: number;
|
||||
state?: 'OPEN' | 'RESOLVED';
|
||||
anchor?: {
|
||||
line: number;
|
||||
lineType: string;
|
||||
fileType: string;
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BitbucketCloudComment {
|
||||
id: number;
|
||||
content: {
|
||||
raw: string;
|
||||
markup: string;
|
||||
html: string;
|
||||
};
|
||||
user: {
|
||||
display_name: string;
|
||||
account_id: string;
|
||||
};
|
||||
created_on: string;
|
||||
updated_on: string;
|
||||
deleted?: boolean;
|
||||
resolved?: boolean;
|
||||
inline?: {
|
||||
to: number;
|
||||
from?: number;
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
// File change types
|
||||
export interface BitbucketServerFileChange {
|
||||
path: {
|
||||
toString: string;
|
||||
};
|
||||
executable: boolean;
|
||||
percentUnchanged: number;
|
||||
type: string;
|
||||
nodeType: string;
|
||||
srcPath?: {
|
||||
toString: string;
|
||||
};
|
||||
linesAdded?: number;
|
||||
linesRemoved?: number;
|
||||
}
|
||||
|
||||
export interface BitbucketCloudFileChange {
|
||||
path: string;
|
||||
type: 'added' | 'modified' | 'removed' | 'renamed';
|
||||
lines_added: number;
|
||||
lines_removed: number;
|
||||
old?: {
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Formatted comment type for response
|
||||
export interface FormattedComment {
|
||||
id: number;
|
||||
author: string;
|
||||
text: string;
|
||||
created_on: string;
|
||||
is_inline: boolean;
|
||||
file_path?: string;
|
||||
line_number?: number;
|
||||
state?: 'OPEN' | 'RESOLVED';
|
||||
parent_id?: number; // For Bitbucket Cloud style replies
|
||||
replies?: FormattedComment[]; // For Bitbucket Server nested replies
|
||||
}
|
||||
|
||||
// Formatted file change type for response
|
||||
export interface FormattedFileChange {
|
||||
path: string;
|
||||
status: 'added' | 'modified' | 'removed' | 'renamed';
|
||||
old_path?: string;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue