This is page 1 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 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | build/ 6 | dist/ 7 | 8 | # Environment files 9 | .env 10 | .env.local 11 | .env.*.local 12 | 13 | # IDE files 14 | .vscode/ 15 | .idea/ 16 | *.swp 17 | *.swo 18 | *~ 19 | 20 | # OS files 21 | .DS_Store 22 | Thumbs.db 23 | 24 | # Logs 25 | logs/ 26 | *.log 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # Test files 32 | test-api.js 33 | *.test.js 34 | 35 | # Temporary files 36 | *.tmp 37 | *.temp 38 | .cache/ 39 | 40 | # Personal configuration 41 | RELOAD_INSTRUCTIONS.md 42 | personal-notes.md 43 | currentTask.yml 44 | ``` -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` 1 | # Source files 2 | src/ 3 | *.ts 4 | !*.d.ts 5 | 6 | # Development files 7 | .gitignore 8 | tsconfig.json 9 | .vscode/ 10 | .idea/ 11 | 12 | # Test files 13 | test/ 14 | *.test.js 15 | *.spec.js 16 | 17 | # Build artifacts 18 | *.log 19 | node_modules/ 20 | .npm/ 21 | 22 | # OS files 23 | .DS_Store 24 | Thumbs.db 25 | 26 | # Editor files 27 | *.swp 28 | *.swo 29 | *~ 30 | 31 | # Documentation source 32 | docs/ 33 | 34 | # Scripts (except built ones) 35 | scripts/ 36 | 37 | # Setup guides (keep README) 38 | SETUP_GUIDE.md 39 | SETUP_GUIDE_SERVER.md 40 | 41 | # Git 42 | .git/ 43 | .gitattributes 44 | 45 | # Other 46 | .env 47 | .env.local 48 | .env.*.local 49 | ``` -------------------------------------------------------------------------------- /memory-bank/.clinerules: -------------------------------------------------------------------------------- ``` 1 | # Bitbucket MCP Server - Project Intelligence 2 | 3 | ## Critical Implementation Paths 4 | 5 | ### Adding New Tools 6 | 1. Create handler in src/handlers/ following existing patterns 7 | 2. Add TypeScript interfaces in src/types/bitbucket.ts 8 | 3. Add type guard in src/types/guards.ts 9 | 4. Add formatter in src/utils/formatters.ts if needed 10 | 5. Register tool in src/tools/definitions.ts 11 | 6. Wire handler in src/index.ts 12 | 7. Update version, CHANGELOG.md, and README.md 13 | 14 | ### API Variant Handling 15 | - Always check `apiClient.getIsServer()` for Cloud vs Server 16 | - Server uses `/rest/api/1.0/` prefix 17 | - Cloud uses direct paths under base URL 18 | - Different parameter names (e.g., pagelen vs limit) 19 | 20 | ### Error Handling Pattern 21 | ```typescript 22 | try { 23 | // API call 24 | } catch (error: any) { 25 | const errorMessage = error.response?.data?.errors?.[0]?.message || error.message; 26 | return { 27 | content: [{ 28 | type: 'text', 29 | text: JSON.stringify({ 30 | error: `Failed to ${action}: ${errorMessage}`, 31 | details: error.response?.data 32 | }, null, 2) 33 | }], 34 | isError: true 35 | }; 36 | } 37 | ``` 38 | 39 | ## User Preferences 40 | 41 | ### Documentation Style 42 | - Comprehensive examples for each tool 43 | - Clear parameter descriptions 44 | - Response format examples 45 | - Note differences between Cloud and Server 46 | 47 | ### Code Style 48 | - TypeScript with strict typing 49 | - ES modules with .js extensions in imports 50 | - Consistent error handling 51 | - Modular architecture 52 | 53 | ## Project-Specific Patterns 54 | 55 | ### Authentication 56 | - Cloud: Username (not email) + App Password 57 | - Server: Email address + HTTP Access Token 58 | - Environment variables for configuration 59 | 60 | ### Pagination Pattern 61 | - `limit` and `start` parameters 62 | - Return `has_more` and `next_start` 63 | - Include `total_count` when available 64 | 65 | ### Response Formatting 66 | - Consistent JSON structure 67 | - Include operation status message 68 | - Provide detailed error information 69 | - Format dates as ISO strings 70 | 71 | ## Known Challenges 72 | 73 | ### Bitbucket API Differences 74 | - Parameter naming varies (e.g., pagelen vs limit) 75 | - Response structures differ significantly 76 | - Some features only available on one variant 77 | - Authentication methods completely different 78 | 79 | ### Search Functionality 80 | - Only available on Bitbucket Server 81 | - Query syntax requires specific format 82 | - No Cloud API equivalent currently 83 | 84 | ## Tool Usage Patterns 85 | 86 | ### List Operations 87 | - Always include pagination parameters 88 | - Return consistent metadata 89 | - Support filtering where applicable 90 | 91 | ### Modification Operations 92 | - Validate required parameters 93 | - Preserve existing data when updating 94 | - Return updated resource in response 95 | 96 | ### File Operations 97 | - Smart truncation for large files 98 | - Type-based default limits 99 | - Support line range selection 100 | 101 | ## Evolution of Decisions 102 | 103 | ### Version 0.3.0 104 | - Modularized codebase into handlers 105 | - Separated types and utilities 106 | - Improved maintainability 107 | 108 | ### Version 0.6.0 109 | - Enhanced PR details with comments/files 110 | - Added parallel API calls for performance 111 | 112 | ### Version 0.9.0 113 | - Added code snippet matching for comments 114 | - Implemented confidence scoring 115 | 116 | ### Version 1.0.0 117 | - Added code search functionality 118 | - Reached feature completeness 119 | - Ready for production use 120 | 121 | ### Version 1.0.1 122 | - Improved search response formatting for AI consumption 123 | - Added simplified formatCodeSearchOutput 124 | - Enhanced HTML entity decoding and tag stripping 125 | - Established live MCP testing workflow 126 | 127 | ## Important Architecture Decisions 128 | 129 | ### Handler Pattern 130 | Each tool category has its own handler class to maintain single responsibility and make the codebase more maintainable. 131 | 132 | ### Type Guards 133 | All tool inputs are validated with type guards to ensure type safety at runtime. 134 | 135 | ### Response Normalization 136 | Different API responses are normalized to consistent formats for easier consumption. 137 | 138 | ### Error Handling 139 | Consistent error handling across all tools with detailed error messages and recovery suggestions. 140 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Bitbucket MCP Server 2 | 3 | [](https://www.npmjs.com/package/@nexus2520/bitbucket-mcp-server) 4 | [](https://opensource.org/licenses/MIT) 5 | 6 | An MCP (Model Context Protocol) server that provides tools for interacting with the Bitbucket API, supporting both Bitbucket Cloud and Bitbucket Server. 7 | 8 | ## Features 9 | 10 | ### Currently Implemented Tools 11 | 12 | #### Core PR Lifecycle Tools 13 | - `get_pull_request` - Retrieve detailed information about a pull request 14 | - `list_pull_requests` - List pull requests with filters (state, author, pagination) 15 | - `create_pull_request` - Create new pull requests 16 | - `update_pull_request` - Update PR details (title, description, reviewers, destination branch) 17 | - `add_comment` - Add comments to pull requests (supports replies) 18 | - `merge_pull_request` - Merge pull requests with various strategies 19 | - `list_pr_commits` - List all commits that are part of a pull request 20 | - `delete_branch` - Delete branches after merge 21 | 22 | #### Branch Management Tools 23 | - `list_branches` - List branches with filtering and pagination 24 | - `delete_branch` - Delete branches (with protection checks) 25 | - `get_branch` - Get detailed branch information including associated PRs 26 | - `list_branch_commits` - List commits in a branch with advanced filtering 27 | 28 | #### File and Directory Tools 29 | - `list_directory_content` - List files and directories in a repository path 30 | - `get_file_content` - Get file content with smart truncation for large files 31 | 32 | #### Code Review Tools 33 | - `get_pull_request_diff` - Get the diff/changes for a pull request 34 | - `approve_pull_request` - Approve a pull request 35 | - `unapprove_pull_request` - Remove approval from a pull request 36 | - `request_changes` - Request changes on a pull request 37 | - `remove_requested_changes` - Remove change request from a pull request 38 | 39 | #### Search Tools 40 | - `search_code` - Search for code across repositories (currently Bitbucket Server only) 41 | 42 | #### Project and Repository Discovery Tools 43 | - `list_projects` - List all accessible Bitbucket projects/workspaces with filtering 44 | - `list_repositories` - List repositories in a project or across all accessible projects 45 | 46 | ## Installation 47 | 48 | ### Using npx (Recommended) 49 | 50 | The easiest way to use this MCP server is directly with npx: 51 | 52 | ```json 53 | { 54 | "mcpServers": { 55 | "bitbucket": { 56 | "command": "npx", 57 | "args": [ 58 | "-y", 59 | "@nexus2520/bitbucket-mcp-server" 60 | ], 61 | "env": { 62 | "BITBUCKET_USERNAME": "your-username", 63 | "BITBUCKET_APP_PASSWORD": "your-app-password" 64 | } 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | For Bitbucket Server: 71 | ```json 72 | { 73 | "mcpServers": { 74 | "bitbucket": { 75 | "command": "npx", 76 | "args": [ 77 | "-y", 78 | "@nexus2520/bitbucket-mcp-server" 79 | ], 80 | "env": { 81 | "BITBUCKET_USERNAME": "[email protected]", 82 | "BITBUCKET_TOKEN": "your-http-access-token", 83 | "BITBUCKET_BASE_URL": "https://bitbucket.yourcompany.com" 84 | } 85 | } 86 | } 87 | } 88 | ``` 89 | 90 | ### From Source 91 | 92 | 1. Clone or download this repository 93 | 2. Install dependencies: 94 | ```bash 95 | npm install 96 | ``` 97 | 3. Build the TypeScript code: 98 | ```bash 99 | npm run build 100 | ``` 101 | 102 | ## Authentication Setup 103 | 104 | This server uses Bitbucket App Passwords for authentication. 105 | 106 | ### Creating an App Password 107 | 108 | 1. Log in to your Bitbucket account 109 | 2. Navigate to: https://bitbucket.org/account/settings/app-passwords/ 110 | 3. Click "Create app password" 111 | 4. Give it a descriptive label (e.g., "MCP Server") 112 | 5. Select the following permissions: 113 | - **Account**: Read 114 | - **Repositories**: Read, Write 115 | - **Pull requests**: Read, Write 116 | 6. Click "Create" 117 | 7. **Important**: Copy the generated password immediately (you won't be able to see it again!) 118 | 119 | ### Running the Setup Script 120 | 121 | ```bash 122 | node scripts/setup-auth.js 123 | ``` 124 | 125 | This will guide you through the authentication setup process. 126 | 127 | ## Configuration 128 | 129 | Add the server to your MCP settings file (usually located at `~/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`): 130 | 131 | ```json 132 | { 133 | "mcpServers": { 134 | "bitbucket": { 135 | "command": "node", 136 | "args": ["/absolute/path/to/bitbucket-mcp-server/build/index.js"], 137 | "env": { 138 | "BITBUCKET_USERNAME": "your-username", 139 | "BITBUCKET_APP_PASSWORD": "your-app-password" 140 | } 141 | } 142 | } 143 | } 144 | ``` 145 | 146 | Replace: 147 | - `/absolute/path/to/bitbucket-mcp-server` with the actual path to this directory 148 | - `your-username` with your Bitbucket username (not email) 149 | - `your-app-password` with the app password you created 150 | 151 | For Bitbucket Server, use: 152 | ```json 153 | { 154 | "mcpServers": { 155 | "bitbucket": { 156 | "command": "node", 157 | "args": ["/absolute/path/to/bitbucket-mcp-server/build/index.js"], 158 | "env": { 159 | "BITBUCKET_USERNAME": "[email protected]", 160 | "BITBUCKET_TOKEN": "your-http-access-token", 161 | "BITBUCKET_BASE_URL": "https://bitbucket.yourcompany.com" 162 | } 163 | } 164 | } 165 | } 166 | ``` 167 | 168 | **Important for Bitbucket Server users:** 169 | - Use your full email address as the username (e.g., "[email protected]") 170 | - This is required for approval/review actions to work correctly 171 | 172 | ## Usage 173 | 174 | Once configured, you can use the available tools: 175 | 176 | ### Get Pull Request 177 | 178 | ```typescript 179 | { 180 | "tool": "get_pull_request", 181 | "arguments": { 182 | "workspace": "PROJ", // Required - your project key 183 | "repository": "my-repo", 184 | "pull_request_id": 123 185 | } 186 | } 187 | ``` 188 | 189 | Returns detailed information about the pull request including: 190 | - Title and description 191 | - Author and reviewers 192 | - Source and destination branches 193 | - Approval status 194 | - Links to web UI and diff 195 | - **Merge commit details** (when PR is merged): 196 | - `merge_commit_hash`: The hash of the merge commit 197 | - `merged_by`: Who performed the merge 198 | - `merged_at`: When the merge occurred 199 | - `merge_commit_message`: The merge commit message 200 | - **Active comments with nested replies** (unresolved comments that need attention): 201 | - `active_comments`: Array of active comments (up to 20 most recent top-level comments) 202 | - Comment text and author 203 | - Creation date 204 | - Whether it's an inline comment (with file path and line number) 205 | - **Nested replies** (for Bitbucket Server): 206 | - `replies`: Array of reply comments with same structure 207 | - Replies can be nested multiple levels deep 208 | - **Parent reference** (for Bitbucket Cloud): 209 | - `parent_id`: ID of the parent comment for replies 210 | - `active_comment_count`: Total count of unresolved comments (including nested replies) 211 | - `total_comment_count`: Total count of all comments (including resolved and replies) 212 | - **File changes**: 213 | - `file_changes`: Array of all files modified in the PR 214 | - File path 215 | - Status (added, modified, removed, or renamed) 216 | - Old path (for renamed files) 217 | - `file_changes_summary`: Summary statistics 218 | - Total files changed 219 | - And more... 220 | 221 | ### Search Code 222 | 223 | Search for code across Bitbucket repositories (currently only supported for Bitbucket Server): 224 | 225 | ```typescript 226 | // Search in a specific repository 227 | { 228 | "tool": "search_code", 229 | "arguments": { 230 | "workspace": "PROJ", 231 | "repository": "my-repo", 232 | "search_query": "TODO", 233 | "limit": 50 234 | } 235 | } 236 | 237 | // Search across all repositories in a workspace 238 | { 239 | "tool": "search_code", 240 | "arguments": { 241 | "workspace": "PROJ", 242 | "search_query": "deprecated", 243 | "file_pattern": "*.java", // Optional: filter by file pattern 244 | "limit": 100 245 | } 246 | } 247 | 248 | // Search with file pattern filtering 249 | { 250 | "tool": "search_code", 251 | "arguments": { 252 | "workspace": "PROJ", 253 | "repository": "frontend-app", 254 | "search_query": "useState", 255 | "file_pattern": "*.tsx", // Only search in .tsx files 256 | "start": 0, 257 | "limit": 25 258 | } 259 | } 260 | ``` 261 | 262 | Returns search results with: 263 | - File path and name 264 | - Repository and project information 265 | - Matched lines with: 266 | - Line number 267 | - Full line content 268 | - Highlighted segments showing exact matches 269 | - Pagination information 270 | 271 | Example response: 272 | ```json 273 | { 274 | "message": "Code search completed successfully", 275 | "workspace": "PROJ", 276 | "repository": "my-repo", 277 | "search_query": "TODO", 278 | "results": [ 279 | { 280 | "file_path": "src/utils/helper.js", 281 | "file_name": "helper.js", 282 | "repository": "my-repo", 283 | "project": "PROJ", 284 | "matches": [ 285 | { 286 | "line_number": 42, 287 | "line_content": " // TODO: Implement error handling", 288 | "highlighted_segments": [ 289 | { "text": " // ", "is_match": false }, 290 | { "text": "TODO", "is_match": true }, 291 | { "text": ": Implement error handling", "is_match": false } 292 | ] 293 | } 294 | ] 295 | } 296 | ], 297 | "total_count": 15, 298 | "start": 0, 299 | "limit": 50, 300 | "has_more": false 301 | } 302 | ``` 303 | 304 | **Note**: This tool currently only works with Bitbucket Server. Bitbucket Cloud support is planned for a future release. 305 | 306 | ### List Pull Requests 307 | 308 | ```typescript 309 | { 310 | "tool": "list_pull_requests", 311 | "arguments": { 312 | "workspace": "PROJ", // Required - your project key 313 | "repository": "my-repo", 314 | "state": "OPEN", // Optional: OPEN, MERGED, DECLINED, ALL (default: OPEN) 315 | "author": "username", // Optional: filter by author (see note below) 316 | "limit": 25, // Optional: max results per page (default: 25) 317 | "start": 0 // Optional: pagination start index (default: 0) 318 | } 319 | } 320 | ``` 321 | 322 | Returns a paginated list of pull requests with: 323 | - Array of pull requests with same details as get_pull_request 324 | - Total count of matching PRs 325 | - Pagination info (has_more, next_start) 326 | 327 | **Note on Author Filter:** 328 | - For Bitbucket Cloud: Use the username (e.g., "johndoe") 329 | - For Bitbucket Server: Use the full email address (e.g., "[email protected]") 330 | 331 | ### Create Pull Request 332 | 333 | ```typescript 334 | { 335 | "tool": "create_pull_request", 336 | "arguments": { 337 | "workspace": "PROJ", 338 | "repository": "my-repo", 339 | "title": "Add new feature", 340 | "source_branch": "feature/new-feature", 341 | "destination_branch": "main", 342 | "description": "This PR adds a new feature...", // Optional 343 | "reviewers": ["john.doe", "jane.smith"], // Optional 344 | "close_source_branch": true // Optional (default: false) 345 | } 346 | } 347 | ``` 348 | 349 | ### Update Pull Request 350 | 351 | ```typescript 352 | { 353 | "tool": "update_pull_request", 354 | "arguments": { 355 | "workspace": "PROJ", 356 | "repository": "my-repo", 357 | "pull_request_id": 123, 358 | "title": "Updated title", // Optional 359 | "description": "Updated description", // Optional 360 | "destination_branch": "develop", // Optional 361 | "reviewers": ["new.reviewer"] // Optional - see note below 362 | } 363 | } 364 | ``` 365 | 366 | **Important Note on Reviewers:** 367 | - When updating a PR without specifying the `reviewers` parameter, existing reviewers and their approval status are preserved 368 | - When providing the `reviewers` parameter: 369 | - The reviewer list is replaced with the new list 370 | - For reviewers that already exist on the PR, their approval status is preserved 371 | - New reviewers are added without approval status 372 | - This prevents accidentally removing reviewers when you only want to update the PR description or title 373 | 374 | ### Add Comment 375 | 376 | Add a comment to a pull request, either as a general comment or inline on specific code: 377 | 378 | ```javascript 379 | // General comment 380 | { 381 | "tool": "add_comment", 382 | "arguments": { 383 | "workspace": "PROJ", 384 | "repository": "my-repo", 385 | "pull_request_id": 123, 386 | "comment_text": "Great work on this PR!" 387 | } 388 | } 389 | 390 | // Inline comment on specific line 391 | { 392 | "tool": "add_comment", 393 | "arguments": { 394 | "workspace": "PROJ", 395 | "repository": "my-repo", 396 | "pull_request_id": 123, 397 | "comment_text": "Consider extracting this into a separate function", 398 | "file_path": "src/utils/helpers.js", 399 | "line_number": 42, 400 | "line_type": "CONTEXT" // ADDED, REMOVED, or CONTEXT 401 | } 402 | } 403 | 404 | // Reply to existing comment 405 | { 406 | "tool": "add_comment", 407 | "arguments": { 408 | "workspace": "PROJ", 409 | "repository": "my-repo", 410 | "pull_request_id": 123, 411 | "comment_text": "I agree with this suggestion", 412 | "parent_comment_id": 456 413 | } 414 | } 415 | 416 | // Add comment with code suggestion (single line) 417 | { 418 | "tool": "add_comment", 419 | "arguments": { 420 | "workspace": "PROJ", 421 | "repository": "my-repo", 422 | "pull_request_id": 123, 423 | "comment_text": "This variable name could be more descriptive.", 424 | "file_path": "src/utils/helpers.js", 425 | "line_number": 42, 426 | "line_type": "CONTEXT", 427 | "suggestion": "const userAuthenticationToken = token;" 428 | } 429 | } 430 | 431 | // Add comment with multi-line code suggestion 432 | { 433 | "tool": "add_comment", 434 | "arguments": { 435 | "workspace": "PROJ", 436 | "repository": "my-repo", 437 | "pull_request_id": 123, 438 | "comment_text": "This function could be simplified using array methods.", 439 | "file_path": "src/utils/calculations.js", 440 | "line_number": 50, 441 | "suggestion_end_line": 55, 442 | "line_type": "CONTEXT", 443 | "suggestion": "function calculateTotal(items) {\n return items.reduce((sum, item) => sum + item.price, 0);\n}" 444 | } 445 | } 446 | ``` 447 | 448 | The suggestion feature formats comments using GitHub-style markdown suggestion blocks that Bitbucket can render. When adding a suggestion: 449 | - `suggestion` is required and contains the replacement code 450 | - `file_path` and `line_number` are required when using suggestions 451 | - `suggestion_end_line` is optional and used for multi-line suggestions (defaults to `line_number`) 452 | - The comment will be formatted with a ````suggestion` markdown block that may be applicable in the Bitbucket UI 453 | 454 | ### Using Code Snippets Instead of Line Numbers 455 | 456 | The `add_comment` tool now supports finding line numbers automatically using code snippets. This is especially useful when AI tools analyze diffs and may struggle with exact line numbers: 457 | 458 | ```javascript 459 | // Add comment using code snippet 460 | { 461 | "tool": "add_comment", 462 | "arguments": { 463 | "workspace": "PROJ", 464 | "repository": "my-repo", 465 | "pull_request_id": 123, 466 | "comment_text": "This variable name could be more descriptive", 467 | "file_path": "src/components/Button.res", 468 | "code_snippet": "let isDisabled = false", 469 | "search_context": { 470 | "before": ["let onClick = () => {"], 471 | "after": ["setLoading(true)"] 472 | } 473 | } 474 | } 475 | 476 | // Handle multiple matches with strategy 477 | { 478 | "tool": "add_comment", 479 | "arguments": { 480 | "workspace": "PROJ", 481 | "repository": "my-repo", 482 | "pull_request_id": 123, 483 | "comment_text": "Consider extracting this", 484 | "file_path": "src/utils/helpers.js", 485 | "code_snippet": "return result;", 486 | "search_context": { 487 | "before": ["const result = calculate();"], 488 | "after": ["}"] 489 | }, 490 | "match_strategy": "best" // Auto-select highest confidence match 491 | } 492 | } 493 | ``` 494 | 495 | **Code Snippet Parameters:** 496 | - `code_snippet`: The exact code line to find (alternative to `line_number`) 497 | - `search_context`: Optional context to disambiguate multiple matches 498 | - `before`: Array of lines that should appear before the target 499 | - `after`: Array of lines that should appear after the target 500 | - `match_strategy`: How to handle multiple matches 501 | - `"strict"` (default): Fail with error showing all matches 502 | - `"best"`: Auto-select the highest confidence match 503 | 504 | **Error Response for Multiple Matches (strict mode):** 505 | ```json 506 | { 507 | "error": { 508 | "code": "MULTIPLE_MATCHES_FOUND", 509 | "message": "Code snippet 'return result;' found in 3 locations", 510 | "occurrences": [ 511 | { 512 | "line_number": 42, 513 | "file_path": "src/utils/helpers.js", 514 | "preview": " const result = calculate();\n> return result;\n}", 515 | "confidence": 0.9, 516 | "line_type": "ADDED" 517 | }, 518 | // ... more matches 519 | ], 520 | "suggestion": "To resolve, either:\n1. Add more context...\n2. Use match_strategy: 'best'...\n3. Use line_number directly" 521 | } 522 | } 523 | ``` 524 | 525 | This feature is particularly useful for: 526 | - AI-powered code review tools that analyze diffs 527 | - Scripts that automatically add comments based on code patterns 528 | - Avoiding line number confusion in large diffs 529 | 530 | **Note on comment replies:** 531 | - Use `parent_comment_id` to reply to any comment (general or inline) 532 | - In `get_pull_request` responses: 533 | - Bitbucket Server shows replies nested in a `replies` array 534 | - Bitbucket Cloud shows a `parent_id` field for reply comments 535 | - You can reply to replies, creating nested conversations 536 | 537 | **Note on inline comments:** 538 | - `file_path`: The path to the file as shown in the diff 539 | - `line_number`: The line number as shown in the diff 540 | - `line_type`: 541 | - `ADDED` - For newly added lines (green in diff) 542 | - `REMOVED` - For deleted lines (red in diff) 543 | - `CONTEXT` - For unchanged context lines 544 | 545 | #### Add Comment - Complete Usage Guide 546 | 547 | The `add_comment` tool supports multiple scenarios. Here's when and how to use each approach: 548 | 549 | **1. General PR Comments (No file/line)** 550 | - Use when: Making overall feedback about the PR 551 | - Required params: `comment_text` only 552 | - Example: "LGTM!", "Please update the documentation" 553 | 554 | **2. Reply to Existing Comments** 555 | - Use when: Continuing a conversation thread 556 | - Required params: `comment_text`, `parent_comment_id` 557 | - Works for both general and inline comment replies 558 | 559 | **3. Inline Comments with Line Number** 560 | - Use when: You know the exact line number from the diff 561 | - Required params: `comment_text`, `file_path`, `line_number` 562 | - Optional: `line_type` (defaults to CONTEXT) 563 | 564 | **4. Inline Comments with Code Snippet** 565 | - Use when: You have the code but not the line number (common for AI tools) 566 | - Required params: `comment_text`, `file_path`, `code_snippet` 567 | - The tool will automatically find the line number 568 | - Add `search_context` if the code appears multiple times 569 | - Use `match_strategy: "best"` to auto-select when multiple matches exist 570 | 571 | **5. Code Suggestions** 572 | - Use when: Proposing specific code changes 573 | - Required params: `comment_text`, `file_path`, `line_number`, `suggestion` 574 | - For multi-line: also add `suggestion_end_line` 575 | - Creates applicable suggestion blocks in Bitbucket UI 576 | 577 | **Decision Flow for AI/Automated Tools:** 578 | ``` 579 | 1. Do you want to suggest code changes? 580 | → Use suggestion with line_number 581 | 582 | 2. Do you have the exact line number? 583 | → Use line_number directly 584 | 585 | 3. Do you have the code snippet but not line number? 586 | → Use code_snippet (add search_context if needed) 587 | 588 | 4. Is it a general comment about the PR? 589 | → Use comment_text only 590 | 591 | 5. Are you replying to another comment? 592 | → Add parent_comment_id 593 | ``` 594 | 595 | **Common Pitfalls to Avoid:** 596 | - Don't use both `line_number` and `code_snippet` - pick one 597 | - Suggestions always need `file_path` and `line_number` 598 | - Code snippets must match exactly (including whitespace) 599 | - REMOVED lines reference the source file, ADDED/CONTEXT reference the destination 600 | 601 | ### Merge Pull Request 602 | 603 | ```typescript 604 | { 605 | "tool": "merge_pull_request", 606 | "arguments": { 607 | "workspace": "PROJ", 608 | "repository": "my-repo", 609 | "pull_request_id": 123, 610 | "merge_strategy": "squash", // Optional: merge-commit, squash, fast-forward 611 | "close_source_branch": true, // Optional 612 | "commit_message": "Custom merge message" // Optional 613 | } 614 | } 615 | ``` 616 | 617 | ### List Branches 618 | 619 | ```typescript 620 | { 621 | "tool": "list_branches", 622 | "arguments": { 623 | "workspace": "PROJ", 624 | "repository": "my-repo", 625 | "filter": "feature", // Optional: filter by name pattern 626 | "limit": 25, // Optional (default: 25) 627 | "start": 0 // Optional: for pagination (default: 0) 628 | } 629 | } 630 | ``` 631 | 632 | Returns a paginated list of branches with: 633 | - Branch name and ID 634 | - Latest commit hash 635 | - Default branch indicator 636 | - Pagination info 637 | 638 | ### Delete Branch 639 | 640 | ```typescript 641 | { 642 | "tool": "delete_branch", 643 | "arguments": { 644 | "workspace": "PROJ", 645 | "repository": "my-repo", 646 | "branch_name": "feature/old-feature", 647 | "force": false // Optional (default: false) 648 | } 649 | } 650 | ``` 651 | 652 | **Note**: Branch deletion requires appropriate permissions. The branch will be permanently deleted. 653 | 654 | ### Get Branch 655 | 656 | ```typescript 657 | { 658 | "tool": "get_branch", 659 | "arguments": { 660 | "workspace": "PROJ", 661 | "repository": "my-repo", 662 | "branch_name": "feature/new-feature", 663 | "include_merged_prs": false // Optional (default: false) 664 | } 665 | } 666 | ``` 667 | 668 | Returns comprehensive branch information including: 669 | - Branch details: 670 | - Name and ID 671 | - Latest commit (hash, message, author, date) 672 | - Default branch indicator 673 | - Open pull requests from this branch: 674 | - PR title and ID 675 | - Destination branch 676 | - Author and reviewers 677 | - Approval status (approved by, changes requested by, pending) 678 | - PR URL 679 | - Merged pull requests (if `include_merged_prs` is true): 680 | - PR title and ID 681 | - Merge date and who merged it 682 | - Statistics: 683 | - Total open PRs count 684 | - Total merged PRs count 685 | - Days since last commit 686 | 687 | This tool is particularly useful for: 688 | - Checking if a branch has open PRs before deletion 689 | - Getting an overview of branch activity 690 | - Understanding PR review status 691 | - Identifying stale branches 692 | 693 | ### List Branch Commits 694 | 695 | Get all commits in a specific branch with advanced filtering options: 696 | 697 | ```typescript 698 | // Basic usage - get recent commits 699 | { 700 | "tool": "list_branch_commits", 701 | "arguments": { 702 | "workspace": "PROJ", 703 | "repository": "my-repo", 704 | "branch_name": "feature/new-feature", 705 | "limit": 50 // Optional (default: 25) 706 | } 707 | } 708 | 709 | // Filter by date range 710 | { 711 | "tool": "list_branch_commits", 712 | "arguments": { 713 | "workspace": "PROJ", 714 | "repository": "my-repo", 715 | "branch_name": "main", 716 | "since": "2025-01-01T00:00:00Z", // ISO date string 717 | "until": "2025-01-15T23:59:59Z" // ISO date string 718 | } 719 | } 720 | 721 | // Filter by author 722 | { 723 | "tool": "list_branch_commits", 724 | "arguments": { 725 | "workspace": "PROJ", 726 | "repository": "my-repo", 727 | "branch_name": "develop", 728 | "author": "[email protected]", // Email or username 729 | "limit": 100 730 | } 731 | } 732 | 733 | // Exclude merge commits 734 | { 735 | "tool": "list_branch_commits", 736 | "arguments": { 737 | "workspace": "PROJ", 738 | "repository": "my-repo", 739 | "branch_name": "release/v2.0", 740 | "include_merge_commits": false 741 | } 742 | } 743 | 744 | // Search in commit messages 745 | { 746 | "tool": "list_branch_commits", 747 | "arguments": { 748 | "workspace": "PROJ", 749 | "repository": "my-repo", 750 | "branch_name": "main", 751 | "search": "bugfix", // Search in commit messages 752 | "limit": 50 753 | } 754 | } 755 | 756 | // Combine multiple filters 757 | { 758 | "tool": "list_branch_commits", 759 | "arguments": { 760 | "workspace": "PROJ", 761 | "repository": "my-repo", 762 | "branch_name": "develop", 763 | "author": "[email protected]", 764 | "since": "2025-01-01T00:00:00Z", 765 | "include_merge_commits": false, 766 | "search": "feature", 767 | "limit": 100, 768 | "start": 0 // For pagination 769 | } 770 | } 771 | 772 | // Include CI/CD build status (Bitbucket Server only) 773 | { 774 | "tool": "list_branch_commits", 775 | "arguments": { 776 | "workspace": "PROJ", 777 | "repository": "my-repo", 778 | "branch_name": "main", 779 | "include_build_status": true, // Fetch build status for each commit 780 | "limit": 50 781 | } 782 | } 783 | ``` 784 | 785 | **Filter Parameters:** 786 | - `since`: ISO date string - only show commits after this date 787 | - `until`: ISO date string - only show commits before this date 788 | - `author`: Filter by author email/username 789 | - `include_merge_commits`: Boolean to include/exclude merge commits (default: true) 790 | - `search`: Search for text in commit messages 791 | - `include_build_status`: Boolean to include CI/CD build status (default: false, Bitbucket Server only) 792 | 793 | Returns detailed commit information: 794 | ```json 795 | { 796 | "branch_name": "feature/new-feature", 797 | "branch_head": "abc123def456", // Latest commit hash 798 | "commits": [ 799 | { 800 | "hash": "abc123def456", 801 | "abbreviated_hash": "abc123d", 802 | "message": "Add new feature implementation", 803 | "author": { 804 | "name": "John Doe", 805 | "email": "[email protected]" 806 | }, 807 | "date": "2025-01-03T10:30:00Z", 808 | "parents": ["parent1hash", "parent2hash"], 809 | "is_merge_commit": false, 810 | "build_status": { // Only present when include_build_status is true 811 | "successful": 5, 812 | "failed": 0, 813 | "in_progress": 1, 814 | "unknown": 0 815 | } 816 | } 817 | // ... more commits 818 | ], 819 | "total_count": 150, 820 | "start": 0, 821 | "limit": 25, 822 | "has_more": true, 823 | "next_start": 25, 824 | "filters_applied": { 825 | "author": "[email protected]", 826 | "since": "2025-01-01", 827 | "include_merge_commits": false, 828 | "include_build_status": true 829 | } 830 | } 831 | ``` 832 | 833 | This tool is particularly useful for: 834 | - Reviewing commit history before releases 835 | - Finding commits by specific authors 836 | - Tracking changes within date ranges 837 | - Searching for specific features or fixes 838 | - Analyzing branch activity patterns 839 | - Monitoring CI/CD build status for commits (Bitbucket Server only) 840 | 841 | ### List PR Commits 842 | 843 | Get all commits that are part of a pull request: 844 | 845 | ```typescript 846 | // Basic usage 847 | { 848 | "tool": "list_pr_commits", 849 | "arguments": { 850 | "workspace": "PROJ", 851 | "repository": "my-repo", 852 | "pull_request_id": 123, 853 | "limit": 50, // Optional (default: 25) 854 | "start": 0 // Optional: for pagination 855 | } 856 | } 857 | 858 | // Include CI/CD build status (Bitbucket Server only) 859 | { 860 | "tool": "list_pr_commits", 861 | "arguments": { 862 | "workspace": "PROJ", 863 | "repository": "my-repo", 864 | "pull_request_id": 123, 865 | "include_build_status": true, // Fetch build status for each commit 866 | "limit": 50 867 | } 868 | } 869 | ``` 870 | 871 | Returns commit information for the PR: 872 | ```json 873 | { 874 | "pull_request_id": 123, 875 | "pull_request_title": "Add awesome feature", 876 | "commits": [ 877 | { 878 | "hash": "def456ghi789", 879 | "abbreviated_hash": "def456g", 880 | "message": "Initial implementation", 881 | "author": { 882 | "name": "Jane Smith", 883 | "email": "[email protected]" 884 | }, 885 | "date": "2025-01-02T14:20:00Z", 886 | "parents": ["parent1hash"], 887 | "is_merge_commit": false, 888 | "build_status": { // Only present when include_build_status is true 889 | "successful": 3, 890 | "failed": 0, 891 | "in_progress": 0, 892 | "unknown": 0 893 | } 894 | } 895 | // ... more commits 896 | ], 897 | "total_count": 5, 898 | "start": 0, 899 | "limit": 25, 900 | "has_more": false 901 | } 902 | ``` 903 | 904 | This tool is particularly useful for: 905 | - Reviewing all changes in a PR before merging 906 | - Understanding the development history of a PR 907 | - Checking commit messages for quality 908 | - Verifying authorship of changes 909 | - Analyzing PR complexity by commit count 910 | - Monitoring CI/CD build status for all PR commits (Bitbucket Server only) 911 | 912 | ### Get Pull Request Diff 913 | 914 | Get the diff/changes for a pull request with optional filtering capabilities: 915 | 916 | ```typescript 917 | // Get full diff (default behavior) 918 | { 919 | "tool": "get_pull_request_diff", 920 | "arguments": { 921 | "workspace": "PROJ", 922 | "repository": "my-repo", 923 | "pull_request_id": 123, 924 | "context_lines": 5 // Optional (default: 3) 925 | } 926 | } 927 | 928 | // Exclude specific file types 929 | { 930 | "tool": "get_pull_request_diff", 931 | "arguments": { 932 | "workspace": "PROJ", 933 | "repository": "my-repo", 934 | "pull_request_id": 123, 935 | "exclude_patterns": ["*.lock", "*.svg", "node_modules/**", "*.min.js"] 936 | } 937 | } 938 | 939 | // Include only specific file types 940 | { 941 | "tool": "get_pull_request_diff", 942 | "arguments": { 943 | "workspace": "PROJ", 944 | "repository": "my-repo", 945 | "pull_request_id": 123, 946 | "include_patterns": ["*.res", "*.resi", "src/**/*.js"] 947 | } 948 | } 949 | 950 | // Get diff for a specific file only 951 | { 952 | "tool": "get_pull_request_diff", 953 | "arguments": { 954 | "workspace": "PROJ", 955 | "repository": "my-repo", 956 | "pull_request_id": 123, 957 | "file_path": "src/components/Button.res" 958 | } 959 | } 960 | 961 | // Combine filters 962 | { 963 | "tool": "get_pull_request_diff", 964 | "arguments": { 965 | "workspace": "PROJ", 966 | "repository": "my-repo", 967 | "pull_request_id": 123, 968 | "include_patterns": ["src/**/*"], 969 | "exclude_patterns": ["*.test.js", "*.spec.js"] 970 | } 971 | } 972 | ``` 973 | 974 | **Filtering Options:** 975 | - `include_patterns`: Array of glob patterns to include (whitelist) 976 | - `exclude_patterns`: Array of glob patterns to exclude (blacklist) 977 | - `file_path`: Get diff for a specific file only 978 | - Patterns support standard glob syntax (e.g., `*.js`, `src/**/*.res`, `!test/**`) 979 | 980 | **Response includes filtering metadata:** 981 | ```json 982 | { 983 | "message": "Pull request diff retrieved successfully", 984 | "pull_request_id": 123, 985 | "diff": "..filtered diff content..", 986 | "filter_metadata": { 987 | "total_files": 15, 988 | "included_files": 12, 989 | "excluded_files": 3, 990 | "excluded_file_list": ["package-lock.json", "logo.svg", "yarn.lock"], 991 | "filters_applied": { 992 | "exclude_patterns": ["*.lock", "*.svg"] 993 | } 994 | } 995 | } 996 | ``` 997 | 998 | ### Approve Pull Request 999 | 1000 | ```typescript 1001 | { 1002 | "tool": "approve_pull_request", 1003 | "arguments": { 1004 | "workspace": "PROJ", 1005 | "repository": "my-repo", 1006 | "pull_request_id": 123 1007 | } 1008 | } 1009 | ``` 1010 | 1011 | ### Request Changes 1012 | 1013 | ```typescript 1014 | { 1015 | "tool": "request_changes", 1016 | "arguments": { 1017 | "workspace": "PROJ", 1018 | "repository": "my-repo", 1019 | "pull_request_id": 123, 1020 | "comment": "Please address the following issues..." // Optional 1021 | } 1022 | } 1023 | ``` 1024 | 1025 | ### List Directory Content 1026 | 1027 | ```typescript 1028 | { 1029 | "tool": "list_directory_content", 1030 | "arguments": { 1031 | "workspace": "PROJ", 1032 | "repository": "my-repo", 1033 | "path": "src/components", // Optional (defaults to root) 1034 | "branch": "main" // Optional (defaults to default branch) 1035 | } 1036 | } 1037 | ``` 1038 | 1039 | Returns directory listing with: 1040 | - Path and branch information 1041 | - Array of contents with: 1042 | - Name 1043 | - Type (file or directory) 1044 | - Size (for files) 1045 | - Full path 1046 | - Total items count 1047 | 1048 | ### Get File Content 1049 | 1050 | ```typescript 1051 | { 1052 | "tool": "get_file_content", 1053 | "arguments": { 1054 | "workspace": "PROJ", 1055 | "repository": "my-repo", 1056 | "file_path": "src/index.ts", 1057 | "branch": "main", // Optional (defaults to default branch) 1058 | "start_line": 1, // Optional: starting line (1-based, use negative for from end) 1059 | "line_count": 100, // Optional: number of lines to return 1060 | "full_content": false // Optional: force full content (default: false) 1061 | } 1062 | } 1063 | ``` 1064 | 1065 | **Smart Truncation Features:** 1066 | - Automatically truncates large files (>50KB) to prevent token overload 1067 | - Default line counts based on file type: 1068 | - Config files (.yml, .json): 200 lines 1069 | - Documentation (.md, .txt): 300 lines 1070 | - Code files (.ts, .js, .py): 500 lines 1071 | - Log files: Last 100 lines 1072 | - Use `start_line: -50` to get last 50 lines (tail functionality) 1073 | - Files larger than 1MB require explicit `full_content: true` or line parameters 1074 | 1075 | Returns file content with: 1076 | - File path and branch 1077 | - File size and encoding 1078 | - Content (full or truncated based on parameters) 1079 | - Line information (if truncated): 1080 | - Total lines in file 1081 | - Range of returned lines 1082 | - Truncation indicator 1083 | - Last modified information (commit, author, date) 1084 | 1085 | Example responses: 1086 | 1087 | ```json 1088 | // Small file - returns full content 1089 | { 1090 | "file_path": "package.json", 1091 | "branch": "main", 1092 | "size": 1234, 1093 | "encoding": "utf-8", 1094 | "content": "{\n \"name\": \"my-project\",\n ...", 1095 | "last_modified": { 1096 | "commit_id": "abc123", 1097 | "author": "John Doe", 1098 | "date": "2025-01-21T10:00:00Z" 1099 | } 1100 | } 1101 | 1102 | // Large file - automatically truncated 1103 | { 1104 | "file_path": "src/components/LargeComponent.tsx", 1105 | "branch": "main", 1106 | "size": 125000, 1107 | "encoding": "utf-8", 1108 | "content": "... first 500 lines ...", 1109 | "line_info": { 1110 | "total_lines": 3500, 1111 | "returned_lines": { 1112 | "start": 1, 1113 | "end": 500 1114 | }, 1115 | "truncated": true, 1116 | "message": "Showing lines 1-500 of 3500. File size: 122.1KB" 1117 | } 1118 | } 1119 | ``` 1120 | 1121 | ### List Projects 1122 | 1123 | List all accessible Bitbucket projects (Server) or workspaces (Cloud): 1124 | 1125 | ```typescript 1126 | // List all accessible projects 1127 | { 1128 | "tool": "list_projects", 1129 | "arguments": { 1130 | "limit": 25, // Optional (default: 25) 1131 | "start": 0 // Optional: for pagination (default: 0) 1132 | } 1133 | } 1134 | 1135 | // Filter by project name 1136 | { 1137 | "tool": "list_projects", 1138 | "arguments": { 1139 | "name": "backend", // Partial name match 1140 | "limit": 50 1141 | } 1142 | } 1143 | 1144 | // Filter by permission level (Bitbucket Server only) 1145 | { 1146 | "tool": "list_projects", 1147 | "arguments": { 1148 | "permission": "PROJECT_WRITE", // PROJECT_READ, PROJECT_WRITE, PROJECT_ADMIN 1149 | "limit": 100 1150 | } 1151 | } 1152 | ``` 1153 | 1154 | **Parameters:** 1155 | - `name`: Filter by project/workspace name (partial match, optional) 1156 | - `permission`: Filter by permission level (Bitbucket Server only, optional) 1157 | - `PROJECT_READ`: Read access 1158 | - `PROJECT_WRITE`: Write access 1159 | - `PROJECT_ADMIN`: Admin access 1160 | - `limit`: Maximum number of projects to return (default: 25) 1161 | - `start`: Start index for pagination (default: 0) 1162 | 1163 | Returns project/workspace information: 1164 | ```json 1165 | { 1166 | "projects": [ 1167 | { 1168 | "key": "PROJ", 1169 | "id": 1234, 1170 | "name": "My Project", 1171 | "description": "Project description", 1172 | "is_public": false, 1173 | "type": "NORMAL", // NORMAL or PERSONAL (Server), WORKSPACE (Cloud) 1174 | "url": "https://bitbucket.yourcompany.com/projects/PROJ" 1175 | } 1176 | // ... more projects 1177 | ], 1178 | "total_count": 15, 1179 | "start": 0, 1180 | "limit": 25, 1181 | "has_more": false, 1182 | "next_start": null 1183 | } 1184 | ``` 1185 | 1186 | **Note**: 1187 | - For Bitbucket Cloud, this returns workspaces (not projects in the traditional sense) 1188 | - For Bitbucket Server, this returns both personal and team projects 1189 | 1190 | This tool is particularly useful for: 1191 | - Discovering available projects/workspaces for your account 1192 | - Finding project keys needed for other API calls 1193 | - Identifying projects you have specific permissions on 1194 | - Browsing organizational structure 1195 | 1196 | ### List Repositories 1197 | 1198 | List repositories within a specific project/workspace or across all accessible repositories: 1199 | 1200 | ```typescript 1201 | // List all repositories in a workspace/project 1202 | { 1203 | "tool": "list_repositories", 1204 | "arguments": { 1205 | "workspace": "PROJ", // Required for Bitbucket Cloud, optional for Server 1206 | "limit": 25, // Optional (default: 25) 1207 | "start": 0 // Optional: for pagination (default: 0) 1208 | } 1209 | } 1210 | 1211 | // List all accessible repositories (Bitbucket Server only) 1212 | { 1213 | "tool": "list_repositories", 1214 | "arguments": { 1215 | "limit": 100 1216 | } 1217 | } 1218 | 1219 | // Filter by repository name 1220 | { 1221 | "tool": "list_repositories", 1222 | "arguments": { 1223 | "workspace": "PROJ", 1224 | "name": "frontend", // Partial name match 1225 | "limit": 50 1226 | } 1227 | } 1228 | 1229 | // Filter by permission level (Bitbucket Server only) 1230 | { 1231 | "tool": "list_repositories", 1232 | "arguments": { 1233 | "workspace": "PROJ", 1234 | "permission": "REPO_WRITE", // REPO_READ, REPO_WRITE, REPO_ADMIN 1235 | "limit": 100 1236 | } 1237 | } 1238 | ``` 1239 | 1240 | **Parameters:** 1241 | - `workspace`: Project key (Server) or workspace slug (Cloud) 1242 | - **Required for Bitbucket Cloud** 1243 | - Optional for Bitbucket Server (omit to list all accessible repos) 1244 | - `name`: Filter by repository name (partial match, optional) 1245 | - `permission`: Filter by permission level (Bitbucket Server only, optional) 1246 | - `REPO_READ`: Read access 1247 | - `REPO_WRITE`: Write access 1248 | - `REPO_ADMIN`: Admin access 1249 | - `limit`: Maximum number of repositories to return (default: 25) 1250 | - `start`: Start index for pagination (default: 0) 1251 | 1252 | Returns repository information: 1253 | ```json 1254 | { 1255 | "repositories": [ 1256 | { 1257 | "slug": "my-repo", 1258 | "id": 5678, 1259 | "name": "My Repository", 1260 | "description": "Repository description", 1261 | "project_key": "PROJ", 1262 | "project_name": "My Project", 1263 | "state": "AVAILABLE", // AVAILABLE, INITIALISING, INITIALISATION_FAILED (Server) 1264 | "is_public": false, 1265 | "is_forkable": true, 1266 | "clone_urls": { 1267 | "http": "https://bitbucket.yourcompany.com/scm/PROJ/my-repo.git", 1268 | "ssh": "ssh://[email protected]:7999/PROJ/my-repo.git" 1269 | }, 1270 | "url": "https://bitbucket.yourcompany.com/projects/PROJ/repos/my-repo" 1271 | } 1272 | // ... more repositories 1273 | ], 1274 | "total_count": 42, 1275 | "start": 0, 1276 | "limit": 25, 1277 | "has_more": true, 1278 | "next_start": 25, 1279 | "workspace": "PROJ" 1280 | } 1281 | ``` 1282 | 1283 | **Important Notes:** 1284 | - **Bitbucket Cloud** requires the `workspace` parameter. If omitted, you'll receive an error message 1285 | - **Bitbucket Server** allows listing all accessible repos by omitting the `workspace` parameter 1286 | - Clone URLs are provided for both HTTP(S) and SSH protocols 1287 | 1288 | This tool is particularly useful for: 1289 | - Discovering available repositories in a project/workspace 1290 | - Finding repository slugs needed for other API calls 1291 | - Identifying repositories you have specific permissions on 1292 | - Getting clone URLs for repositories 1293 | - Browsing repository structure within an organization 1294 | 1295 | ## Development 1296 | 1297 | - `npm run dev` - Watch mode for development 1298 | - `npm run build` - Build the TypeScript code 1299 | - `npm start` - Run the built server 1300 | 1301 | ## Troubleshooting 1302 | 1303 | 1. **Authentication errors**: Double-check your username and app password 1304 | 2. **404 errors**: Verify the workspace, repository slug, and PR ID 1305 | 3. **Permission errors**: Ensure your app password has the required permissions 1306 | 1307 | ## License 1308 | 1309 | MIT 1310 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "build"] 19 | } 20 | ``` -------------------------------------------------------------------------------- /src/utils/suggestion-formatter.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Formats a comment with a code suggestion in markdown format 3 | * that Bitbucket can render as an applicable suggestion 4 | */ 5 | export function formatSuggestionComment( 6 | commentText: string, 7 | suggestion: string, 8 | startLine?: number, 9 | endLine?: number 10 | ): string { 11 | // Add line range info if it's a multi-line suggestion 12 | const lineInfo = startLine && endLine && endLine > startLine 13 | ? ` (lines ${startLine}-${endLine})` 14 | : ''; 15 | 16 | // Format with GitHub-style suggestion markdown 17 | return `${commentText}${lineInfo} 18 | 19 | \`\`\`suggestion 20 | ${suggestion} 21 | \`\`\``; 22 | } 23 | ``` -------------------------------------------------------------------------------- /scripts/setup-auth.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | console.log(` 4 | =========================================== 5 | Bitbucket MCP Server - Authentication Setup 6 | =========================================== 7 | 8 | To use this MCP server, you need to create a Bitbucket App Password. 9 | 10 | Follow these steps: 11 | 12 | 1. Log in to your Bitbucket account 13 | 2. Go to: https://bitbucket.org/account/settings/app-passwords/ 14 | 3. Click "Create app password" 15 | 4. Give it a label (e.g., "MCP Server") 16 | 5. Select the following permissions: 17 | - Account: Read 18 | - Repositories: Read, Write 19 | - Pull requests: Read, Write 20 | 6. Click "Create" 21 | 7. Copy the generated app password (you won't be able to see it again!) 22 | 23 | You'll need to provide: 24 | - Your Bitbucket username (not email) 25 | - The app password you just created 26 | - Your default workspace/organization (optional) 27 | 28 | Example workspace: If your repository URL is: 29 | https://bitbucket.org/mycompany/my-repo 30 | Then your workspace is: mycompany 31 | 32 | These will be added to your MCP settings configuration. 33 | 34 | Press Enter to continue... 35 | `); 36 | 37 | // Wait for user to press Enter 38 | process.stdin.once('data', () => { 39 | console.log(` 40 | Next steps: 41 | 1. The MCP server will be configured with your credentials 42 | 2. You'll be able to use the 'get_pull_request' tool 43 | 3. More tools can be added later (create_pull_request, list_pull_requests, etc.) 44 | 45 | Configuration complete! 46 | `); 47 | process.exit(0); 48 | }); 49 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@nexus2520/bitbucket-mcp-server", 3 | "version": "1.1.2", 4 | "description": "MCP server for Bitbucket API integration - supports both Cloud and Server", 5 | "type": "module", 6 | "main": "./build/index.js", 7 | "bin": { 8 | "bitbucket-mcp-server": "build/index.js" 9 | }, 10 | "files": [ 11 | "build/**/*", 12 | "README.md", 13 | "LICENSE", 14 | "CHANGELOG.md" 15 | ], 16 | "scripts": { 17 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 18 | "dev": "tsc --watch", 19 | "start": "node build/index.js", 20 | "prepublishOnly": "npm run build" 21 | }, 22 | "keywords": [ 23 | "mcp", 24 | "bitbucket", 25 | "api", 26 | "model-context-protocol", 27 | "bitbucket-server", 28 | "bitbucket-cloud", 29 | "pull-request", 30 | "code-review" 31 | ], 32 | "author": "Parth Dogra", 33 | "license": "MIT", 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/pdogra1299/bitbucket-mcp-server.git" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/pdogra1299/bitbucket-mcp-server/issues" 40 | }, 41 | "homepage": "https://github.com/pdogra1299/bitbucket-mcp-server#readme", 42 | "engines": { 43 | "node": ">=16.0.0" 44 | }, 45 | "dependencies": { 46 | "@modelcontextprotocol/sdk": "^1.12.1", 47 | "axios": "^1.10.0", 48 | "minimatch": "^9.0.3" 49 | }, 50 | "devDependencies": { 51 | "@types/minimatch": "^5.1.2", 52 | "@types/node": "^22.15.29", 53 | "typescript": "^5.8.3" 54 | } 55 | } 56 | ``` -------------------------------------------------------------------------------- /memory-bank/projectbrief.yml: -------------------------------------------------------------------------------- ```yaml 1 | # Project Brief - Bitbucket MCP Server 2 | 3 | project_name: "bitbucket-mcp-server" 4 | version: "1.0.0" 5 | description: "MCP (Model Context Protocol) server for Bitbucket API integration" 6 | primary_purpose: "Provide tools for interacting with Bitbucket APIs via MCP" 7 | 8 | key_features: 9 | - "Support for both Bitbucket Cloud and Server" 10 | - "19 comprehensive tools for PR management, branch operations, and code review" 11 | - "Modular architecture with separate handlers for different tool categories" 12 | - "Smart file content handling with automatic truncation" 13 | - "Advanced PR commenting with code suggestions and snippet matching" 14 | - "Code search functionality across repositories" 15 | 16 | scope: 17 | included: 18 | - "Pull Request lifecycle management" 19 | - "Branch management and operations" 20 | - "Code review and approval workflows" 21 | - "File and directory operations" 22 | - "Code search (Bitbucket Server only)" 23 | - "Authentication via app passwords/tokens" 24 | 25 | excluded: 26 | - "Repository creation/deletion" 27 | - "User management" 28 | - "Pipeline/build operations" 29 | - "Wiki/documentation management" 30 | - "Bitbucket Cloud code search (future enhancement)" 31 | 32 | constraints: 33 | technical: 34 | - "Node.js >= 16.0.0 required" 35 | - "TypeScript with ES modules" 36 | - "MCP SDK for protocol implementation" 37 | 38 | authentication: 39 | - "Bitbucket Cloud: App passwords required" 40 | - "Bitbucket Server: HTTP access tokens required" 41 | - "Different username formats for Cloud vs Server" 42 | 43 | success_criteria: 44 | - "Seamless integration with MCP-compatible clients" 45 | - "Reliable API interactions with proper error handling" 46 | - "Consistent response formatting across tools" 47 | - "Maintainable and extensible codebase" 48 | ``` -------------------------------------------------------------------------------- /memory-bank/productContext.yml: -------------------------------------------------------------------------------- ```yaml 1 | # Product Context - Bitbucket MCP Server 2 | 3 | problem_statement: | 4 | Developers using AI assistants need programmatic access to Bitbucket operations 5 | without leaving their development environment. Manual API interactions are 6 | time-consuming and error-prone. 7 | 8 | target_users: 9 | primary: 10 | - "Software developers using AI coding assistants" 11 | - "DevOps engineers automating PR workflows" 12 | - "Code reviewers needing efficient review tools" 13 | 14 | secondary: 15 | - "Project managers tracking development progress" 16 | - "QA engineers reviewing code changes" 17 | 18 | user_needs: 19 | core: 20 | - "Create and manage pull requests programmatically" 21 | - "Review code changes with AI assistance" 22 | - "Automate branch management tasks" 23 | - "Search code across repositories" 24 | - "Access file contents without cloning" 25 | 26 | workflow: 27 | - "Comment on PRs with code suggestions" 28 | - "Approve/request changes on PRs" 29 | - "List and filter pull requests" 30 | - "Get comprehensive PR details including comments" 31 | - "Manage branch lifecycle" 32 | 33 | value_proposition: 34 | - "Seamless Bitbucket integration within AI assistants" 35 | - "Unified interface for Cloud and Server variants" 36 | - "Time savings through automation" 37 | - "Reduced context switching" 38 | - "Enhanced code review quality with AI" 39 | 40 | user_experience_goals: 41 | - "Simple tool invocation syntax" 42 | - "Clear and consistent response formats" 43 | - "Helpful error messages with recovery suggestions" 44 | - "Smart defaults (pagination, truncation)" 45 | - "Flexible filtering and search options" 46 | 47 | adoption_strategy: 48 | - "npm package for easy installation" 49 | - "Comprehensive documentation with examples" 50 | - "Support for both Cloud and Server" 51 | - "Gradual feature addition based on user needs" 52 | ``` -------------------------------------------------------------------------------- /SETUP_GUIDE_SERVER.md: -------------------------------------------------------------------------------- ```markdown 1 | # Bitbucket Server MCP Setup Guide 2 | 3 | Since you're using Bitbucket Server (self-hosted), you'll need to create an HTTP access token instead of an app password. 4 | 5 | ## Step 1: Your Username 6 | 7 | Your Bitbucket Server username (not email address) 8 | 9 | ## Step 2: Create an HTTP Access Token 10 | 11 | 1. **Navigate to HTTP Access Tokens**: 12 | - You mentioned you can see "HTTP access tokens" in your account settings 13 | - Click on that option 14 | 15 | 2. **Create a new token**: 16 | - Click "Create token" or similar button 17 | - Give it a descriptive name like "MCP Server Integration" 18 | - Set an expiration date (or leave it without expiration if allowed) 19 | - Select the following permissions: 20 | - **Repository**: Read, Write 21 | - **Pull request**: Read, Write 22 | - **Project**: Read (if available) 23 | 24 | 3. **Generate and copy the token**: 25 | - Click "Create" or "Generate" 26 | - **IMPORTANT**: Copy the token immediately! It will look like a long string of random characters 27 | - You won't be able to see this token again 28 | 29 | ## Step 3: Find Your Bitbucket Server URL 30 | 31 | Your Bitbucket Server URL is the base URL you use to access Bitbucket. For example: 32 | - `https://bitbucket.yourcompany.com` 33 | - `https://git.yourcompany.com` 34 | - `https://bitbucket.internal.company.net` 35 | 36 | ## Step 4: Find Your Project/Workspace 37 | 38 | In Bitbucket Server, repositories are organized by projects. Look at any repository URL: 39 | - Example: `https://bitbucket.company.com/projects/PROJ/repos/my-repo` 40 | - In this case, "PROJ" is your project key 41 | 42 | ## Example Configuration 43 | 44 | For Bitbucket Server, your configuration will look like: 45 | 46 | ``` 47 | Username: your.username 48 | Token: [Your HTTP access token] 49 | Base URL: https://bitbucket.yourcompany.com 50 | Project/Workspace: PROJ (or whatever your project key is) 51 | ``` 52 | 53 | ## Next Steps 54 | 55 | Once you have: 56 | 1. Your username 57 | 2. An HTTP access token from the "HTTP access tokens" section 58 | 3. Your Bitbucket Server base URL 59 | 4. Your project key 60 | 61 | You can configure the MCP server for Bitbucket Server. 62 | ``` -------------------------------------------------------------------------------- /memory-bank/progress.yml: -------------------------------------------------------------------------------- ```yaml 1 | # Progress - Bitbucket MCP Server 2 | 3 | project_status: 4 | overall_progress: "95%" 5 | phase: "Post-1.0 Improvements" 6 | version: "1.0.1" 7 | release_status: "Production with improvements" 8 | 9 | milestones_completed: 10 | - name: "Core PR Tools" 11 | completion_date: "2025-01-06" 12 | features: 13 | - "get_pull_request with merge details" 14 | - "list_pull_requests with filtering" 15 | - "create_pull_request" 16 | - "update_pull_request" 17 | - "merge_pull_request" 18 | 19 | - name: "Enhanced Commenting" 20 | completion_date: "2025-01-26" 21 | features: 22 | - "Inline comments" 23 | - "Code suggestions" 24 | - "Code snippet matching" 25 | - "Nested replies support" 26 | 27 | - name: "Code Review Tools" 28 | completion_date: "2025-01-26" 29 | features: 30 | - "get_pull_request_diff with filtering" 31 | - "approve_pull_request" 32 | - "request_changes" 33 | - "Review state management" 34 | 35 | - name: "Branch Management" 36 | completion_date: "2025-01-21" 37 | features: 38 | - "list_branches" 39 | - "delete_branch" 40 | - "get_branch with PR info" 41 | - "list_branch_commits" 42 | 43 | - name: "File Operations" 44 | completion_date: "2025-01-21" 45 | features: 46 | - "list_directory_content" 47 | - "get_file_content with smart truncation" 48 | 49 | - name: "Code Search" 50 | completion_date: "2025-07-25" 51 | features: 52 | - "search_code for Bitbucket Server" 53 | - "File pattern filtering" 54 | - "Highlighted search results" 55 | 56 | active_development: 57 | current_sprint: "Post 1.0 Planning" 58 | in_progress: [] 59 | blocked: [] 60 | 61 | testing_status: 62 | unit_tests: "Not implemented" 63 | integration_tests: "Manual testing completed" 64 | user_acceptance: "In production use" 65 | known_issues: [] 66 | 67 | deployment_status: 68 | npm_package: "Published as @nexus2520/bitbucket-mcp-server" 69 | version_published: "1.0.0" 70 | documentation: "Comprehensive README with examples" 71 | adoption_metrics: 72 | - "npm weekly downloads tracking" 73 | - "GitHub stars and issues" 74 | 75 | performance_metrics: 76 | api_response_time: "< 2s average" 77 | memory_usage: "< 100MB typical" 78 | concurrent_operations: "Supports parallel API calls" 79 | 80 | next_release_planning: 81 | version: "1.1.0" 82 | planned_features: 83 | - "Bitbucket Cloud search support" 84 | - "Repository management tools" 85 | - "Pipeline/build integration" 86 | timeline: "TBD based on user feedback" 87 | ``` -------------------------------------------------------------------------------- /SETUP_GUIDE.md: -------------------------------------------------------------------------------- ```markdown 1 | # Bitbucket MCP Server Setup Guide 2 | 3 | ## Step 1: Find Your Bitbucket Username 4 | 5 | 1. **Log in to Bitbucket**: Go to https://bitbucket.org and log in with your credentials 6 | 7 | 2. **Find your username**: 8 | - After logging in, click on your profile avatar in the top-right corner 9 | - Click on "Personal settings" or go directly to: https://bitbucket.org/account/settings/ 10 | - Your username will be displayed at the top of the page 11 | - **Note**: Your username is NOT your email address. It's usually a shorter identifier like "johndoe" or "jdoe123" 12 | 13 | ## Step 2: Create an App Password 14 | 15 | 1. **Navigate to App Passwords**: 16 | - While logged in, go to: https://bitbucket.org/account/settings/app-passwords/ 17 | - Or from your account settings, look for "App passwords" in the left sidebar under "Access management" 18 | 19 | 2. **Create a new app password**: 20 | - Click the "Create app password" button 21 | - Give it a descriptive label like "MCP Server" or "Bitbucket MCP Integration" 22 | 23 | 3. **Select permissions** (IMPORTANT - select these specific permissions): 24 | - ✅ **Account**: Read 25 | - ✅ **Repositories**: Read, Write 26 | - ✅ **Pull requests**: Read, Write 27 | - You can leave other permissions unchecked 28 | 29 | 4. **Generate the password**: 30 | - Click "Create" 31 | - **IMPORTANT**: Copy the generated password immediately! It will look something like: `ATBBxxxxxxxxxxxxxxxxxxxxx` 32 | - You won't be able to see this password again after closing the dialog 33 | 34 | ## Step 3: Find Your Workspace (Optional but Recommended) 35 | 36 | Your workspace is the organization or team name in Bitbucket. To find it: 37 | 38 | 1. Look at any of your repository URLs: 39 | - Example: `https://bitbucket.org/mycompany/my-repo` 40 | - In this case, "mycompany" is your workspace 41 | 42 | 2. Or go to your workspace dashboard: 43 | - Click on "Workspaces" in the top navigation 44 | - Your workspaces will be listed there 45 | 46 | ## Example Credentials 47 | 48 | Here's what your credentials should look like: 49 | 50 | ``` 51 | Username: johndoe # Your Bitbucket username (NOT email) 52 | App Password: ATBB3xXx... # The generated app password 53 | Workspace: mycompany # Your organization/workspace name 54 | ``` 55 | 56 | ## Common Issues 57 | 58 | 1. **"Username not found"**: Make sure you're using your Bitbucket username, not your email address 59 | 2. **"Invalid app password"**: Ensure you copied the entire app password including the "ATBB" prefix 60 | 3. **"Permission denied"**: Check that your app password has the required permissions (Account: Read, Repositories: Read/Write, Pull requests: Read/Write) 61 | 62 | ## Next Steps 63 | 64 | Once you have these credentials, share them with me and I'll configure the MCP server for you. The credentials will be stored securely in your MCP settings configuration. 65 | ``` -------------------------------------------------------------------------------- /memory-bank/techContext.yml: -------------------------------------------------------------------------------- ```yaml 1 | # Technical Context - Bitbucket MCP Server 2 | 3 | core_technologies: 4 | language: "TypeScript" 5 | version: "5.8.3" 6 | module_system: "ES Modules" 7 | runtime: 8 | name: "Node.js" 9 | version: ">= 16.0.0" 10 | package_manager: "npm" 11 | 12 | libraries_and_bindings: 13 | - name: "@modelcontextprotocol/sdk" 14 | version: "^1.12.1" 15 | purpose: "MCP protocol implementation" 16 | 17 | - name: "axios" 18 | version: "^1.10.0" 19 | purpose: "HTTP client for API requests" 20 | 21 | - name: "minimatch" 22 | version: "^9.0.3" 23 | purpose: "Glob pattern matching for file filtering" 24 | 25 | development_environment: 26 | build_tools: 27 | - "TypeScript compiler (tsc)" 28 | - "npm scripts for build automation" 29 | 30 | commands: 31 | build: "npm run build" 32 | dev: "npm run dev" 33 | start: "npm start" 34 | publish: "npm publish" 35 | 36 | project_structure: 37 | src_directory: "src/" 38 | build_directory: "build/" 39 | entry_point: "src/index.ts" 40 | compiled_entry: "build/index.js" 41 | 42 | technical_patterns: 43 | - name: "Shebang for CLI execution" 44 | description: "#!/usr/bin/env node at top of index.ts" 45 | usage: "Enables direct execution as CLI tool" 46 | 47 | - name: "ES Module imports" 48 | description: "Using .js extensions in TypeScript imports" 49 | usage: "Required for ES module compatibility" 50 | examples: 51 | - "import { Server } from '@modelcontextprotocol/sdk/server/index.js'" 52 | 53 | - name: "Type-safe error handling" 54 | description: "Custom ApiError interface with typed errors" 55 | usage: "Consistent error handling across API calls" 56 | 57 | - name: "Environment variable configuration" 58 | description: "Process.env for authentication and base URL" 59 | usage: "Flexible configuration without code changes" 60 | 61 | api_integration: 62 | bitbucket_cloud: 63 | base_url: "https://api.bitbucket.org/2.0" 64 | auth_method: "Basic Auth with App Password" 65 | api_style: "RESTful with JSON" 66 | pagination: "page-based with pagelen parameter" 67 | 68 | bitbucket_server: 69 | base_url: "Custom URL (e.g., https://bitbucket.company.com)" 70 | auth_method: "Bearer token (HTTP Access Token)" 71 | api_style: "RESTful with JSON" 72 | pagination: "offset-based with start/limit" 73 | api_version: "/rest/api/1.0" 74 | search_api: "/rest/search/latest" 75 | 76 | deployment: 77 | package_name: "@nexus2520/bitbucket-mcp-server" 78 | registry: "npm public registry" 79 | distribution: 80 | - "Compiled JavaScript in build/" 81 | - "Type definitions excluded" 82 | - "Source maps excluded" 83 | 84 | execution_methods: 85 | - "npx -y @nexus2520/bitbucket-mcp-server" 86 | - "Direct node execution after install" 87 | - "MCP client integration" 88 | 89 | security_considerations: 90 | - "Credentials stored in environment variables" 91 | - "No credential logging or exposure" 92 | - "HTTPS only for API communications" 93 | - "Token/password validation on startup" 94 | 95 | performance_optimizations: 96 | - "Parallel API calls for PR details" 97 | - "Smart file truncation to prevent token overflow" 98 | - "Pagination for large result sets" 99 | - "Early exit on authentication failure" 100 | 101 | compatibility: 102 | mcp_clients: 103 | - "Cline (VSCode extension)" 104 | - "Other MCP-compatible AI assistants" 105 | 106 | bitbucket_versions: 107 | - "Bitbucket Cloud (latest API)" 108 | - "Bitbucket Server 7.x+" 109 | - "Bitbucket Data Center" 110 | ``` -------------------------------------------------------------------------------- /memory-bank/systemPatterns.yml: -------------------------------------------------------------------------------- ```yaml 1 | # System Patterns - Bitbucket MCP Server 2 | 3 | architecture_overview: 4 | high_level_architecture: | 5 | MCP Server implementation with modular handler architecture. 6 | Main server class delegates tool calls to specialized handlers. 7 | Each handler manages a specific domain (PRs, branches, reviews, files, search). 8 | 9 | component_relationships: | 10 | - BitbucketMCPServer (main) → Handler classes → BitbucketApiClient → Axios 11 | - Tool definitions → MCP SDK → Client applications 12 | - Type guards validate inputs → Handlers process → Formatters standardize output 13 | 14 | design_patterns: 15 | - name: "Handler Pattern" 16 | category: "architecture" 17 | description: "Separate handler classes for different tool categories" 18 | usage: "Organizing related tools and maintaining single responsibility" 19 | implementation: 20 | - "PullRequestHandlers for PR lifecycle" 21 | - "BranchHandlers for branch operations" 22 | - "ReviewHandlers for code review tools" 23 | - "FileHandlers for file/directory operations" 24 | - "SearchHandlers for code search" 25 | example_files: 26 | - "src/handlers/*.ts" 27 | related_patterns: 28 | - "Dependency Injection" 29 | 30 | - name: "API Client Abstraction" 31 | category: "integration" 32 | description: "Unified client handling both Cloud and Server APIs" 33 | usage: "Abstracting API differences between Bitbucket variants" 34 | implementation: 35 | - "Single makeRequest method for all HTTP operations" 36 | - "Automatic auth header selection (Bearer vs Basic)" 37 | - "Consistent error handling across variants" 38 | example_files: 39 | - "src/utils/api-client.ts" 40 | 41 | - name: "Type Guard Pattern" 42 | category: "validation" 43 | description: "Runtime type checking for tool arguments" 44 | usage: "Ensuring type safety for dynamic tool inputs" 45 | implementation: 46 | - "Guard functions return type predicates" 47 | - "Comprehensive validation of required/optional fields" 48 | - "Array and nested object validation" 49 | example_files: 50 | - "src/types/guards.ts" 51 | 52 | - name: "Response Formatting" 53 | category: "data_transformation" 54 | description: "Consistent response formatting across API variants" 55 | usage: "Normalizing different API response structures" 56 | implementation: 57 | - "formatServerResponse/formatCloudResponse for PRs" 58 | - "Unified FormattedXXX interfaces" 59 | - "Separate formatters for different data types" 60 | example_files: 61 | - "src/utils/formatters.ts" 62 | 63 | project_specific_patterns: 64 | mcp_patterns: 65 | - name: "Tool Definition Structure" 66 | description: "Standardized tool definition with inputSchema" 67 | implementation: 68 | - "Name, description, and JSON schema for each tool" 69 | - "Required vs optional parameter specification" 70 | - "Enum constraints for valid values" 71 | 72 | - name: "Error Response Pattern" 73 | description: "Consistent error handling and reporting" 74 | implementation: 75 | - "Return isError: true for tool failures" 76 | - "Include detailed error messages" 77 | - "Provide context-specific error details" 78 | 79 | bitbucket_patterns: 80 | - name: "Pagination Pattern" 81 | description: "Consistent pagination across list operations" 82 | implementation: 83 | - "limit and start parameters" 84 | - "has_more and next_start in responses" 85 | - "total_count for result sets" 86 | 87 | - name: "Dual API Support" 88 | description: "Supporting both Cloud and Server APIs" 89 | implementation: 90 | - "isServer flag determines API paths" 91 | - "Different parameter names mapped appropriately" 92 | - "Response structure normalization" 93 | 94 | code_patterns: 95 | - name: "Smart Truncation" 96 | description: "Intelligent file content truncation" 97 | implementation: 98 | - "File type-based default limits" 99 | - "Size-based automatic truncation" 100 | - "Line range selection support" 101 | 102 | - name: "Code Snippet Matching" 103 | description: "Finding line numbers from code snippets" 104 | implementation: 105 | - "Exact text matching with context" 106 | - "Confidence scoring for multiple matches" 107 | - "Strategy selection (strict vs best)" 108 | ``` -------------------------------------------------------------------------------- /memory-bank/activeContext.yml: -------------------------------------------------------------------------------- ```yaml 1 | # Active Context - Bitbucket MCP Server 2 | 3 | current_focus_areas: 4 | - name: "Search Code Feature Implementation" 5 | status: "completed" 6 | priority: "high" 7 | team: "frontend" 8 | timeline: "2025-07-25" 9 | 10 | recent_changes: 11 | - date: "2025-08-08" 12 | feature: "testing_and_release_v1.0.1" 13 | description: "Comprehensive testing of search functionality and release of version 1.0.1" 14 | status: "completed" 15 | files_affected: 16 | - "package.json" 17 | - "CHANGELOG.md" 18 | - "memory-bank/activeContext.yml" 19 | - "memory-bank/progress.yml" 20 | technical_details: "Tested search functionality with real Bitbucket data, verified context-aware patterns, file filtering, and error handling" 21 | business_impact: "Validated production readiness of search improvements and properly versioned the release" 22 | patterns_introduced: 23 | - "Live MCP testing workflow using connected Bitbucket server" 24 | - "Real-world validation of search functionality" 25 | 26 | - date: "2025-07-25" 27 | feature: "code_search_response_fix" 28 | description: "Fixed search_code tool response handling to match actual Bitbucket API structure" 29 | status: "completed" 30 | files_affected: 31 | - "src/utils/formatters.ts" 32 | - "src/handlers/search-handlers.ts" 33 | technical_details: "Added formatCodeSearchOutput for simplified AI-friendly output showing only filename, line number, and text" 34 | business_impact: "Search results now properly formatted for AI consumption with cleaner output" 35 | patterns_introduced: 36 | - "Simplified formatter pattern for AI-friendly output" 37 | 38 | - date: "2025-07-25" 39 | feature: "code_search" 40 | description: "Added search_code tool for searching code across repositories" 41 | status: "completed" 42 | files_affected: 43 | - "src/handlers/search-handlers.ts" 44 | - "src/types/bitbucket.ts" 45 | - "src/utils/formatters.ts" 46 | - "src/types/guards.ts" 47 | - "src/tools/definitions.ts" 48 | - "src/index.ts" 49 | technical_details: "Implemented Bitbucket Server search API integration" 50 | business_impact: "Enables code search functionality for Bitbucket Server users" 51 | patterns_introduced: 52 | - "SearchHandlers following modular architecture" 53 | - "Search result formatting pattern" 54 | 55 | patterns_discovered: 56 | - name: "Modular Handler Architecture" 57 | description: "Each tool category has its own handler class" 58 | usage: "Add new handlers for new tool categories" 59 | implementation: "Create handler class, add to index.ts, register tools" 60 | 61 | - name: "API Variant Handling" 62 | description: "Single handler supports both Cloud and Server APIs" 63 | usage: "Check isServer flag and adjust API paths/parameters" 64 | implementation: "Use apiClient.getIsServer() to determine variant" 65 | 66 | - name: "Simplified AI Formatter Pattern" 67 | description: "Separate formatters for detailed vs AI-friendly output" 68 | usage: "Create simplified formatters that extract only essential information for AI" 69 | implementation: "formatCodeSearchOutput strips HTML, shows only file:line:text format" 70 | 71 | recent_learnings: 72 | - date: "2025-07-25" 73 | topic: "Bitbucket Search API Response Structure" 74 | description: "API returns file as string (not object), uses hitContexts with HTML formatting" 75 | impact: "Need to parse HTML <em> tags and handle different response structure" 76 | 77 | - date: "2025-07-25" 78 | topic: "Bitbucket Search API" 79 | description: "Search API only available on Bitbucket Server, not Cloud" 80 | impact: "Search tool marked as Server-only with future Cloud support planned" 81 | 82 | - date: "2025-07-25" 83 | topic: "Version Management" 84 | description: "Major version 1.0.0 indicates stable API with comprehensive features" 85 | impact: "Project ready for production use" 86 | 87 | key_decisions: 88 | - decision: "Separate handler for search tools" 89 | rationale: "Maintains single responsibility and modularity" 90 | date: "2025-07-25" 91 | 92 | - decision: "YAML format for Memory Bank" 93 | rationale: "Better merge conflict handling and structured data" 94 | date: "2025-07-25" 95 | 96 | current_challenges: 97 | - "Bitbucket Cloud search API not yet available" 98 | - "Need to handle different search syntaxes across platforms" 99 | 100 | next_priorities: 101 | - "Add Bitbucket Cloud search when API becomes available" 102 | - "Enhance search with more filtering options" 103 | - "Add support for searching in other entities (commits, PRs)" 104 | ``` -------------------------------------------------------------------------------- /src/utils/api-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios, { AxiosInstance, AxiosError } from 'axios'; 2 | import { BitbucketServerBuildSummary } from '../types/bitbucket.js'; 3 | 4 | export interface ApiError { 5 | status?: number; 6 | message: string; 7 | isAxiosError: boolean; 8 | originalError?: AxiosError; 9 | } 10 | 11 | export class BitbucketApiClient { 12 | private axiosInstance: AxiosInstance; 13 | private isServer: boolean; 14 | 15 | constructor( 16 | baseURL: string, 17 | username: string, 18 | password?: string, 19 | token?: string 20 | ) { 21 | this.isServer = !!token; 22 | 23 | const axiosConfig: any = { 24 | baseURL, 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | }; 29 | 30 | // Use token auth for Bitbucket Server, basic auth for Cloud 31 | if (token) { 32 | // Bitbucket Server uses Bearer token 33 | axiosConfig.headers['Authorization'] = `Bearer ${token}`; 34 | } else { 35 | // Bitbucket Cloud uses basic auth with app password 36 | axiosConfig.auth = { 37 | username, 38 | password, 39 | }; 40 | } 41 | 42 | this.axiosInstance = axios.create(axiosConfig); 43 | } 44 | 45 | async makeRequest<T>( 46 | method: 'get' | 'post' | 'put' | 'delete', 47 | path: string, 48 | data?: any, 49 | config?: any 50 | ): Promise<T> { 51 | try { 52 | let response; 53 | if (method === 'get') { 54 | // For GET, config is the second parameter 55 | response = await this.axiosInstance[method](path, config || {}); 56 | } else if (method === 'delete') { 57 | // For DELETE, we might need to pass data in config 58 | if (data) { 59 | response = await this.axiosInstance[method](path, { ...config, data }); 60 | } else { 61 | response = await this.axiosInstance[method](path, config || {}); 62 | } 63 | } else { 64 | // For POST and PUT, data is second, config is third 65 | response = await this.axiosInstance[method](path, data, config); 66 | } 67 | return response.data; 68 | } catch (error) { 69 | if (axios.isAxiosError(error)) { 70 | const status = error.response?.status; 71 | const message = error.response?.data?.errors?.[0]?.message || 72 | error.response?.data?.error?.message || 73 | error.response?.data?.message || 74 | error.message; 75 | 76 | throw { 77 | status, 78 | message, 79 | isAxiosError: true, 80 | originalError: error 81 | } as ApiError; 82 | } 83 | throw error; 84 | } 85 | } 86 | 87 | handleApiError(error: any, context: string) { 88 | if (error.isAxiosError) { 89 | const { status, message } = error as ApiError; 90 | 91 | if (status === 404) { 92 | return { 93 | content: [ 94 | { 95 | type: 'text', 96 | text: `Not found: ${context}`, 97 | }, 98 | ], 99 | isError: true, 100 | }; 101 | } else if (status === 401) { 102 | return { 103 | content: [ 104 | { 105 | type: 'text', 106 | text: `Authentication failed. Please check your ${this.isServer ? 'BITBUCKET_TOKEN' : 'BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD'}`, 107 | }, 108 | ], 109 | isError: true, 110 | }; 111 | } else if (status === 403) { 112 | return { 113 | content: [ 114 | { 115 | type: 'text', 116 | text: `Permission denied: ${context}. Ensure your credentials have the necessary permissions.`, 117 | }, 118 | ], 119 | isError: true, 120 | }; 121 | } 122 | 123 | return { 124 | content: [ 125 | { 126 | type: 'text', 127 | text: `Bitbucket API error: ${message}`, 128 | }, 129 | ], 130 | isError: true, 131 | }; 132 | } 133 | throw error; 134 | } 135 | 136 | getIsServer(): boolean { 137 | return this.isServer; 138 | } 139 | 140 | async getBuildSummaries( 141 | workspace: string, 142 | repository: string, 143 | commitIds: string[] 144 | ): Promise<BitbucketServerBuildSummary> { 145 | if (!this.isServer) { 146 | // Build summaries only available for Bitbucket Server 147 | return {}; 148 | } 149 | 150 | if (commitIds.length === 0) { 151 | return {}; 152 | } 153 | 154 | try { 155 | // Build query string with multiple commitId parameters 156 | const apiPath = `/rest/ui/latest/projects/${workspace}/repos/${repository}/build-summaries`; 157 | 158 | // Create params with custom serializer for multiple commitId parameters 159 | const response = await this.makeRequest<BitbucketServerBuildSummary>( 160 | 'get', 161 | apiPath, 162 | undefined, 163 | { 164 | params: { commitId: commitIds }, 165 | paramsSerializer: (params: any) => { 166 | // Custom serializer to create multiple commitId= parameters 167 | if (params.commitId && Array.isArray(params.commitId)) { 168 | return params.commitId.map((id: string) => `commitId=${encodeURIComponent(id)}`).join('&'); 169 | } 170 | return ''; 171 | } 172 | } 173 | ); 174 | 175 | return response; 176 | } catch (error) { 177 | // If build-summaries endpoint fails, return empty object (graceful degradation) 178 | console.error('Failed to fetch build summaries:', error); 179 | return {}; 180 | } 181 | } 182 | } 183 | ``` -------------------------------------------------------------------------------- /src/utils/diff-parser.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { minimatch } from 'minimatch'; 2 | 3 | export interface DiffSection { 4 | filePath: string; 5 | oldPath?: string; // For renamed files 6 | content: string; 7 | isNew: boolean; 8 | isDeleted: boolean; 9 | isRenamed: boolean; 10 | isBinary: boolean; 11 | } 12 | 13 | export interface FilterOptions { 14 | includePatterns?: string[]; 15 | excludePatterns?: string[]; 16 | filePath?: string; 17 | } 18 | 19 | export interface FilteredResult { 20 | sections: DiffSection[]; 21 | metadata: { 22 | totalFiles: number; 23 | includedFiles: number; 24 | excludedFiles: number; 25 | excludedFileList: string[]; 26 | }; 27 | } 28 | 29 | export class DiffParser { 30 | /** 31 | * Parse a unified diff into file sections 32 | */ 33 | parseDiffIntoSections(diff: string): DiffSection[] { 34 | const sections: DiffSection[] = []; 35 | 36 | // Split by file boundaries - handle both formats 37 | const fileChunks = diff.split(/(?=^diff --git)/gm).filter(chunk => chunk.trim()); 38 | 39 | for (const chunk of fileChunks) { 40 | const section = this.parseFileSection(chunk); 41 | if (section) { 42 | sections.push(section); 43 | } 44 | } 45 | 46 | return sections; 47 | } 48 | 49 | /** 50 | * Parse a single file section from the diff 51 | */ 52 | private parseFileSection(chunk: string): DiffSection | null { 53 | const lines = chunk.split('\n'); 54 | if (lines.length === 0) return null; 55 | 56 | // Extract file paths from the diff header 57 | let filePath = ''; 58 | let oldPath: string | undefined; 59 | let isNew = false; 60 | let isDeleted = false; 61 | let isRenamed = false; 62 | let isBinary = false; 63 | 64 | // Look for diff --git line - handle both standard and Bitbucket Server formats 65 | const gitDiffMatch = lines[0].match(/^diff --git (?:a\/|src:\/\/)(.+?) (?:b\/|dst:\/\/)(.+?)$/); 66 | if (gitDiffMatch) { 67 | const [, aPath, bPath] = gitDiffMatch; 68 | filePath = bPath; 69 | 70 | // Check subsequent lines for file status 71 | for (let i = 1; i < Math.min(lines.length, 10); i++) { 72 | const line = lines[i]; 73 | 74 | if (line.startsWith('new file mode')) { 75 | isNew = true; 76 | } else if (line.startsWith('deleted file mode')) { 77 | isDeleted = true; 78 | filePath = aPath; // Use the original path for deleted files 79 | } else if (line.startsWith('rename from')) { 80 | isRenamed = true; 81 | oldPath = line.replace('rename from ', ''); 82 | } else if (line.includes('Binary files') && line.includes('differ')) { 83 | isBinary = true; 84 | } else if (line.startsWith('--- ')) { 85 | // Alternative way to detect new/deleted 86 | if (line.includes('/dev/null')) { 87 | isNew = true; 88 | } 89 | } else if (line.startsWith('+++ ')) { 90 | if (line.includes('/dev/null')) { 91 | isDeleted = true; 92 | } 93 | // Extract path from +++ line if needed - handle both formats 94 | const match = line.match(/^\+\+\+ (?:b\/|dst:\/\/)(.+)$/); 95 | if (match && !filePath) { 96 | filePath = match[1]; 97 | } 98 | } 99 | } 100 | } 101 | 102 | // Fallback: try to extract from --- and +++ lines 103 | if (!filePath) { 104 | for (const line of lines) { 105 | if (line.startsWith('+++ ')) { 106 | const match = line.match(/^\+\+\+ (?:b\/|dst:\/\/)(.+)$/); 107 | if (match) { 108 | filePath = match[1]; 109 | break; 110 | } 111 | } else if (line.startsWith('--- ')) { 112 | const match = line.match(/^--- (?:a\/|src:\/\/)(.+)$/); 113 | if (match) { 114 | filePath = match[1]; 115 | } 116 | } 117 | } 118 | } 119 | 120 | if (!filePath) return null; 121 | 122 | return { 123 | filePath, 124 | oldPath, 125 | content: chunk, 126 | isNew, 127 | isDeleted, 128 | isRenamed, 129 | isBinary 130 | }; 131 | } 132 | 133 | /** 134 | * Apply filters to diff sections 135 | */ 136 | filterSections(sections: DiffSection[], options: FilterOptions): FilteredResult { 137 | const excludedFileList: string[] = []; 138 | let filteredSections = sections; 139 | 140 | // If specific file path is requested, only keep that file 141 | if (options.filePath) { 142 | filteredSections = sections.filter(section => 143 | section.filePath === options.filePath || 144 | section.oldPath === options.filePath 145 | ); 146 | 147 | // Track excluded files 148 | sections.forEach(section => { 149 | if (section.filePath !== options.filePath && 150 | section.oldPath !== options.filePath) { 151 | excludedFileList.push(section.filePath); 152 | } 153 | }); 154 | } else { 155 | // Apply exclude patterns first (blacklist) 156 | if (options.excludePatterns && options.excludePatterns.length > 0) { 157 | filteredSections = filteredSections.filter(section => { 158 | const shouldExclude = options.excludePatterns!.some(pattern => 159 | minimatch(section.filePath, pattern, { matchBase: true }) 160 | ); 161 | 162 | if (shouldExclude) { 163 | excludedFileList.push(section.filePath); 164 | return false; 165 | } 166 | return true; 167 | }); 168 | } 169 | 170 | // Apply include patterns if specified (whitelist) 171 | if (options.includePatterns && options.includePatterns.length > 0) { 172 | filteredSections = filteredSections.filter(section => { 173 | const shouldInclude = options.includePatterns!.some(pattern => 174 | minimatch(section.filePath, pattern, { matchBase: true }) 175 | ); 176 | 177 | if (!shouldInclude) { 178 | excludedFileList.push(section.filePath); 179 | return false; 180 | } 181 | return true; 182 | }); 183 | } 184 | } 185 | 186 | return { 187 | sections: filteredSections, 188 | metadata: { 189 | totalFiles: sections.length, 190 | includedFiles: filteredSections.length, 191 | excludedFiles: sections.length - filteredSections.length, 192 | excludedFileList 193 | } 194 | }; 195 | } 196 | 197 | /** 198 | * Reconstruct a unified diff from filtered sections 199 | */ 200 | reconstructDiff(sections: DiffSection[]): string { 201 | if (sections.length === 0) { 202 | return ''; 203 | } 204 | 205 | // Join all sections with proper spacing 206 | return sections.map(section => section.content).join('\n'); 207 | } 208 | } 209 | ``` -------------------------------------------------------------------------------- /src/handlers/search-handlers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BitbucketApiClient } from '../utils/api-client.js'; 2 | import { 3 | BitbucketServerSearchRequest, 4 | BitbucketServerSearchResult, 5 | FormattedSearchResult 6 | } from '../types/bitbucket.js'; 7 | import { formatSearchResults, formatCodeSearchOutput } from '../utils/formatters.js'; 8 | 9 | interface SearchContext { 10 | assignment: string[]; 11 | declaration: string[]; 12 | usage: string[]; 13 | exact: string[]; 14 | any: string[]; 15 | } 16 | 17 | function buildContextualPatterns(searchTerm: string): SearchContext { 18 | return { 19 | assignment: [ 20 | `${searchTerm} =`, // Variable assignment 21 | `${searchTerm}:`, // Object property, JSON key 22 | `= ${searchTerm}`, // Right-hand assignment 23 | ], 24 | declaration: [ 25 | `${searchTerm} =`, // Variable definition 26 | `${searchTerm}:`, // Object key, parameter definition 27 | `function ${searchTerm}`, // Function declaration 28 | `class ${searchTerm}`, // Class declaration 29 | `interface ${searchTerm}`, // Interface declaration 30 | `const ${searchTerm}`, // Const declaration 31 | `let ${searchTerm}`, // Let declaration 32 | `var ${searchTerm}`, // Var declaration 33 | ], 34 | usage: [ 35 | `.${searchTerm}`, // Property/method access 36 | `${searchTerm}(`, // Function call 37 | `${searchTerm}.`, // Method chaining 38 | `${searchTerm}[`, // Array/object access 39 | `(${searchTerm}`, // Parameter usage 40 | ], 41 | exact: [ 42 | `"${searchTerm}"`, // Exact quoted match 43 | ], 44 | any: [ 45 | `"${searchTerm}"`, // Exact match 46 | `${searchTerm} =`, // Assignment 47 | `${searchTerm}:`, // Object property 48 | `.${searchTerm}`, // Property access 49 | `${searchTerm}(`, // Function call 50 | `function ${searchTerm}`, // Function definition 51 | `class ${searchTerm}`, // Class definition 52 | ] 53 | }; 54 | } 55 | 56 | function buildSmartQuery( 57 | searchTerm: string, 58 | searchContext: string = 'any', 59 | includePatterns: string[] = [] 60 | ): string { 61 | const contextPatterns = buildContextualPatterns(searchTerm); 62 | 63 | let patterns: string[] = []; 64 | 65 | // Add patterns based on context 66 | if (searchContext in contextPatterns) { 67 | patterns = [...contextPatterns[searchContext as keyof SearchContext]]; 68 | } else { 69 | patterns = [...contextPatterns.any]; 70 | } 71 | 72 | // Add user-provided patterns 73 | if (includePatterns && includePatterns.length > 0) { 74 | patterns = [...patterns, ...includePatterns]; 75 | } 76 | 77 | // Remove duplicates and join with OR 78 | const uniquePatterns = [...new Set(patterns)]; 79 | 80 | // If only one pattern, return it without parentheses 81 | if (uniquePatterns.length === 1) { 82 | return uniquePatterns[0]; 83 | } 84 | 85 | // Wrap each pattern in quotes for safety and join with OR 86 | const quotedPatterns = uniquePatterns.map(pattern => `"${pattern}"`); 87 | return `(${quotedPatterns.join(' OR ')})`; 88 | } 89 | 90 | export class SearchHandlers { 91 | constructor( 92 | private apiClient: BitbucketApiClient, 93 | private baseUrl: string 94 | ) {} 95 | 96 | async handleSearchCode(args: any) { 97 | try { 98 | const { 99 | workspace, 100 | repository, 101 | search_query, 102 | search_context = 'any', 103 | file_pattern, 104 | include_patterns = [], 105 | limit = 25, 106 | start = 0 107 | } = args; 108 | 109 | if (!workspace || !search_query) { 110 | throw new Error('Workspace and search_query are required'); 111 | } 112 | 113 | // Only works for Bitbucket Server currently 114 | if (!this.apiClient.getIsServer()) { 115 | throw new Error('Code search is currently only supported for Bitbucket Server'); 116 | } 117 | 118 | // Build the enhanced query string 119 | let query = `project:${workspace}`; 120 | if (repository) { 121 | query += ` repo:${repository}`; 122 | } 123 | if (file_pattern) { 124 | query += ` path:${file_pattern}`; 125 | } 126 | 127 | // Build smart search patterns 128 | const smartQuery = buildSmartQuery(search_query, search_context, include_patterns); 129 | query += ` ${smartQuery}`; 130 | 131 | // Prepare the request payload 132 | const payload: BitbucketServerSearchRequest = { 133 | query: query.trim(), 134 | entities: { 135 | code: { 136 | start: start, 137 | limit: limit 138 | } 139 | } 140 | }; 141 | 142 | // Make the API request (no query params needed, pagination is in payload) 143 | const response = await this.apiClient.makeRequest<BitbucketServerSearchResult>( 144 | 'post', 145 | `/rest/search/latest/search?avatarSize=64`, 146 | payload 147 | ); 148 | 149 | const searchResult = response; 150 | 151 | // Use simplified formatter for cleaner output 152 | const simplifiedOutput = formatCodeSearchOutput(searchResult); 153 | 154 | // Prepare pagination info 155 | const hasMore = searchResult.code?.isLastPage === false; 156 | const nextStart = hasMore ? (searchResult.code?.nextStart || start + limit) : undefined; 157 | const totalCount = searchResult.code?.count || 0; 158 | 159 | // Build a concise response with search context info 160 | let resultText = `Code search results for "${search_query}"`; 161 | if (search_context !== 'any') { 162 | resultText += ` (context: ${search_context})`; 163 | } 164 | resultText += ` in ${workspace}`; 165 | if (repository) { 166 | resultText += `/${repository}`; 167 | } 168 | 169 | // Show the actual search query used 170 | resultText += `\n\nSearch query: ${query.trim()}`; 171 | resultText += `\n\n${simplifiedOutput}`; 172 | 173 | if (totalCount > 0) { 174 | resultText += `\n\nTotal matches: ${totalCount}`; 175 | if (hasMore) { 176 | resultText += ` (showing ${start + 1}-${start + (searchResult.code?.values?.length || 0)})`; 177 | } 178 | } 179 | 180 | return { 181 | content: [{ 182 | type: 'text', 183 | text: resultText 184 | }] 185 | }; 186 | } catch (error: any) { 187 | const errorMessage = error.response?.data?.errors?.[0]?.message || error.message; 188 | return { 189 | content: [{ 190 | type: 'text', 191 | text: JSON.stringify({ 192 | error: `Failed to search code: ${errorMessage}`, 193 | details: error.response?.data 194 | }, null, 2) 195 | }], 196 | isError: true 197 | }; 198 | } 199 | } 200 | } 201 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | McpError, 9 | } from '@modelcontextprotocol/sdk/types.js'; 10 | 11 | import { BitbucketApiClient } from './utils/api-client.js'; 12 | import { PullRequestHandlers } from './handlers/pull-request-handlers.js'; 13 | import { BranchHandlers } from './handlers/branch-handlers.js'; 14 | import { ReviewHandlers } from './handlers/review-handlers.js'; 15 | import { FileHandlers } from './handlers/file-handlers.js'; 16 | import { SearchHandlers } from './handlers/search-handlers.js'; 17 | import { ProjectHandlers } from './handlers/project-handlers.js'; 18 | import { toolDefinitions } from './tools/definitions.js'; 19 | 20 | // Get environment variables 21 | const BITBUCKET_USERNAME = process.env.BITBUCKET_USERNAME; 22 | const BITBUCKET_APP_PASSWORD = process.env.BITBUCKET_APP_PASSWORD; 23 | const BITBUCKET_TOKEN = process.env.BITBUCKET_TOKEN; // For Bitbucket Server 24 | const BITBUCKET_BASE_URL = process.env.BITBUCKET_BASE_URL || 'https://api.bitbucket.org/2.0'; 25 | 26 | // Check for either app password (Cloud) or token (Server) 27 | if (!BITBUCKET_USERNAME || (!BITBUCKET_APP_PASSWORD && !BITBUCKET_TOKEN)) { 28 | console.error('Error: BITBUCKET_USERNAME and either BITBUCKET_APP_PASSWORD (for Cloud) or BITBUCKET_TOKEN (for Server) are required'); 29 | console.error('Please set these in your MCP settings configuration'); 30 | process.exit(1); 31 | } 32 | 33 | class BitbucketMCPServer { 34 | private server: Server; 35 | private apiClient: BitbucketApiClient; 36 | private pullRequestHandlers: PullRequestHandlers; 37 | private branchHandlers: BranchHandlers; 38 | private reviewHandlers: ReviewHandlers; 39 | private fileHandlers: FileHandlers; 40 | private searchHandlers: SearchHandlers; 41 | private projectHandlers: ProjectHandlers; 42 | 43 | constructor() { 44 | this.server = new Server( 45 | { 46 | name: 'bitbucket-mcp-server', 47 | version: '1.1.2', 48 | }, 49 | { 50 | capabilities: { 51 | tools: {}, 52 | }, 53 | } 54 | ); 55 | 56 | // Initialize API client 57 | this.apiClient = new BitbucketApiClient( 58 | BITBUCKET_BASE_URL, 59 | BITBUCKET_USERNAME!, 60 | BITBUCKET_APP_PASSWORD, 61 | BITBUCKET_TOKEN 62 | ); 63 | 64 | // Initialize handlers 65 | this.pullRequestHandlers = new PullRequestHandlers( 66 | this.apiClient, 67 | BITBUCKET_BASE_URL, 68 | BITBUCKET_USERNAME! 69 | ); 70 | this.branchHandlers = new BranchHandlers(this.apiClient, BITBUCKET_BASE_URL); 71 | this.reviewHandlers = new ReviewHandlers(this.apiClient, BITBUCKET_USERNAME!); 72 | this.fileHandlers = new FileHandlers(this.apiClient, BITBUCKET_BASE_URL); 73 | this.searchHandlers = new SearchHandlers(this.apiClient, BITBUCKET_BASE_URL); 74 | this.projectHandlers = new ProjectHandlers(this.apiClient, BITBUCKET_BASE_URL); 75 | 76 | this.setupToolHandlers(); 77 | 78 | // Error handling 79 | this.server.onerror = (error) => console.error('[MCP Error]', error); 80 | process.on('SIGINT', async () => { 81 | await this.server.close(); 82 | process.exit(0); 83 | }); 84 | } 85 | 86 | private setupToolHandlers() { 87 | // List available tools 88 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 89 | tools: toolDefinitions, 90 | })); 91 | 92 | // Handle tool calls 93 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 94 | switch (request.params.name) { 95 | // Pull Request tools 96 | case 'get_pull_request': 97 | return this.pullRequestHandlers.handleGetPullRequest(request.params.arguments); 98 | case 'list_pull_requests': 99 | return this.pullRequestHandlers.handleListPullRequests(request.params.arguments); 100 | case 'create_pull_request': 101 | return this.pullRequestHandlers.handleCreatePullRequest(request.params.arguments); 102 | case 'update_pull_request': 103 | return this.pullRequestHandlers.handleUpdatePullRequest(request.params.arguments); 104 | case 'add_comment': 105 | return this.pullRequestHandlers.handleAddComment(request.params.arguments); 106 | case 'merge_pull_request': 107 | return this.pullRequestHandlers.handleMergePullRequest(request.params.arguments); 108 | case 'list_pr_commits': 109 | return this.pullRequestHandlers.handleListPrCommits(request.params.arguments); 110 | 111 | // Branch tools 112 | case 'list_branches': 113 | return this.branchHandlers.handleListBranches(request.params.arguments); 114 | case 'delete_branch': 115 | return this.branchHandlers.handleDeleteBranch(request.params.arguments); 116 | case 'get_branch': 117 | return this.branchHandlers.handleGetBranch(request.params.arguments); 118 | case 'list_branch_commits': 119 | return this.branchHandlers.handleListBranchCommits(request.params.arguments); 120 | 121 | // Code Review tools 122 | case 'get_pull_request_diff': 123 | return this.reviewHandlers.handleGetPullRequestDiff(request.params.arguments); 124 | case 'approve_pull_request': 125 | return this.reviewHandlers.handleApprovePullRequest(request.params.arguments); 126 | case 'unapprove_pull_request': 127 | return this.reviewHandlers.handleUnapprovePullRequest(request.params.arguments); 128 | case 'request_changes': 129 | return this.reviewHandlers.handleRequestChanges(request.params.arguments); 130 | case 'remove_requested_changes': 131 | return this.reviewHandlers.handleRemoveRequestedChanges(request.params.arguments); 132 | 133 | // File tools 134 | case 'list_directory_content': 135 | return this.fileHandlers.handleListDirectoryContent(request.params.arguments); 136 | case 'get_file_content': 137 | return this.fileHandlers.handleGetFileContent(request.params.arguments); 138 | 139 | // Search tools 140 | case 'search_code': 141 | return this.searchHandlers.handleSearchCode(request.params.arguments); 142 | 143 | // Project tools 144 | case 'list_projects': 145 | return this.projectHandlers.handleListProjects(request.params.arguments); 146 | case 'list_repositories': 147 | return this.projectHandlers.handleListRepositories(request.params.arguments); 148 | 149 | default: 150 | throw new McpError( 151 | ErrorCode.MethodNotFound, 152 | `Unknown tool: ${request.params.name}` 153 | ); 154 | } 155 | }); 156 | } 157 | 158 | async run() { 159 | const transport = new StdioServerTransport(); 160 | await this.server.connect(transport); 161 | console.error(`Bitbucket MCP server running on stdio (${this.apiClient.getIsServer() ? 'Server' : 'Cloud'} mode)`); 162 | } 163 | } 164 | 165 | const server = new BitbucketMCPServer(); 166 | server.run().catch(console.error); 167 | ``` -------------------------------------------------------------------------------- /src/handlers/project-handlers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { BitbucketApiClient } from '../utils/api-client.js'; 3 | import { 4 | isListProjectsArgs, 5 | isListRepositoriesArgs 6 | } from '../types/guards.js'; 7 | import { 8 | BitbucketServerProject, 9 | BitbucketCloudProject, 10 | BitbucketServerRepository, 11 | BitbucketCloudRepository 12 | } from '../types/bitbucket.js'; 13 | 14 | export class ProjectHandlers { 15 | constructor( 16 | private apiClient: BitbucketApiClient, 17 | private baseUrl: string 18 | ) {} 19 | 20 | async handleListProjects(args: any) { 21 | if (!isListProjectsArgs(args)) { 22 | throw new McpError( 23 | ErrorCode.InvalidParams, 24 | 'Invalid arguments for list_projects' 25 | ); 26 | } 27 | 28 | const { name, permission, limit = 25, start = 0 } = args; 29 | 30 | try { 31 | let apiPath: string; 32 | let params: any = {}; 33 | let projects: any[] = []; 34 | let totalCount = 0; 35 | let nextPageStart: number | null = null; 36 | 37 | if (this.apiClient.getIsServer()) { 38 | // Bitbucket Server API 39 | apiPath = `/rest/api/1.0/projects`; 40 | params = { 41 | limit, 42 | start 43 | }; 44 | 45 | if (name) { 46 | params.name = name; 47 | } 48 | if (permission) { 49 | params.permission = permission; 50 | } 51 | 52 | const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); 53 | 54 | // Format projects 55 | projects = (response.values || []).map((project: BitbucketServerProject) => ({ 56 | key: project.key, 57 | id: project.id, 58 | name: project.name, 59 | description: project.description || '', 60 | is_public: project.public, 61 | type: project.type, 62 | url: `${this.baseUrl}/projects/${project.key}` 63 | })); 64 | 65 | totalCount = response.size || projects.length; 66 | if (!response.isLastPage && response.nextPageStart !== undefined) { 67 | nextPageStart = response.nextPageStart; 68 | } 69 | } else { 70 | // Bitbucket Cloud API 71 | apiPath = `/workspaces`; 72 | params = { 73 | pagelen: limit, 74 | page: Math.floor(start / limit) + 1 75 | }; 76 | 77 | // Cloud uses workspaces, not projects exactly 78 | const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); 79 | 80 | projects = (response.values || []).map((workspace: any) => ({ 81 | key: workspace.slug, 82 | id: workspace.uuid, 83 | name: workspace.name, 84 | description: '', 85 | is_public: !workspace.is_private, 86 | type: 'WORKSPACE', 87 | url: workspace.links.html.href 88 | })); 89 | 90 | totalCount = response.size || projects.length; 91 | if (response.next) { 92 | nextPageStart = start + limit; 93 | } 94 | } 95 | 96 | return { 97 | content: [ 98 | { 99 | type: 'text', 100 | text: JSON.stringify({ 101 | projects, 102 | total_count: totalCount, 103 | start, 104 | limit, 105 | has_more: nextPageStart !== null, 106 | next_start: nextPageStart 107 | }, null, 2), 108 | }, 109 | ], 110 | }; 111 | } catch (error) { 112 | return this.apiClient.handleApiError(error, 'listing projects'); 113 | } 114 | } 115 | 116 | async handleListRepositories(args: any) { 117 | if (!isListRepositoriesArgs(args)) { 118 | throw new McpError( 119 | ErrorCode.InvalidParams, 120 | 'Invalid arguments for list_repositories' 121 | ); 122 | } 123 | 124 | const { workspace, name, permission, limit = 25, start = 0 } = args; 125 | 126 | try { 127 | let apiPath: string; 128 | let params: any = {}; 129 | let repositories: any[] = []; 130 | let totalCount = 0; 131 | let nextPageStart: number | null = null; 132 | 133 | if (this.apiClient.getIsServer()) { 134 | // Bitbucket Server API 135 | if (workspace) { 136 | // List repos in a specific project 137 | apiPath = `/rest/api/1.0/projects/${workspace}/repos`; 138 | } else { 139 | // List all accessible repos 140 | apiPath = `/rest/api/1.0/repos`; 141 | } 142 | 143 | params = { 144 | limit, 145 | start 146 | }; 147 | 148 | if (name) { 149 | params.name = name; 150 | } 151 | if (permission) { 152 | params.permission = permission; 153 | } 154 | if (!workspace && name) { 155 | // When listing all repos and filtering by name 156 | params.projectname = name; 157 | } 158 | 159 | const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); 160 | 161 | // Format repositories 162 | repositories = (response.values || []).map((repo: BitbucketServerRepository) => ({ 163 | slug: repo.slug, 164 | id: repo.id, 165 | name: repo.name, 166 | description: repo.description || '', 167 | project_key: repo.project.key, 168 | project_name: repo.project.name, 169 | state: repo.state, 170 | is_public: repo.public, 171 | is_forkable: repo.forkable, 172 | clone_urls: { 173 | http: repo.links.clone.find(c => c.name === 'http')?.href || '', 174 | ssh: repo.links.clone.find(c => c.name === 'ssh')?.href || '' 175 | }, 176 | url: `${this.baseUrl}/projects/${repo.project.key}/repos/${repo.slug}` 177 | })); 178 | 179 | totalCount = response.size || repositories.length; 180 | if (!response.isLastPage && response.nextPageStart !== undefined) { 181 | nextPageStart = response.nextPageStart; 182 | } 183 | } else { 184 | // Bitbucket Cloud API 185 | if (workspace) { 186 | // List repos in a specific workspace 187 | apiPath = `/repositories/${workspace}`; 188 | } else { 189 | // Cloud doesn't support listing all repos without workspace 190 | // We'll return an error message 191 | return { 192 | content: [ 193 | { 194 | type: 'text', 195 | text: JSON.stringify({ 196 | error: 'Bitbucket Cloud requires a workspace parameter to list repositories. Please provide a workspace.' 197 | }, null, 2), 198 | }, 199 | ], 200 | isError: true, 201 | }; 202 | } 203 | 204 | params = { 205 | pagelen: limit, 206 | page: Math.floor(start / limit) + 1 207 | }; 208 | 209 | const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); 210 | 211 | repositories = (response.values || []).map((repo: BitbucketCloudRepository) => ({ 212 | slug: repo.slug, 213 | id: repo.uuid, 214 | name: repo.name, 215 | description: repo.description || '', 216 | project_key: repo.project?.key || '', 217 | project_name: repo.project?.name || '', 218 | state: 'AVAILABLE', 219 | is_public: !repo.is_private, 220 | is_forkable: true, 221 | clone_urls: { 222 | http: repo.links.clone.find(c => c.name === 'https')?.href || '', 223 | ssh: repo.links.clone.find(c => c.name === 'ssh')?.href || '' 224 | }, 225 | url: repo.links.html.href 226 | })); 227 | 228 | totalCount = response.size || repositories.length; 229 | if (response.next) { 230 | nextPageStart = start + limit; 231 | } 232 | } 233 | 234 | return { 235 | content: [ 236 | { 237 | type: 'text', 238 | text: JSON.stringify({ 239 | repositories, 240 | total_count: totalCount, 241 | start, 242 | limit, 243 | has_more: nextPageStart !== null, 244 | next_start: nextPageStart, 245 | workspace: workspace || 'all' 246 | }, null, 2), 247 | }, 248 | ], 249 | }; 250 | } catch (error) { 251 | return this.apiClient.handleApiError(error, workspace ? `listing repositories in ${workspace}` : 'listing repositories'); 252 | } 253 | } 254 | } 255 | ``` -------------------------------------------------------------------------------- /src/utils/formatters.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | BitbucketServerPullRequest, 3 | BitbucketCloudPullRequest, 4 | MergeInfo, 5 | BitbucketServerCommit, 6 | BitbucketCloudCommit, 7 | FormattedCommit, 8 | BitbucketServerSearchResult, 9 | FormattedSearchResult 10 | } from '../types/bitbucket.js'; 11 | 12 | export function formatServerResponse( 13 | pr: BitbucketServerPullRequest, 14 | mergeInfo?: MergeInfo, 15 | baseUrl?: string 16 | ): any { 17 | const webUrl = `${baseUrl}/projects/${pr.toRef.repository.project.key}/repos/${pr.toRef.repository.slug}/pull-requests/${pr.id}`; 18 | 19 | return { 20 | id: pr.id, 21 | title: pr.title, 22 | description: pr.description || 'No description provided', 23 | state: pr.state, 24 | is_open: pr.open, 25 | is_closed: pr.closed, 26 | author: pr.author.user.displayName, 27 | author_username: pr.author.user.name, 28 | author_email: pr.author.user.emailAddress, 29 | source_branch: pr.fromRef.displayId, 30 | destination_branch: pr.toRef.displayId, 31 | source_commit: pr.fromRef.latestCommit, 32 | destination_commit: pr.toRef.latestCommit, 33 | reviewers: pr.reviewers.map(r => ({ 34 | name: r.user.displayName, 35 | approved: r.approved, 36 | status: r.status, 37 | })), 38 | participants: pr.participants.map(p => ({ 39 | name: p.user.displayName, 40 | role: p.role, 41 | approved: p.approved, 42 | status: p.status, 43 | })), 44 | created_on: new Date(pr.createdDate).toLocaleString(), 45 | updated_on: new Date(pr.updatedDate).toLocaleString(), 46 | web_url: webUrl, 47 | api_url: pr.links.self[0]?.href || '', 48 | is_locked: pr.locked, 49 | // Add merge commit details 50 | is_merged: pr.state === 'MERGED', 51 | merge_commit_hash: mergeInfo?.mergeCommitHash || pr.properties?.mergeCommit?.id || null, 52 | merged_by: mergeInfo?.mergedBy || null, 53 | merged_at: mergeInfo?.mergedAt || null, 54 | merge_commit_message: mergeInfo?.mergeCommitMessage || null, 55 | }; 56 | } 57 | 58 | export function formatCloudResponse(pr: BitbucketCloudPullRequest): any { 59 | return { 60 | id: pr.id, 61 | title: pr.title, 62 | description: pr.description || 'No description provided', 63 | state: pr.state, 64 | author: pr.author.display_name, 65 | source_branch: pr.source.branch.name, 66 | destination_branch: pr.destination.branch.name, 67 | reviewers: pr.reviewers.map(r => r.display_name), 68 | participants: pr.participants.map(p => ({ 69 | name: p.user.display_name, 70 | role: p.role, 71 | approved: p.approved, 72 | })), 73 | created_on: new Date(pr.created_on).toLocaleString(), 74 | updated_on: new Date(pr.updated_on).toLocaleString(), 75 | web_url: pr.links.html.href, 76 | api_url: pr.links.self.href, 77 | diff_url: pr.links.diff.href, 78 | is_merged: pr.state === 'MERGED', 79 | merge_commit_hash: pr.merge_commit?.hash || null, 80 | merged_by: pr.closed_by?.display_name || null, 81 | merged_at: pr.state === 'MERGED' ? pr.updated_on : null, 82 | merge_commit_message: null, // Would need additional API call to get this 83 | close_source_branch: pr.close_source_branch, 84 | }; 85 | } 86 | 87 | export function formatServerCommit(commit: BitbucketServerCommit): FormattedCommit { 88 | return { 89 | hash: commit.id, 90 | abbreviated_hash: commit.displayId, 91 | message: commit.message, 92 | author: { 93 | name: commit.author.name, 94 | email: commit.author.emailAddress, 95 | }, 96 | date: new Date(commit.authorTimestamp).toISOString(), 97 | parents: commit.parents.map(p => p.id), 98 | is_merge_commit: commit.parents.length > 1, 99 | }; 100 | } 101 | 102 | export function formatCloudCommit(commit: BitbucketCloudCommit): FormattedCommit { 103 | // Parse the author raw string which is in format "Name <email>" 104 | const authorMatch = commit.author.raw.match(/^(.+?)\s*<(.+?)>$/); 105 | const authorName = authorMatch ? authorMatch[1] : (commit.author.user?.display_name || commit.author.raw); 106 | const authorEmail = authorMatch ? authorMatch[2] : ''; 107 | 108 | return { 109 | hash: commit.hash, 110 | abbreviated_hash: commit.hash.substring(0, 7), 111 | message: commit.message, 112 | author: { 113 | name: authorName, 114 | email: authorEmail, 115 | }, 116 | date: commit.date, 117 | parents: commit.parents.map(p => p.hash), 118 | is_merge_commit: commit.parents.length > 1, 119 | }; 120 | } 121 | 122 | export function formatSearchResults(searchResult: BitbucketServerSearchResult): FormattedSearchResult[] { 123 | const results: FormattedSearchResult[] = []; 124 | 125 | if (!searchResult.code?.values) { 126 | return results; 127 | } 128 | 129 | for (const value of searchResult.code.values) { 130 | // Extract file name from path 131 | const fileName = value.file.split('/').pop() || value.file; 132 | 133 | const formattedResult: FormattedSearchResult = { 134 | file_path: value.file, 135 | file_name: fileName, 136 | repository: value.repository.slug, 137 | project: value.repository.project.key, 138 | matches: [] 139 | }; 140 | 141 | // Process hitContexts (array of arrays of line contexts) 142 | if (value.hitContexts && value.hitContexts.length > 0) { 143 | for (const contextGroup of value.hitContexts) { 144 | for (const lineContext of contextGroup) { 145 | // Parse HTML to extract text and highlight information 146 | const { text, segments } = parseHighlightedText(lineContext.text); 147 | 148 | formattedResult.matches.push({ 149 | line_number: lineContext.line, 150 | line_content: text, 151 | highlighted_segments: segments 152 | }); 153 | } 154 | } 155 | } 156 | 157 | results.push(formattedResult); 158 | } 159 | 160 | return results; 161 | } 162 | 163 | // Helper function to parse HTML-formatted text with <em> tags 164 | function parseHighlightedText(htmlText: string): { 165 | text: string; 166 | segments: Array<{ text: string; is_match: boolean }>; 167 | } { 168 | // Decode HTML entities 169 | const decodedText = htmlText 170 | .replace(/"/g, '"') 171 | .replace(/</g, '<') 172 | .replace(/>/g, '>') 173 | .replace(/&/g, '&') 174 | .replace(///g, '/'); 175 | 176 | // Remove HTML tags and track highlighted segments 177 | const segments: Array<{ text: string; is_match: boolean }> = []; 178 | let plainText = ''; 179 | let currentPos = 0; 180 | 181 | // Match all <em> tags and their content 182 | const emRegex = /<em>(.*?)<\/em>/g; 183 | let lastEnd = 0; 184 | let match; 185 | 186 | while ((match = emRegex.exec(decodedText)) !== null) { 187 | // Add non-highlighted text before this match 188 | if (match.index > lastEnd) { 189 | const beforeText = decodedText.substring(lastEnd, match.index); 190 | segments.push({ text: beforeText, is_match: false }); 191 | plainText += beforeText; 192 | } 193 | 194 | // Add highlighted text 195 | const highlightedText = match[1]; 196 | segments.push({ text: highlightedText, is_match: true }); 197 | plainText += highlightedText; 198 | 199 | lastEnd = match.index + match[0].length; 200 | } 201 | 202 | // Add any remaining non-highlighted text 203 | if (lastEnd < decodedText.length) { 204 | const remainingText = decodedText.substring(lastEnd); 205 | segments.push({ text: remainingText, is_match: false }); 206 | plainText += remainingText; 207 | } 208 | 209 | // If no <em> tags were found, the entire text is non-highlighted 210 | if (segments.length === 0) { 211 | segments.push({ text: decodedText, is_match: false }); 212 | plainText = decodedText; 213 | } 214 | 215 | return { text: plainText, segments }; 216 | } 217 | 218 | // Simplified formatter for MCP tool output 219 | export function formatCodeSearchOutput(searchResult: BitbucketServerSearchResult): string { 220 | if (!searchResult.code?.values || searchResult.code.values.length === 0) { 221 | return 'No results found'; 222 | } 223 | 224 | const outputLines: string[] = []; 225 | 226 | for (const value of searchResult.code.values) { 227 | outputLines.push(`File: ${value.file}`); 228 | 229 | // Process all hit contexts 230 | if (value.hitContexts && value.hitContexts.length > 0) { 231 | for (const contextGroup of value.hitContexts) { 232 | for (const lineContext of contextGroup) { 233 | // Remove HTML tags and decode entities 234 | const cleanText = lineContext.text 235 | .replace(/<em>/g, '') 236 | .replace(/<\/em>/g, '') 237 | .replace(/"/g, '"') 238 | .replace(/</g, '<') 239 | .replace(/>/g, '>') 240 | .replace(/&/g, '&') 241 | .replace(///g, '/') 242 | .replace(/'/g, "'"); 243 | 244 | outputLines.push(` Line ${lineContext.line}: ${cleanText}`); 245 | } 246 | } 247 | } 248 | 249 | outputLines.push(''); // Empty line between files 250 | } 251 | 252 | return outputLines.join('\n').trim(); 253 | } 254 | ``` -------------------------------------------------------------------------------- /src/handlers/review-handlers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { BitbucketApiClient } from '../utils/api-client.js'; 3 | import { 4 | isGetPullRequestDiffArgs, 5 | isApprovePullRequestArgs, 6 | isRequestChangesArgs 7 | } from '../types/guards.js'; 8 | import { DiffParser } from '../utils/diff-parser.js'; 9 | 10 | export class ReviewHandlers { 11 | constructor( 12 | private apiClient: BitbucketApiClient, 13 | private username: string 14 | ) {} 15 | 16 | async handleGetPullRequestDiff(args: any) { 17 | if (!isGetPullRequestDiffArgs(args)) { 18 | throw new McpError( 19 | ErrorCode.InvalidParams, 20 | 'Invalid arguments for get_pull_request_diff' 21 | ); 22 | } 23 | 24 | const { 25 | workspace, 26 | repository, 27 | pull_request_id, 28 | context_lines = 3, 29 | include_patterns, 30 | exclude_patterns, 31 | file_path 32 | } = args; 33 | 34 | try { 35 | let apiPath: string; 36 | let config: any = {}; 37 | 38 | if (this.apiClient.getIsServer()) { 39 | // Bitbucket Server API 40 | apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/diff`; 41 | config.params = { contextLines: context_lines }; 42 | } else { 43 | // Bitbucket Cloud API 44 | apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/diff`; 45 | config.params = { context: context_lines }; 46 | } 47 | 48 | // For diff, we want the raw text response 49 | config.headers = { 'Accept': 'text/plain' }; 50 | 51 | const rawDiff = await this.apiClient.makeRequest<string>('get', apiPath, undefined, config); 52 | 53 | // Check if filtering is needed 54 | const needsFiltering = file_path || include_patterns || exclude_patterns; 55 | 56 | if (!needsFiltering) { 57 | // Return raw diff without filtering 58 | return { 59 | content: [ 60 | { 61 | type: 'text', 62 | text: JSON.stringify({ 63 | message: 'Pull request diff retrieved successfully', 64 | pull_request_id, 65 | diff: rawDiff 66 | }, null, 2), 67 | }, 68 | ], 69 | }; 70 | } 71 | 72 | // Apply filtering 73 | const diffParser = new DiffParser(); 74 | const sections = diffParser.parseDiffIntoSections(rawDiff); 75 | 76 | const filterOptions = { 77 | includePatterns: include_patterns, 78 | excludePatterns: exclude_patterns, 79 | filePath: file_path 80 | }; 81 | 82 | const filteredResult = diffParser.filterSections(sections, filterOptions); 83 | const filteredDiff = diffParser.reconstructDiff(filteredResult.sections); 84 | 85 | // Build response with filtering metadata 86 | const response: any = { 87 | message: 'Pull request diff retrieved successfully', 88 | pull_request_id, 89 | diff: filteredDiff 90 | }; 91 | 92 | // Add filter metadata 93 | if (filteredResult.metadata.excludedFiles > 0 || file_path || include_patterns || exclude_patterns) { 94 | response.filter_metadata = { 95 | total_files: filteredResult.metadata.totalFiles, 96 | included_files: filteredResult.metadata.includedFiles, 97 | excluded_files: filteredResult.metadata.excludedFiles 98 | }; 99 | 100 | if (filteredResult.metadata.excludedFileList.length > 0) { 101 | response.filter_metadata.excluded_file_list = filteredResult.metadata.excludedFileList; 102 | } 103 | 104 | response.filter_metadata.filters_applied = {}; 105 | if (file_path) { 106 | response.filter_metadata.filters_applied.file_path = file_path; 107 | } 108 | if (include_patterns) { 109 | response.filter_metadata.filters_applied.include_patterns = include_patterns; 110 | } 111 | if (exclude_patterns) { 112 | response.filter_metadata.filters_applied.exclude_patterns = exclude_patterns; 113 | } 114 | } 115 | 116 | return { 117 | content: [ 118 | { 119 | type: 'text', 120 | text: JSON.stringify(response, null, 2), 121 | }, 122 | ], 123 | }; 124 | } catch (error) { 125 | return this.apiClient.handleApiError(error, `getting diff for pull request ${pull_request_id} in ${workspace}/${repository}`); 126 | } 127 | } 128 | 129 | async handleApprovePullRequest(args: any) { 130 | if (!isApprovePullRequestArgs(args)) { 131 | throw new McpError( 132 | ErrorCode.InvalidParams, 133 | 'Invalid arguments for approve_pull_request' 134 | ); 135 | } 136 | 137 | const { workspace, repository, pull_request_id } = args; 138 | 139 | try { 140 | let apiPath: string; 141 | 142 | if (this.apiClient.getIsServer()) { 143 | // Bitbucket Server API - use participants endpoint 144 | // Convert email format: @ to _ for the API 145 | const username = this.username.replace('@', '_'); 146 | apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`; 147 | await this.apiClient.makeRequest<any>('put', apiPath, { status: 'APPROVED' }); 148 | } else { 149 | // Bitbucket Cloud API 150 | apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/approve`; 151 | await this.apiClient.makeRequest<any>('post', apiPath); 152 | } 153 | 154 | return { 155 | content: [ 156 | { 157 | type: 'text', 158 | text: JSON.stringify({ 159 | message: 'Pull request approved successfully', 160 | pull_request_id, 161 | approved_by: this.username 162 | }, null, 2), 163 | }, 164 | ], 165 | }; 166 | } catch (error) { 167 | return this.apiClient.handleApiError(error, `approving pull request ${pull_request_id} in ${workspace}/${repository}`); 168 | } 169 | } 170 | 171 | async handleUnapprovePullRequest(args: any) { 172 | if (!isApprovePullRequestArgs(args)) { 173 | throw new McpError( 174 | ErrorCode.InvalidParams, 175 | 'Invalid arguments for unapprove_pull_request' 176 | ); 177 | } 178 | 179 | const { workspace, repository, pull_request_id } = args; 180 | 181 | try { 182 | let apiPath: string; 183 | 184 | if (this.apiClient.getIsServer()) { 185 | // Bitbucket Server API - use participants endpoint 186 | const username = this.username.replace('@', '_'); 187 | apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`; 188 | await this.apiClient.makeRequest<any>('put', apiPath, { status: 'UNAPPROVED' }); 189 | } else { 190 | // Bitbucket Cloud API 191 | apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/approve`; 192 | await this.apiClient.makeRequest<any>('delete', apiPath); 193 | } 194 | 195 | return { 196 | content: [ 197 | { 198 | type: 'text', 199 | text: JSON.stringify({ 200 | message: 'Pull request approval removed successfully', 201 | pull_request_id, 202 | unapproved_by: this.username 203 | }, null, 2), 204 | }, 205 | ], 206 | }; 207 | } catch (error) { 208 | return this.apiClient.handleApiError(error, `removing approval from pull request ${pull_request_id} in ${workspace}/${repository}`); 209 | } 210 | } 211 | 212 | async handleRequestChanges(args: any) { 213 | if (!isRequestChangesArgs(args)) { 214 | throw new McpError( 215 | ErrorCode.InvalidParams, 216 | 'Invalid arguments for request_changes' 217 | ); 218 | } 219 | 220 | const { workspace, repository, pull_request_id, comment } = args; 221 | 222 | try { 223 | if (this.apiClient.getIsServer()) { 224 | // Bitbucket Server API - use needs-work status 225 | const username = this.username.replace('@', '_'); 226 | const apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`; 227 | await this.apiClient.makeRequest<any>('put', apiPath, { status: 'NEEDS_WORK' }); 228 | 229 | // Add comment if provided 230 | if (comment) { 231 | const commentPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/comments`; 232 | await this.apiClient.makeRequest<any>('post', commentPath, { text: comment }); 233 | } 234 | } else { 235 | // Bitbucket Cloud API - use request-changes status 236 | const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/request-changes`; 237 | await this.apiClient.makeRequest<any>('post', apiPath); 238 | 239 | // Add comment if provided 240 | if (comment) { 241 | const commentPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/comments`; 242 | await this.apiClient.makeRequest<any>('post', commentPath, { 243 | content: { raw: comment } 244 | }); 245 | } 246 | } 247 | 248 | return { 249 | content: [ 250 | { 251 | type: 'text', 252 | text: JSON.stringify({ 253 | message: 'Changes requested on pull request', 254 | pull_request_id, 255 | requested_by: this.username, 256 | comment: comment || 'No comment provided' 257 | }, null, 2), 258 | }, 259 | ], 260 | }; 261 | } catch (error) { 262 | return this.apiClient.handleApiError(error, `requesting changes on pull request ${pull_request_id} in ${workspace}/${repository}`); 263 | } 264 | } 265 | 266 | async handleRemoveRequestedChanges(args: any) { 267 | if (!isApprovePullRequestArgs(args)) { 268 | throw new McpError( 269 | ErrorCode.InvalidParams, 270 | 'Invalid arguments for remove_requested_changes' 271 | ); 272 | } 273 | 274 | const { workspace, repository, pull_request_id } = args; 275 | 276 | try { 277 | if (this.apiClient.getIsServer()) { 278 | // Bitbucket Server API - remove needs-work status 279 | const username = this.username.replace('@', '_'); 280 | const apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`; 281 | await this.apiClient.makeRequest<any>('put', apiPath, { status: 'UNAPPROVED' }); 282 | } else { 283 | // Bitbucket Cloud API 284 | const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/request-changes`; 285 | await this.apiClient.makeRequest<any>('delete', apiPath); 286 | } 287 | 288 | return { 289 | content: [ 290 | { 291 | type: 'text', 292 | text: JSON.stringify({ 293 | message: 'Change request removed from pull request', 294 | pull_request_id, 295 | removed_by: this.username 296 | }, null, 2), 297 | }, 298 | ], 299 | }; 300 | } catch (error) { 301 | return this.apiClient.handleApiError(error, `removing change request from pull request ${pull_request_id} in ${workspace}/${repository}`); 302 | } 303 | } 304 | } 305 | ``` -------------------------------------------------------------------------------- /src/types/bitbucket.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Bitbucket Server API response types 2 | export interface BitbucketServerPullRequest { 3 | id: number; 4 | version: number; 5 | title: string; 6 | description?: string; 7 | state: string; 8 | open: boolean; 9 | closed: boolean; 10 | createdDate: number; 11 | updatedDate: number; 12 | fromRef: { 13 | id: string; 14 | displayId: string; 15 | latestCommit: string; 16 | repository: { 17 | slug: string; 18 | name: string; 19 | project: { 20 | key: string; 21 | }; 22 | }; 23 | }; 24 | toRef: { 25 | id: string; 26 | displayId: string; 27 | latestCommit: string; 28 | repository: { 29 | slug: string; 30 | name: string; 31 | project: { 32 | key: string; 33 | }; 34 | }; 35 | }; 36 | locked: boolean; 37 | author: { 38 | user: { 39 | name: string; 40 | emailAddress: string; 41 | displayName: string; 42 | }; 43 | role: string; 44 | approved: boolean; 45 | status: string; 46 | }; 47 | reviewers: Array<{ 48 | user: { 49 | name: string; 50 | emailAddress: string; 51 | displayName: string; 52 | }; 53 | role: string; 54 | approved: boolean; 55 | status: string; 56 | }>; 57 | participants: Array<{ 58 | user: { 59 | name: string; 60 | emailAddress: string; 61 | displayName: string; 62 | }; 63 | role: string; 64 | approved: boolean; 65 | status: string; 66 | }>; 67 | links: { 68 | self: Array<{ 69 | href: string; 70 | }>; 71 | }; 72 | properties?: { 73 | mergeCommit?: { 74 | id: string; 75 | displayId: string; 76 | }; 77 | }; 78 | } 79 | 80 | // Bitbucket Server Activity types 81 | export interface BitbucketServerActivity { 82 | id: number; 83 | createdDate: number; 84 | user: { 85 | name: string; 86 | emailAddress: string; 87 | displayName: string; 88 | }; 89 | action: string; 90 | comment?: any; 91 | commit?: { 92 | id: string; 93 | displayId: string; 94 | message?: string; 95 | }; 96 | } 97 | 98 | // Bitbucket Server Branch types 99 | export interface BitbucketServerBranch { 100 | id: string; 101 | displayId: string; 102 | type: string; 103 | latestCommit: string; 104 | latestChangeset: string; 105 | isDefault: boolean; 106 | metadata?: { 107 | "com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata": { 108 | author: { 109 | name: string; 110 | emailAddress: string; 111 | }; 112 | authorTimestamp: number; 113 | message: string; 114 | }; 115 | }; 116 | } 117 | 118 | // Bitbucket Server Directory Entry 119 | export interface BitbucketServerDirectoryEntry { 120 | path: { 121 | name: string; 122 | toString: string; 123 | }; 124 | type: 'FILE' | 'DIRECTORY'; 125 | size?: number; 126 | contentId?: string; 127 | } 128 | 129 | // Bitbucket Cloud API response types 130 | export interface BitbucketCloudPullRequest { 131 | id: number; 132 | title: string; 133 | description: string; 134 | state: string; 135 | author: { 136 | display_name: string; 137 | account_id: string; 138 | }; 139 | source: { 140 | branch: { 141 | name: string; 142 | }; 143 | repository: { 144 | full_name: string; 145 | }; 146 | }; 147 | destination: { 148 | branch: { 149 | name: string; 150 | }; 151 | repository: { 152 | full_name: string; 153 | }; 154 | }; 155 | reviewers: Array<{ 156 | display_name: string; 157 | account_id: string; 158 | }>; 159 | participants: Array<{ 160 | user: { 161 | display_name: string; 162 | account_id: string; 163 | }; 164 | role: string; 165 | approved: boolean; 166 | }>; 167 | created_on: string; 168 | updated_on: string; 169 | links: { 170 | html: { 171 | href: string; 172 | }; 173 | self: { 174 | href: string; 175 | }; 176 | diff: { 177 | href: string; 178 | }; 179 | }; 180 | merge_commit?: { 181 | hash: string; 182 | }; 183 | close_source_branch: boolean; 184 | closed_by?: { 185 | display_name: string; 186 | account_id: string; 187 | }; 188 | } 189 | 190 | // Bitbucket Cloud Branch types 191 | export interface BitbucketCloudBranch { 192 | name: string; 193 | target: { 194 | hash: string; 195 | type: string; 196 | message: string; 197 | author: { 198 | raw: string; 199 | user?: { 200 | display_name: string; 201 | account_id: string; 202 | }; 203 | }; 204 | date: string; 205 | }; 206 | type: string; 207 | } 208 | 209 | // Bitbucket Cloud Directory Entry 210 | export interface BitbucketCloudDirectoryEntry { 211 | path: string; 212 | type: 'commit_file' | 'commit_directory'; 213 | size?: number; 214 | commit?: { 215 | hash: string; 216 | }; 217 | links?: { 218 | self: { 219 | href: string; 220 | }; 221 | html: { 222 | href: string; 223 | }; 224 | }; 225 | } 226 | 227 | // Bitbucket Cloud File Metadata 228 | export interface BitbucketCloudFileMetadata { 229 | path: string; 230 | size: number; 231 | encoding?: string; 232 | mimetype?: string; 233 | links: { 234 | self: { 235 | href: string; 236 | }; 237 | html: { 238 | href: string; 239 | }; 240 | download: { 241 | href: string; 242 | }; 243 | }; 244 | commit?: { 245 | hash: string; 246 | author?: { 247 | raw: string; 248 | user?: { 249 | display_name: string; 250 | account_id: string; 251 | }; 252 | }; 253 | date?: string; 254 | message?: string; 255 | }; 256 | } 257 | 258 | // Merge info type for enhanced PR details 259 | export interface MergeInfo { 260 | mergeCommitHash?: string; 261 | mergedBy?: string; 262 | mergedAt?: string; 263 | mergeCommitMessage?: string; 264 | } 265 | 266 | // Comment types 267 | export interface BitbucketServerComment { 268 | id: number; 269 | version: number; 270 | text: string; 271 | author: { 272 | name: string; 273 | emailAddress: string; 274 | displayName: string; 275 | }; 276 | createdDate: number; 277 | updatedDate: number; 278 | state?: 'OPEN' | 'RESOLVED'; 279 | anchor?: { 280 | line: number; 281 | lineType: string; 282 | fileType: string; 283 | path: string; 284 | }; 285 | } 286 | 287 | export interface BitbucketCloudComment { 288 | id: number; 289 | content: { 290 | raw: string; 291 | markup: string; 292 | html: string; 293 | }; 294 | user: { 295 | display_name: string; 296 | account_id: string; 297 | }; 298 | created_on: string; 299 | updated_on: string; 300 | deleted?: boolean; 301 | resolved?: boolean; 302 | inline?: { 303 | to: number; 304 | from?: number; 305 | path: string; 306 | }; 307 | } 308 | 309 | // File change types 310 | export interface BitbucketServerFileChange { 311 | path: { 312 | toString: string; 313 | }; 314 | executable: boolean; 315 | percentUnchanged: number; 316 | type: string; 317 | nodeType: string; 318 | srcPath?: { 319 | toString: string; 320 | }; 321 | linesAdded?: number; 322 | linesRemoved?: number; 323 | } 324 | 325 | export interface BitbucketCloudFileChange { 326 | path: string; 327 | type: 'added' | 'modified' | 'removed' | 'renamed'; 328 | lines_added: number; 329 | lines_removed: number; 330 | old?: { 331 | path: string; 332 | }; 333 | } 334 | 335 | // Formatted comment type for response 336 | export interface FormattedComment { 337 | id: number; 338 | author: string; 339 | text: string; 340 | created_on: string; 341 | is_inline: boolean; 342 | file_path?: string; 343 | line_number?: number; 344 | state?: 'OPEN' | 'RESOLVED'; 345 | parent_id?: number; // For Bitbucket Cloud style replies 346 | replies?: FormattedComment[]; // For Bitbucket Server nested replies 347 | } 348 | 349 | // Formatted file change type for response 350 | export interface FormattedFileChange { 351 | path: string; 352 | status: 'added' | 'modified' | 'removed' | 'renamed'; 353 | old_path?: string; 354 | } 355 | 356 | // Types for code snippet matching 357 | export interface CodeMatch { 358 | line_number: number; 359 | line_type: 'ADDED' | 'REMOVED' | 'CONTEXT'; 360 | exact_content: string; 361 | preview: string; 362 | confidence: number; 363 | context: { 364 | lines_before: string[]; 365 | lines_after: string[]; 366 | }; 367 | sequential_position?: number; // Position within diff (for ADDED lines) 368 | hunk_info?: { 369 | hunk_index: number; 370 | destination_start: number; 371 | line_in_hunk: number; 372 | }; 373 | } 374 | 375 | export interface MultipleMatchesError { 376 | code: 'MULTIPLE_MATCHES_FOUND'; 377 | message: string; 378 | occurrences: Array<{ 379 | line_number: number; 380 | file_path: string; 381 | preview: string; 382 | confidence: number; 383 | line_type: 'ADDED' | 'REMOVED' | 'CONTEXT'; 384 | }>; 385 | suggestion: string; 386 | } 387 | 388 | // Commit types 389 | export interface BitbucketServerCommit { 390 | id: string; 391 | displayId: string; 392 | message: string; 393 | author: { 394 | name: string; 395 | emailAddress: string; 396 | }; 397 | authorTimestamp: number; 398 | committer?: { 399 | name: string; 400 | emailAddress: string; 401 | }; 402 | committerTimestamp?: number; 403 | parents: Array<{ 404 | id: string; 405 | displayId: string; 406 | }>; 407 | } 408 | 409 | export interface BitbucketCloudCommit { 410 | hash: string; 411 | message: string; 412 | author: { 413 | raw: string; 414 | user?: { 415 | display_name: string; 416 | account_id: string; 417 | }; 418 | }; 419 | date: string; 420 | parents: Array<{ 421 | hash: string; 422 | type: string; 423 | }>; 424 | links?: { 425 | self: { 426 | href: string; 427 | }; 428 | html: { 429 | href: string; 430 | }; 431 | }; 432 | } 433 | 434 | export interface FormattedCommit { 435 | hash: string; 436 | abbreviated_hash: string; 437 | message: string; 438 | author: { 439 | name: string; 440 | email: string; 441 | }; 442 | date: string; 443 | parents: string[]; 444 | is_merge_commit: boolean; 445 | build_status?: BuildStatus; 446 | } 447 | 448 | // Search types 449 | export interface BitbucketServerSearchRequest { 450 | query: string; 451 | entities: { 452 | code?: { 453 | start?: number; 454 | limit?: number; 455 | }; 456 | commits?: { 457 | start?: number; 458 | limit?: number; 459 | }; 460 | pull_requests?: { 461 | start?: number; 462 | limit?: number; 463 | }; 464 | }; 465 | } 466 | 467 | export interface BitbucketServerSearchResult { 468 | scope?: { 469 | repository?: { 470 | slug: string; 471 | name: string; 472 | project: { 473 | key: string; 474 | name: string; 475 | }; 476 | }; 477 | type: string; 478 | }; 479 | code?: { 480 | category: string; 481 | isLastPage: boolean; 482 | count: number; 483 | start: number; 484 | nextStart?: number; 485 | values: Array<{ 486 | file: string; // Just the file path as string 487 | repository: { 488 | slug: string; 489 | name: string; 490 | project: { 491 | key: string; 492 | name: string; 493 | }; 494 | }; 495 | hitContexts: Array<Array<{ 496 | line: number; 497 | text: string; // HTML-formatted with <em> tags 498 | }>>; 499 | pathMatches: Array<any>; 500 | hitCount: number; 501 | }>; 502 | }; 503 | query?: { 504 | substituted: boolean; 505 | }; 506 | } 507 | 508 | export interface FormattedSearchResult { 509 | file_path: string; 510 | file_name: string; 511 | repository: string; 512 | project: string; 513 | matches: Array<{ 514 | line_number: number; 515 | line_content: string; 516 | highlighted_segments: Array<{ 517 | text: string; 518 | is_match: boolean; 519 | }>; 520 | }>; 521 | } 522 | 523 | // Build status types for Bitbucket Server 524 | export interface BitbucketServerBuildSummary { 525 | [commitId: string]: { 526 | failed?: number; 527 | inProgress?: number; 528 | successful?: number; 529 | unknown?: number; 530 | }; 531 | } 532 | 533 | export interface BuildStatus { 534 | successful: number; 535 | failed: number; 536 | in_progress: number; 537 | unknown: number; 538 | } 539 | 540 | // Project and Repository types 541 | export interface BitbucketServerProject { 542 | key: string; 543 | id: number; 544 | name: string; 545 | description?: string; 546 | public: boolean; 547 | type: 'NORMAL' | 'PERSONAL'; 548 | links: { 549 | self: Array<{ 550 | href: string; 551 | }>; 552 | }; 553 | } 554 | 555 | export interface BitbucketCloudProject { 556 | key: string; 557 | uuid: string; 558 | name: string; 559 | description?: string; 560 | is_private: boolean; 561 | links: { 562 | html: { 563 | href: string; 564 | }; 565 | }; 566 | } 567 | 568 | export interface BitbucketServerRepository { 569 | slug: string; 570 | id: number; 571 | name: string; 572 | description?: string; 573 | hierarchyId: string; 574 | scmId: string; 575 | state: 'AVAILABLE' | 'INITIALISING' | 'INITIALISATION_FAILED'; 576 | statusMessage: string; 577 | forkable: boolean; 578 | project: { 579 | key: string; 580 | id: number; 581 | name: string; 582 | public: boolean; 583 | type: string; 584 | }; 585 | public: boolean; 586 | links: { 587 | clone: Array<{ 588 | href: string; 589 | name: string; 590 | }>; 591 | self: Array<{ 592 | href: string; 593 | }>; 594 | }; 595 | } 596 | 597 | export interface BitbucketCloudRepository { 598 | slug: string; 599 | uuid: string; 600 | name: string; 601 | full_name: string; 602 | description?: string; 603 | scm: string; 604 | is_private: boolean; 605 | owner: { 606 | display_name: string; 607 | uuid: string; 608 | }; 609 | project: { 610 | key: string; 611 | name: string; 612 | }; 613 | mainbranch?: { 614 | name: string; 615 | type: string; 616 | }; 617 | links: { 618 | html: { 619 | href: string; 620 | }; 621 | clone: Array<{ 622 | href: string; 623 | name: string; 624 | }>; 625 | }; 626 | } 627 | ``` -------------------------------------------------------------------------------- /src/handlers/file-handlers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { BitbucketApiClient } from '../utils/api-client.js'; 3 | import { 4 | isListDirectoryContentArgs, 5 | isGetFileContentArgs 6 | } from '../types/guards.js'; 7 | import { 8 | BitbucketServerDirectoryEntry, 9 | BitbucketCloudDirectoryEntry, 10 | BitbucketCloudFileMetadata 11 | } from '../types/bitbucket.js'; 12 | import * as path from 'path'; 13 | 14 | export class FileHandlers { 15 | // Default lines by file extension 16 | private readonly DEFAULT_LINES_BY_EXT: Record<string, number> = { 17 | '.yml': 200, '.yaml': 200, '.json': 200, // Config files 18 | '.md': 300, '.txt': 300, // Docs 19 | '.ts': 500, '.js': 500, '.py': 500, // Code 20 | '.tsx': 500, '.jsx': 500, '.java': 500, // More code 21 | '.log': -100 // Last 100 lines for logs 22 | }; 23 | 24 | constructor( 25 | private apiClient: BitbucketApiClient, 26 | private baseUrl: string 27 | ) {} 28 | 29 | async handleListDirectoryContent(args: any) { 30 | if (!isListDirectoryContentArgs(args)) { 31 | throw new McpError( 32 | ErrorCode.InvalidParams, 33 | 'Invalid arguments for list_directory_content' 34 | ); 35 | } 36 | 37 | const { workspace, repository, path: dirPath = '', branch } = args; 38 | 39 | try { 40 | let apiPath: string; 41 | let params: any = {}; 42 | let response: any; 43 | 44 | if (this.apiClient.getIsServer()) { 45 | // Bitbucket Server API 46 | apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/browse`; 47 | if (dirPath) { 48 | apiPath += `/${dirPath}`; 49 | } 50 | if (branch) { 51 | params.at = `refs/heads/${branch}`; 52 | } 53 | 54 | response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); 55 | } else { 56 | // Bitbucket Cloud API 57 | const branchOrDefault = branch || 'HEAD'; 58 | apiPath = `/repositories/${workspace}/${repository}/src/${branchOrDefault}`; 59 | if (dirPath) { 60 | apiPath += `/${dirPath}`; 61 | } 62 | 63 | response = await this.apiClient.makeRequest<any>('get', apiPath); 64 | } 65 | 66 | // Format the response 67 | let contents: any[] = []; 68 | let actualBranch = branch; 69 | 70 | if (this.apiClient.getIsServer()) { 71 | // Bitbucket Server response 72 | const entries = response.children?.values || []; 73 | contents = entries.map((entry: BitbucketServerDirectoryEntry) => ({ 74 | name: entry.path.name, 75 | type: entry.type === 'FILE' ? 'file' : 'directory', 76 | size: entry.size, 77 | path: dirPath ? `${dirPath}/${entry.path.name}` : entry.path.name 78 | })); 79 | 80 | // Get the actual branch from the response if available 81 | if (!branch && response.path?.components) { 82 | // Server returns default branch info in the response 83 | actualBranch = 'default'; 84 | } 85 | } else { 86 | // Bitbucket Cloud response 87 | const entries = response.values || []; 88 | contents = entries.map((entry: BitbucketCloudDirectoryEntry) => ({ 89 | name: entry.path.split('/').pop() || entry.path, 90 | type: entry.type === 'commit_file' ? 'file' : 'directory', 91 | size: entry.size, 92 | path: entry.path 93 | })); 94 | 95 | // Cloud returns the branch in the response 96 | actualBranch = branch || response.commit?.branch || 'main'; 97 | } 98 | 99 | return { 100 | content: [ 101 | { 102 | type: 'text', 103 | text: JSON.stringify({ 104 | path: dirPath || '/', 105 | branch: actualBranch, 106 | contents, 107 | total_items: contents.length 108 | }, null, 2), 109 | }, 110 | ], 111 | }; 112 | } catch (error) { 113 | return this.apiClient.handleApiError(error, `listing directory '${dirPath}' in ${workspace}/${repository}`); 114 | } 115 | } 116 | 117 | async handleGetFileContent(args: any) { 118 | if (!isGetFileContentArgs(args)) { 119 | throw new McpError( 120 | ErrorCode.InvalidParams, 121 | 'Invalid arguments for get_file_content' 122 | ); 123 | } 124 | 125 | const { workspace, repository, file_path, branch, start_line, line_count, full_content = false } = args; 126 | 127 | try { 128 | let fileContent: string; 129 | let fileMetadata: any = {}; 130 | const fileSizeLimit = 1024 * 1024; // 1MB default limit 131 | 132 | if (this.apiClient.getIsServer()) { 133 | // Bitbucket Server - get file metadata first to check size 134 | const browsePath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/browse/${file_path}`; 135 | const browseParams: any = {}; 136 | if (branch) { 137 | browseParams.at = `refs/heads/${branch}`; 138 | } 139 | 140 | try { 141 | const metadataResponse = await this.apiClient.makeRequest<any>('get', browsePath, undefined, { params: browseParams }); 142 | fileMetadata = { 143 | size: metadataResponse.size || 0, 144 | path: file_path 145 | }; 146 | 147 | // Check file size 148 | if (!full_content && fileMetadata.size > fileSizeLimit) { 149 | return { 150 | content: [ 151 | { 152 | type: 'text', 153 | text: JSON.stringify({ 154 | error: 'File too large', 155 | file_path, 156 | size: fileMetadata.size, 157 | size_mb: (fileMetadata.size / (1024 * 1024)).toFixed(2), 158 | message: `File exceeds size limit. Use full_content: true to force retrieval or use start_line/line_count for partial content.` 159 | }, null, 2), 160 | }, 161 | ], 162 | isError: true, 163 | }; 164 | } 165 | } catch (e) { 166 | // If browse fails, continue to try raw endpoint 167 | } 168 | 169 | // Get raw content 170 | const rawPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/raw/${file_path}`; 171 | const rawParams: any = {}; 172 | if (branch) { 173 | rawParams.at = `refs/heads/${branch}`; 174 | } 175 | 176 | const response = await this.apiClient.makeRequest<any>('get', rawPath, undefined, { 177 | params: rawParams, 178 | responseType: 'text', 179 | headers: { 'Accept': 'text/plain' } 180 | }); 181 | 182 | fileContent = response; 183 | } else { 184 | // Bitbucket Cloud - first get metadata 185 | const branchOrDefault = branch || 'HEAD'; 186 | const metaPath = `/repositories/${workspace}/${repository}/src/${branchOrDefault}/${file_path}`; 187 | 188 | const metadataResponse = await this.apiClient.makeRequest<BitbucketCloudFileMetadata>('get', metaPath); 189 | 190 | fileMetadata = { 191 | size: metadataResponse.size, 192 | encoding: metadataResponse.encoding, 193 | path: metadataResponse.path, 194 | commit: metadataResponse.commit 195 | }; 196 | 197 | // Check file size 198 | if (!full_content && fileMetadata.size > fileSizeLimit) { 199 | return { 200 | content: [ 201 | { 202 | type: 'text', 203 | text: JSON.stringify({ 204 | error: 'File too large', 205 | file_path, 206 | size: fileMetadata.size, 207 | size_mb: (fileMetadata.size / (1024 * 1024)).toFixed(2), 208 | message: `File exceeds size limit. Use full_content: true to force retrieval or use start_line/line_count for partial content.` 209 | }, null, 2), 210 | }, 211 | ], 212 | isError: true, 213 | }; 214 | } 215 | 216 | // Follow the download link to get actual content 217 | const downloadUrl = metadataResponse.links.download.href; 218 | const downloadResponse = await this.apiClient.makeRequest<any>('get', downloadUrl, undefined, { 219 | baseURL: '', // Use full URL 220 | responseType: 'text', 221 | headers: { 'Accept': 'text/plain' } 222 | }); 223 | 224 | fileContent = downloadResponse; 225 | } 226 | 227 | // Apply line filtering if requested 228 | let processedContent = fileContent; 229 | let lineInfo: any = null; 230 | 231 | if (!full_content || start_line !== undefined || line_count !== undefined) { 232 | const lines = fileContent.split('\n'); 233 | const totalLines = lines.length; 234 | 235 | // Determine default line count based on file extension 236 | const ext = path.extname(file_path).toLowerCase(); 237 | const defaultLineCount = this.DEFAULT_LINES_BY_EXT[ext] || 500; 238 | const shouldUseTail = defaultLineCount < 0; 239 | 240 | // Calculate start and end indices 241 | let startIdx: number; 242 | let endIdx: number; 243 | 244 | if (start_line !== undefined) { 245 | if (start_line < 0) { 246 | // Negative start_line means from end 247 | startIdx = Math.max(0, totalLines + start_line); 248 | endIdx = totalLines; 249 | } else { 250 | // 1-based to 0-based index 251 | startIdx = Math.max(0, start_line - 1); 252 | endIdx = startIdx + (line_count || Math.abs(defaultLineCount)); 253 | } 254 | } else if (!full_content && fileMetadata.size > 50 * 1024) { 255 | // Auto-truncate large files 256 | if (shouldUseTail) { 257 | startIdx = Math.max(0, totalLines + defaultLineCount); 258 | endIdx = totalLines; 259 | } else { 260 | startIdx = 0; 261 | endIdx = Math.abs(defaultLineCount); 262 | } 263 | } else { 264 | // Return full content for small files 265 | startIdx = 0; 266 | endIdx = totalLines; 267 | } 268 | 269 | // Ensure indices are within bounds 270 | startIdx = Math.max(0, Math.min(startIdx, totalLines)); 271 | endIdx = Math.max(startIdx, Math.min(endIdx, totalLines)); 272 | 273 | // Extract the requested lines 274 | const selectedLines = lines.slice(startIdx, endIdx); 275 | processedContent = selectedLines.join('\n'); 276 | 277 | lineInfo = { 278 | total_lines: totalLines, 279 | returned_lines: { 280 | start: startIdx + 1, 281 | end: endIdx 282 | }, 283 | truncated: startIdx > 0 || endIdx < totalLines, 284 | message: endIdx < totalLines 285 | ? `Showing lines ${startIdx + 1}-${endIdx} of ${totalLines}. File size: ${(fileMetadata.size / 1024).toFixed(1)}KB` 286 | : null 287 | }; 288 | } 289 | 290 | // Build response 291 | const response: any = { 292 | file_path, 293 | branch: branch || (this.apiClient.getIsServer() ? 'default' : 'main'), 294 | size: fileMetadata.size || fileContent.length, 295 | encoding: fileMetadata.encoding || 'utf-8', 296 | content: processedContent 297 | }; 298 | 299 | if (lineInfo) { 300 | response.line_info = lineInfo; 301 | } 302 | 303 | if (fileMetadata.commit) { 304 | response.last_modified = { 305 | commit_id: fileMetadata.commit.hash, 306 | author: fileMetadata.commit.author?.user?.display_name || fileMetadata.commit.author?.raw, 307 | date: fileMetadata.commit.date, 308 | message: fileMetadata.commit.message 309 | }; 310 | } 311 | 312 | return { 313 | content: [ 314 | { 315 | type: 'text', 316 | text: JSON.stringify(response, null, 2), 317 | }, 318 | ], 319 | }; 320 | } catch (error: any) { 321 | // Handle specific not found error 322 | if (error.status === 404) { 323 | return { 324 | content: [ 325 | { 326 | type: 'text', 327 | text: `File '${file_path}' not found in ${workspace}/${repository}${branch ? ` on branch '${branch}'` : ''}`, 328 | }, 329 | ], 330 | isError: true, 331 | }; 332 | } 333 | return this.apiClient.handleApiError(error, `getting file content for '${file_path}' in ${workspace}/${repository}`); 334 | } 335 | } 336 | 337 | // Helper method to get default line count based on file extension 338 | private getDefaultLines(filePath: string, fileSize: number): { full: boolean } | { start: number; count: number } { 339 | // Small files: return full content 340 | if (fileSize < 50 * 1024) { // 50KB 341 | return { full: true }; 342 | } 343 | 344 | const ext = path.extname(filePath).toLowerCase(); 345 | const defaultLines = this.DEFAULT_LINES_BY_EXT[ext] || 500; 346 | 347 | return { 348 | start: defaultLines < 0 ? defaultLines : 1, 349 | count: Math.abs(defaultLines) 350 | }; 351 | } 352 | } 353 | ```