#
tokens: 34497/50000 5/30 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/pdogra1299/bitbucket-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── memory-bank
│   ├── .clinerules
│   ├── activeContext.yml
│   ├── productContext.yml
│   ├── progress.yml
│   ├── projectbrief.yml
│   ├── systemPatterns.yml
│   └── techContext.yml
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── setup-auth.js
├── SETUP_GUIDE_SERVER.md
├── SETUP_GUIDE.md
├── src
│   ├── handlers
│   │   ├── branch-handlers.ts
│   │   ├── file-handlers.ts
│   │   ├── project-handlers.ts
│   │   ├── pull-request-handlers.ts
│   │   ├── review-handlers.ts
│   │   └── search-handlers.ts
│   ├── index.ts
│   ├── tools
│   │   └── definitions.ts
│   ├── types
│   │   ├── bitbucket.ts
│   │   └── guards.ts
│   └── utils
│       ├── api-client.ts
│       ├── diff-parser.ts
│       ├── formatters.ts
│       └── suggestion-formatter.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/types/guards.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Type guards for tool arguments
  2 | export const isGetPullRequestArgs = (
  3 |   args: any
  4 | ): args is { workspace: string; repository: string; pull_request_id: number } =>
  5 |   typeof args === 'object' &&
  6 |   args !== null &&
  7 |   typeof args.workspace === 'string' &&
  8 |   typeof args.repository === 'string' &&
  9 |   typeof args.pull_request_id === 'number';
 10 | 
 11 | export const isListPullRequestsArgs = (
 12 |   args: any
 13 | ): args is { 
 14 |   workspace: string; 
 15 |   repository: string; 
 16 |   state?: string; 
 17 |   author?: string;
 18 |   limit?: number;
 19 |   start?: number;
 20 | } =>
 21 |   typeof args === 'object' &&
 22 |   args !== null &&
 23 |   typeof args.workspace === 'string' &&
 24 |   typeof args.repository === 'string' &&
 25 |   (args.state === undefined || typeof args.state === 'string') &&
 26 |   (args.author === undefined || typeof args.author === 'string') &&
 27 |   (args.limit === undefined || typeof args.limit === 'number') &&
 28 |   (args.start === undefined || typeof args.start === 'number');
 29 | 
 30 | export const isCreatePullRequestArgs = (
 31 |   args: any
 32 | ): args is {
 33 |   workspace: string;
 34 |   repository: string;
 35 |   title: string;
 36 |   source_branch: string;
 37 |   destination_branch: string;
 38 |   description?: string;
 39 |   reviewers?: string[];
 40 |   close_source_branch?: boolean;
 41 | } =>
 42 |   typeof args === 'object' &&
 43 |   args !== null &&
 44 |   typeof args.workspace === 'string' &&
 45 |   typeof args.repository === 'string' &&
 46 |   typeof args.title === 'string' &&
 47 |   typeof args.source_branch === 'string' &&
 48 |   typeof args.destination_branch === 'string' &&
 49 |   (args.description === undefined || typeof args.description === 'string') &&
 50 |   (args.reviewers === undefined || Array.isArray(args.reviewers)) &&
 51 |   (args.close_source_branch === undefined || typeof args.close_source_branch === 'boolean');
 52 | 
 53 | export const isUpdatePullRequestArgs = (
 54 |   args: any
 55 | ): args is {
 56 |   workspace: string;
 57 |   repository: string;
 58 |   pull_request_id: number;
 59 |   title?: string;
 60 |   description?: string;
 61 |   destination_branch?: string;
 62 |   reviewers?: string[];
 63 | } =>
 64 |   typeof args === 'object' &&
 65 |   args !== null &&
 66 |   typeof args.workspace === 'string' &&
 67 |   typeof args.repository === 'string' &&
 68 |   typeof args.pull_request_id === 'number' &&
 69 |   (args.title === undefined || typeof args.title === 'string') &&
 70 |   (args.description === undefined || typeof args.description === 'string') &&
 71 |   (args.destination_branch === undefined || typeof args.destination_branch === 'string') &&
 72 |   (args.reviewers === undefined || Array.isArray(args.reviewers));
 73 | 
 74 | export const isAddCommentArgs = (
 75 |   args: any
 76 | ): args is {
 77 |   workspace: string;
 78 |   repository: string;
 79 |   pull_request_id: number;
 80 |   comment_text: string;
 81 |   parent_comment_id?: number;
 82 |   file_path?: string;
 83 |   line_number?: number;
 84 |   line_type?: 'ADDED' | 'REMOVED' | 'CONTEXT';
 85 |   suggestion?: string;
 86 |   suggestion_end_line?: number;
 87 |   code_snippet?: string;
 88 |   search_context?: {
 89 |     before?: string[];
 90 |     after?: string[];
 91 |   };
 92 |   match_strategy?: 'strict' | 'best';
 93 | } =>
 94 |   typeof args === 'object' &&
 95 |   args !== null &&
 96 |   typeof args.workspace === 'string' &&
 97 |   typeof args.repository === 'string' &&
 98 |   typeof args.pull_request_id === 'number' &&
 99 |   typeof args.comment_text === 'string' &&
100 |   (args.parent_comment_id === undefined || typeof args.parent_comment_id === 'number') &&
101 |   (args.file_path === undefined || typeof args.file_path === 'string') &&
102 |   (args.line_number === undefined || typeof args.line_number === 'number') &&
103 |   (args.line_type === undefined || ['ADDED', 'REMOVED', 'CONTEXT'].includes(args.line_type)) &&
104 |   (args.suggestion === undefined || typeof args.suggestion === 'string') &&
105 |   (args.suggestion_end_line === undefined || typeof args.suggestion_end_line === 'number') &&
106 |   (args.code_snippet === undefined || typeof args.code_snippet === 'string') &&
107 |   (args.search_context === undefined || (
108 |     typeof args.search_context === 'object' &&
109 |     (args.search_context.before === undefined || Array.isArray(args.search_context.before)) &&
110 |     (args.search_context.after === undefined || Array.isArray(args.search_context.after))
111 |   )) &&
112 |   (args.match_strategy === undefined || ['strict', 'best'].includes(args.match_strategy));
113 | 
114 | export const isMergePullRequestArgs = (
115 |   args: any
116 | ): args is {
117 |   workspace: string;
118 |   repository: string;
119 |   pull_request_id: number;
120 |   merge_strategy?: string;
121 |   close_source_branch?: boolean;
122 |   commit_message?: string;
123 | } =>
124 |   typeof args === 'object' &&
125 |   args !== null &&
126 |   typeof args.workspace === 'string' &&
127 |   typeof args.repository === 'string' &&
128 |   typeof args.pull_request_id === 'number' &&
129 |   (args.merge_strategy === undefined || typeof args.merge_strategy === 'string') &&
130 |   (args.close_source_branch === undefined || typeof args.close_source_branch === 'boolean') &&
131 |   (args.commit_message === undefined || typeof args.commit_message === 'string');
132 | 
133 | export const isDeleteBranchArgs = (
134 |   args: any
135 | ): args is {
136 |   workspace: string;
137 |   repository: string;
138 |   branch_name: string;
139 |   force?: boolean;
140 | } =>
141 |   typeof args === 'object' &&
142 |   args !== null &&
143 |   typeof args.workspace === 'string' &&
144 |   typeof args.repository === 'string' &&
145 |   typeof args.branch_name === 'string' &&
146 |   (args.force === undefined || typeof args.force === 'boolean');
147 | 
148 | export const isListBranchesArgs = (
149 |   args: any
150 | ): args is {
151 |   workspace: string;
152 |   repository: string;
153 |   filter?: string;
154 |   limit?: number;
155 |   start?: number;
156 | } =>
157 |   typeof args === 'object' &&
158 |   args !== null &&
159 |   typeof args.workspace === 'string' &&
160 |   typeof args.repository === 'string' &&
161 |   (args.filter === undefined || typeof args.filter === 'string') &&
162 |   (args.limit === undefined || typeof args.limit === 'number') &&
163 |   (args.start === undefined || typeof args.start === 'number');
164 | 
165 | export const isGetPullRequestDiffArgs = (
166 |   args: any
167 | ): args is {
168 |   workspace: string;
169 |   repository: string;
170 |   pull_request_id: number;
171 |   context_lines?: number;
172 |   include_patterns?: string[];
173 |   exclude_patterns?: string[];
174 |   file_path?: string;
175 | } =>
176 |   typeof args === 'object' &&
177 |   args !== null &&
178 |   typeof args.workspace === 'string' &&
179 |   typeof args.repository === 'string' &&
180 |   typeof args.pull_request_id === 'number' &&
181 |   (args.context_lines === undefined || typeof args.context_lines === 'number') &&
182 |   (args.include_patterns === undefined || (Array.isArray(args.include_patterns) && args.include_patterns.every((p: any) => typeof p === 'string'))) &&
183 |   (args.exclude_patterns === undefined || (Array.isArray(args.exclude_patterns) && args.exclude_patterns.every((p: any) => typeof p === 'string'))) &&
184 |   (args.file_path === undefined || typeof args.file_path === 'string');
185 | 
186 | export const isApprovePullRequestArgs = (
187 |   args: any
188 | ): args is {
189 |   workspace: string;
190 |   repository: string;
191 |   pull_request_id: number;
192 | } =>
193 |   typeof args === 'object' &&
194 |   args !== null &&
195 |   typeof args.workspace === 'string' &&
196 |   typeof args.repository === 'string' &&
197 |   typeof args.pull_request_id === 'number';
198 | 
199 | export const isRequestChangesArgs = (
200 |   args: any
201 | ): args is {
202 |   workspace: string;
203 |   repository: string;
204 |   pull_request_id: number;
205 |   comment?: string;
206 | } =>
207 |   typeof args === 'object' &&
208 |   args !== null &&
209 |   typeof args.workspace === 'string' &&
210 |   typeof args.repository === 'string' &&
211 |   typeof args.pull_request_id === 'number' &&
212 |   (args.comment === undefined || typeof args.comment === 'string');
213 | 
214 | export const isGetBranchArgs = (
215 |   args: any
216 | ): args is {
217 |   workspace: string;
218 |   repository: string;
219 |   branch_name: string;
220 |   include_merged_prs?: boolean;
221 | } =>
222 |   typeof args === 'object' &&
223 |   args !== null &&
224 |   typeof args.workspace === 'string' &&
225 |   typeof args.repository === 'string' &&
226 |   typeof args.branch_name === 'string' &&
227 |   (args.include_merged_prs === undefined || typeof args.include_merged_prs === 'boolean');
228 | 
229 | export const isListDirectoryContentArgs = (
230 |   args: any
231 | ): args is {
232 |   workspace: string;
233 |   repository: string;
234 |   path?: string;
235 |   branch?: string;
236 | } =>
237 |   typeof args === 'object' &&
238 |   args !== null &&
239 |   typeof args.workspace === 'string' &&
240 |   typeof args.repository === 'string' &&
241 |   (args.path === undefined || typeof args.path === 'string') &&
242 |   (args.branch === undefined || typeof args.branch === 'string');
243 | 
244 | export const isGetFileContentArgs = (
245 |   args: any
246 | ): args is {
247 |   workspace: string;
248 |   repository: string;
249 |   file_path: string;
250 |   branch?: string;
251 |   start_line?: number;
252 |   line_count?: number;
253 |   full_content?: boolean;
254 | } =>
255 |   typeof args === 'object' &&
256 |   args !== null &&
257 |   typeof args.workspace === 'string' &&
258 |   typeof args.repository === 'string' &&
259 |   typeof args.file_path === 'string' &&
260 |   (args.branch === undefined || typeof args.branch === 'string') &&
261 |   (args.start_line === undefined || typeof args.start_line === 'number') &&
262 |   (args.line_count === undefined || typeof args.line_count === 'number') &&
263 |   (args.full_content === undefined || typeof args.full_content === 'boolean');
264 | 
265 | export const isListBranchCommitsArgs = (
266 |   args: any
267 | ): args is {
268 |   workspace: string;
269 |   repository: string;
270 |   branch_name: string;
271 |   limit?: number;
272 |   start?: number;
273 |   since?: string;
274 |   until?: string;
275 |   author?: string;
276 |   include_merge_commits?: boolean;
277 |   search?: string;
278 |   include_build_status?: boolean;
279 | } =>
280 |   typeof args === 'object' &&
281 |   args !== null &&
282 |   typeof args.workspace === 'string' &&
283 |   typeof args.repository === 'string' &&
284 |   typeof args.branch_name === 'string' &&
285 |   (args.limit === undefined || typeof args.limit === 'number') &&
286 |   (args.start === undefined || typeof args.start === 'number') &&
287 |   (args.since === undefined || typeof args.since === 'string') &&
288 |   (args.until === undefined || typeof args.until === 'string') &&
289 |   (args.author === undefined || typeof args.author === 'string') &&
290 |   (args.include_merge_commits === undefined || typeof args.include_merge_commits === 'boolean') &&
291 |   (args.search === undefined || typeof args.search === 'string') &&
292 |   (args.include_build_status === undefined || typeof args.include_build_status === 'boolean');
293 | 
294 | export const isListPrCommitsArgs = (
295 |   args: any
296 | ): args is {
297 |   workspace: string;
298 |   repository: string;
299 |   pull_request_id: number;
300 |   limit?: number;
301 |   start?: number;
302 |   include_build_status?: boolean;
303 | } =>
304 |   typeof args === 'object' &&
305 |   args !== null &&
306 |   typeof args.workspace === 'string' &&
307 |   typeof args.repository === 'string' &&
308 |   typeof args.pull_request_id === 'number' &&
309 |   (args.limit === undefined || typeof args.limit === 'number') &&
310 |   (args.start === undefined || typeof args.start === 'number') &&
311 |   (args.include_build_status === undefined || typeof args.include_build_status === 'boolean');
312 | 
313 | export const isSearchCodeArgs = (
314 |   args: any
315 | ): args is {
316 |   workspace: string;
317 |   repository?: string;
318 |   search_query: string;
319 |   file_pattern?: string;
320 |   limit?: number;
321 |   start?: number;
322 | } =>
323 |   typeof args === 'object' &&
324 |   args !== null &&
325 |   typeof args.workspace === 'string' &&
326 |   typeof args.search_query === 'string' &&
327 |   (args.repository === undefined || typeof args.repository === 'string') &&
328 |   (args.file_pattern === undefined || typeof args.file_pattern === 'string') &&
329 |   (args.limit === undefined || typeof args.limit === 'number') &&
330 |   (args.start === undefined || typeof args.start === 'number');
331 | 
332 | export const isListProjectsArgs = (
333 |   args: any
334 | ): args is {
335 |   name?: string;
336 |   permission?: string;
337 |   limit?: number;
338 |   start?: number;
339 | } =>
340 |   typeof args === 'object' &&
341 |   args !== null &&
342 |   (args.name === undefined || typeof args.name === 'string') &&
343 |   (args.permission === undefined || typeof args.permission === 'string') &&
344 |   (args.limit === undefined || typeof args.limit === 'number') &&
345 |   (args.start === undefined || typeof args.start === 'number');
346 | 
347 | export const isListRepositoriesArgs = (
348 |   args: any
349 | ): args is {
350 |   workspace?: string;
351 |   name?: string;
352 |   permission?: string;
353 |   limit?: number;
354 |   start?: number;
355 | } =>
356 |   typeof args === 'object' &&
357 |   args !== null &&
358 |   (args.workspace === undefined || typeof args.workspace === 'string') &&
359 |   (args.name === undefined || typeof args.name === 'string') &&
360 |   (args.permission === undefined || typeof args.permission === 'string') &&
361 |   (args.limit === undefined || typeof args.limit === 'number') &&
362 |   (args.start === undefined || typeof args.start === 'number');
363 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Changelog
  2 | 
  3 | All notable changes to this project will be documented in this file.
  4 | 
  5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
  6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
  7 | 
  8 | ## [1.1.2] - 2025-10-14
  9 | 
 10 | ### Added
 11 | - **CI/CD build status support in `list_pr_commits` tool**:
 12 |   - Added `include_build_status` optional parameter to fetch build/CI status for pull request commits
 13 |   - Returns build status with counts: successful, failed, in_progress, and unknown builds
 14 |   - Uses same Bitbucket Server UI API endpoint as `list_branch_commits` for consistency
 15 |   - Graceful degradation: failures in fetching build status don't break commit listing
 16 |   - Currently only supports Bitbucket Server (Cloud has different build status APIs)
 17 |   - Useful for tracking CI/CD pipeline status for all commits in a pull request
 18 | 
 19 | ### Changed
 20 | - Enhanced README.md with comprehensive documentation for new v1.1.0 features:
 21 |   - Added usage examples and documentation for `include_build_status` parameter in `list_branch_commits` tool
 22 |   - Added complete documentation for `list_projects` tool with examples, parameters, and response format
 23 |   - Added complete documentation for `list_repositories` tool with examples, parameters, and response format
 24 |   - Improved feature list to include new Project and Repository Discovery Tools section
 25 | 
 26 | ## [1.1.0] - 2025-10-14
 27 | 
 28 | ### Added
 29 | - **CI/CD build status support in `list_branch_commits` tool**:
 30 |   - Added `include_build_status` optional parameter to fetch build/CI status for commits
 31 |   - Returns build status with counts: successful, failed, in_progress, and unknown builds
 32 |   - Uses Bitbucket Server UI API endpoint for efficient batch fetching of build summaries
 33 |   - Graceful degradation: failures in fetching build status don't break commit listing
 34 |   - Currently only supports Bitbucket Server (Cloud has different build status APIs)
 35 |   - Useful for tracking CI/CD pipeline status alongside commit history
 36 | 
 37 | - **New `list_projects` tool for project/workspace discovery**:
 38 |   - List all accessible Bitbucket projects (Server) or workspaces (Cloud)
 39 |   - Optional filtering by project name and permission level
 40 |   - Returns project metadata: key, ID, name, description, visibility, and type
 41 |   - Pagination support with `limit` and `start` parameters
 42 |   - Works with both Bitbucket Server and Cloud with unified response format
 43 | 
 44 | - **New `list_repositories` tool for repository discovery**:
 45 |   - List repositories within a specific project/workspace or across all accessible repos
 46 |   - Optional filtering by repository name and permission level
 47 |   - Returns comprehensive repository details:
 48 |     - Basic info: slug, ID, name, description, state
 49 |     - Project association: project key and name
 50 |     - Clone URLs for both HTTP(S) and SSH
 51 |     - Repository settings: visibility, forkable status
 52 |   - Pagination support with `limit` and `start` parameters
 53 |   - Bitbucket Cloud requires workspace parameter (documented in response)
 54 |   - Bitbucket Server supports listing all accessible repos without workspace filter
 55 | 
 56 | ### Changed
 57 | - Added `ProjectHandlers` class following the modular architecture pattern
 58 | - Enhanced TypeScript interfaces with project and repository types:
 59 |   - `BitbucketServerProject` and `BitbucketCloudProject`
 60 |   - `BitbucketServerRepository` and `BitbucketCloudRepository`
 61 |   - `BitbucketServerBuildSummary` and `BuildStatus`
 62 | - Added custom params serializer for multiple `commitId` parameters in build status API
 63 | - Enhanced `FormattedCommit` interface with optional `build_status` field
 64 | - Updated API client with `getBuildSummaries` method for batch build status fetching
 65 | 
 66 | ## [1.0.1] - 2025-08-08
 67 | 
 68 | ### Fixed
 69 | - **Improved search_code tool response formatting**:
 70 |   - Added simplified `formatCodeSearchOutput` for cleaner AI consumption
 71 |   - Enhanced HTML entity decoding (handles ", <, >, &, /, ')
 72 |   - Improved response structure showing file paths and line numbers clearly
 73 |   - Removed HTML formatting tags for better readability
 74 | 
 75 | ### Changed
 76 | - Search results now use simplified formatter by default for better AI tool integration
 77 | - Enhanced query display to show actual search patterns used
 78 | 
 79 | ## [1.0.0] - 2025-07-25
 80 | 
 81 | ### Added
 82 | - **New `search_code` tool for searching code across repositories**:
 83 |   - Search for code snippets, functions, or any text within Bitbucket repositories
 84 |   - Supports searching within a specific repository or across all repositories in a workspace
 85 |   - File path pattern filtering with glob patterns (e.g., `*.java`, `src/**/*.ts`)
 86 |   - Returns matched lines with highlighted segments showing exact matches
 87 |   - Pagination support for large result sets
 88 |   - Currently only supports Bitbucket Server (Cloud API support planned for future)
 89 | - Added `SearchHandlers` class following the modular architecture pattern
 90 | - Added TypeScript interfaces for search requests and responses
 91 | - Added `formatSearchResults` formatter function for consistent output
 92 | 
 93 | ### Changed
 94 | - Major version bump to 1.0.0 indicating stable API with comprehensive feature set
 95 | - Enhanced documentation with search examples
 96 | 
 97 | ## [0.10.0] - 2025-07-03
 98 | 
 99 | ### Added
100 | - **New `list_branch_commits` tool for retrieving commit history**:
101 |   - List all commits in a specific branch with detailed information
102 |   - Advanced filtering options:
103 |     - `since` and `until` parameters for date range filtering (ISO date strings)
104 |     - `author` parameter to filter by author email/username
105 |     - `include_merge_commits` parameter to include/exclude merge commits (default: true)
106 |     - `search` parameter to search in commit messages
107 |   - Returns branch head information and paginated commit list
108 |   - Each commit includes hash, message, author details, date, parents, and merge status
109 |   - Supports both Bitbucket Server and Cloud APIs with appropriate parameter mapping
110 |   - Useful for reviewing commit history, tracking changes, and analyzing branch activity
111 | 
112 | - **New `list_pr_commits` tool for pull request commits**:
113 |   - List all commits that are part of a specific pull request
114 |   - Returns PR title and paginated commit list
115 |   - Simpler than branch commits - focused specifically on PR changes
116 |   - Each commit includes same detailed information as branch commits
117 |   - Supports pagination with `limit` and `start` parameters
118 |   - Useful for reviewing all changes in a PR before merging
119 | 
120 | ### Changed
121 | - Added new TypeScript interfaces for commit types:
122 |   - `BitbucketServerCommit` and `BitbucketCloudCommit` for API responses
123 |   - `FormattedCommit` for consistent commit representation
124 | - Added formatter functions `formatServerCommit` and `formatCloudCommit` for unified output
125 | - Enhanced type guards with `isListBranchCommitsArgs` and `isListPrCommitsArgs`
126 | 
127 | ## [0.9.1] - 2025-01-27
128 | 
129 | ### Fixed
130 | - **Fixed `update_pull_request` reviewer preservation**:
131 |   - When updating a PR without specifying reviewers, existing reviewers are now preserved
132 |   - Previously, omitting the `reviewers` parameter would clear all reviewers
133 |   - Now properly includes existing reviewers in the API request when not explicitly updating them
134 |   - When updating reviewers, approval status is preserved for existing reviewers
135 |   - This prevents accidentally removing reviewers when only updating PR title or description
136 | 
137 | ### Changed
138 | - Updated tool documentation to clarify reviewer behavior in `update_pull_request`
139 | - Enhanced README with detailed explanation of reviewer handling
140 | 
141 | ## [0.9.0] - 2025-01-26
142 | 
143 | ### Added
144 | - **Code snippet support in `add_comment` tool**:
145 |   - Added `code_snippet` parameter to find line numbers automatically using code text
146 |   - Added `search_context` parameter with `before` and `after` arrays to disambiguate multiple matches
147 |   - Added `match_strategy` parameter with options:
148 |     - `"strict"` (default): Fails with detailed error when multiple matches found
149 |     - `"best"`: Auto-selects the highest confidence match
150 |   - Returns detailed error with all occurrences when multiple matches found in strict mode
151 |   - Particularly useful for AI-powered code review tools that analyze diffs
152 | - Created comprehensive line matching algorithm that:
153 |   - Parses diffs to find exact code snippets
154 |   - Calculates confidence scores based on context matching
155 |   - Handles added, removed, and context lines appropriately
156 | 
157 | ### Changed
158 | - Enhanced `add_comment` tool to resolve line numbers from code snippets when `line_number` is not provided
159 | - Improved error messages to include preview and suggestions for resolving ambiguous matches
160 | 
161 | ## [0.8.0] - 2025-01-26
162 | 
163 | ### Added
164 | - **Code suggestions support in `add_comment` tool**:
165 |   - Added `suggestion` parameter to add code suggestions in comments
166 |   - Added `suggestion_end_line` parameter for multi-line suggestions
167 |   - Suggestions are formatted using GitHub-style markdown ````suggestion` blocks
168 |   - Works with both single-line and multi-line code replacements
169 |   - Requires `file_path` and `line_number` to be specified when using suggestions
170 |   - Compatible with both Bitbucket Cloud and Server
171 | - Created `suggestion-formatter.ts` utility for formatting suggestion comments
172 | 
173 | ### Changed
174 | - Enhanced `add_comment` tool to validate suggestion requirements
175 | - Updated tool response to indicate when a comment contains a suggestion
176 | 
177 | ## [0.7.0] - 2025-01-26
178 | 
179 | ### Added
180 | - **Enhanced `get_pull_request_diff` with filtering capabilities**:
181 |   - Added `include_patterns` parameter to filter diff by file patterns (whitelist)
182 |   - Added `exclude_patterns` parameter to exclude files from diff (blacklist)
183 |   - Added `file_path` parameter to get diff for a specific file only
184 |   - Patterns support standard glob syntax (e.g., `*.js`, `src/**/*.res`, `node_modules/**`)
185 |   - Response includes filtering metadata showing total files, included/excluded counts, and excluded file list
186 | - Added `minimatch` dependency for glob pattern matching
187 | - Created `DiffParser` utility class for parsing and filtering unified diff format
188 | 
189 | ### Changed
190 | - Modified `get_pull_request_diff` tool to support optional filtering without breaking existing usage
191 | - Updated tool definition and type guards to include new optional parameters
192 | - Enhanced documentation with comprehensive examples of filtering usage
193 | 
194 | ## [0.6.1] - 2025-01-26
195 | 
196 | ### Added
197 | - Support for nested comment replies in Bitbucket Server
198 |   - Added `replies` field to `FormattedComment` interface to support nested comment threads
199 |   - Comments now include nested replies that are still relevant (not orphaned or resolved)
200 |   - Total and active comment counts now include nested replies
201 | 
202 | ### Changed
203 | - Updated comment fetching logic to handle Bitbucket Server's nested comment structure
204 |   - Server uses `comments` array inside each comment object for replies
205 |   - Cloud continues to use `parent` field for reply relationships
206 | - Improved comment filtering to exclude orphaned inline comments when code has changed
207 | 
208 | ### Fixed
209 | - Fixed missing comment replies in PR details - replies are now properly included in the response
210 | 
211 | ## [0.6.0] - 2025-01-26
212 | 
213 | ### Added
214 | - **Enhanced `get_pull_request` with active comments and file changes**:
215 |   - Fetches and displays active (unresolved) comments that need attention
216 |   - Shows up to 20 most recent active comments with:
217 |     - Comment text, author, and creation date
218 |     - Inline comment details (file path and line number)
219 |     - Comment state (OPEN/RESOLVED for Server)
220 |   - Provides comment counts:
221 |     - `active_comment_count`: Total unresolved comments
222 |     - `total_comment_count`: Total comments including resolved
223 |   - Includes file change statistics:
224 |     - List of all modified files with lines added/removed
225 |     - File status (added, modified, removed, renamed)
226 |     - Summary statistics (total files, lines added/removed)
227 | - Added new TypeScript interfaces for comments and file changes
228 | - Added `FormattedComment` and `FormattedFileChange` types for consistent response format
229 | 
230 | ### Changed
231 | - Modified `handleGetPullRequest` to make parallel API calls for better performance
232 | - Enhanced error handling to gracefully continue if comment/file fetching fails
233 | 
234 | ## [0.5.0] - 2025-01-21
235 | 
236 | ### Added
237 | - **New file and directory handling tools**:
238 |   - `list_directory_content` - List files and directories in any repository path
239 |     - Shows file/directory type, size, and full paths
240 |     - Supports browsing specific branches
241 |     - Works with both Bitbucket Server and Cloud APIs
242 |   - `get_file_content` - Retrieve file content with smart truncation for large files
243 |     - Automatic smart defaults by file type (config: 200 lines, docs: 300 lines, code: 500 lines)
244 |     - Pagination support with `start_line` and `line_count` parameters
245 |     - Tail functionality using negative `start_line` values (e.g., -50 for last 50 lines)
246 |     - Automatic truncation for files >50KB to prevent token overload
247 |     - Files >1MB require explicit `full_content: true` parameter
248 |     - Returns metadata including file size, encoding, and last modified info
249 | - Added `FileHandlers` class following existing modular architecture patterns
250 | - Added TypeScript interfaces for file/directory entries and metadata
251 | - Added type guards `isListDirectoryContentArgs` and `isGetFileContentArgs`
252 | 
253 | ### Changed
254 | - Enhanced documentation with comprehensive examples for file handling tools
255 | 
256 | ## [0.4.0] - 2025-01-21
257 | 
258 | ### Added
259 | - **New `get_branch` tool for comprehensive branch information**:
260 |   - Returns detailed branch information including name, ID, and latest commit details
261 |   - Lists all open pull requests originating from the branch with approval status
262 |   - Optionally includes merged pull requests when `include_merged_prs` is true
263 |   - Provides useful statistics like PR counts and days since last commit
264 |   - Supports both Bitbucket Server and Cloud APIs
265 |   - Particularly useful for checking if a branch has open PRs before deletion
266 | - Added TypeScript interfaces for `BitbucketServerBranch` and `BitbucketCloudBranch`
267 | - Added type guard `isGetBranchArgs` for input validation
268 | 
269 | ### Changed
270 | - Updated documentation to include the new `get_branch` tool with comprehensive examples
271 | 
272 | ## [0.3.0] - 2025-01-06
273 | 
274 | ### Added
275 | - **Enhanced merge commit details in `get_pull_request`**:
276 |   - Added `merge_commit_hash` field for both Cloud and Server
277 |   - Added `merged_by` field showing who performed the merge
278 |   - Added `merged_at` timestamp for when the merge occurred
279 |   - Added `merge_commit_message` with the merge commit message
280 |   - For Bitbucket Server: Fetches merge details from activities API when PR is merged
281 |   - For Bitbucket Cloud: Extracts merge information from existing response fields
282 | 
283 | ### Changed
284 | - **Major code refactoring for better maintainability**:
285 |   - Split monolithic `index.ts` into modular architecture
286 |   - Created separate handler classes for different tool categories:
287 |     - `PullRequestHandlers` for PR lifecycle operations
288 |     - `BranchHandlers` for branch management
289 |     - `ReviewHandlers` for code review tools
290 |   - Extracted types into dedicated files (`types/bitbucket.ts`, `types/guards.ts`)
291 |   - Created utility modules (`utils/api-client.ts`, `utils/formatters.ts`)
292 |   - Centralized tool definitions in `tools/definitions.ts`
293 | - Improved error handling and API client abstraction
294 | - Better separation of concerns between Cloud and Server implementations
295 | 
296 | ### Fixed
297 | - Improved handling of merge commit information retrieval failures
298 | - Fixed API parameter passing for GET requests across all handlers (was passing config as third parameter instead of fourth)
299 | - Updated Bitbucket Server branch listing to use `/rest/api/latest/` endpoint with proper parameters
300 | - Branch filtering now works correctly with the `filterText` parameter for Bitbucket Server
301 | 
302 | ## [0.2.0] - 2025-06-04
303 | 
304 | ### Added
305 | - Complete implementation of all Bitbucket MCP tools
306 | - Support for both Bitbucket Cloud and Server
307 | - Core PR lifecycle tools:
308 |   - `create_pull_request` - Create new pull requests
309 |   - `update_pull_request` - Update PR details
310 |   - `merge_pull_request` - Merge pull requests
311 |   - `list_branches` - List repository branches
312 |   - `delete_branch` - Delete branches
313 | - Enhanced `add_comment` with inline comment support
314 | - Code review tools:
315 |   - `get_pull_request_diff` - Get PR diff/changes
316 |   - `approve_pull_request` - Approve PRs
317 |   - `unapprove_pull_request` - Remove approval
318 |   - `request_changes` - Request changes on PRs
319 |   - `remove_requested_changes` - Remove change requests
320 | - npm package configuration for easy installation via npx
321 | 
322 | ### Fixed
323 | - Author filter for Bitbucket Server (uses `role.1=AUTHOR` and `username.1=email`)
324 | - Branch deletion handling for 204 No Content responses
325 | 
326 | ### Changed
327 | - Package name to `@nexus2520/bitbucket-mcp-server` for npm publishing
328 | 
329 | ## [0.1.0] - 2025-06-03
330 | 
331 | ### Added
332 | - Initial implementation with basic tools:
333 |   - `get_pull_request` - Get PR details
334 |   - `list_pull_requests` - List PRs with filters
335 | - Support for Bitbucket Cloud with app passwords
336 | - Support for Bitbucket Server with HTTP access tokens
337 | - Authentication setup script
338 | - Comprehensive documentation
339 | 
```

--------------------------------------------------------------------------------
/src/handlers/branch-handlers.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
  2 | import { BitbucketApiClient } from '../utils/api-client.js';
  3 | import {
  4 |   isListBranchesArgs,
  5 |   isDeleteBranchArgs,
  6 |   isGetBranchArgs,
  7 |   isListBranchCommitsArgs
  8 | } from '../types/guards.js';
  9 | import { 
 10 |   BitbucketServerBranch, 
 11 |   BitbucketCloudBranch,
 12 |   BitbucketServerCommit,
 13 |   BitbucketCloudCommit,
 14 |   FormattedCommit
 15 | } from '../types/bitbucket.js';
 16 | import { formatServerCommit, formatCloudCommit } from '../utils/formatters.js';
 17 | 
 18 | export class BranchHandlers {
 19 |   constructor(
 20 |     private apiClient: BitbucketApiClient,
 21 |     private baseUrl: string
 22 |   ) {}
 23 | 
 24 |   async handleListBranches(args: any) {
 25 |     if (!isListBranchesArgs(args)) {
 26 |       throw new McpError(
 27 |         ErrorCode.InvalidParams,
 28 |         'Invalid arguments for list_branches'
 29 |       );
 30 |     }
 31 | 
 32 |     const { workspace, repository, filter, limit = 25, start = 0 } = args;
 33 | 
 34 |     try {
 35 |       let apiPath: string;
 36 |       let params: any = {};
 37 | 
 38 |       if (this.apiClient.getIsServer()) {
 39 |         // Bitbucket Server API - using latest version for better filtering support
 40 |         apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`;
 41 |         params = {
 42 |           limit,
 43 |           start,
 44 |           details: true,
 45 |           orderBy: 'MODIFICATION'
 46 |         };
 47 |         if (filter) {
 48 |           params.filterText = filter;
 49 |         }
 50 |       } else {
 51 |         // Bitbucket Cloud API
 52 |         apiPath = `/repositories/${workspace}/${repository}/refs/branches`;
 53 |         params = {
 54 |           pagelen: limit,
 55 |           page: Math.floor(start / limit) + 1,
 56 |         };
 57 |         if (filter) {
 58 |           params.q = `name ~ "${filter}"`;
 59 |         }
 60 |       }
 61 | 
 62 |       const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
 63 | 
 64 |       // Format the response
 65 |       let branches: any[] = [];
 66 |       let totalCount = 0;
 67 |       let nextPageStart = null;
 68 | 
 69 |       if (this.apiClient.getIsServer()) {
 70 |         // Bitbucket Server response
 71 |         branches = (response.values || []).map((branch: any) => ({
 72 |           name: branch.displayId,
 73 |           id: branch.id,
 74 |           latest_commit: branch.latestCommit,
 75 |           is_default: branch.isDefault || false
 76 |         }));
 77 |         totalCount = response.size || 0;
 78 |         if (!response.isLastPage && response.nextPageStart !== undefined) {
 79 |           nextPageStart = response.nextPageStart;
 80 |         }
 81 |       } else {
 82 |         // Bitbucket Cloud response
 83 |         branches = (response.values || []).map((branch: any) => ({
 84 |           name: branch.name,
 85 |           target: branch.target.hash,
 86 |           is_default: branch.name === 'main' || branch.name === 'master'
 87 |         }));
 88 |         totalCount = response.size || 0;
 89 |         if (response.next) {
 90 |           nextPageStart = start + limit;
 91 |         }
 92 |       }
 93 | 
 94 |       return {
 95 |         content: [
 96 |           {
 97 |             type: 'text',
 98 |             text: JSON.stringify({
 99 |               branches,
100 |               total_count: totalCount,
101 |               start,
102 |               limit,
103 |               has_more: nextPageStart !== null,
104 |               next_start: nextPageStart
105 |             }, null, 2),
106 |           },
107 |         ],
108 |       };
109 |     } catch (error) {
110 |       return this.apiClient.handleApiError(error, `listing branches in ${workspace}/${repository}`);
111 |     }
112 |   }
113 | 
114 |   async handleDeleteBranch(args: any) {
115 |     if (!isDeleteBranchArgs(args)) {
116 |       throw new McpError(
117 |         ErrorCode.InvalidParams,
118 |         'Invalid arguments for delete_branch'
119 |       );
120 |     }
121 | 
122 |     const { workspace, repository, branch_name, force } = args;
123 | 
124 |     try {
125 |       let apiPath: string;
126 | 
127 |       if (this.apiClient.getIsServer()) {
128 |         // First, we need to get the branch details to find the latest commit
129 |         const branchesPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`;
130 |         const branchesResponse = await this.apiClient.makeRequest<any>('get', branchesPath, undefined, {
131 |           params: {
132 |             filterText: branch_name,
133 |             limit: 100
134 |           }
135 |         });
136 |         
137 |         // Find the exact branch
138 |         const branch = branchesResponse.values?.find((b: any) => b.displayId === branch_name);
139 |         if (!branch) {
140 |           throw new Error(`Branch '${branch_name}' not found`);
141 |         }
142 |         
143 |         // Now delete using branch-utils endpoint with correct format
144 |         apiPath = `/rest/branch-utils/latest/projects/${workspace}/repos/${repository}/branches`;
145 |         
146 |         try {
147 |           await this.apiClient.makeRequest<any>('delete', apiPath, {
148 |             name: branch_name,
149 |             endPoint: branch.latestCommit
150 |           });
151 |         } catch (deleteError: any) {
152 |           // If the error is about empty response but status is 204 (No Content), it's successful
153 |           if (deleteError.originalError?.response?.status === 204 || 
154 |               deleteError.message?.includes('No content to map')) {
155 |             // Branch was deleted successfully
156 |           } else {
157 |             throw deleteError;
158 |           }
159 |         }
160 |       } else {
161 |         // Bitbucket Cloud API
162 |         apiPath = `/repositories/${workspace}/${repository}/refs/branches/${branch_name}`;
163 |         try {
164 |           await this.apiClient.makeRequest<any>('delete', apiPath);
165 |         } catch (deleteError: any) {
166 |           // If the error is about empty response but status is 204 (No Content), it's successful
167 |           if (deleteError.originalError?.response?.status === 204 || 
168 |               deleteError.message?.includes('No content to map')) {
169 |             // Branch was deleted successfully
170 |           } else {
171 |             throw deleteError;
172 |           }
173 |         }
174 |       }
175 | 
176 |       return {
177 |         content: [
178 |           {
179 |             type: 'text',
180 |             text: JSON.stringify({
181 |               message: `Branch '${branch_name}' deleted successfully`,
182 |               branch: branch_name,
183 |               repository: `${workspace}/${repository}`
184 |             }, null, 2),
185 |           },
186 |         ],
187 |       };
188 |     } catch (error) {
189 |       return this.apiClient.handleApiError(error, `deleting branch '${branch_name}' in ${workspace}/${repository}`);
190 |     }
191 |   }
192 | 
193 |   async handleGetBranch(args: any) {
194 |     if (!isGetBranchArgs(args)) {
195 |       throw new McpError(
196 |         ErrorCode.InvalidParams,
197 |         'Invalid arguments for get_branch'
198 |       );
199 |     }
200 | 
201 |     const { workspace, repository, branch_name, include_merged_prs = false } = args;
202 | 
203 |     try {
204 |       // Step 1: Get branch details
205 |       let branchInfo: any;
206 |       let branchCommitInfo: any = {};
207 | 
208 |       if (this.apiClient.getIsServer()) {
209 |         // Bitbucket Server - get branch details
210 |         const branchesPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`;
211 |         const branchesResponse = await this.apiClient.makeRequest<any>('get', branchesPath, undefined, {
212 |           params: {
213 |             filterText: branch_name,
214 |             limit: 100,
215 |             details: true
216 |           }
217 |         });
218 |         
219 |         // Find the exact branch
220 |         const branch = branchesResponse.values?.find((b: BitbucketServerBranch) => b.displayId === branch_name);
221 |         if (!branch) {
222 |           throw new Error(`Branch '${branch_name}' not found`);
223 |         }
224 | 
225 |         branchInfo = {
226 |           name: branch.displayId,
227 |           id: branch.id,
228 |           latest_commit: {
229 |             id: branch.latestCommit,
230 |             message: branch.metadata?.['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata']?.message || null,
231 |             author: branch.metadata?.['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata']?.author || null,
232 |             date: branch.metadata?.['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata']?.authorTimestamp 
233 |               ? new Date(branch.metadata['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata'].authorTimestamp).toISOString()
234 |               : null
235 |           },
236 |           is_default: branch.isDefault || false
237 |         };
238 |       } else {
239 |         // Bitbucket Cloud - get branch details
240 |         const branchPath = `/repositories/${workspace}/${repository}/refs/branches/${encodeURIComponent(branch_name)}`;
241 |         const branch = await this.apiClient.makeRequest<BitbucketCloudBranch>('get', branchPath);
242 | 
243 |         branchInfo = {
244 |           name: branch.name,
245 |           id: `refs/heads/${branch.name}`,
246 |           latest_commit: {
247 |             id: branch.target.hash,
248 |             message: branch.target.message,
249 |             author: branch.target.author.user?.display_name || branch.target.author.raw,
250 |             date: branch.target.date
251 |           },
252 |           is_default: false // Will check this with default branch info
253 |         };
254 | 
255 |         // Check if this is the default branch
256 |         try {
257 |           const repoPath = `/repositories/${workspace}/${repository}`;
258 |           const repoInfo = await this.apiClient.makeRequest<any>('get', repoPath);
259 |           branchInfo.is_default = branch.name === repoInfo.mainbranch?.name;
260 |         } catch (e) {
261 |           // Ignore error, just assume not default
262 |         }
263 |       }
264 | 
265 |       // Step 2: Get open PRs from this branch
266 |       let openPRs: any[] = [];
267 |       
268 |       if (this.apiClient.getIsServer()) {
269 |         // Bitbucket Server
270 |         const prPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
271 |         const prResponse = await this.apiClient.makeRequest<any>('get', prPath, undefined, {
272 |           params: {
273 |             state: 'OPEN',
274 |             direction: 'OUTGOING',
275 |             at: `refs/heads/${branch_name}`,
276 |             limit: 100
277 |           }
278 |         });
279 | 
280 |         openPRs = (prResponse.values || []).map((pr: any) => ({
281 |           id: pr.id,
282 |           title: pr.title,
283 |           destination_branch: pr.toRef.displayId,
284 |           author: pr.author.user.displayName,
285 |           created_on: new Date(pr.createdDate).toISOString(),
286 |           reviewers: pr.reviewers.map((r: any) => r.user.displayName),
287 |           approval_status: {
288 |             approved_by: pr.reviewers.filter((r: any) => r.approved).map((r: any) => r.user.displayName),
289 |             changes_requested_by: pr.reviewers.filter((r: any) => r.status === 'NEEDS_WORK').map((r: any) => r.user.displayName),
290 |             pending: pr.reviewers.filter((r: any) => !r.approved && r.status !== 'NEEDS_WORK').map((r: any) => r.user.displayName)
291 |           },
292 |           url: `${this.baseUrl}/projects/${workspace}/repos/${repository}/pull-requests/${pr.id}`
293 |         }));
294 |       } else {
295 |         // Bitbucket Cloud
296 |         const prPath = `/repositories/${workspace}/${repository}/pullrequests`;
297 |         const prResponse = await this.apiClient.makeRequest<any>('get', prPath, undefined, {
298 |           params: {
299 |             state: 'OPEN',
300 |             q: `source.branch.name="${branch_name}"`,
301 |             pagelen: 50
302 |           }
303 |         });
304 | 
305 |         openPRs = (prResponse.values || []).map((pr: any) => ({
306 |           id: pr.id,
307 |           title: pr.title,
308 |           destination_branch: pr.destination.branch.name,
309 |           author: pr.author.display_name,
310 |           created_on: pr.created_on,
311 |           reviewers: pr.reviewers.map((r: any) => r.display_name),
312 |           approval_status: {
313 |             approved_by: pr.participants.filter((p: any) => p.approved).map((p: any) => p.user.display_name),
314 |             changes_requested_by: [], // Cloud doesn't have explicit "changes requested" status
315 |             pending: pr.reviewers.filter((r: any) => !pr.participants.find((p: any) => p.user.account_id === r.account_id && p.approved))
316 |               .map((r: any) => r.display_name)
317 |           },
318 |           url: pr.links.html.href
319 |         }));
320 |       }
321 | 
322 |       // Step 3: Optionally get merged PRs
323 |       let mergedPRs: any[] = [];
324 |       
325 |       if (include_merged_prs) {
326 |         if (this.apiClient.getIsServer()) {
327 |           // Bitbucket Server
328 |           const mergedPrPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
329 |           const mergedPrResponse = await this.apiClient.makeRequest<any>('get', mergedPrPath, undefined, {
330 |             params: {
331 |               state: 'MERGED',
332 |               direction: 'OUTGOING',
333 |               at: `refs/heads/${branch_name}`,
334 |               limit: 25
335 |             }
336 |           });
337 | 
338 |           mergedPRs = (mergedPrResponse.values || []).map((pr: any) => ({
339 |             id: pr.id,
340 |             title: pr.title,
341 |             merged_at: new Date(pr.updatedDate).toISOString(), // Using updated date as merge date
342 |             merged_by: pr.participants.find((p: any) => p.role === 'PARTICIPANT' && p.approved)?.user.displayName || 'Unknown'
343 |           }));
344 |         } else {
345 |           // Bitbucket Cloud
346 |           const mergedPrPath = `/repositories/${workspace}/${repository}/pullrequests`;
347 |           const mergedPrResponse = await this.apiClient.makeRequest<any>('get', mergedPrPath, undefined, {
348 |             params: {
349 |               state: 'MERGED',
350 |               q: `source.branch.name="${branch_name}"`,
351 |               pagelen: 25
352 |             }
353 |           });
354 | 
355 |           mergedPRs = (mergedPrResponse.values || []).map((pr: any) => ({
356 |             id: pr.id,
357 |             title: pr.title,
358 |             merged_at: pr.updated_on,
359 |             merged_by: pr.closed_by?.display_name || 'Unknown'
360 |           }));
361 |         }
362 |       }
363 | 
364 |       // Step 4: Calculate statistics
365 |       const daysSinceLastCommit = branchInfo.latest_commit.date 
366 |         ? Math.floor((Date.now() - new Date(branchInfo.latest_commit.date).getTime()) / (1000 * 60 * 60 * 24))
367 |         : null;
368 | 
369 |       // Step 5: Format and return combined response
370 |       return {
371 |         content: [
372 |           {
373 |             type: 'text',
374 |             text: JSON.stringify({
375 |               branch: branchInfo,
376 |               open_pull_requests: openPRs,
377 |               merged_pull_requests: mergedPRs,
378 |               statistics: {
379 |                 total_open_prs: openPRs.length,
380 |                 total_merged_prs: mergedPRs.length,
381 |                 days_since_last_commit: daysSinceLastCommit
382 |               }
383 |             }, null, 2),
384 |           },
385 |         ],
386 |       };
387 |     } catch (error: any) {
388 |       // Handle specific not found error
389 |       if (error.message?.includes('not found')) {
390 |         return {
391 |           content: [
392 |             {
393 |               type: 'text',
394 |               text: `Branch '${branch_name}' not found in ${workspace}/${repository}`,
395 |             },
396 |           ],
397 |           isError: true,
398 |         };
399 |       }
400 |       return this.apiClient.handleApiError(error, `getting branch '${branch_name}' in ${workspace}/${repository}`);
401 |     }
402 |   }
403 | 
404 |   async handleListBranchCommits(args: any) {
405 |     if (!isListBranchCommitsArgs(args)) {
406 |       throw new McpError(
407 |         ErrorCode.InvalidParams,
408 |         'Invalid arguments for list_branch_commits'
409 |       );
410 |     }
411 | 
412 |     const {
413 |       workspace,
414 |       repository,
415 |       branch_name,
416 |       limit = 25,
417 |       start = 0,
418 |       since,
419 |       until,
420 |       author,
421 |       include_merge_commits = true,
422 |       search,
423 |       include_build_status = false
424 |     } = args;
425 | 
426 |     try {
427 |       let apiPath: string;
428 |       let params: any = {};
429 |       let commits: FormattedCommit[] = [];
430 |       let totalCount = 0;
431 |       let nextPageStart: number | null = null;
432 | 
433 |       if (this.apiClient.getIsServer()) {
434 |         // Bitbucket Server API
435 |         apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/commits`;
436 |         params = {
437 |           until: `refs/heads/${branch_name}`,
438 |           limit,
439 |           start,
440 |           withCounts: true
441 |         };
442 | 
443 |         // Add filters
444 |         if (since) {
445 |           params.since = since;
446 |         }
447 |         if (!include_merge_commits) {
448 |           params.merges = 'exclude';
449 |         }
450 | 
451 |         const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
452 | 
453 |         // Format commits
454 |         commits = (response.values || []).map((commit: BitbucketServerCommit) => formatServerCommit(commit));
455 |         
456 |         // Apply client-side filters for Server API
457 |         if (author) {
458 |           // Filter by author email or name
459 |           commits = commits.filter(c => 
460 |             c.author.email === author || 
461 |             c.author.name === author ||
462 |             c.author.email.toLowerCase() === author.toLowerCase() ||
463 |             c.author.name.toLowerCase() === author.toLowerCase()
464 |           );
465 |         }
466 |         
467 |         // Filter by date if 'until' is provided (Server API doesn't support 'until' param directly)
468 |         if (until) {
469 |           const untilDate = new Date(until).getTime();
470 |           commits = commits.filter(c => new Date(c.date).getTime() <= untilDate);
471 |         }
472 | 
473 |         // Filter by message search if provided
474 |         if (search) {
475 |           const searchLower = search.toLowerCase();
476 |           commits = commits.filter(c => c.message.toLowerCase().includes(searchLower));
477 |         }
478 | 
479 |         // If we applied client-side filters, update the total count
480 |         if (author || until || search) {
481 |           totalCount = commits.length;
482 |           // Can't determine if there are more results when filtering client-side
483 |           nextPageStart = null;
484 |         } else {
485 |           totalCount = response.size || commits.length;
486 |           if (!response.isLastPage && response.nextPageStart !== undefined) {
487 |             nextPageStart = response.nextPageStart;
488 |           }
489 |         }
490 |       } else {
491 |         // Bitbucket Cloud API
492 |         apiPath = `/repositories/${workspace}/${repository}/commits/${encodeURIComponent(branch_name)}`;
493 |         params = {
494 |           pagelen: limit,
495 |           page: Math.floor(start / limit) + 1
496 |         };
497 | 
498 |         // Build query string for filters
499 |         const queryParts: string[] = [];
500 |         if (author) {
501 |           queryParts.push(`author.raw ~ "${author}"`);
502 |         }
503 |         if (!include_merge_commits) {
504 |           // Cloud API doesn't have direct merge exclusion, we'll filter client-side
505 |         }
506 |         if (queryParts.length > 0) {
507 |           params.q = queryParts.join(' AND ');
508 |         }
509 | 
510 |         const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
511 | 
512 |         // Format commits
513 |         let cloudCommits = (response.values || []).map((commit: BitbucketCloudCommit) => formatCloudCommit(commit));
514 | 
515 |         // Apply client-side filters
516 |         if (!include_merge_commits) {
517 |           cloudCommits = cloudCommits.filter((c: FormattedCommit) => !c.is_merge_commit);
518 |         }
519 |         if (since) {
520 |           const sinceDate = new Date(since).getTime();
521 |           cloudCommits = cloudCommits.filter((c: FormattedCommit) => new Date(c.date).getTime() >= sinceDate);
522 |         }
523 |         if (until) {
524 |           const untilDate = new Date(until).getTime();
525 |           cloudCommits = cloudCommits.filter((c: FormattedCommit) => new Date(c.date).getTime() <= untilDate);
526 |         }
527 |         if (search) {
528 |           const searchLower = search.toLowerCase();
529 |           cloudCommits = cloudCommits.filter((c: FormattedCommit) => c.message.toLowerCase().includes(searchLower));
530 |         }
531 | 
532 |         commits = cloudCommits;
533 |         totalCount = response.size || commits.length;
534 |         if (response.next) {
535 |           nextPageStart = start + limit;
536 |         }
537 |       }
538 | 
539 |       // Fetch build status if requested (Server only)
540 |       if (include_build_status && this.apiClient.getIsServer() && commits.length > 0) {
541 |         try {
542 |           // Extract commit hashes (use full hash, not abbreviated)
543 |           const commitIds = commits.map(c => c.hash);
544 | 
545 |           // Fetch build summaries for all commits
546 |           const buildSummaries = await this.apiClient.getBuildSummaries(
547 |             workspace,
548 |             repository,
549 |             commitIds
550 |           );
551 | 
552 |           // Merge build status into commits
553 |           commits = commits.map(commit => {
554 |             const buildData = buildSummaries[commit.hash];
555 |             if (buildData) {
556 |               return {
557 |                 ...commit,
558 |                 build_status: {
559 |                   successful: buildData.successful || 0,
560 |                   failed: buildData.failed || 0,
561 |                   in_progress: buildData.inProgress || 0,
562 |                   unknown: buildData.unknown || 0
563 |                 }
564 |               };
565 |             }
566 |             return commit;
567 |           });
568 |         } catch (error) {
569 |           // Gracefully degrade - log error but don't fail the entire request
570 |           console.error('Failed to fetch build status:', error);
571 |         }
572 |       }
573 | 
574 |       // Get branch head info
575 |       let branchHead: string | null = null;
576 |       try {
577 |         if (this.apiClient.getIsServer()) {
578 |           const branchesPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`;
579 |           const branchesResponse = await this.apiClient.makeRequest<any>('get', branchesPath, undefined, {
580 |             params: { filterText: branch_name, limit: 1 }
581 |           });
582 |           const branch = branchesResponse.values?.find((b: any) => b.displayId === branch_name);
583 |           branchHead = branch?.latestCommit || null;
584 |         } else {
585 |           const branchPath = `/repositories/${workspace}/${repository}/refs/branches/${encodeURIComponent(branch_name)}`;
586 |           const branch = await this.apiClient.makeRequest<any>('get', branchPath);
587 |           branchHead = branch.target?.hash || null;
588 |         }
589 |       } catch (e) {
590 |         // Ignore error, branch head is optional
591 |       }
592 | 
593 |       // Build filters applied summary
594 |       const filtersApplied: any = {};
595 |       if (author) filtersApplied.author = author;
596 |       if (since) filtersApplied.since = since;
597 |       if (until) filtersApplied.until = until;
598 |       if (include_merge_commits !== undefined) filtersApplied.include_merge_commits = include_merge_commits;
599 |       if (search) filtersApplied.search = search;
600 |       if (include_build_status) filtersApplied.include_build_status = include_build_status;
601 | 
602 |       return {
603 |         content: [
604 |           {
605 |             type: 'text',
606 |             text: JSON.stringify({
607 |               branch_name,
608 |               branch_head: branchHead,
609 |               commits,
610 |               total_count: totalCount,
611 |               start,
612 |               limit,
613 |               has_more: nextPageStart !== null,
614 |               next_start: nextPageStart,
615 |               filters_applied: filtersApplied
616 |             }, null, 2),
617 |           },
618 |         ],
619 |       };
620 |     } catch (error) {
621 |       return this.apiClient.handleApiError(error, `listing commits for branch '${branch_name}' in ${workspace}/${repository}`);
622 |     }
623 |   }
624 | }
625 | 
```

--------------------------------------------------------------------------------
/src/tools/definitions.ts:
--------------------------------------------------------------------------------

```typescript
  1 | export const toolDefinitions = [
  2 |   {
  3 |     name: 'get_pull_request',
  4 |     description: 'Get details of a Bitbucket pull request including merge commit information',
  5 |     inputSchema: {
  6 |       type: 'object',
  7 |       properties: {
  8 |         workspace: {
  9 |           type: 'string',
 10 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
 11 |         },
 12 |         repository: {
 13 |           type: 'string',
 14 |           description: 'Repository slug (e.g., "my-repo")',
 15 |         },
 16 |         pull_request_id: {
 17 |           type: 'number',
 18 |           description: 'Pull request ID',
 19 |         },
 20 |       },
 21 |       required: ['workspace', 'repository', 'pull_request_id'],
 22 |     },
 23 |   },
 24 |   {
 25 |     name: 'list_pull_requests',
 26 |     description: 'List pull requests for a repository with optional filters',
 27 |     inputSchema: {
 28 |       type: 'object',
 29 |       properties: {
 30 |         workspace: {
 31 |           type: 'string',
 32 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
 33 |         },
 34 |         repository: {
 35 |           type: 'string',
 36 |           description: 'Repository slug (e.g., "my-repo")',
 37 |         },
 38 |         state: {
 39 |           type: 'string',
 40 |           description: 'Filter by PR state: OPEN, MERGED, DECLINED, ALL (default: OPEN)',
 41 |           enum: ['OPEN', 'MERGED', 'DECLINED', 'ALL'],
 42 |         },
 43 |         author: {
 44 |           type: 'string',
 45 |           description: 'Filter by author username',
 46 |         },
 47 |         limit: {
 48 |           type: 'number',
 49 |           description: 'Maximum number of PRs to return (default: 25)',
 50 |         },
 51 |         start: {
 52 |           type: 'number',
 53 |           description: 'Start index for pagination (default: 0)',
 54 |         },
 55 |       },
 56 |       required: ['workspace', 'repository'],
 57 |     },
 58 |   },
 59 |   {
 60 |     name: 'create_pull_request',
 61 |     description: 'Create a new pull request',
 62 |     inputSchema: {
 63 |       type: 'object',
 64 |       properties: {
 65 |         workspace: {
 66 |           type: 'string',
 67 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
 68 |         },
 69 |         repository: {
 70 |           type: 'string',
 71 |           description: 'Repository slug (e.g., "my-repo")',
 72 |         },
 73 |         title: {
 74 |           type: 'string',
 75 |           description: 'Title of the pull request',
 76 |         },
 77 |         source_branch: {
 78 |           type: 'string',
 79 |           description: 'Source branch name',
 80 |         },
 81 |         destination_branch: {
 82 |           type: 'string',
 83 |           description: 'Destination branch name (e.g., "main", "master")',
 84 |         },
 85 |         description: {
 86 |           type: 'string',
 87 |           description: 'Description of the pull request (optional)',
 88 |         },
 89 |         reviewers: {
 90 |           type: 'array',
 91 |           items: { type: 'string' },
 92 |           description: 'Array of reviewer usernames/emails (optional)',
 93 |         },
 94 |         close_source_branch: {
 95 |           type: 'boolean',
 96 |           description: 'Whether to close source branch after merge (optional, default: false)',
 97 |         },
 98 |       },
 99 |       required: ['workspace', 'repository', 'title', 'source_branch', 'destination_branch'],
100 |     },
101 |   },
102 |   {
103 |     name: 'update_pull_request',
104 |     description: 'Update an existing pull request. When updating without specifying reviewers, existing reviewers and their approval status will be preserved.',
105 |     inputSchema: {
106 |       type: 'object',
107 |       properties: {
108 |         workspace: {
109 |           type: 'string',
110 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
111 |         },
112 |         repository: {
113 |           type: 'string',
114 |           description: 'Repository slug (e.g., "my-repo")',
115 |         },
116 |         pull_request_id: {
117 |           type: 'number',
118 |           description: 'Pull request ID',
119 |         },
120 |         title: {
121 |           type: 'string',
122 |           description: 'New title (optional)',
123 |         },
124 |         description: {
125 |           type: 'string',
126 |           description: 'New description (optional)',
127 |         },
128 |         destination_branch: {
129 |           type: 'string',
130 |           description: 'New destination branch (optional)',
131 |         },
132 |         reviewers: {
133 |           type: 'array',
134 |           items: { type: 'string' },
135 |           description: 'New list of reviewer usernames/emails. If provided, replaces the reviewer list (preserving approval status for existing reviewers). If omitted, existing reviewers are preserved. (optional)',
136 |         },
137 |       },
138 |       required: ['workspace', 'repository', 'pull_request_id'],
139 |     },
140 |   },
141 |   {
142 |     name: 'add_comment',
143 |     description: 'Add a comment to a pull request. Supports: 1) General PR comments, 2) Replies to existing comments, 3) Inline comments on specific code lines (using line_number OR code_snippet), 4) Code suggestions for single or multi-line replacements. For inline comments, you can either provide exact line_number or use code_snippet to auto-detect the line.',
144 |     inputSchema: {
145 |       type: 'object',
146 |       properties: {
147 |         workspace: {
148 |           type: 'string',
149 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
150 |         },
151 |         repository: {
152 |           type: 'string',
153 |           description: 'Repository slug (e.g., "my-repo")',
154 |         },
155 |         pull_request_id: {
156 |           type: 'number',
157 |           description: 'Pull request ID',
158 |         },
159 |         comment_text: {
160 |           type: 'string',
161 |           description: 'The main comment text. For suggestions, this is the explanation before the code suggestion.',
162 |         },
163 |         parent_comment_id: {
164 |           type: 'number',
165 |           description: 'ID of comment to reply to. Use this to create threaded conversations (optional)',
166 |         },
167 |         file_path: {
168 |           type: 'string',
169 |           description: 'File path for inline comment. Required for inline comments. Example: "src/components/Button.js" (optional)',
170 |         },
171 |         line_number: {
172 |           type: 'number',
173 |           description: 'Exact line number in the file. Use this OR code_snippet, not both. Required with file_path unless using code_snippet (optional)',
174 |         },
175 |         line_type: {
176 |           type: 'string',
177 |           description: 'Type of line: ADDED (green/new lines), REMOVED (red/deleted lines), or CONTEXT (unchanged lines). Default: CONTEXT',
178 |           enum: ['ADDED', 'REMOVED', 'CONTEXT'],
179 |         },
180 |         suggestion: {
181 |           type: 'string',
182 |           description: 'Replacement code for a suggestion. Creates a suggestion block that can be applied in Bitbucket UI. Requires file_path and line_number. For multi-line, include newlines in the string (optional)',
183 |         },
184 |         suggestion_end_line: {
185 |           type: 'number',
186 |           description: 'For multi-line suggestions: the last line number to replace. If not provided, only replaces the single line at line_number (optional)',
187 |         },
188 |         code_snippet: {
189 |           type: 'string',
190 |           description: 'Exact code text from the diff to find and comment on. Use this instead of line_number for auto-detection. Must match exactly including whitespace (optional)',
191 |         },
192 |         search_context: {
193 |           type: 'object',
194 |           properties: {
195 |             before: {
196 |               type: 'array',
197 |               items: { type: 'string' },
198 |               description: 'Array of code lines that appear BEFORE the target line. Helps disambiguate when code_snippet appears multiple times',
199 |             },
200 |             after: {
201 |               type: 'array',
202 |               items: { type: 'string' },
203 |               description: 'Array of code lines that appear AFTER the target line. Helps disambiguate when code_snippet appears multiple times',
204 |             },
205 |           },
206 |           description: 'Additional context lines to help locate the exact position when using code_snippet. Useful when the same code appears multiple times (optional)',
207 |         },
208 |         match_strategy: {
209 |           type: 'string',
210 |           enum: ['strict', 'best'],
211 |           description: 'How to handle multiple matches when using code_snippet. "strict": fail with detailed error showing all matches. "best": automatically pick the highest confidence match. Default: "strict"',
212 |         },
213 |       },
214 |       required: ['workspace', 'repository', 'pull_request_id', 'comment_text'],
215 |     },
216 |   },
217 |   {
218 |     name: 'merge_pull_request',
219 |     description: 'Merge a pull request',
220 |     inputSchema: {
221 |       type: 'object',
222 |       properties: {
223 |         workspace: {
224 |           type: 'string',
225 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
226 |         },
227 |         repository: {
228 |           type: 'string',
229 |           description: 'Repository slug (e.g., "my-repo")',
230 |         },
231 |         pull_request_id: {
232 |           type: 'number',
233 |           description: 'Pull request ID',
234 |         },
235 |         merge_strategy: {
236 |           type: 'string',
237 |           description: 'Merge strategy: merge-commit, squash, fast-forward (optional)',
238 |           enum: ['merge-commit', 'squash', 'fast-forward'],
239 |         },
240 |         close_source_branch: {
241 |           type: 'boolean',
242 |           description: 'Whether to close source branch after merge (optional)',
243 |         },
244 |         commit_message: {
245 |           type: 'string',
246 |           description: 'Custom merge commit message (optional)',
247 |         },
248 |       },
249 |       required: ['workspace', 'repository', 'pull_request_id'],
250 |     },
251 |   },
252 |   {
253 |     name: 'list_branches',
254 |     description: 'List branches in a repository',
255 |     inputSchema: {
256 |       type: 'object',
257 |       properties: {
258 |         workspace: {
259 |           type: 'string',
260 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
261 |         },
262 |         repository: {
263 |           type: 'string',
264 |           description: 'Repository slug (e.g., "my-repo")',
265 |         },
266 |         filter: {
267 |           type: 'string',
268 |           description: 'Filter branches by name pattern (optional)',
269 |         },
270 |         limit: {
271 |           type: 'number',
272 |           description: 'Maximum number of branches to return (default: 25)',
273 |         },
274 |         start: {
275 |           type: 'number',
276 |           description: 'Start index for pagination (default: 0)',
277 |         },
278 |       },
279 |       required: ['workspace', 'repository'],
280 |     },
281 |   },
282 |   {
283 |     name: 'delete_branch',
284 |     description: 'Delete a branch',
285 |     inputSchema: {
286 |       type: 'object',
287 |       properties: {
288 |         workspace: {
289 |           type: 'string',
290 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
291 |         },
292 |         repository: {
293 |           type: 'string',
294 |           description: 'Repository slug (e.g., "my-repo")',
295 |         },
296 |         branch_name: {
297 |           type: 'string',
298 |           description: 'Branch name to delete',
299 |         },
300 |         force: {
301 |           type: 'boolean',
302 |           description: 'Force delete even if branch is not merged (optional, default: false)',
303 |         },
304 |       },
305 |       required: ['workspace', 'repository', 'branch_name'],
306 |     },
307 |   },
308 |   {
309 |     name: 'get_pull_request_diff',
310 |     description: 'Get the diff/changes for a pull request with optional filtering',
311 |     inputSchema: {
312 |       type: 'object',
313 |       properties: {
314 |         workspace: {
315 |           type: 'string',
316 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
317 |         },
318 |         repository: {
319 |           type: 'string',
320 |           description: 'Repository slug (e.g., "my-repo")',
321 |         },
322 |         pull_request_id: {
323 |           type: 'number',
324 |           description: 'Pull request ID',
325 |         },
326 |         context_lines: {
327 |           type: 'number',
328 |           description: 'Number of context lines around changes (optional, default: 3)',
329 |         },
330 |         include_patterns: {
331 |           type: 'array',
332 |           items: { type: 'string' },
333 |           description: 'Array of glob patterns to include (e.g., ["*.res", "src/**/*.js"]) (optional)',
334 |         },
335 |         exclude_patterns: {
336 |           type: 'array',
337 |           items: { type: 'string' },
338 |           description: 'Array of glob patterns to exclude (e.g., ["*.lock", "*.svg"]) (optional)',
339 |         },
340 |         file_path: {
341 |           type: 'string',
342 |           description: 'Specific file path to get diff for (e.g., "src/index.ts") (optional)',
343 |         },
344 |       },
345 |       required: ['workspace', 'repository', 'pull_request_id'],
346 |     },
347 |   },
348 |   {
349 |     name: 'approve_pull_request',
350 |     description: 'Approve a pull request',
351 |     inputSchema: {
352 |       type: 'object',
353 |       properties: {
354 |         workspace: {
355 |           type: 'string',
356 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
357 |         },
358 |         repository: {
359 |           type: 'string',
360 |           description: 'Repository slug (e.g., "my-repo")',
361 |         },
362 |         pull_request_id: {
363 |           type: 'number',
364 |           description: 'Pull request ID',
365 |         },
366 |       },
367 |       required: ['workspace', 'repository', 'pull_request_id'],
368 |     },
369 |   },
370 |   {
371 |     name: 'unapprove_pull_request',
372 |     description: 'Remove approval from a pull request',
373 |     inputSchema: {
374 |       type: 'object',
375 |       properties: {
376 |         workspace: {
377 |           type: 'string',
378 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
379 |         },
380 |         repository: {
381 |           type: 'string',
382 |           description: 'Repository slug (e.g., "my-repo")',
383 |         },
384 |         pull_request_id: {
385 |           type: 'number',
386 |           description: 'Pull request ID',
387 |         },
388 |       },
389 |       required: ['workspace', 'repository', 'pull_request_id'],
390 |     },
391 |   },
392 |   {
393 |     name: 'request_changes',
394 |     description: 'Request changes on a pull request',
395 |     inputSchema: {
396 |       type: 'object',
397 |       properties: {
398 |         workspace: {
399 |           type: 'string',
400 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
401 |         },
402 |         repository: {
403 |           type: 'string',
404 |           description: 'Repository slug (e.g., "my-repo")',
405 |         },
406 |         pull_request_id: {
407 |           type: 'number',
408 |           description: 'Pull request ID',
409 |         },
410 |         comment: {
411 |           type: 'string',
412 |           description: 'Comment explaining requested changes (optional)',
413 |         },
414 |       },
415 |       required: ['workspace', 'repository', 'pull_request_id'],
416 |     },
417 |   },
418 |   {
419 |     name: 'remove_requested_changes',
420 |     description: 'Remove change request from a pull request',
421 |     inputSchema: {
422 |       type: 'object',
423 |       properties: {
424 |         workspace: {
425 |           type: 'string',
426 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
427 |         },
428 |         repository: {
429 |           type: 'string',
430 |           description: 'Repository slug (e.g., "my-repo")',
431 |         },
432 |         pull_request_id: {
433 |           type: 'number',
434 |           description: 'Pull request ID',
435 |         },
436 |       },
437 |       required: ['workspace', 'repository', 'pull_request_id'],
438 |     },
439 |   },
440 |   {
441 |     name: 'get_branch',
442 |     description: 'Get detailed information about a branch including associated pull requests',
443 |     inputSchema: {
444 |       type: 'object',
445 |       properties: {
446 |         workspace: {
447 |           type: 'string',
448 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
449 |         },
450 |         repository: {
451 |           type: 'string',
452 |           description: 'Repository slug (e.g., "my-repo")',
453 |         },
454 |         branch_name: {
455 |           type: 'string',
456 |           description: 'Branch name to get details for',
457 |         },
458 |         include_merged_prs: {
459 |           type: 'boolean',
460 |           description: 'Include merged PRs from this branch (default: false)',
461 |         },
462 |       },
463 |       required: ['workspace', 'repository', 'branch_name'],
464 |     },
465 |   },
466 |   {
467 |     name: 'list_directory_content',
468 |     description: 'List files and directories in a repository path',
469 |     inputSchema: {
470 |       type: 'object',
471 |       properties: {
472 |         workspace: {
473 |           type: 'string',
474 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
475 |         },
476 |         repository: {
477 |           type: 'string',
478 |           description: 'Repository slug (e.g., "my-repo")',
479 |         },
480 |         path: {
481 |           type: 'string',
482 |           description: 'Directory path (optional, defaults to root, e.g., "src/components")',
483 |         },
484 |         branch: {
485 |           type: 'string',
486 |           description: 'Branch name (optional, defaults to default branch)',
487 |         },
488 |       },
489 |       required: ['workspace', 'repository'],
490 |     },
491 |   },
492 |   {
493 |     name: 'get_file_content',
494 |     description: 'Get file content from a repository with smart truncation for large files',
495 |     inputSchema: {
496 |       type: 'object',
497 |       properties: {
498 |         workspace: {
499 |           type: 'string',
500 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
501 |         },
502 |         repository: {
503 |           type: 'string',
504 |           description: 'Repository slug (e.g., "my-repo")',
505 |         },
506 |         file_path: {
507 |           type: 'string',
508 |           description: 'Path to the file (e.g., "src/index.ts")',
509 |         },
510 |         branch: {
511 |           type: 'string',
512 |           description: 'Branch name (optional, defaults to default branch)',
513 |         },
514 |         start_line: {
515 |           type: 'number',
516 |           description: 'Starting line number (1-based). Use negative for lines from end (optional)',
517 |         },
518 |         line_count: {
519 |           type: 'number',
520 |           description: 'Number of lines to return (optional, default varies by file size)',
521 |         },
522 |         full_content: {
523 |           type: 'boolean',
524 |           description: 'Force return full content regardless of size (optional, default: false)',
525 |         },
526 |       },
527 |       required: ['workspace', 'repository', 'file_path'],
528 |     },
529 |   },
530 |   {
531 |     name: 'list_branch_commits',
532 |     description: 'List commits in a branch with detailed information and filtering options',
533 |     inputSchema: {
534 |       type: 'object',
535 |       properties: {
536 |         workspace: {
537 |           type: 'string',
538 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
539 |         },
540 |         repository: {
541 |           type: 'string',
542 |           description: 'Repository slug (e.g., "my-repo")',
543 |         },
544 |         branch_name: {
545 |           type: 'string',
546 |           description: 'Branch name to get commits from',
547 |         },
548 |         limit: {
549 |           type: 'number',
550 |           description: 'Maximum number of commits to return (default: 25)',
551 |         },
552 |         start: {
553 |           type: 'number',
554 |           description: 'Start index for pagination (default: 0)',
555 |         },
556 |         since: {
557 |           type: 'string',
558 |           description: 'ISO date string - only show commits after this date (optional)',
559 |         },
560 |         until: {
561 |           type: 'string',
562 |           description: 'ISO date string - only show commits before this date (optional)',
563 |         },
564 |         author: {
565 |           type: 'string',
566 |           description: 'Filter by author email/username (optional)',
567 |         },
568 |         include_merge_commits: {
569 |           type: 'boolean',
570 |           description: 'Include merge commits in results (default: true)',
571 |         },
572 |         search: {
573 |           type: 'string',
574 |           description: 'Search for text in commit messages (optional)',
575 |         },
576 |         include_build_status: {
577 |           type: 'boolean',
578 |           description: 'Include CI/CD build status for each commit (Bitbucket Server only, default: false)',
579 |         },
580 |       },
581 |       required: ['workspace', 'repository', 'branch_name'],
582 |     },
583 |   },
584 |   {
585 |     name: 'list_pr_commits',
586 |     description: 'List all commits that are part of a pull request',
587 |     inputSchema: {
588 |       type: 'object',
589 |       properties: {
590 |         workspace: {
591 |           type: 'string',
592 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
593 |         },
594 |         repository: {
595 |           type: 'string',
596 |           description: 'Repository slug (e.g., "my-repo")',
597 |         },
598 |         pull_request_id: {
599 |           type: 'number',
600 |           description: 'Pull request ID',
601 |         },
602 |         limit: {
603 |           type: 'number',
604 |           description: 'Maximum number of commits to return (default: 25)',
605 |         },
606 |         start: {
607 |           type: 'number',
608 |           description: 'Start index for pagination (default: 0)',
609 |         },
610 |         include_build_status: {
611 |           type: 'boolean',
612 |           description: 'Include CI/CD build status for each commit (Bitbucket Server only, default: false)',
613 |         },
614 |       },
615 |       required: ['workspace', 'repository', 'pull_request_id'],
616 |     },
617 |   },
618 |   {
619 |     name: 'search_code',
620 |     description: 'Search for code across Bitbucket repositories with enhanced context-aware search patterns (currently only supported for Bitbucket Server)',
621 |     inputSchema: {
622 |       type: 'object',
623 |       properties: {
624 |         workspace: {
625 |           type: 'string',
626 |           description: 'Bitbucket workspace/project key (e.g., "PROJ")',
627 |         },
628 |         repository: {
629 |           type: 'string',
630 |           description: 'Repository slug to search in (optional, searches all repos if not specified)',
631 |         },
632 |         search_query: {
633 |           type: 'string',
634 |           description: 'The search term or phrase to look for in code (e.g., "variable")',
635 |         },
636 |         search_context: {
637 |           type: 'string',
638 |           enum: ['assignment', 'declaration', 'usage', 'exact', 'any'],
639 |           description: 'Context to search for: assignment (term=value), declaration (defining term), usage (calling/accessing term), exact (quoted match), or any (all patterns)',
640 |         },
641 |         file_pattern: {
642 |           type: 'string',
643 |           description: 'File path pattern to filter results (e.g., "*.java", "src/**/*.ts") (optional)',
644 |         },
645 |         include_patterns: {
646 |           type: 'array',
647 |           items: { type: 'string' },
648 |           description: 'Additional custom search patterns to include (e.g., ["variable =", ".variable"]) (optional)',
649 |         },
650 |         limit: {
651 |           type: 'number',
652 |           description: 'Maximum number of results to return (default: 25)',
653 |         },
654 |         start: {
655 |           type: 'number',
656 |           description: 'Start index for pagination (default: 0)',
657 |         },
658 |       },
659 |       required: ['workspace', 'search_query'],
660 |     },
661 |   },
662 |   {
663 |     name: 'list_projects',
664 |     description: 'List all accessible Bitbucket projects with optional filtering',
665 |     inputSchema: {
666 |       type: 'object',
667 |       properties: {
668 |         name: {
669 |           type: 'string',
670 |           description: 'Filter by project name (partial match, optional)',
671 |         },
672 |         permission: {
673 |           type: 'string',
674 |           description: 'Filter by permission level (e.g., PROJECT_READ, PROJECT_WRITE, PROJECT_ADMIN, optional)',
675 |         },
676 |         limit: {
677 |           type: 'number',
678 |           description: 'Maximum number of projects to return (default: 25)',
679 |         },
680 |         start: {
681 |           type: 'number',
682 |           description: 'Start index for pagination (default: 0)',
683 |         },
684 |       },
685 |       required: [],
686 |     },
687 |   },
688 |   {
689 |     name: 'list_repositories',
690 |     description: 'List repositories in a project or across all accessible projects',
691 |     inputSchema: {
692 |       type: 'object',
693 |       properties: {
694 |         workspace: {
695 |           type: 'string',
696 |           description: 'Bitbucket workspace/project key to filter repositories (optional, if not provided lists all accessible repos)',
697 |         },
698 |         name: {
699 |           type: 'string',
700 |           description: 'Filter by repository name (partial match, optional)',
701 |         },
702 |         permission: {
703 |           type: 'string',
704 |           description: 'Filter by permission level (e.g., REPO_READ, REPO_WRITE, REPO_ADMIN, optional)',
705 |         },
706 |         limit: {
707 |           type: 'number',
708 |           description: 'Maximum number of repositories to return (default: 25)',
709 |         },
710 |         start: {
711 |           type: 'number',
712 |           description: 'Start index for pagination (default: 0)',
713 |         },
714 |       },
715 |       required: [],
716 |     },
717 |   },
718 | ];
719 | 
```

--------------------------------------------------------------------------------
/src/handlers/pull-request-handlers.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
   2 | import { BitbucketApiClient } from '../utils/api-client.js';
   3 | import { formatServerResponse, formatCloudResponse, formatServerCommit, formatCloudCommit } from '../utils/formatters.js';
   4 | import { formatSuggestionComment } from '../utils/suggestion-formatter.js';
   5 | import { DiffParser } from '../utils/diff-parser.js';
   6 | import { 
   7 |   BitbucketServerPullRequest, 
   8 |   BitbucketCloudPullRequest, 
   9 |   BitbucketServerActivity,
  10 |   MergeInfo,
  11 |   BitbucketCloudComment,
  12 |   BitbucketCloudFileChange,
  13 |   FormattedComment,
  14 |   FormattedFileChange,
  15 |   CodeMatch,
  16 |   MultipleMatchesError,
  17 |   BitbucketServerCommit,
  18 |   BitbucketCloudCommit,
  19 |   FormattedCommit
  20 | } from '../types/bitbucket.js';
  21 | import {
  22 |   isGetPullRequestArgs,
  23 |   isListPullRequestsArgs,
  24 |   isCreatePullRequestArgs,
  25 |   isUpdatePullRequestArgs,
  26 |   isAddCommentArgs,
  27 |   isMergePullRequestArgs,
  28 |   isListPrCommitsArgs
  29 | } from '../types/guards.js';
  30 | 
  31 | export class PullRequestHandlers {
  32 |   constructor(
  33 |     private apiClient: BitbucketApiClient,
  34 |     private baseUrl: string,
  35 |     private username: string
  36 |   ) {}
  37 | 
  38 |   private async getFilteredPullRequestDiff(
  39 |     workspace: string,
  40 |     repository: string,
  41 |     pullRequestId: number,
  42 |     filePath: string,
  43 |     contextLines: number = 3
  44 |   ): Promise<string> {
  45 |     let apiPath: string;
  46 |     let config: any = {};
  47 | 
  48 |     if (this.apiClient.getIsServer()) {
  49 |       // Bitbucket Server API
  50 |       apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/diff`;
  51 |       config.params = { contextLines };
  52 |     } else {
  53 |       // Bitbucket Cloud API
  54 |       apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diff`;
  55 |       config.params = { context: contextLines };
  56 |     }
  57 | 
  58 |     config.headers = { 'Accept': 'text/plain' };
  59 |     
  60 |     const rawDiff = await this.apiClient.makeRequest<string>('get', apiPath, undefined, config);
  61 | 
  62 |     const diffParser = new DiffParser();
  63 |     const sections = diffParser.parseDiffIntoSections(rawDiff);
  64 |     
  65 |     const filterOptions = {
  66 |       filePath: filePath
  67 |     };
  68 |     
  69 |     const filteredResult = diffParser.filterSections(sections, filterOptions);
  70 |     const filteredDiff = diffParser.reconstructDiff(filteredResult.sections);
  71 | 
  72 |     return filteredDiff;
  73 |   }
  74 | 
  75 |   async handleGetPullRequest(args: any) {
  76 |     if (!isGetPullRequestArgs(args)) {
  77 |       throw new McpError(
  78 |         ErrorCode.InvalidParams,
  79 |         'Invalid arguments for get_pull_request'
  80 |       );
  81 |     }
  82 | 
  83 |     const { workspace, repository, pull_request_id } = args;
  84 | 
  85 |     try {
  86 |       const apiPath = this.apiClient.getIsServer()
  87 |         ? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
  88 |         : `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
  89 |       
  90 |       const pr = await this.apiClient.makeRequest<any>('get', apiPath);
  91 | 
  92 |       let mergeInfo: MergeInfo = {};
  93 | 
  94 |       if (this.apiClient.getIsServer() && pr.state === 'MERGED') {
  95 |         try {
  96 |           const activitiesPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/activities`;
  97 |           const activitiesResponse = await this.apiClient.makeRequest<any>('get', activitiesPath, undefined, {
  98 |             params: { limit: 100 }
  99 |           });
 100 |           
 101 |           const activities = activitiesResponse.values || [];
 102 |           const mergeActivity = activities.find((a: BitbucketServerActivity) => a.action === 'MERGED');
 103 |           
 104 |           if (mergeActivity) {
 105 |             mergeInfo.mergeCommitHash = mergeActivity.commit?.id || null;
 106 |             mergeInfo.mergedBy = mergeActivity.user?.displayName || null;
 107 |             mergeInfo.mergedAt = new Date(mergeActivity.createdDate).toISOString();
 108 |             
 109 |             if (mergeActivity.commit?.id) {
 110 |               try {
 111 |                 const commitPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/commits/${mergeActivity.commit.id}`;
 112 |                 const commitResponse = await this.apiClient.makeRequest<any>('get', commitPath);
 113 |                 mergeInfo.mergeCommitMessage = commitResponse.message || null;
 114 |               } catch (commitError) {
 115 |                 console.error('Failed to fetch merge commit message:', commitError);
 116 |               }
 117 |             }
 118 |           }
 119 |         } catch (activitiesError) {
 120 |           console.error('Failed to fetch PR activities:', activitiesError);
 121 |         }
 122 |       }
 123 | 
 124 |       let comments: FormattedComment[] = [];
 125 |       let activeCommentCount = 0;
 126 |       let totalCommentCount = 0;
 127 |       let fileChanges: FormattedFileChange[] = [];
 128 |       let fileChangesSummary: any = null;
 129 | 
 130 |       try {
 131 |         const [commentsResult, fileChangesResult] = await Promise.all([
 132 |           this.fetchPullRequestComments(workspace, repository, pull_request_id),
 133 |           this.fetchPullRequestFileChanges(workspace, repository, pull_request_id)
 134 |         ]);
 135 | 
 136 |         comments = commentsResult.comments;
 137 |         activeCommentCount = commentsResult.activeCount;
 138 |         totalCommentCount = commentsResult.totalCount;
 139 |         fileChanges = fileChangesResult.fileChanges;
 140 |         fileChangesSummary = fileChangesResult.summary;
 141 |       } catch (error) {
 142 |         console.error('Failed to fetch additional PR data:', error);
 143 |       }
 144 | 
 145 |       const formattedResponse = this.apiClient.getIsServer() 
 146 |         ? formatServerResponse(pr as BitbucketServerPullRequest, mergeInfo, this.baseUrl)
 147 |         : formatCloudResponse(pr as BitbucketCloudPullRequest);
 148 | 
 149 |       const enhancedResponse = {
 150 |         ...formattedResponse,
 151 |         active_comments: comments,
 152 |         active_comment_count: activeCommentCount,
 153 |         total_comment_count: totalCommentCount,
 154 |         file_changes: fileChanges,
 155 |         file_changes_summary: fileChangesSummary
 156 |       };
 157 | 
 158 |       return {
 159 |         content: [
 160 |           {
 161 |             type: 'text',
 162 |             text: JSON.stringify(enhancedResponse, null, 2),
 163 |           },
 164 |         ],
 165 |       };
 166 |     } catch (error) {
 167 |       return this.apiClient.handleApiError(error, `getting pull request ${pull_request_id} in ${workspace}/${repository}`);
 168 |     }
 169 |   }
 170 | 
 171 |   async handleListPullRequests(args: any) {
 172 |     if (!isListPullRequestsArgs(args)) {
 173 |       throw new McpError(
 174 |         ErrorCode.InvalidParams,
 175 |         'Invalid arguments for list_pull_requests'
 176 |       );
 177 |     }
 178 | 
 179 |     const { workspace, repository, state = 'OPEN', author, limit = 25, start = 0 } = args;
 180 | 
 181 |     try {
 182 |       let apiPath: string;
 183 |       let params: any = {};
 184 | 
 185 |       if (this.apiClient.getIsServer()) {
 186 |         // Bitbucket Server API
 187 |         apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
 188 |         params = {
 189 |           state: state === 'ALL' ? undefined : state,
 190 |           limit,
 191 |           start,
 192 |         };
 193 |         if (author) {
 194 |           params['role.1'] = 'AUTHOR';
 195 |           params['username.1'] = author;
 196 |         }
 197 |       } else {
 198 |         // Bitbucket Cloud API
 199 |         apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
 200 |         params = {
 201 |           state: state === 'ALL' ? undefined : state,
 202 |           pagelen: limit,
 203 |           page: Math.floor(start / limit) + 1,
 204 |         };
 205 |         if (author) {
 206 |           params['q'] = `author.username="${author}"`;
 207 |         }
 208 |       }
 209 | 
 210 |       const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
 211 | 
 212 |       let pullRequests: any[] = [];
 213 |       let totalCount = 0;
 214 |       let nextPageStart = null;
 215 | 
 216 |       if (this.apiClient.getIsServer()) {
 217 |         pullRequests = (response.values || []).map((pr: BitbucketServerPullRequest) => 
 218 |           formatServerResponse(pr, undefined, this.baseUrl)
 219 |         );
 220 |         totalCount = response.size || 0;
 221 |         if (!response.isLastPage && response.nextPageStart !== undefined) {
 222 |           nextPageStart = response.nextPageStart;
 223 |         }
 224 |       } else {
 225 |         pullRequests = (response.values || []).map((pr: BitbucketCloudPullRequest) => 
 226 |           formatCloudResponse(pr)
 227 |         );
 228 |         totalCount = response.size || 0;
 229 |         if (response.next) {
 230 |           nextPageStart = start + limit;
 231 |         }
 232 |       }
 233 | 
 234 |       return {
 235 |         content: [
 236 |           {
 237 |             type: 'text',
 238 |             text: JSON.stringify({
 239 |               pull_requests: pullRequests,
 240 |               total_count: totalCount,
 241 |               start,
 242 |               limit,
 243 |               has_more: nextPageStart !== null,
 244 |               next_start: nextPageStart,
 245 |             }, null, 2),
 246 |           },
 247 |         ],
 248 |       };
 249 |     } catch (error) {
 250 |       return this.apiClient.handleApiError(error, `listing pull requests in ${workspace}/${repository}`);
 251 |     }
 252 |   }
 253 | 
 254 |   async handleCreatePullRequest(args: any) {
 255 |     if (!isCreatePullRequestArgs(args)) {
 256 |       throw new McpError(
 257 |         ErrorCode.InvalidParams,
 258 |         'Invalid arguments for create_pull_request'
 259 |       );
 260 |     }
 261 | 
 262 |     const { workspace, repository, title, source_branch, destination_branch, description, reviewers, close_source_branch } = args;
 263 | 
 264 |     try {
 265 |       let apiPath: string;
 266 |       let requestBody: any;
 267 | 
 268 |       if (this.apiClient.getIsServer()) {
 269 |         // Bitbucket Server API
 270 |         apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
 271 |         requestBody = {
 272 |           title,
 273 |           description: description || '',
 274 |           fromRef: {
 275 |             id: `refs/heads/${source_branch}`,
 276 |             repository: {
 277 |               slug: repository,
 278 |               project: {
 279 |                 key: workspace
 280 |               }
 281 |             }
 282 |           },
 283 |           toRef: {
 284 |             id: `refs/heads/${destination_branch}`,
 285 |             repository: {
 286 |               slug: repository,
 287 |               project: {
 288 |                 key: workspace
 289 |               }
 290 |             }
 291 |           },
 292 |           reviewers: reviewers?.map(r => ({ user: { name: r } })) || []
 293 |         };
 294 |       } else {
 295 |         // Bitbucket Cloud API
 296 |         apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
 297 |         requestBody = {
 298 |           title,
 299 |           description: description || '',
 300 |           source: {
 301 |             branch: {
 302 |               name: source_branch
 303 |             }
 304 |           },
 305 |           destination: {
 306 |             branch: {
 307 |               name: destination_branch
 308 |             }
 309 |           },
 310 |           close_source_branch: close_source_branch || false,
 311 |           reviewers: reviewers?.map(r => ({ username: r })) || []
 312 |         };
 313 |       }
 314 | 
 315 |       const pr = await this.apiClient.makeRequest<any>('post', apiPath, requestBody);
 316 |       
 317 |       const formattedResponse = this.apiClient.getIsServer() 
 318 |         ? formatServerResponse(pr as BitbucketServerPullRequest, undefined, this.baseUrl)
 319 |         : formatCloudResponse(pr as BitbucketCloudPullRequest);
 320 | 
 321 |       return {
 322 |         content: [
 323 |           {
 324 |             type: 'text',
 325 |             text: JSON.stringify({
 326 |               message: 'Pull request created successfully',
 327 |               pull_request: formattedResponse
 328 |             }, null, 2),
 329 |           },
 330 |         ],
 331 |       };
 332 |     } catch (error) {
 333 |       return this.apiClient.handleApiError(error, `creating pull request in ${workspace}/${repository}`);
 334 |     }
 335 |   }
 336 | 
 337 |   async handleUpdatePullRequest(args: any) {
 338 |     if (!isUpdatePullRequestArgs(args)) {
 339 |       throw new McpError(
 340 |         ErrorCode.InvalidParams,
 341 |         'Invalid arguments for update_pull_request'
 342 |       );
 343 |     }
 344 | 
 345 |     const { workspace, repository, pull_request_id, title, description, destination_branch, reviewers } = args;
 346 | 
 347 |     try {
 348 |       let apiPath: string;
 349 |       let requestBody: any = {};
 350 | 
 351 |       if (this.apiClient.getIsServer()) {
 352 |         // Bitbucket Server API
 353 |         apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`;
 354 |         
 355 |         // First get the current PR to get version number and existing data
 356 |         const currentPr = await this.apiClient.makeRequest<any>('get', apiPath);
 357 |         
 358 |         requestBody.version = currentPr.version;
 359 |         if (title !== undefined) requestBody.title = title;
 360 |         if (description !== undefined) requestBody.description = description;
 361 |         if (destination_branch !== undefined) {
 362 |           requestBody.toRef = {
 363 |             id: `refs/heads/${destination_branch}`,
 364 |             repository: {
 365 |               slug: repository,
 366 |               project: {
 367 |                 key: workspace
 368 |               }
 369 |             }
 370 |           };
 371 |         }
 372 |         
 373 |         // Handle reviewers: preserve existing ones if not explicitly updating
 374 |         if (reviewers !== undefined) {
 375 |           // User wants to update reviewers
 376 |           // Create a map of existing reviewers for preservation of approval status
 377 |           const existingReviewersMap = new Map(
 378 |             currentPr.reviewers.map((r: any) => [r.user.name, r])
 379 |           );
 380 |           
 381 |           requestBody.reviewers = reviewers.map(username => {
 382 |             const existing = existingReviewersMap.get(username);
 383 |             if (existing) {
 384 |               // Preserve existing reviewer's full data including approval status
 385 |               return existing;
 386 |             } else {
 387 |               // Add new reviewer (without approval status)
 388 |               return { user: { name: username } };
 389 |             }
 390 |           });
 391 |         } else {
 392 |           // No reviewers provided - preserve existing reviewers with their full data
 393 |           requestBody.reviewers = currentPr.reviewers;
 394 |         }
 395 |       } else {
 396 |         // Bitbucket Cloud API
 397 |         apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
 398 |         
 399 |         if (title !== undefined) requestBody.title = title;
 400 |         if (description !== undefined) requestBody.description = description;
 401 |         if (destination_branch !== undefined) {
 402 |           requestBody.destination = {
 403 |             branch: {
 404 |               name: destination_branch
 405 |             }
 406 |           };
 407 |         }
 408 |         if (reviewers !== undefined) {
 409 |           requestBody.reviewers = reviewers.map(r => ({ username: r }));
 410 |         }
 411 |       }
 412 | 
 413 |       const pr = await this.apiClient.makeRequest<any>('put', apiPath, requestBody);
 414 |       
 415 |       const formattedResponse = this.apiClient.getIsServer() 
 416 |         ? formatServerResponse(pr as BitbucketServerPullRequest, undefined, this.baseUrl)
 417 |         : formatCloudResponse(pr as BitbucketCloudPullRequest);
 418 | 
 419 |       return {
 420 |         content: [
 421 |           {
 422 |             type: 'text',
 423 |             text: JSON.stringify({
 424 |               message: 'Pull request updated successfully',
 425 |               pull_request: formattedResponse
 426 |             }, null, 2),
 427 |           },
 428 |         ],
 429 |       };
 430 |     } catch (error) {
 431 |       return this.apiClient.handleApiError(error, `updating pull request ${pull_request_id} in ${workspace}/${repository}`);
 432 |     }
 433 |   }
 434 | 
 435 |   async handleAddComment(args: any) {
 436 |     if (!isAddCommentArgs(args)) {
 437 |       throw new McpError(
 438 |         ErrorCode.InvalidParams,
 439 |         'Invalid arguments for add_comment'
 440 |       );
 441 |     }
 442 | 
 443 |     let { 
 444 |       workspace, 
 445 |       repository, 
 446 |       pull_request_id, 
 447 |       comment_text, 
 448 |       parent_comment_id, 
 449 |       file_path, 
 450 |       line_number, 
 451 |       line_type,
 452 |       suggestion,
 453 |       suggestion_end_line,
 454 |       code_snippet,
 455 |       search_context,
 456 |       match_strategy = 'strict'
 457 |     } = args;
 458 | 
 459 |     let sequentialPosition: number | undefined;
 460 |     if (code_snippet && !line_number && file_path) {
 461 |       try {
 462 |         const resolved = await this.resolveLineFromCode(
 463 |           workspace,
 464 |           repository,
 465 |           pull_request_id,
 466 |           file_path,
 467 |           code_snippet,
 468 |           search_context,
 469 |           match_strategy
 470 |         );
 471 |         
 472 |         line_number = resolved.line_number;
 473 |         line_type = resolved.line_type;
 474 |         sequentialPosition = resolved.sequential_position;
 475 |       } catch (error) {
 476 |         throw error;
 477 |       }
 478 |     }
 479 | 
 480 |     if (suggestion && (!file_path || !line_number)) {
 481 |       throw new McpError(
 482 |         ErrorCode.InvalidParams,
 483 |         'Suggestions require file_path and line_number to be specified'
 484 |       );
 485 |     }
 486 | 
 487 |     const isInlineComment = file_path !== undefined && line_number !== undefined;
 488 | 
 489 |     let finalCommentText = comment_text;
 490 |     if (suggestion) {
 491 |       finalCommentText = formatSuggestionComment(
 492 |         comment_text,
 493 |         suggestion,
 494 |         line_number,
 495 |         suggestion_end_line || line_number
 496 |       );
 497 |     }
 498 | 
 499 |     try {
 500 |       let apiPath: string;
 501 |       let requestBody: any;
 502 | 
 503 |       if (this.apiClient.getIsServer()) {
 504 |         // Bitbucket Server API
 505 |         apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/comments`;
 506 |         requestBody = {
 507 |           text: finalCommentText
 508 |         };
 509 |         
 510 |         if (parent_comment_id !== undefined) {
 511 |           requestBody.parent = { id: parent_comment_id };
 512 |         }
 513 |         
 514 |         if (isInlineComment) {
 515 |           requestBody.anchor = {
 516 |             line: line_number,
 517 |             lineType: line_type || 'CONTEXT', 
 518 |             fileType: line_type === 'REMOVED' ? 'FROM' : 'TO',
 519 |             path: file_path,
 520 |             diffType: 'EFFECTIVE'
 521 |           };
 522 |           
 523 |         }
 524 |       } else {
 525 |         // Bitbucket Cloud API
 526 |         apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/comments`;
 527 |         requestBody = {
 528 |           content: {
 529 |             raw: finalCommentText
 530 |           }
 531 |         };
 532 |         
 533 |         if (parent_comment_id !== undefined) {
 534 |           requestBody.parent = { id: parent_comment_id };
 535 |         }
 536 |         
 537 |         if (isInlineComment) {
 538 |           requestBody.inline = {
 539 |             to: line_number,
 540 |             path: file_path
 541 |           };
 542 |         }
 543 |       }
 544 | 
 545 |       const comment = await this.apiClient.makeRequest<any>('post', apiPath, requestBody);
 546 | 
 547 |       const responseMessage = suggestion 
 548 |         ? 'Comment with code suggestion added successfully'
 549 |         : (isInlineComment ? 'Inline comment added successfully' : 'Comment added successfully');
 550 | 
 551 |       return {
 552 |         content: [
 553 |           {
 554 |             type: 'text',
 555 |             text: JSON.stringify({
 556 |               message: responseMessage,
 557 |               comment: {
 558 |                 id: comment.id,
 559 |                 text: this.apiClient.getIsServer() ? comment.text : comment.content.raw,
 560 |                 author: this.apiClient.getIsServer() ? comment.author.displayName : comment.user.display_name,
 561 |                 created_on: this.apiClient.getIsServer() ? new Date(comment.createdDate).toLocaleString() : comment.created_on,
 562 |                 file_path: isInlineComment ? file_path : undefined,
 563 |                 line_number: isInlineComment ? line_number : undefined,
 564 |                 line_type: isInlineComment ? (line_type || 'CONTEXT') : undefined,
 565 |                 has_suggestion: !!suggestion,
 566 |                 suggestion_lines: suggestion ? (suggestion_end_line ? `${line_number}-${suggestion_end_line}` : `${line_number}`) : undefined
 567 |               }
 568 |             }, null, 2),
 569 |           },
 570 |         ],
 571 |       };
 572 |     } catch (error) {
 573 |       return this.apiClient.handleApiError(error, `adding ${isInlineComment ? 'inline ' : ''}comment to pull request ${pull_request_id} in ${workspace}/${repository}`);
 574 |     }
 575 |   }
 576 | 
 577 |   async handleMergePullRequest(args: any) {
 578 |     if (!isMergePullRequestArgs(args)) {
 579 |       throw new McpError(
 580 |         ErrorCode.InvalidParams,
 581 |         'Invalid arguments for merge_pull_request'
 582 |       );
 583 |     }
 584 | 
 585 |     const { workspace, repository, pull_request_id, merge_strategy, close_source_branch, commit_message } = args;
 586 | 
 587 |     try {
 588 |       let apiPath: string;
 589 |       let requestBody: any = {};
 590 | 
 591 |       if (this.apiClient.getIsServer()) {
 592 |         // Bitbucket Server API
 593 |         apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/merge`;
 594 |         
 595 |         // Get current PR version
 596 |         const prPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`;
 597 |         const currentPr = await this.apiClient.makeRequest<any>('get', prPath);
 598 |         
 599 |         requestBody.version = currentPr.version;
 600 |         if (commit_message) {
 601 |           requestBody.message = commit_message;
 602 |         }
 603 |       } else {
 604 |         // Bitbucket Cloud API
 605 |         apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/merge`;
 606 |         
 607 |         if (merge_strategy) {
 608 |           requestBody.merge_strategy = merge_strategy;
 609 |         }
 610 |         if (close_source_branch !== undefined) {
 611 |           requestBody.close_source_branch = close_source_branch;
 612 |         }
 613 |         if (commit_message) {
 614 |           requestBody.message = commit_message;
 615 |         }
 616 |       }
 617 | 
 618 |       const result = await this.apiClient.makeRequest<any>('post', apiPath, requestBody);
 619 | 
 620 |       return {
 621 |         content: [
 622 |           {
 623 |             type: 'text',
 624 |             text: JSON.stringify({
 625 |               message: 'Pull request merged successfully',
 626 |               merge_commit: this.apiClient.getIsServer() ? result.properties?.mergeCommit : result.merge_commit?.hash,
 627 |               pull_request_id
 628 |             }, null, 2),
 629 |           },
 630 |         ],
 631 |       };
 632 |     } catch (error) {
 633 |       return this.apiClient.handleApiError(error, `merging pull request ${pull_request_id} in ${workspace}/${repository}`);
 634 |     }
 635 |   }
 636 | 
 637 |   private async fetchPullRequestComments(
 638 |     workspace: string,
 639 |     repository: string,
 640 |     pullRequestId: number
 641 |   ): Promise<{ comments: FormattedComment[]; activeCount: number; totalCount: number }> {
 642 |     try {
 643 |       let comments: FormattedComment[] = [];
 644 |       let activeCount = 0;
 645 |       let totalCount = 0;
 646 | 
 647 |       if (this.apiClient.getIsServer()) {
 648 |         const processNestedComments = (comment: any, anchor: any): FormattedComment => {
 649 |           const formattedComment: FormattedComment = {
 650 |             id: comment.id,
 651 |             author: comment.author.displayName,
 652 |             text: comment.text,
 653 |             created_on: new Date(comment.createdDate).toISOString(),
 654 |             is_inline: !!anchor,
 655 |             file_path: anchor?.path,
 656 |             line_number: anchor?.line,
 657 |             state: comment.state
 658 |           };
 659 | 
 660 |           if (comment.comments && comment.comments.length > 0) {
 661 |             formattedComment.replies = comment.comments
 662 |               .filter((reply: any) => {
 663 |                 if (reply.state === 'RESOLVED') return false;
 664 |                 if (anchor && anchor.orphaned === true) return false;
 665 |                 return true;
 666 |               })
 667 |               .map((reply: any) => processNestedComments(reply, anchor));
 668 |           }
 669 | 
 670 |           return formattedComment;
 671 |         };
 672 | 
 673 |         const countAllComments = (comment: any): number => {
 674 |           let count = 1;
 675 |           if (comment.comments && comment.comments.length > 0) {
 676 |             count += comment.comments.reduce((sum: number, reply: any) => sum + countAllComments(reply), 0);
 677 |           }
 678 |           return count;
 679 |         };
 680 | 
 681 |         const countActiveComments = (comment: any, anchor: any): number => {
 682 |           let count = 0;
 683 |           
 684 |           if (comment.state !== 'RESOLVED' && (!anchor || anchor.orphaned !== true)) {
 685 |             count = 1;
 686 |           }
 687 |           
 688 |           if (comment.comments && comment.comments.length > 0) {
 689 |             count += comment.comments.reduce((sum: number, reply: any) => sum + countActiveComments(reply, anchor), 0);
 690 |           }
 691 |           
 692 |           return count;
 693 |         };
 694 | 
 695 |         const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/activities`;
 696 |         const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
 697 |           params: { limit: 1000 }
 698 |         });
 699 | 
 700 |         const activities = response.values || [];
 701 |         
 702 |         const commentActivities = activities.filter((a: any) => 
 703 |           a.action === 'COMMENTED' && a.comment
 704 |         );
 705 | 
 706 |         totalCount = commentActivities.reduce((sum: number, activity: any) => {
 707 |           return sum + countAllComments(activity.comment);
 708 |         }, 0);
 709 | 
 710 |         activeCount = commentActivities.reduce((sum: number, activity: any) => {
 711 |           return sum + countActiveComments(activity.comment, activity.commentAnchor);
 712 |         }, 0);
 713 | 
 714 |         const processedComments = commentActivities
 715 |           .filter((a: any) => {
 716 |             const c = a.comment;
 717 |             const anchor = a.commentAnchor;
 718 |             
 719 |             if (c.state === 'RESOLVED') return false;
 720 |             if (anchor && anchor.orphaned === true) return false;
 721 |             
 722 |             return true;
 723 |           })
 724 |           .map((a: any) => processNestedComments(a.comment, a.commentAnchor));
 725 | 
 726 |         comments = processedComments.slice(0, 20);
 727 |       } else {
 728 |         const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/comments`;
 729 |         const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
 730 |           params: { pagelen: 100 }
 731 |         });
 732 | 
 733 |         const allComments = response.values || [];
 734 |         totalCount = allComments.length;
 735 | 
 736 |         const activeComments = allComments
 737 |           .filter((c: BitbucketCloudComment) => !c.deleted && !c.resolved)
 738 |           .slice(0, 20);
 739 | 
 740 |         activeCount = allComments.filter((c: BitbucketCloudComment) => !c.deleted && !c.resolved).length;
 741 | 
 742 |         comments = activeComments.map((c: BitbucketCloudComment) => ({
 743 |           id: c.id,
 744 |           author: c.user.display_name,
 745 |           text: c.content.raw,
 746 |           created_on: c.created_on,
 747 |           is_inline: !!c.inline,
 748 |           file_path: c.inline?.path,
 749 |           line_number: c.inline?.to
 750 |         }));
 751 |       }
 752 | 
 753 |       return { comments, activeCount, totalCount };
 754 |     } catch (error) {
 755 |       console.error('Failed to fetch comments:', error);
 756 |       return { comments: [], activeCount: 0, totalCount: 0 };
 757 |     }
 758 |   }
 759 | 
 760 |   private async fetchPullRequestFileChanges(
 761 |     workspace: string,
 762 |     repository: string,
 763 |     pullRequestId: number
 764 |   ): Promise<{ fileChanges: FormattedFileChange[]; summary: any }> {
 765 |     try {
 766 |       let fileChanges: FormattedFileChange[] = [];
 767 |       let totalLinesAdded = 0;
 768 |       let totalLinesRemoved = 0;
 769 | 
 770 |       if (this.apiClient.getIsServer()) {
 771 |         const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/changes`;
 772 |         const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
 773 |           params: { limit: 1000 }
 774 |         });
 775 | 
 776 |         const changes = response.values || [];
 777 | 
 778 |         fileChanges = changes.map((change: any) => {
 779 |           let status: 'added' | 'modified' | 'removed' | 'renamed' = 'modified';
 780 |           if (change.type === 'ADD') status = 'added';
 781 |           else if (change.type === 'DELETE') status = 'removed';
 782 |           else if (change.type === 'MOVE' || change.type === 'RENAME') status = 'renamed';
 783 | 
 784 |           return {
 785 |             path: change.path.toString,
 786 |             status,
 787 |             old_path: change.srcPath?.toString
 788 |           };
 789 |         });
 790 |       } else {
 791 |         const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diffstat`;
 792 |         const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
 793 |           params: { pagelen: 100 }
 794 |         });
 795 | 
 796 |         const diffstats = response.values || [];
 797 | 
 798 |         fileChanges = diffstats.map((stat: BitbucketCloudFileChange) => {
 799 |           totalLinesAdded += stat.lines_added;
 800 |           totalLinesRemoved += stat.lines_removed;
 801 | 
 802 |           return {
 803 |             path: stat.path,
 804 |             status: stat.type,
 805 |             old_path: stat.old?.path
 806 |           };
 807 |         });
 808 |       }
 809 | 
 810 |       const summary = {
 811 |         total_files: fileChanges.length
 812 |       };
 813 | 
 814 |       return { fileChanges, summary };
 815 |     } catch (error) {
 816 |       console.error('Failed to fetch file changes:', error);
 817 |       return {
 818 |         fileChanges: [],
 819 |         summary: {
 820 |           total_files: 0
 821 |         }
 822 |       };
 823 |     }
 824 |   }
 825 | 
 826 |   private async resolveLineFromCode(
 827 |     workspace: string,
 828 |     repository: string,
 829 |     pullRequestId: number,
 830 |     filePath: string,
 831 |     codeSnippet: string,
 832 |     searchContext?: { before?: string[]; after?: string[] },
 833 |     matchStrategy: 'strict' | 'best' = 'strict'
 834 |   ): Promise<{ 
 835 |     line_number: number; 
 836 |     line_type: 'ADDED' | 'REMOVED' | 'CONTEXT'; 
 837 |     sequential_position?: number;
 838 |     hunk_info?: any;
 839 |     diff_context?: string;
 840 |     diff_content_preview?: string;
 841 |     calculation_details?: string;
 842 |   }> {
 843 |     try {
 844 |       const diffContent = await this.getFilteredPullRequestDiff(workspace, repository, pullRequestId, filePath);
 845 |       
 846 |       const parser = new DiffParser();
 847 |       const sections = parser.parseDiffIntoSections(diffContent);
 848 |       
 849 |       let fileSection = sections[0];
 850 |       if (!this.apiClient.getIsServer()) {
 851 |         fileSection = sections.find(s => s.filePath === filePath) || sections[0];
 852 |       }
 853 | 
 854 |       if (!fileSection) {
 855 |         throw new McpError(
 856 |           ErrorCode.InvalidParams,
 857 |           `File ${filePath} not found in pull request diff`
 858 |         );
 859 |       }
 860 | 
 861 |       const matches = this.findCodeMatches(
 862 |         fileSection.content,
 863 |         codeSnippet,
 864 |         searchContext
 865 |       );
 866 |       
 867 |       if (matches.length === 0) {
 868 |         throw new McpError(
 869 |           ErrorCode.InvalidParams,
 870 |           `Code snippet not found in ${filePath}`
 871 |         );
 872 |       }
 873 | 
 874 |       if (matches.length === 1) {
 875 |         return {
 876 |           line_number: matches[0].line_number,
 877 |           line_type: matches[0].line_type,
 878 |           sequential_position: matches[0].sequential_position,
 879 |           hunk_info: matches[0].hunk_info,
 880 |           diff_context: matches[0].preview,
 881 |           diff_content_preview: diffContent.split('\n').slice(0, 50).join('\n'),
 882 |           calculation_details: `Direct line number from diff: ${matches[0].line_number}`
 883 |         };
 884 |       }
 885 | 
 886 |       if (matchStrategy === 'best') {
 887 |         const best = this.selectBestMatch(matches);
 888 |         
 889 |         return {
 890 |           line_number: best.line_number,
 891 |           line_type: best.line_type,
 892 |           sequential_position: best.sequential_position,
 893 |           hunk_info: best.hunk_info,
 894 |           diff_context: best.preview,
 895 |           diff_content_preview: diffContent.split('\n').slice(0, 50).join('\n'),
 896 |           calculation_details: `Best match selected from ${matches.length} matches, line: ${best.line_number}`
 897 |         };
 898 |       }
 899 | 
 900 |       const error: MultipleMatchesError = {
 901 |         code: 'MULTIPLE_MATCHES_FOUND',
 902 |         message: `Code snippet '${codeSnippet.substring(0, 50)}...' found in ${matches.length} locations`,
 903 |         occurrences: matches.map(m => ({
 904 |           line_number: m.line_number,
 905 |           file_path: filePath,
 906 |           preview: m.preview,
 907 |           confidence: m.confidence,
 908 |           line_type: m.line_type
 909 |         })),
 910 |         suggestion: 'To resolve, either:\n1. Add more context to uniquely identify the location\n2. Use match_strategy: \'best\' to auto-select highest confidence match\n3. Use line_number directly'
 911 |       };
 912 | 
 913 |       throw new McpError(
 914 |         ErrorCode.InvalidParams,
 915 |         JSON.stringify({ error })
 916 |       );
 917 |     } catch (error) {
 918 |       if (error instanceof McpError) {
 919 |         throw error;
 920 |       }
 921 |       throw new McpError(
 922 |         ErrorCode.InternalError,
 923 |         `Failed to resolve line from code: ${error instanceof Error ? error.message : String(error)}`
 924 |       );
 925 |     }
 926 |   }
 927 | 
 928 |   private findCodeMatches(
 929 |     diffContent: string,
 930 |     codeSnippet: string,
 931 |     searchContext?: { before?: string[]; after?: string[] }
 932 |   ): CodeMatch[] {
 933 |     const lines = diffContent.split('\n');
 934 |     const matches: CodeMatch[] = [];
 935 |     let currentDestLine = 0; // Destination file line number
 936 |     let currentSrcLine = 0;  // Source file line number
 937 |     let inHunk = false;
 938 |     let sequentialAddedCount = 0; // Track sequential ADDED lines
 939 |     let currentHunkIndex = -1;
 940 |     let currentHunkDestStart = 0;
 941 |     let currentHunkSrcStart = 0;
 942 |     let destPositionInHunk = 0; // Track position in destination file relative to hunk start
 943 |     let srcPositionInHunk = 0;  // Track position in source file relative to hunk start
 944 | 
 945 |     for (let i = 0; i < lines.length; i++) {
 946 |       const line = lines[i];
 947 | 
 948 |       if (line.startsWith('@@')) {
 949 |         const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
 950 |         if (match) {
 951 |           currentHunkSrcStart = parseInt(match[1]);
 952 |           currentHunkDestStart = parseInt(match[2]);
 953 |           currentSrcLine = currentHunkSrcStart;
 954 |           currentDestLine = currentHunkDestStart;
 955 |           inHunk = true;
 956 |           currentHunkIndex++;
 957 |           destPositionInHunk = 0;
 958 |           srcPositionInHunk = 0;
 959 |           continue;
 960 |         }
 961 |       }
 962 | 
 963 |       if (!inHunk) continue;
 964 | 
 965 |       if (line === '') {
 966 |         inHunk = false;
 967 |         continue;
 968 |       }
 969 | 
 970 |       let lineType: 'ADDED' | 'REMOVED' | 'CONTEXT';
 971 |       let lineContent = '';
 972 |       let lineNumber = 0;
 973 | 
 974 |       if (line.startsWith('+')) {
 975 |         lineType = 'ADDED';
 976 |         lineContent = line.substring(1);
 977 |         lineNumber = currentHunkDestStart + destPositionInHunk;
 978 |         destPositionInHunk++;
 979 |         sequentialAddedCount++;
 980 |       } else if (line.startsWith('-')) {
 981 |         lineType = 'REMOVED';
 982 |         lineContent = line.substring(1);
 983 |         lineNumber = currentHunkSrcStart + srcPositionInHunk;
 984 |         srcPositionInHunk++;
 985 |       } else if (line.startsWith(' ')) {
 986 |         lineType = 'CONTEXT';
 987 |         lineContent = line.substring(1);
 988 |         lineNumber = currentHunkDestStart + destPositionInHunk;
 989 |         destPositionInHunk++;
 990 |         srcPositionInHunk++;
 991 |       } else {
 992 |         inHunk = false;
 993 |         continue;
 994 |       }
 995 | 
 996 |       if (lineContent.trim() === codeSnippet.trim()) {
 997 |         const confidence = this.calculateConfidence(
 998 |           lines,
 999 |           i,
1000 |           searchContext,
1001 |           lineType
1002 |         );
1003 | 
1004 |         matches.push({
1005 |           line_number: lineNumber,
1006 |           line_type: lineType,
1007 |           exact_content: codeSnippet,
1008 |           preview: this.getPreview(lines, i),
1009 |           confidence,
1010 |           context: this.extractContext(lines, i),
1011 |           sequential_position: lineType === 'ADDED' ? sequentialAddedCount : undefined,
1012 |           hunk_info: {
1013 |             hunk_index: currentHunkIndex,
1014 |             destination_start: currentHunkDestStart,
1015 |             line_in_hunk: destPositionInHunk
1016 |           }
1017 |         });
1018 |       }
1019 | 
1020 |       if (lineType === 'ADDED') {
1021 |         currentDestLine++;
1022 |       } else if (lineType === 'REMOVED') {
1023 |         currentSrcLine++;
1024 |       } else if (lineType === 'CONTEXT') {
1025 |         currentSrcLine++;
1026 |         currentDestLine++;
1027 |       }
1028 |     }
1029 | 
1030 |     return matches;
1031 |   }
1032 | 
1033 |   private calculateConfidence(
1034 |     lines: string[],
1035 |     index: number,
1036 |     searchContext?: { before?: string[]; after?: string[] },
1037 |     lineType?: 'ADDED' | 'REMOVED' | 'CONTEXT'
1038 |   ): number {
1039 |     let confidence = 0.5; // Base confidence
1040 | 
1041 |     if (!searchContext) {
1042 |       return confidence;
1043 |     }
1044 | 
1045 |     if (searchContext.before) {
1046 |       let matchedBefore = 0;
1047 |       for (let j = 0; j < searchContext.before.length; j++) {
1048 |         const contextLine = searchContext.before[searchContext.before.length - 1 - j];
1049 |         const checkIndex = index - j - 1;
1050 |         if (checkIndex >= 0) {
1051 |           const checkLine = lines[checkIndex].substring(1);
1052 |           if (checkLine.trim() === contextLine.trim()) {
1053 |             matchedBefore++;
1054 |           }
1055 |         }
1056 |       }
1057 |       confidence += (matchedBefore / searchContext.before.length) * 0.3;
1058 |     }
1059 | 
1060 |     if (searchContext.after) {
1061 |       let matchedAfter = 0;
1062 |       for (let j = 0; j < searchContext.after.length; j++) {
1063 |         const contextLine = searchContext.after[j];
1064 |         const checkIndex = index + j + 1;
1065 |         if (checkIndex < lines.length) {
1066 |           const checkLine = lines[checkIndex].substring(1);
1067 |           if (checkLine.trim() === contextLine.trim()) {
1068 |             matchedAfter++;
1069 |           }
1070 |         }
1071 |       }
1072 |       confidence += (matchedAfter / searchContext.after.length) * 0.3;
1073 |     }
1074 | 
1075 |     if (lineType === 'ADDED') {
1076 |       confidence += 0.1;
1077 |     }
1078 | 
1079 |     return Math.min(confidence, 1.0);
1080 |   }
1081 | 
1082 |   private getPreview(lines: string[], index: number): string {
1083 |     const start = Math.max(0, index - 1);
1084 |     const end = Math.min(lines.length, index + 2);
1085 |     const previewLines = [];
1086 | 
1087 |     for (let i = start; i < end; i++) {
1088 |       const prefix = i === index ? '> ' : '  ';
1089 |       previewLines.push(prefix + lines[i]);
1090 |     }
1091 | 
1092 |     return previewLines.join('\n');
1093 |   }
1094 | 
1095 |   private extractContext(lines: string[], index: number): { lines_before: string[]; lines_after: string[] } {
1096 |     const linesBefore: string[] = [];
1097 |     const linesAfter: string[] = [];
1098 | 
1099 |     for (let i = Math.max(0, index - 2); i < index; i++) {
1100 |       if (lines[i].match(/^[+\- ]/)) {
1101 |         linesBefore.push(lines[i].substring(1));
1102 |       }
1103 |     }
1104 | 
1105 |     for (let i = index + 1; i < Math.min(lines.length, index + 3); i++) {
1106 |       if (lines[i].match(/^[+\- ]/)) {
1107 |         linesAfter.push(lines[i].substring(1));
1108 |       }
1109 |     }
1110 | 
1111 |     return {
1112 |       lines_before: linesBefore,
1113 |       lines_after: linesAfter
1114 |     };
1115 |   }
1116 | 
1117 |   private selectBestMatch(matches: CodeMatch[]): CodeMatch {
1118 |     return matches.sort((a, b) => b.confidence - a.confidence)[0];
1119 |   }
1120 | 
1121 |   async handleListPrCommits(args: any) {
1122 |     if (!isListPrCommitsArgs(args)) {
1123 |       throw new McpError(
1124 |         ErrorCode.InvalidParams,
1125 |         'Invalid arguments for list_pr_commits'
1126 |       );
1127 |     }
1128 | 
1129 |     const { workspace, repository, pull_request_id, limit = 25, start = 0, include_build_status = false } = args;
1130 | 
1131 |     try {
1132 |       // First get the PR details to include in response
1133 |       const prPath = this.apiClient.getIsServer()
1134 |         ? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
1135 |         : `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
1136 |       
1137 |       let prTitle = '';
1138 |       try {
1139 |         const pr = await this.apiClient.makeRequest<any>('get', prPath);
1140 |         prTitle = pr.title;
1141 |       } catch (e) {
1142 |         // Ignore error, PR title is optional
1143 |       }
1144 | 
1145 |       let apiPath: string;
1146 |       let params: any = {};
1147 |       let commits: FormattedCommit[] = [];
1148 |       let totalCount = 0;
1149 |       let nextPageStart: number | null = null;
1150 | 
1151 |       if (this.apiClient.getIsServer()) {
1152 |         // Bitbucket Server API
1153 |         apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/commits`;
1154 |         params = {
1155 |           limit,
1156 |           start,
1157 |           withCounts: true
1158 |         };
1159 | 
1160 |         const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
1161 | 
1162 |         // Format commits
1163 |         commits = (response.values || []).map((commit: BitbucketServerCommit) => formatServerCommit(commit));
1164 | 
1165 |         totalCount = response.size || commits.length;
1166 |         if (!response.isLastPage && response.nextPageStart !== undefined) {
1167 |           nextPageStart = response.nextPageStart;
1168 |         }
1169 |       } else {
1170 |         // Bitbucket Cloud API
1171 |         apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/commits`;
1172 |         params = {
1173 |           pagelen: limit,
1174 |           page: Math.floor(start / limit) + 1
1175 |         };
1176 | 
1177 |         const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
1178 | 
1179 |         // Format commits
1180 |         commits = (response.values || []).map((commit: BitbucketCloudCommit) => formatCloudCommit(commit));
1181 | 
1182 |         totalCount = response.size || commits.length;
1183 |         if (response.next) {
1184 |           nextPageStart = start + limit;
1185 |         }
1186 |       }
1187 | 
1188 |       // Fetch build status if requested (Server only)
1189 |       if (include_build_status && this.apiClient.getIsServer() && commits.length > 0) {
1190 |         try {
1191 |           const commitIds = commits.map(c => c.hash);
1192 |           const buildSummaries = await this.apiClient.getBuildSummaries(
1193 |             workspace,
1194 |             repository,
1195 |             commitIds
1196 |           );
1197 | 
1198 |           // Enhance commits with build status
1199 |           commits = commits.map(commit => {
1200 |             const buildData = buildSummaries[commit.hash];
1201 |             if (buildData) {
1202 |               return {
1203 |                 ...commit,
1204 |                 build_status: {
1205 |                   successful: buildData.successful || 0,
1206 |                   failed: buildData.failed || 0,
1207 |                   in_progress: buildData.inProgress || 0,
1208 |                   unknown: buildData.unknown || 0
1209 |                 }
1210 |               };
1211 |             }
1212 |             return commit;
1213 |           });
1214 |         } catch (error) {
1215 |           console.error('Failed to fetch build status for PR commits:', error);
1216 |           // Graceful degradation - continue without build status
1217 |         }
1218 |       }
1219 | 
1220 |       return {
1221 |         content: [
1222 |           {
1223 |             type: 'text',
1224 |             text: JSON.stringify({
1225 |               pull_request_id,
1226 |               pull_request_title: prTitle,
1227 |               commits,
1228 |               total_count: totalCount,
1229 |               start,
1230 |               limit,
1231 |               has_more: nextPageStart !== null,
1232 |               next_start: nextPageStart
1233 |             }, null, 2),
1234 |           },
1235 |         ],
1236 |       };
1237 |     } catch (error) {
1238 |       return this.apiClient.handleApiError(error, `listing commits for pull request ${pull_request_id} in ${workspace}/${repository}`);
1239 |     }
1240 |   }
1241 | }
1242 | 
```
Page 2/2FirstPrevNextLast