This is page 1 of 2. Use http://codebase.md/pdogra1299/bitbucket-mcp-server?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: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ # Build output build/ dist/ # Environment files .env .env.local .env.*.local # IDE files .vscode/ .idea/ *.swp *.swo *~ # OS files .DS_Store Thumbs.db # Logs logs/ *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Test files test-api.js *.test.js # Temporary files *.tmp *.temp .cache/ # Personal configuration RELOAD_INSTRUCTIONS.md personal-notes.md currentTask.yml ``` -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` # Source files src/ *.ts !*.d.ts # Development files .gitignore tsconfig.json .vscode/ .idea/ # Test files test/ *.test.js *.spec.js # Build artifacts *.log node_modules/ .npm/ # OS files .DS_Store Thumbs.db # Editor files *.swp *.swo *~ # Documentation source docs/ # Scripts (except built ones) scripts/ # Setup guides (keep README) SETUP_GUIDE.md SETUP_GUIDE_SERVER.md # Git .git/ .gitattributes # Other .env .env.local .env.*.local ``` -------------------------------------------------------------------------------- /memory-bank/.clinerules: -------------------------------------------------------------------------------- ``` # Bitbucket MCP Server - Project Intelligence ## Critical Implementation Paths ### Adding New Tools 1. Create handler in src/handlers/ following existing patterns 2. Add TypeScript interfaces in src/types/bitbucket.ts 3. Add type guard in src/types/guards.ts 4. Add formatter in src/utils/formatters.ts if needed 5. Register tool in src/tools/definitions.ts 6. Wire handler in src/index.ts 7. Update version, CHANGELOG.md, and README.md ### API Variant Handling - Always check `apiClient.getIsServer()` for Cloud vs Server - Server uses `/rest/api/1.0/` prefix - Cloud uses direct paths under base URL - Different parameter names (e.g., pagelen vs limit) ### Error Handling Pattern ```typescript try { // API call } catch (error: any) { const errorMessage = error.response?.data?.errors?.[0]?.message || error.message; return { content: [{ type: 'text', text: JSON.stringify({ error: `Failed to ${action}: ${errorMessage}`, details: error.response?.data }, null, 2) }], isError: true }; } ``` ## User Preferences ### Documentation Style - Comprehensive examples for each tool - Clear parameter descriptions - Response format examples - Note differences between Cloud and Server ### Code Style - TypeScript with strict typing - ES modules with .js extensions in imports - Consistent error handling - Modular architecture ## Project-Specific Patterns ### Authentication - Cloud: Username (not email) + App Password - Server: Email address + HTTP Access Token - Environment variables for configuration ### Pagination Pattern - `limit` and `start` parameters - Return `has_more` and `next_start` - Include `total_count` when available ### Response Formatting - Consistent JSON structure - Include operation status message - Provide detailed error information - Format dates as ISO strings ## Known Challenges ### Bitbucket API Differences - Parameter naming varies (e.g., pagelen vs limit) - Response structures differ significantly - Some features only available on one variant - Authentication methods completely different ### Search Functionality - Only available on Bitbucket Server - Query syntax requires specific format - No Cloud API equivalent currently ## Tool Usage Patterns ### List Operations - Always include pagination parameters - Return consistent metadata - Support filtering where applicable ### Modification Operations - Validate required parameters - Preserve existing data when updating - Return updated resource in response ### File Operations - Smart truncation for large files - Type-based default limits - Support line range selection ## Evolution of Decisions ### Version 0.3.0 - Modularized codebase into handlers - Separated types and utilities - Improved maintainability ### Version 0.6.0 - Enhanced PR details with comments/files - Added parallel API calls for performance ### Version 0.9.0 - Added code snippet matching for comments - Implemented confidence scoring ### Version 1.0.0 - Added code search functionality - Reached feature completeness - Ready for production use ### Version 1.0.1 - Improved search response formatting for AI consumption - Added simplified formatCodeSearchOutput - Enhanced HTML entity decoding and tag stripping - Established live MCP testing workflow ## Important Architecture Decisions ### Handler Pattern Each tool category has its own handler class to maintain single responsibility and make the codebase more maintainable. ### Type Guards All tool inputs are validated with type guards to ensure type safety at runtime. ### Response Normalization Different API responses are normalized to consistent formats for easier consumption. ### Error Handling Consistent error handling across all tools with detailed error messages and recovery suggestions. ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Bitbucket MCP Server [](https://www.npmjs.com/package/@nexus2520/bitbucket-mcp-server) [](https://opensource.org/licenses/MIT) An MCP (Model Context Protocol) server that provides tools for interacting with the Bitbucket API, supporting both Bitbucket Cloud and Bitbucket Server. ## Features ### Currently Implemented Tools #### Core PR Lifecycle Tools - `get_pull_request` - Retrieve detailed information about a pull request - `list_pull_requests` - List pull requests with filters (state, author, pagination) - `create_pull_request` - Create new pull requests - `update_pull_request` - Update PR details (title, description, reviewers, destination branch) - `add_comment` - Add comments to pull requests (supports replies) - `merge_pull_request` - Merge pull requests with various strategies - `list_pr_commits` - List all commits that are part of a pull request - `delete_branch` - Delete branches after merge #### Branch Management Tools - `list_branches` - List branches with filtering and pagination - `delete_branch` - Delete branches (with protection checks) - `get_branch` - Get detailed branch information including associated PRs - `list_branch_commits` - List commits in a branch with advanced filtering #### File and Directory Tools - `list_directory_content` - List files and directories in a repository path - `get_file_content` - Get file content with smart truncation for large files #### Code Review Tools - `get_pull_request_diff` - Get the diff/changes for a pull request - `approve_pull_request` - Approve a pull request - `unapprove_pull_request` - Remove approval from a pull request - `request_changes` - Request changes on a pull request - `remove_requested_changes` - Remove change request from a pull request #### Search Tools - `search_code` - Search for code across repositories (currently Bitbucket Server only) #### Project and Repository Discovery Tools - `list_projects` - List all accessible Bitbucket projects/workspaces with filtering - `list_repositories` - List repositories in a project or across all accessible projects ## Installation ### Using npx (Recommended) The easiest way to use this MCP server is directly with npx: ```json { "mcpServers": { "bitbucket": { "command": "npx", "args": [ "-y", "@nexus2520/bitbucket-mcp-server" ], "env": { "BITBUCKET_USERNAME": "your-username", "BITBUCKET_APP_PASSWORD": "your-app-password" } } } } ``` For Bitbucket Server: ```json { "mcpServers": { "bitbucket": { "command": "npx", "args": [ "-y", "@nexus2520/bitbucket-mcp-server" ], "env": { "BITBUCKET_USERNAME": "[email protected]", "BITBUCKET_TOKEN": "your-http-access-token", "BITBUCKET_BASE_URL": "https://bitbucket.yourcompany.com" } } } } ``` ### From Source 1. Clone or download this repository 2. Install dependencies: ```bash npm install ``` 3. Build the TypeScript code: ```bash npm run build ``` ## Authentication Setup This server uses Bitbucket App Passwords for authentication. ### Creating an App Password 1. Log in to your Bitbucket account 2. Navigate to: https://bitbucket.org/account/settings/app-passwords/ 3. Click "Create app password" 4. Give it a descriptive label (e.g., "MCP Server") 5. Select the following permissions: - **Account**: Read - **Repositories**: Read, Write - **Pull requests**: Read, Write 6. Click "Create" 7. **Important**: Copy the generated password immediately (you won't be able to see it again!) ### Running the Setup Script ```bash node scripts/setup-auth.js ``` This will guide you through the authentication setup process. ## Configuration Add the server to your MCP settings file (usually located at `~/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`): ```json { "mcpServers": { "bitbucket": { "command": "node", "args": ["/absolute/path/to/bitbucket-mcp-server/build/index.js"], "env": { "BITBUCKET_USERNAME": "your-username", "BITBUCKET_APP_PASSWORD": "your-app-password" } } } } ``` Replace: - `/absolute/path/to/bitbucket-mcp-server` with the actual path to this directory - `your-username` with your Bitbucket username (not email) - `your-app-password` with the app password you created For Bitbucket Server, use: ```json { "mcpServers": { "bitbucket": { "command": "node", "args": ["/absolute/path/to/bitbucket-mcp-server/build/index.js"], "env": { "BITBUCKET_USERNAME": "[email protected]", "BITBUCKET_TOKEN": "your-http-access-token", "BITBUCKET_BASE_URL": "https://bitbucket.yourcompany.com" } } } } ``` **Important for Bitbucket Server users:** - Use your full email address as the username (e.g., "[email protected]") - This is required for approval/review actions to work correctly ## Usage Once configured, you can use the available tools: ### Get Pull Request ```typescript { "tool": "get_pull_request", "arguments": { "workspace": "PROJ", // Required - your project key "repository": "my-repo", "pull_request_id": 123 } } ``` Returns detailed information about the pull request including: - Title and description - Author and reviewers - Source and destination branches - Approval status - Links to web UI and diff - **Merge commit details** (when PR is merged): - `merge_commit_hash`: The hash of the merge commit - `merged_by`: Who performed the merge - `merged_at`: When the merge occurred - `merge_commit_message`: The merge commit message - **Active comments with nested replies** (unresolved comments that need attention): - `active_comments`: Array of active comments (up to 20 most recent top-level comments) - Comment text and author - Creation date - Whether it's an inline comment (with file path and line number) - **Nested replies** (for Bitbucket Server): - `replies`: Array of reply comments with same structure - Replies can be nested multiple levels deep - **Parent reference** (for Bitbucket Cloud): - `parent_id`: ID of the parent comment for replies - `active_comment_count`: Total count of unresolved comments (including nested replies) - `total_comment_count`: Total count of all comments (including resolved and replies) - **File changes**: - `file_changes`: Array of all files modified in the PR - File path - Status (added, modified, removed, or renamed) - Old path (for renamed files) - `file_changes_summary`: Summary statistics - Total files changed - And more... ### Search Code Search for code across Bitbucket repositories (currently only supported for Bitbucket Server): ```typescript // Search in a specific repository { "tool": "search_code", "arguments": { "workspace": "PROJ", "repository": "my-repo", "search_query": "TODO", "limit": 50 } } // Search across all repositories in a workspace { "tool": "search_code", "arguments": { "workspace": "PROJ", "search_query": "deprecated", "file_pattern": "*.java", // Optional: filter by file pattern "limit": 100 } } // Search with file pattern filtering { "tool": "search_code", "arguments": { "workspace": "PROJ", "repository": "frontend-app", "search_query": "useState", "file_pattern": "*.tsx", // Only search in .tsx files "start": 0, "limit": 25 } } ``` Returns search results with: - File path and name - Repository and project information - Matched lines with: - Line number - Full line content - Highlighted segments showing exact matches - Pagination information Example response: ```json { "message": "Code search completed successfully", "workspace": "PROJ", "repository": "my-repo", "search_query": "TODO", "results": [ { "file_path": "src/utils/helper.js", "file_name": "helper.js", "repository": "my-repo", "project": "PROJ", "matches": [ { "line_number": 42, "line_content": " // TODO: Implement error handling", "highlighted_segments": [ { "text": " // ", "is_match": false }, { "text": "TODO", "is_match": true }, { "text": ": Implement error handling", "is_match": false } ] } ] } ], "total_count": 15, "start": 0, "limit": 50, "has_more": false } ``` **Note**: This tool currently only works with Bitbucket Server. Bitbucket Cloud support is planned for a future release. ### List Pull Requests ```typescript { "tool": "list_pull_requests", "arguments": { "workspace": "PROJ", // Required - your project key "repository": "my-repo", "state": "OPEN", // Optional: OPEN, MERGED, DECLINED, ALL (default: OPEN) "author": "username", // Optional: filter by author (see note below) "limit": 25, // Optional: max results per page (default: 25) "start": 0 // Optional: pagination start index (default: 0) } } ``` Returns a paginated list of pull requests with: - Array of pull requests with same details as get_pull_request - Total count of matching PRs - Pagination info (has_more, next_start) **Note on Author Filter:** - For Bitbucket Cloud: Use the username (e.g., "johndoe") - For Bitbucket Server: Use the full email address (e.g., "[email protected]") ### Create Pull Request ```typescript { "tool": "create_pull_request", "arguments": { "workspace": "PROJ", "repository": "my-repo", "title": "Add new feature", "source_branch": "feature/new-feature", "destination_branch": "main", "description": "This PR adds a new feature...", // Optional "reviewers": ["john.doe", "jane.smith"], // Optional "close_source_branch": true // Optional (default: false) } } ``` ### Update Pull Request ```typescript { "tool": "update_pull_request", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "title": "Updated title", // Optional "description": "Updated description", // Optional "destination_branch": "develop", // Optional "reviewers": ["new.reviewer"] // Optional - see note below } } ``` **Important Note on Reviewers:** - When updating a PR without specifying the `reviewers` parameter, existing reviewers and their approval status are preserved - When providing the `reviewers` parameter: - The reviewer list is replaced with the new list - For reviewers that already exist on the PR, their approval status is preserved - New reviewers are added without approval status - This prevents accidentally removing reviewers when you only want to update the PR description or title ### Add Comment Add a comment to a pull request, either as a general comment or inline on specific code: ```javascript // General comment { "tool": "add_comment", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "comment_text": "Great work on this PR!" } } // Inline comment on specific line { "tool": "add_comment", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "comment_text": "Consider extracting this into a separate function", "file_path": "src/utils/helpers.js", "line_number": 42, "line_type": "CONTEXT" // ADDED, REMOVED, or CONTEXT } } // Reply to existing comment { "tool": "add_comment", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "comment_text": "I agree with this suggestion", "parent_comment_id": 456 } } // Add comment with code suggestion (single line) { "tool": "add_comment", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "comment_text": "This variable name could be more descriptive.", "file_path": "src/utils/helpers.js", "line_number": 42, "line_type": "CONTEXT", "suggestion": "const userAuthenticationToken = token;" } } // Add comment with multi-line code suggestion { "tool": "add_comment", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "comment_text": "This function could be simplified using array methods.", "file_path": "src/utils/calculations.js", "line_number": 50, "suggestion_end_line": 55, "line_type": "CONTEXT", "suggestion": "function calculateTotal(items) {\n return items.reduce((sum, item) => sum + item.price, 0);\n}" } } ``` The suggestion feature formats comments using GitHub-style markdown suggestion blocks that Bitbucket can render. When adding a suggestion: - `suggestion` is required and contains the replacement code - `file_path` and `line_number` are required when using suggestions - `suggestion_end_line` is optional and used for multi-line suggestions (defaults to `line_number`) - The comment will be formatted with a ````suggestion` markdown block that may be applicable in the Bitbucket UI ### Using Code Snippets Instead of Line Numbers 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: ```javascript // Add comment using code snippet { "tool": "add_comment", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "comment_text": "This variable name could be more descriptive", "file_path": "src/components/Button.res", "code_snippet": "let isDisabled = false", "search_context": { "before": ["let onClick = () => {"], "after": ["setLoading(true)"] } } } // Handle multiple matches with strategy { "tool": "add_comment", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "comment_text": "Consider extracting this", "file_path": "src/utils/helpers.js", "code_snippet": "return result;", "search_context": { "before": ["const result = calculate();"], "after": ["}"] }, "match_strategy": "best" // Auto-select highest confidence match } } ``` **Code Snippet Parameters:** - `code_snippet`: The exact code line to find (alternative to `line_number`) - `search_context`: Optional context to disambiguate multiple matches - `before`: Array of lines that should appear before the target - `after`: Array of lines that should appear after the target - `match_strategy`: How to handle multiple matches - `"strict"` (default): Fail with error showing all matches - `"best"`: Auto-select the highest confidence match **Error Response for Multiple Matches (strict mode):** ```json { "error": { "code": "MULTIPLE_MATCHES_FOUND", "message": "Code snippet 'return result;' found in 3 locations", "occurrences": [ { "line_number": 42, "file_path": "src/utils/helpers.js", "preview": " const result = calculate();\n> return result;\n}", "confidence": 0.9, "line_type": "ADDED" }, // ... more matches ], "suggestion": "To resolve, either:\n1. Add more context...\n2. Use match_strategy: 'best'...\n3. Use line_number directly" } } ``` This feature is particularly useful for: - AI-powered code review tools that analyze diffs - Scripts that automatically add comments based on code patterns - Avoiding line number confusion in large diffs **Note on comment replies:** - Use `parent_comment_id` to reply to any comment (general or inline) - In `get_pull_request` responses: - Bitbucket Server shows replies nested in a `replies` array - Bitbucket Cloud shows a `parent_id` field for reply comments - You can reply to replies, creating nested conversations **Note on inline comments:** - `file_path`: The path to the file as shown in the diff - `line_number`: The line number as shown in the diff - `line_type`: - `ADDED` - For newly added lines (green in diff) - `REMOVED` - For deleted lines (red in diff) - `CONTEXT` - For unchanged context lines #### Add Comment - Complete Usage Guide The `add_comment` tool supports multiple scenarios. Here's when and how to use each approach: **1. General PR Comments (No file/line)** - Use when: Making overall feedback about the PR - Required params: `comment_text` only - Example: "LGTM!", "Please update the documentation" **2. Reply to Existing Comments** - Use when: Continuing a conversation thread - Required params: `comment_text`, `parent_comment_id` - Works for both general and inline comment replies **3. Inline Comments with Line Number** - Use when: You know the exact line number from the diff - Required params: `comment_text`, `file_path`, `line_number` - Optional: `line_type` (defaults to CONTEXT) **4. Inline Comments with Code Snippet** - Use when: You have the code but not the line number (common for AI tools) - Required params: `comment_text`, `file_path`, `code_snippet` - The tool will automatically find the line number - Add `search_context` if the code appears multiple times - Use `match_strategy: "best"` to auto-select when multiple matches exist **5. Code Suggestions** - Use when: Proposing specific code changes - Required params: `comment_text`, `file_path`, `line_number`, `suggestion` - For multi-line: also add `suggestion_end_line` - Creates applicable suggestion blocks in Bitbucket UI **Decision Flow for AI/Automated Tools:** ``` 1. Do you want to suggest code changes? → Use suggestion with line_number 2. Do you have the exact line number? → Use line_number directly 3. Do you have the code snippet but not line number? → Use code_snippet (add search_context if needed) 4. Is it a general comment about the PR? → Use comment_text only 5. Are you replying to another comment? → Add parent_comment_id ``` **Common Pitfalls to Avoid:** - Don't use both `line_number` and `code_snippet` - pick one - Suggestions always need `file_path` and `line_number` - Code snippets must match exactly (including whitespace) - REMOVED lines reference the source file, ADDED/CONTEXT reference the destination ### Merge Pull Request ```typescript { "tool": "merge_pull_request", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "merge_strategy": "squash", // Optional: merge-commit, squash, fast-forward "close_source_branch": true, // Optional "commit_message": "Custom merge message" // Optional } } ``` ### List Branches ```typescript { "tool": "list_branches", "arguments": { "workspace": "PROJ", "repository": "my-repo", "filter": "feature", // Optional: filter by name pattern "limit": 25, // Optional (default: 25) "start": 0 // Optional: for pagination (default: 0) } } ``` Returns a paginated list of branches with: - Branch name and ID - Latest commit hash - Default branch indicator - Pagination info ### Delete Branch ```typescript { "tool": "delete_branch", "arguments": { "workspace": "PROJ", "repository": "my-repo", "branch_name": "feature/old-feature", "force": false // Optional (default: false) } } ``` **Note**: Branch deletion requires appropriate permissions. The branch will be permanently deleted. ### Get Branch ```typescript { "tool": "get_branch", "arguments": { "workspace": "PROJ", "repository": "my-repo", "branch_name": "feature/new-feature", "include_merged_prs": false // Optional (default: false) } } ``` Returns comprehensive branch information including: - Branch details: - Name and ID - Latest commit (hash, message, author, date) - Default branch indicator - Open pull requests from this branch: - PR title and ID - Destination branch - Author and reviewers - Approval status (approved by, changes requested by, pending) - PR URL - Merged pull requests (if `include_merged_prs` is true): - PR title and ID - Merge date and who merged it - Statistics: - Total open PRs count - Total merged PRs count - Days since last commit This tool is particularly useful for: - Checking if a branch has open PRs before deletion - Getting an overview of branch activity - Understanding PR review status - Identifying stale branches ### List Branch Commits Get all commits in a specific branch with advanced filtering options: ```typescript // Basic usage - get recent commits { "tool": "list_branch_commits", "arguments": { "workspace": "PROJ", "repository": "my-repo", "branch_name": "feature/new-feature", "limit": 50 // Optional (default: 25) } } // Filter by date range { "tool": "list_branch_commits", "arguments": { "workspace": "PROJ", "repository": "my-repo", "branch_name": "main", "since": "2025-01-01T00:00:00Z", // ISO date string "until": "2025-01-15T23:59:59Z" // ISO date string } } // Filter by author { "tool": "list_branch_commits", "arguments": { "workspace": "PROJ", "repository": "my-repo", "branch_name": "develop", "author": "[email protected]", // Email or username "limit": 100 } } // Exclude merge commits { "tool": "list_branch_commits", "arguments": { "workspace": "PROJ", "repository": "my-repo", "branch_name": "release/v2.0", "include_merge_commits": false } } // Search in commit messages { "tool": "list_branch_commits", "arguments": { "workspace": "PROJ", "repository": "my-repo", "branch_name": "main", "search": "bugfix", // Search in commit messages "limit": 50 } } // Combine multiple filters { "tool": "list_branch_commits", "arguments": { "workspace": "PROJ", "repository": "my-repo", "branch_name": "develop", "author": "[email protected]", "since": "2025-01-01T00:00:00Z", "include_merge_commits": false, "search": "feature", "limit": 100, "start": 0 // For pagination } } // Include CI/CD build status (Bitbucket Server only) { "tool": "list_branch_commits", "arguments": { "workspace": "PROJ", "repository": "my-repo", "branch_name": "main", "include_build_status": true, // Fetch build status for each commit "limit": 50 } } ``` **Filter Parameters:** - `since`: ISO date string - only show commits after this date - `until`: ISO date string - only show commits before this date - `author`: Filter by author email/username - `include_merge_commits`: Boolean to include/exclude merge commits (default: true) - `search`: Search for text in commit messages - `include_build_status`: Boolean to include CI/CD build status (default: false, Bitbucket Server only) Returns detailed commit information: ```json { "branch_name": "feature/new-feature", "branch_head": "abc123def456", // Latest commit hash "commits": [ { "hash": "abc123def456", "abbreviated_hash": "abc123d", "message": "Add new feature implementation", "author": { "name": "John Doe", "email": "[email protected]" }, "date": "2025-01-03T10:30:00Z", "parents": ["parent1hash", "parent2hash"], "is_merge_commit": false, "build_status": { // Only present when include_build_status is true "successful": 5, "failed": 0, "in_progress": 1, "unknown": 0 } } // ... more commits ], "total_count": 150, "start": 0, "limit": 25, "has_more": true, "next_start": 25, "filters_applied": { "author": "[email protected]", "since": "2025-01-01", "include_merge_commits": false, "include_build_status": true } } ``` This tool is particularly useful for: - Reviewing commit history before releases - Finding commits by specific authors - Tracking changes within date ranges - Searching for specific features or fixes - Analyzing branch activity patterns - Monitoring CI/CD build status for commits (Bitbucket Server only) ### List PR Commits Get all commits that are part of a pull request: ```typescript // Basic usage { "tool": "list_pr_commits", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "limit": 50, // Optional (default: 25) "start": 0 // Optional: for pagination } } // Include CI/CD build status (Bitbucket Server only) { "tool": "list_pr_commits", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "include_build_status": true, // Fetch build status for each commit "limit": 50 } } ``` Returns commit information for the PR: ```json { "pull_request_id": 123, "pull_request_title": "Add awesome feature", "commits": [ { "hash": "def456ghi789", "abbreviated_hash": "def456g", "message": "Initial implementation", "author": { "name": "Jane Smith", "email": "[email protected]" }, "date": "2025-01-02T14:20:00Z", "parents": ["parent1hash"], "is_merge_commit": false, "build_status": { // Only present when include_build_status is true "successful": 3, "failed": 0, "in_progress": 0, "unknown": 0 } } // ... more commits ], "total_count": 5, "start": 0, "limit": 25, "has_more": false } ``` This tool is particularly useful for: - Reviewing all changes in a PR before merging - Understanding the development history of a PR - Checking commit messages for quality - Verifying authorship of changes - Analyzing PR complexity by commit count - Monitoring CI/CD build status for all PR commits (Bitbucket Server only) ### Get Pull Request Diff Get the diff/changes for a pull request with optional filtering capabilities: ```typescript // Get full diff (default behavior) { "tool": "get_pull_request_diff", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "context_lines": 5 // Optional (default: 3) } } // Exclude specific file types { "tool": "get_pull_request_diff", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "exclude_patterns": ["*.lock", "*.svg", "node_modules/**", "*.min.js"] } } // Include only specific file types { "tool": "get_pull_request_diff", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "include_patterns": ["*.res", "*.resi", "src/**/*.js"] } } // Get diff for a specific file only { "tool": "get_pull_request_diff", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "file_path": "src/components/Button.res" } } // Combine filters { "tool": "get_pull_request_diff", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "include_patterns": ["src/**/*"], "exclude_patterns": ["*.test.js", "*.spec.js"] } } ``` **Filtering Options:** - `include_patterns`: Array of glob patterns to include (whitelist) - `exclude_patterns`: Array of glob patterns to exclude (blacklist) - `file_path`: Get diff for a specific file only - Patterns support standard glob syntax (e.g., `*.js`, `src/**/*.res`, `!test/**`) **Response includes filtering metadata:** ```json { "message": "Pull request diff retrieved successfully", "pull_request_id": 123, "diff": "..filtered diff content..", "filter_metadata": { "total_files": 15, "included_files": 12, "excluded_files": 3, "excluded_file_list": ["package-lock.json", "logo.svg", "yarn.lock"], "filters_applied": { "exclude_patterns": ["*.lock", "*.svg"] } } } ``` ### Approve Pull Request ```typescript { "tool": "approve_pull_request", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123 } } ``` ### Request Changes ```typescript { "tool": "request_changes", "arguments": { "workspace": "PROJ", "repository": "my-repo", "pull_request_id": 123, "comment": "Please address the following issues..." // Optional } } ``` ### List Directory Content ```typescript { "tool": "list_directory_content", "arguments": { "workspace": "PROJ", "repository": "my-repo", "path": "src/components", // Optional (defaults to root) "branch": "main" // Optional (defaults to default branch) } } ``` Returns directory listing with: - Path and branch information - Array of contents with: - Name - Type (file or directory) - Size (for files) - Full path - Total items count ### Get File Content ```typescript { "tool": "get_file_content", "arguments": { "workspace": "PROJ", "repository": "my-repo", "file_path": "src/index.ts", "branch": "main", // Optional (defaults to default branch) "start_line": 1, // Optional: starting line (1-based, use negative for from end) "line_count": 100, // Optional: number of lines to return "full_content": false // Optional: force full content (default: false) } } ``` **Smart Truncation Features:** - Automatically truncates large files (>50KB) to prevent token overload - Default line counts based on file type: - Config files (.yml, .json): 200 lines - Documentation (.md, .txt): 300 lines - Code files (.ts, .js, .py): 500 lines - Log files: Last 100 lines - Use `start_line: -50` to get last 50 lines (tail functionality) - Files larger than 1MB require explicit `full_content: true` or line parameters Returns file content with: - File path and branch - File size and encoding - Content (full or truncated based on parameters) - Line information (if truncated): - Total lines in file - Range of returned lines - Truncation indicator - Last modified information (commit, author, date) Example responses: ```json // Small file - returns full content { "file_path": "package.json", "branch": "main", "size": 1234, "encoding": "utf-8", "content": "{\n \"name\": \"my-project\",\n ...", "last_modified": { "commit_id": "abc123", "author": "John Doe", "date": "2025-01-21T10:00:00Z" } } // Large file - automatically truncated { "file_path": "src/components/LargeComponent.tsx", "branch": "main", "size": 125000, "encoding": "utf-8", "content": "... first 500 lines ...", "line_info": { "total_lines": 3500, "returned_lines": { "start": 1, "end": 500 }, "truncated": true, "message": "Showing lines 1-500 of 3500. File size: 122.1KB" } } ``` ### List Projects List all accessible Bitbucket projects (Server) or workspaces (Cloud): ```typescript // List all accessible projects { "tool": "list_projects", "arguments": { "limit": 25, // Optional (default: 25) "start": 0 // Optional: for pagination (default: 0) } } // Filter by project name { "tool": "list_projects", "arguments": { "name": "backend", // Partial name match "limit": 50 } } // Filter by permission level (Bitbucket Server only) { "tool": "list_projects", "arguments": { "permission": "PROJECT_WRITE", // PROJECT_READ, PROJECT_WRITE, PROJECT_ADMIN "limit": 100 } } ``` **Parameters:** - `name`: Filter by project/workspace name (partial match, optional) - `permission`: Filter by permission level (Bitbucket Server only, optional) - `PROJECT_READ`: Read access - `PROJECT_WRITE`: Write access - `PROJECT_ADMIN`: Admin access - `limit`: Maximum number of projects to return (default: 25) - `start`: Start index for pagination (default: 0) Returns project/workspace information: ```json { "projects": [ { "key": "PROJ", "id": 1234, "name": "My Project", "description": "Project description", "is_public": false, "type": "NORMAL", // NORMAL or PERSONAL (Server), WORKSPACE (Cloud) "url": "https://bitbucket.yourcompany.com/projects/PROJ" } // ... more projects ], "total_count": 15, "start": 0, "limit": 25, "has_more": false, "next_start": null } ``` **Note**: - For Bitbucket Cloud, this returns workspaces (not projects in the traditional sense) - For Bitbucket Server, this returns both personal and team projects This tool is particularly useful for: - Discovering available projects/workspaces for your account - Finding project keys needed for other API calls - Identifying projects you have specific permissions on - Browsing organizational structure ### List Repositories List repositories within a specific project/workspace or across all accessible repositories: ```typescript // List all repositories in a workspace/project { "tool": "list_repositories", "arguments": { "workspace": "PROJ", // Required for Bitbucket Cloud, optional for Server "limit": 25, // Optional (default: 25) "start": 0 // Optional: for pagination (default: 0) } } // List all accessible repositories (Bitbucket Server only) { "tool": "list_repositories", "arguments": { "limit": 100 } } // Filter by repository name { "tool": "list_repositories", "arguments": { "workspace": "PROJ", "name": "frontend", // Partial name match "limit": 50 } } // Filter by permission level (Bitbucket Server only) { "tool": "list_repositories", "arguments": { "workspace": "PROJ", "permission": "REPO_WRITE", // REPO_READ, REPO_WRITE, REPO_ADMIN "limit": 100 } } ``` **Parameters:** - `workspace`: Project key (Server) or workspace slug (Cloud) - **Required for Bitbucket Cloud** - Optional for Bitbucket Server (omit to list all accessible repos) - `name`: Filter by repository name (partial match, optional) - `permission`: Filter by permission level (Bitbucket Server only, optional) - `REPO_READ`: Read access - `REPO_WRITE`: Write access - `REPO_ADMIN`: Admin access - `limit`: Maximum number of repositories to return (default: 25) - `start`: Start index for pagination (default: 0) Returns repository information: ```json { "repositories": [ { "slug": "my-repo", "id": 5678, "name": "My Repository", "description": "Repository description", "project_key": "PROJ", "project_name": "My Project", "state": "AVAILABLE", // AVAILABLE, INITIALISING, INITIALISATION_FAILED (Server) "is_public": false, "is_forkable": true, "clone_urls": { "http": "https://bitbucket.yourcompany.com/scm/PROJ/my-repo.git", "ssh": "ssh://[email protected]:7999/PROJ/my-repo.git" }, "url": "https://bitbucket.yourcompany.com/projects/PROJ/repos/my-repo" } // ... more repositories ], "total_count": 42, "start": 0, "limit": 25, "has_more": true, "next_start": 25, "workspace": "PROJ" } ``` **Important Notes:** - **Bitbucket Cloud** requires the `workspace` parameter. If omitted, you'll receive an error message - **Bitbucket Server** allows listing all accessible repos by omitting the `workspace` parameter - Clone URLs are provided for both HTTP(S) and SSH protocols This tool is particularly useful for: - Discovering available repositories in a project/workspace - Finding repository slugs needed for other API calls - Identifying repositories you have specific permissions on - Getting clone URLs for repositories - Browsing repository structure within an organization ## Development - `npm run dev` - Watch mode for development - `npm run build` - Build the TypeScript code - `npm start` - Run the built server ## Troubleshooting 1. **Authentication errors**: Double-check your username and app password 2. **404 errors**: Verify the workspace, repository slug, and PR ID 3. **Permission errors**: Ensure your app password has the required permissions ## License MIT ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "build"] } ``` -------------------------------------------------------------------------------- /src/utils/suggestion-formatter.ts: -------------------------------------------------------------------------------- ```typescript /** * Formats a comment with a code suggestion in markdown format * that Bitbucket can render as an applicable suggestion */ export function formatSuggestionComment( commentText: string, suggestion: string, startLine?: number, endLine?: number ): string { // Add line range info if it's a multi-line suggestion const lineInfo = startLine && endLine && endLine > startLine ? ` (lines ${startLine}-${endLine})` : ''; // Format with GitHub-style suggestion markdown return `${commentText}${lineInfo} \`\`\`suggestion ${suggestion} \`\`\``; } ``` -------------------------------------------------------------------------------- /scripts/setup-auth.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node console.log(` =========================================== Bitbucket MCP Server - Authentication Setup =========================================== To use this MCP server, you need to create a Bitbucket App Password. Follow these steps: 1. Log in to your Bitbucket account 2. Go to: https://bitbucket.org/account/settings/app-passwords/ 3. Click "Create app password" 4. Give it a label (e.g., "MCP Server") 5. Select the following permissions: - Account: Read - Repositories: Read, Write - Pull requests: Read, Write 6. Click "Create" 7. Copy the generated app password (you won't be able to see it again!) You'll need to provide: - Your Bitbucket username (not email) - The app password you just created - Your default workspace/organization (optional) Example workspace: If your repository URL is: https://bitbucket.org/mycompany/my-repo Then your workspace is: mycompany These will be added to your MCP settings configuration. Press Enter to continue... `); // Wait for user to press Enter process.stdin.once('data', () => { console.log(` Next steps: 1. The MCP server will be configured with your credentials 2. You'll be able to use the 'get_pull_request' tool 3. More tools can be added later (create_pull_request, list_pull_requests, etc.) Configuration complete! `); process.exit(0); }); ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@nexus2520/bitbucket-mcp-server", "version": "1.1.2", "description": "MCP server for Bitbucket API integration - supports both Cloud and Server", "type": "module", "main": "./build/index.js", "bin": { "bitbucket-mcp-server": "build/index.js" }, "files": [ "build/**/*", "README.md", "LICENSE", "CHANGELOG.md" ], "scripts": { "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", "dev": "tsc --watch", "start": "node build/index.js", "prepublishOnly": "npm run build" }, "keywords": [ "mcp", "bitbucket", "api", "model-context-protocol", "bitbucket-server", "bitbucket-cloud", "pull-request", "code-review" ], "author": "Parth Dogra", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/pdogra1299/bitbucket-mcp-server.git" }, "bugs": { "url": "https://github.com/pdogra1299/bitbucket-mcp-server/issues" }, "homepage": "https://github.com/pdogra1299/bitbucket-mcp-server#readme", "engines": { "node": ">=16.0.0" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", "axios": "^1.10.0", "minimatch": "^9.0.3" }, "devDependencies": { "@types/minimatch": "^5.1.2", "@types/node": "^22.15.29", "typescript": "^5.8.3" } } ``` -------------------------------------------------------------------------------- /memory-bank/projectbrief.yml: -------------------------------------------------------------------------------- ```yaml # Project Brief - Bitbucket MCP Server project_name: "bitbucket-mcp-server" version: "1.0.0" description: "MCP (Model Context Protocol) server for Bitbucket API integration" primary_purpose: "Provide tools for interacting with Bitbucket APIs via MCP" key_features: - "Support for both Bitbucket Cloud and Server" - "19 comprehensive tools for PR management, branch operations, and code review" - "Modular architecture with separate handlers for different tool categories" - "Smart file content handling with automatic truncation" - "Advanced PR commenting with code suggestions and snippet matching" - "Code search functionality across repositories" scope: included: - "Pull Request lifecycle management" - "Branch management and operations" - "Code review and approval workflows" - "File and directory operations" - "Code search (Bitbucket Server only)" - "Authentication via app passwords/tokens" excluded: - "Repository creation/deletion" - "User management" - "Pipeline/build operations" - "Wiki/documentation management" - "Bitbucket Cloud code search (future enhancement)" constraints: technical: - "Node.js >= 16.0.0 required" - "TypeScript with ES modules" - "MCP SDK for protocol implementation" authentication: - "Bitbucket Cloud: App passwords required" - "Bitbucket Server: HTTP access tokens required" - "Different username formats for Cloud vs Server" success_criteria: - "Seamless integration with MCP-compatible clients" - "Reliable API interactions with proper error handling" - "Consistent response formatting across tools" - "Maintainable and extensible codebase" ``` -------------------------------------------------------------------------------- /memory-bank/productContext.yml: -------------------------------------------------------------------------------- ```yaml # Product Context - Bitbucket MCP Server problem_statement: | Developers using AI assistants need programmatic access to Bitbucket operations without leaving their development environment. Manual API interactions are time-consuming and error-prone. target_users: primary: - "Software developers using AI coding assistants" - "DevOps engineers automating PR workflows" - "Code reviewers needing efficient review tools" secondary: - "Project managers tracking development progress" - "QA engineers reviewing code changes" user_needs: core: - "Create and manage pull requests programmatically" - "Review code changes with AI assistance" - "Automate branch management tasks" - "Search code across repositories" - "Access file contents without cloning" workflow: - "Comment on PRs with code suggestions" - "Approve/request changes on PRs" - "List and filter pull requests" - "Get comprehensive PR details including comments" - "Manage branch lifecycle" value_proposition: - "Seamless Bitbucket integration within AI assistants" - "Unified interface for Cloud and Server variants" - "Time savings through automation" - "Reduced context switching" - "Enhanced code review quality with AI" user_experience_goals: - "Simple tool invocation syntax" - "Clear and consistent response formats" - "Helpful error messages with recovery suggestions" - "Smart defaults (pagination, truncation)" - "Flexible filtering and search options" adoption_strategy: - "npm package for easy installation" - "Comprehensive documentation with examples" - "Support for both Cloud and Server" - "Gradual feature addition based on user needs" ``` -------------------------------------------------------------------------------- /SETUP_GUIDE_SERVER.md: -------------------------------------------------------------------------------- ```markdown # Bitbucket Server MCP Setup Guide Since you're using Bitbucket Server (self-hosted), you'll need to create an HTTP access token instead of an app password. ## Step 1: Your Username Your Bitbucket Server username (not email address) ## Step 2: Create an HTTP Access Token 1. **Navigate to HTTP Access Tokens**: - You mentioned you can see "HTTP access tokens" in your account settings - Click on that option 2. **Create a new token**: - Click "Create token" or similar button - Give it a descriptive name like "MCP Server Integration" - Set an expiration date (or leave it without expiration if allowed) - Select the following permissions: - **Repository**: Read, Write - **Pull request**: Read, Write - **Project**: Read (if available) 3. **Generate and copy the token**: - Click "Create" or "Generate" - **IMPORTANT**: Copy the token immediately! It will look like a long string of random characters - You won't be able to see this token again ## Step 3: Find Your Bitbucket Server URL Your Bitbucket Server URL is the base URL you use to access Bitbucket. For example: - `https://bitbucket.yourcompany.com` - `https://git.yourcompany.com` - `https://bitbucket.internal.company.net` ## Step 4: Find Your Project/Workspace In Bitbucket Server, repositories are organized by projects. Look at any repository URL: - Example: `https://bitbucket.company.com/projects/PROJ/repos/my-repo` - In this case, "PROJ" is your project key ## Example Configuration For Bitbucket Server, your configuration will look like: ``` Username: your.username Token: [Your HTTP access token] Base URL: https://bitbucket.yourcompany.com Project/Workspace: PROJ (or whatever your project key is) ``` ## Next Steps Once you have: 1. Your username 2. An HTTP access token from the "HTTP access tokens" section 3. Your Bitbucket Server base URL 4. Your project key You can configure the MCP server for Bitbucket Server. ``` -------------------------------------------------------------------------------- /memory-bank/progress.yml: -------------------------------------------------------------------------------- ```yaml # Progress - Bitbucket MCP Server project_status: overall_progress: "95%" phase: "Post-1.0 Improvements" version: "1.0.1" release_status: "Production with improvements" milestones_completed: - name: "Core PR Tools" completion_date: "2025-01-06" features: - "get_pull_request with merge details" - "list_pull_requests with filtering" - "create_pull_request" - "update_pull_request" - "merge_pull_request" - name: "Enhanced Commenting" completion_date: "2025-01-26" features: - "Inline comments" - "Code suggestions" - "Code snippet matching" - "Nested replies support" - name: "Code Review Tools" completion_date: "2025-01-26" features: - "get_pull_request_diff with filtering" - "approve_pull_request" - "request_changes" - "Review state management" - name: "Branch Management" completion_date: "2025-01-21" features: - "list_branches" - "delete_branch" - "get_branch with PR info" - "list_branch_commits" - name: "File Operations" completion_date: "2025-01-21" features: - "list_directory_content" - "get_file_content with smart truncation" - name: "Code Search" completion_date: "2025-07-25" features: - "search_code for Bitbucket Server" - "File pattern filtering" - "Highlighted search results" active_development: current_sprint: "Post 1.0 Planning" in_progress: [] blocked: [] testing_status: unit_tests: "Not implemented" integration_tests: "Manual testing completed" user_acceptance: "In production use" known_issues: [] deployment_status: npm_package: "Published as @nexus2520/bitbucket-mcp-server" version_published: "1.0.0" documentation: "Comprehensive README with examples" adoption_metrics: - "npm weekly downloads tracking" - "GitHub stars and issues" performance_metrics: api_response_time: "< 2s average" memory_usage: "< 100MB typical" concurrent_operations: "Supports parallel API calls" next_release_planning: version: "1.1.0" planned_features: - "Bitbucket Cloud search support" - "Repository management tools" - "Pipeline/build integration" timeline: "TBD based on user feedback" ``` -------------------------------------------------------------------------------- /SETUP_GUIDE.md: -------------------------------------------------------------------------------- ```markdown # Bitbucket MCP Server Setup Guide ## Step 1: Find Your Bitbucket Username 1. **Log in to Bitbucket**: Go to https://bitbucket.org and log in with your credentials 2. **Find your username**: - After logging in, click on your profile avatar in the top-right corner - Click on "Personal settings" or go directly to: https://bitbucket.org/account/settings/ - Your username will be displayed at the top of the page - **Note**: Your username is NOT your email address. It's usually a shorter identifier like "johndoe" or "jdoe123" ## Step 2: Create an App Password 1. **Navigate to App Passwords**: - While logged in, go to: https://bitbucket.org/account/settings/app-passwords/ - Or from your account settings, look for "App passwords" in the left sidebar under "Access management" 2. **Create a new app password**: - Click the "Create app password" button - Give it a descriptive label like "MCP Server" or "Bitbucket MCP Integration" 3. **Select permissions** (IMPORTANT - select these specific permissions): - ✅ **Account**: Read - ✅ **Repositories**: Read, Write - ✅ **Pull requests**: Read, Write - You can leave other permissions unchecked 4. **Generate the password**: - Click "Create" - **IMPORTANT**: Copy the generated password immediately! It will look something like: `ATBBxxxxxxxxxxxxxxxxxxxxx` - You won't be able to see this password again after closing the dialog ## Step 3: Find Your Workspace (Optional but Recommended) Your workspace is the organization or team name in Bitbucket. To find it: 1. Look at any of your repository URLs: - Example: `https://bitbucket.org/mycompany/my-repo` - In this case, "mycompany" is your workspace 2. Or go to your workspace dashboard: - Click on "Workspaces" in the top navigation - Your workspaces will be listed there ## Example Credentials Here's what your credentials should look like: ``` Username: johndoe # Your Bitbucket username (NOT email) App Password: ATBB3xXx... # The generated app password Workspace: mycompany # Your organization/workspace name ``` ## Common Issues 1. **"Username not found"**: Make sure you're using your Bitbucket username, not your email address 2. **"Invalid app password"**: Ensure you copied the entire app password including the "ATBB" prefix 3. **"Permission denied"**: Check that your app password has the required permissions (Account: Read, Repositories: Read/Write, Pull requests: Read/Write) ## Next Steps 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. ``` -------------------------------------------------------------------------------- /memory-bank/techContext.yml: -------------------------------------------------------------------------------- ```yaml # Technical Context - Bitbucket MCP Server core_technologies: language: "TypeScript" version: "5.8.3" module_system: "ES Modules" runtime: name: "Node.js" version: ">= 16.0.0" package_manager: "npm" libraries_and_bindings: - name: "@modelcontextprotocol/sdk" version: "^1.12.1" purpose: "MCP protocol implementation" - name: "axios" version: "^1.10.0" purpose: "HTTP client for API requests" - name: "minimatch" version: "^9.0.3" purpose: "Glob pattern matching for file filtering" development_environment: build_tools: - "TypeScript compiler (tsc)" - "npm scripts for build automation" commands: build: "npm run build" dev: "npm run dev" start: "npm start" publish: "npm publish" project_structure: src_directory: "src/" build_directory: "build/" entry_point: "src/index.ts" compiled_entry: "build/index.js" technical_patterns: - name: "Shebang for CLI execution" description: "#!/usr/bin/env node at top of index.ts" usage: "Enables direct execution as CLI tool" - name: "ES Module imports" description: "Using .js extensions in TypeScript imports" usage: "Required for ES module compatibility" examples: - "import { Server } from '@modelcontextprotocol/sdk/server/index.js'" - name: "Type-safe error handling" description: "Custom ApiError interface with typed errors" usage: "Consistent error handling across API calls" - name: "Environment variable configuration" description: "Process.env for authentication and base URL" usage: "Flexible configuration without code changes" api_integration: bitbucket_cloud: base_url: "https://api.bitbucket.org/2.0" auth_method: "Basic Auth with App Password" api_style: "RESTful with JSON" pagination: "page-based with pagelen parameter" bitbucket_server: base_url: "Custom URL (e.g., https://bitbucket.company.com)" auth_method: "Bearer token (HTTP Access Token)" api_style: "RESTful with JSON" pagination: "offset-based with start/limit" api_version: "/rest/api/1.0" search_api: "/rest/search/latest" deployment: package_name: "@nexus2520/bitbucket-mcp-server" registry: "npm public registry" distribution: - "Compiled JavaScript in build/" - "Type definitions excluded" - "Source maps excluded" execution_methods: - "npx -y @nexus2520/bitbucket-mcp-server" - "Direct node execution after install" - "MCP client integration" security_considerations: - "Credentials stored in environment variables" - "No credential logging or exposure" - "HTTPS only for API communications" - "Token/password validation on startup" performance_optimizations: - "Parallel API calls for PR details" - "Smart file truncation to prevent token overflow" - "Pagination for large result sets" - "Early exit on authentication failure" compatibility: mcp_clients: - "Cline (VSCode extension)" - "Other MCP-compatible AI assistants" bitbucket_versions: - "Bitbucket Cloud (latest API)" - "Bitbucket Server 7.x+" - "Bitbucket Data Center" ``` -------------------------------------------------------------------------------- /memory-bank/systemPatterns.yml: -------------------------------------------------------------------------------- ```yaml # System Patterns - Bitbucket MCP Server architecture_overview: high_level_architecture: | MCP Server implementation with modular handler architecture. Main server class delegates tool calls to specialized handlers. Each handler manages a specific domain (PRs, branches, reviews, files, search). component_relationships: | - BitbucketMCPServer (main) → Handler classes → BitbucketApiClient → Axios - Tool definitions → MCP SDK → Client applications - Type guards validate inputs → Handlers process → Formatters standardize output design_patterns: - name: "Handler Pattern" category: "architecture" description: "Separate handler classes for different tool categories" usage: "Organizing related tools and maintaining single responsibility" implementation: - "PullRequestHandlers for PR lifecycle" - "BranchHandlers for branch operations" - "ReviewHandlers for code review tools" - "FileHandlers for file/directory operations" - "SearchHandlers for code search" example_files: - "src/handlers/*.ts" related_patterns: - "Dependency Injection" - name: "API Client Abstraction" category: "integration" description: "Unified client handling both Cloud and Server APIs" usage: "Abstracting API differences between Bitbucket variants" implementation: - "Single makeRequest method for all HTTP operations" - "Automatic auth header selection (Bearer vs Basic)" - "Consistent error handling across variants" example_files: - "src/utils/api-client.ts" - name: "Type Guard Pattern" category: "validation" description: "Runtime type checking for tool arguments" usage: "Ensuring type safety for dynamic tool inputs" implementation: - "Guard functions return type predicates" - "Comprehensive validation of required/optional fields" - "Array and nested object validation" example_files: - "src/types/guards.ts" - name: "Response Formatting" category: "data_transformation" description: "Consistent response formatting across API variants" usage: "Normalizing different API response structures" implementation: - "formatServerResponse/formatCloudResponse for PRs" - "Unified FormattedXXX interfaces" - "Separate formatters for different data types" example_files: - "src/utils/formatters.ts" project_specific_patterns: mcp_patterns: - name: "Tool Definition Structure" description: "Standardized tool definition with inputSchema" implementation: - "Name, description, and JSON schema for each tool" - "Required vs optional parameter specification" - "Enum constraints for valid values" - name: "Error Response Pattern" description: "Consistent error handling and reporting" implementation: - "Return isError: true for tool failures" - "Include detailed error messages" - "Provide context-specific error details" bitbucket_patterns: - name: "Pagination Pattern" description: "Consistent pagination across list operations" implementation: - "limit and start parameters" - "has_more and next_start in responses" - "total_count for result sets" - name: "Dual API Support" description: "Supporting both Cloud and Server APIs" implementation: - "isServer flag determines API paths" - "Different parameter names mapped appropriately" - "Response structure normalization" code_patterns: - name: "Smart Truncation" description: "Intelligent file content truncation" implementation: - "File type-based default limits" - "Size-based automatic truncation" - "Line range selection support" - name: "Code Snippet Matching" description: "Finding line numbers from code snippets" implementation: - "Exact text matching with context" - "Confidence scoring for multiple matches" - "Strategy selection (strict vs best)" ``` -------------------------------------------------------------------------------- /memory-bank/activeContext.yml: -------------------------------------------------------------------------------- ```yaml # Active Context - Bitbucket MCP Server current_focus_areas: - name: "Search Code Feature Implementation" status: "completed" priority: "high" team: "frontend" timeline: "2025-07-25" recent_changes: - date: "2025-08-08" feature: "testing_and_release_v1.0.1" description: "Comprehensive testing of search functionality and release of version 1.0.1" status: "completed" files_affected: - "package.json" - "CHANGELOG.md" - "memory-bank/activeContext.yml" - "memory-bank/progress.yml" technical_details: "Tested search functionality with real Bitbucket data, verified context-aware patterns, file filtering, and error handling" business_impact: "Validated production readiness of search improvements and properly versioned the release" patterns_introduced: - "Live MCP testing workflow using connected Bitbucket server" - "Real-world validation of search functionality" - date: "2025-07-25" feature: "code_search_response_fix" description: "Fixed search_code tool response handling to match actual Bitbucket API structure" status: "completed" files_affected: - "src/utils/formatters.ts" - "src/handlers/search-handlers.ts" technical_details: "Added formatCodeSearchOutput for simplified AI-friendly output showing only filename, line number, and text" business_impact: "Search results now properly formatted for AI consumption with cleaner output" patterns_introduced: - "Simplified formatter pattern for AI-friendly output" - date: "2025-07-25" feature: "code_search" description: "Added search_code tool for searching code across repositories" status: "completed" files_affected: - "src/handlers/search-handlers.ts" - "src/types/bitbucket.ts" - "src/utils/formatters.ts" - "src/types/guards.ts" - "src/tools/definitions.ts" - "src/index.ts" technical_details: "Implemented Bitbucket Server search API integration" business_impact: "Enables code search functionality for Bitbucket Server users" patterns_introduced: - "SearchHandlers following modular architecture" - "Search result formatting pattern" patterns_discovered: - name: "Modular Handler Architecture" description: "Each tool category has its own handler class" usage: "Add new handlers for new tool categories" implementation: "Create handler class, add to index.ts, register tools" - name: "API Variant Handling" description: "Single handler supports both Cloud and Server APIs" usage: "Check isServer flag and adjust API paths/parameters" implementation: "Use apiClient.getIsServer() to determine variant" - name: "Simplified AI Formatter Pattern" description: "Separate formatters for detailed vs AI-friendly output" usage: "Create simplified formatters that extract only essential information for AI" implementation: "formatCodeSearchOutput strips HTML, shows only file:line:text format" recent_learnings: - date: "2025-07-25" topic: "Bitbucket Search API Response Structure" description: "API returns file as string (not object), uses hitContexts with HTML formatting" impact: "Need to parse HTML <em> tags and handle different response structure" - date: "2025-07-25" topic: "Bitbucket Search API" description: "Search API only available on Bitbucket Server, not Cloud" impact: "Search tool marked as Server-only with future Cloud support planned" - date: "2025-07-25" topic: "Version Management" description: "Major version 1.0.0 indicates stable API with comprehensive features" impact: "Project ready for production use" key_decisions: - decision: "Separate handler for search tools" rationale: "Maintains single responsibility and modularity" date: "2025-07-25" - decision: "YAML format for Memory Bank" rationale: "Better merge conflict handling and structured data" date: "2025-07-25" current_challenges: - "Bitbucket Cloud search API not yet available" - "Need to handle different search syntaxes across platforms" next_priorities: - "Add Bitbucket Cloud search when API becomes available" - "Enhance search with more filtering options" - "Add support for searching in other entities (commits, PRs)" ``` -------------------------------------------------------------------------------- /src/utils/api-client.ts: -------------------------------------------------------------------------------- ```typescript import axios, { AxiosInstance, AxiosError } from 'axios'; import { BitbucketServerBuildSummary } from '../types/bitbucket.js'; export interface ApiError { status?: number; message: string; isAxiosError: boolean; originalError?: AxiosError; } export class BitbucketApiClient { private axiosInstance: AxiosInstance; private isServer: boolean; constructor( baseURL: string, username: string, password?: string, token?: string ) { this.isServer = !!token; const axiosConfig: any = { baseURL, headers: { 'Content-Type': 'application/json', }, }; // Use token auth for Bitbucket Server, basic auth for Cloud if (token) { // Bitbucket Server uses Bearer token axiosConfig.headers['Authorization'] = `Bearer ${token}`; } else { // Bitbucket Cloud uses basic auth with app password axiosConfig.auth = { username, password, }; } this.axiosInstance = axios.create(axiosConfig); } async makeRequest<T>( method: 'get' | 'post' | 'put' | 'delete', path: string, data?: any, config?: any ): Promise<T> { try { let response; if (method === 'get') { // For GET, config is the second parameter response = await this.axiosInstance[method](path, config || {}); } else if (method === 'delete') { // For DELETE, we might need to pass data in config if (data) { response = await this.axiosInstance[method](path, { ...config, data }); } else { response = await this.axiosInstance[method](path, config || {}); } } else { // For POST and PUT, data is second, config is third response = await this.axiosInstance[method](path, data, config); } return response.data; } catch (error) { if (axios.isAxiosError(error)) { const status = error.response?.status; const message = error.response?.data?.errors?.[0]?.message || error.response?.data?.error?.message || error.response?.data?.message || error.message; throw { status, message, isAxiosError: true, originalError: error } as ApiError; } throw error; } } handleApiError(error: any, context: string) { if (error.isAxiosError) { const { status, message } = error as ApiError; if (status === 404) { return { content: [ { type: 'text', text: `Not found: ${context}`, }, ], isError: true, }; } else if (status === 401) { return { content: [ { type: 'text', text: `Authentication failed. Please check your ${this.isServer ? 'BITBUCKET_TOKEN' : 'BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD'}`, }, ], isError: true, }; } else if (status === 403) { return { content: [ { type: 'text', text: `Permission denied: ${context}. Ensure your credentials have the necessary permissions.`, }, ], isError: true, }; } return { content: [ { type: 'text', text: `Bitbucket API error: ${message}`, }, ], isError: true, }; } throw error; } getIsServer(): boolean { return this.isServer; } async getBuildSummaries( workspace: string, repository: string, commitIds: string[] ): Promise<BitbucketServerBuildSummary> { if (!this.isServer) { // Build summaries only available for Bitbucket Server return {}; } if (commitIds.length === 0) { return {}; } try { // Build query string with multiple commitId parameters const apiPath = `/rest/ui/latest/projects/${workspace}/repos/${repository}/build-summaries`; // Create params with custom serializer for multiple commitId parameters const response = await this.makeRequest<BitbucketServerBuildSummary>( 'get', apiPath, undefined, { params: { commitId: commitIds }, paramsSerializer: (params: any) => { // Custom serializer to create multiple commitId= parameters if (params.commitId && Array.isArray(params.commitId)) { return params.commitId.map((id: string) => `commitId=${encodeURIComponent(id)}`).join('&'); } return ''; } } ); return response; } catch (error) { // If build-summaries endpoint fails, return empty object (graceful degradation) console.error('Failed to fetch build summaries:', error); return {}; } } } ``` -------------------------------------------------------------------------------- /src/utils/diff-parser.ts: -------------------------------------------------------------------------------- ```typescript import { minimatch } from 'minimatch'; export interface DiffSection { filePath: string; oldPath?: string; // For renamed files content: string; isNew: boolean; isDeleted: boolean; isRenamed: boolean; isBinary: boolean; } export interface FilterOptions { includePatterns?: string[]; excludePatterns?: string[]; filePath?: string; } export interface FilteredResult { sections: DiffSection[]; metadata: { totalFiles: number; includedFiles: number; excludedFiles: number; excludedFileList: string[]; }; } export class DiffParser { /** * Parse a unified diff into file sections */ parseDiffIntoSections(diff: string): DiffSection[] { const sections: DiffSection[] = []; // Split by file boundaries - handle both formats const fileChunks = diff.split(/(?=^diff --git)/gm).filter(chunk => chunk.trim()); for (const chunk of fileChunks) { const section = this.parseFileSection(chunk); if (section) { sections.push(section); } } return sections; } /** * Parse a single file section from the diff */ private parseFileSection(chunk: string): DiffSection | null { const lines = chunk.split('\n'); if (lines.length === 0) return null; // Extract file paths from the diff header let filePath = ''; let oldPath: string | undefined; let isNew = false; let isDeleted = false; let isRenamed = false; let isBinary = false; // Look for diff --git line - handle both standard and Bitbucket Server formats const gitDiffMatch = lines[0].match(/^diff --git (?:a\/|src:\/\/)(.+?) (?:b\/|dst:\/\/)(.+?)$/); if (gitDiffMatch) { const [, aPath, bPath] = gitDiffMatch; filePath = bPath; // Check subsequent lines for file status for (let i = 1; i < Math.min(lines.length, 10); i++) { const line = lines[i]; if (line.startsWith('new file mode')) { isNew = true; } else if (line.startsWith('deleted file mode')) { isDeleted = true; filePath = aPath; // Use the original path for deleted files } else if (line.startsWith('rename from')) { isRenamed = true; oldPath = line.replace('rename from ', ''); } else if (line.includes('Binary files') && line.includes('differ')) { isBinary = true; } else if (line.startsWith('--- ')) { // Alternative way to detect new/deleted if (line.includes('/dev/null')) { isNew = true; } } else if (line.startsWith('+++ ')) { if (line.includes('/dev/null')) { isDeleted = true; } // Extract path from +++ line if needed - handle both formats const match = line.match(/^\+\+\+ (?:b\/|dst:\/\/)(.+)$/); if (match && !filePath) { filePath = match[1]; } } } } // Fallback: try to extract from --- and +++ lines if (!filePath) { for (const line of lines) { if (line.startsWith('+++ ')) { const match = line.match(/^\+\+\+ (?:b\/|dst:\/\/)(.+)$/); if (match) { filePath = match[1]; break; } } else if (line.startsWith('--- ')) { const match = line.match(/^--- (?:a\/|src:\/\/)(.+)$/); if (match) { filePath = match[1]; } } } } if (!filePath) return null; return { filePath, oldPath, content: chunk, isNew, isDeleted, isRenamed, isBinary }; } /** * Apply filters to diff sections */ filterSections(sections: DiffSection[], options: FilterOptions): FilteredResult { const excludedFileList: string[] = []; let filteredSections = sections; // If specific file path is requested, only keep that file if (options.filePath) { filteredSections = sections.filter(section => section.filePath === options.filePath || section.oldPath === options.filePath ); // Track excluded files sections.forEach(section => { if (section.filePath !== options.filePath && section.oldPath !== options.filePath) { excludedFileList.push(section.filePath); } }); } else { // Apply exclude patterns first (blacklist) if (options.excludePatterns && options.excludePatterns.length > 0) { filteredSections = filteredSections.filter(section => { const shouldExclude = options.excludePatterns!.some(pattern => minimatch(section.filePath, pattern, { matchBase: true }) ); if (shouldExclude) { excludedFileList.push(section.filePath); return false; } return true; }); } // Apply include patterns if specified (whitelist) if (options.includePatterns && options.includePatterns.length > 0) { filteredSections = filteredSections.filter(section => { const shouldInclude = options.includePatterns!.some(pattern => minimatch(section.filePath, pattern, { matchBase: true }) ); if (!shouldInclude) { excludedFileList.push(section.filePath); return false; } return true; }); } } return { sections: filteredSections, metadata: { totalFiles: sections.length, includedFiles: filteredSections.length, excludedFiles: sections.length - filteredSections.length, excludedFileList } }; } /** * Reconstruct a unified diff from filtered sections */ reconstructDiff(sections: DiffSection[]): string { if (sections.length === 0) { return ''; } // Join all sections with proper spacing return sections.map(section => section.content).join('\n'); } } ``` -------------------------------------------------------------------------------- /src/handlers/search-handlers.ts: -------------------------------------------------------------------------------- ```typescript import { BitbucketApiClient } from '../utils/api-client.js'; import { BitbucketServerSearchRequest, BitbucketServerSearchResult, FormattedSearchResult } from '../types/bitbucket.js'; import { formatSearchResults, formatCodeSearchOutput } from '../utils/formatters.js'; interface SearchContext { assignment: string[]; declaration: string[]; usage: string[]; exact: string[]; any: string[]; } function buildContextualPatterns(searchTerm: string): SearchContext { return { assignment: [ `${searchTerm} =`, // Variable assignment `${searchTerm}:`, // Object property, JSON key `= ${searchTerm}`, // Right-hand assignment ], declaration: [ `${searchTerm} =`, // Variable definition `${searchTerm}:`, // Object key, parameter definition `function ${searchTerm}`, // Function declaration `class ${searchTerm}`, // Class declaration `interface ${searchTerm}`, // Interface declaration `const ${searchTerm}`, // Const declaration `let ${searchTerm}`, // Let declaration `var ${searchTerm}`, // Var declaration ], usage: [ `.${searchTerm}`, // Property/method access `${searchTerm}(`, // Function call `${searchTerm}.`, // Method chaining `${searchTerm}[`, // Array/object access `(${searchTerm}`, // Parameter usage ], exact: [ `"${searchTerm}"`, // Exact quoted match ], any: [ `"${searchTerm}"`, // Exact match `${searchTerm} =`, // Assignment `${searchTerm}:`, // Object property `.${searchTerm}`, // Property access `${searchTerm}(`, // Function call `function ${searchTerm}`, // Function definition `class ${searchTerm}`, // Class definition ] }; } function buildSmartQuery( searchTerm: string, searchContext: string = 'any', includePatterns: string[] = [] ): string { const contextPatterns = buildContextualPatterns(searchTerm); let patterns: string[] = []; // Add patterns based on context if (searchContext in contextPatterns) { patterns = [...contextPatterns[searchContext as keyof SearchContext]]; } else { patterns = [...contextPatterns.any]; } // Add user-provided patterns if (includePatterns && includePatterns.length > 0) { patterns = [...patterns, ...includePatterns]; } // Remove duplicates and join with OR const uniquePatterns = [...new Set(patterns)]; // If only one pattern, return it without parentheses if (uniquePatterns.length === 1) { return uniquePatterns[0]; } // Wrap each pattern in quotes for safety and join with OR const quotedPatterns = uniquePatterns.map(pattern => `"${pattern}"`); return `(${quotedPatterns.join(' OR ')})`; } export class SearchHandlers { constructor( private apiClient: BitbucketApiClient, private baseUrl: string ) {} async handleSearchCode(args: any) { try { const { workspace, repository, search_query, search_context = 'any', file_pattern, include_patterns = [], limit = 25, start = 0 } = args; if (!workspace || !search_query) { throw new Error('Workspace and search_query are required'); } // Only works for Bitbucket Server currently if (!this.apiClient.getIsServer()) { throw new Error('Code search is currently only supported for Bitbucket Server'); } // Build the enhanced query string let query = `project:${workspace}`; if (repository) { query += ` repo:${repository}`; } if (file_pattern) { query += ` path:${file_pattern}`; } // Build smart search patterns const smartQuery = buildSmartQuery(search_query, search_context, include_patterns); query += ` ${smartQuery}`; // Prepare the request payload const payload: BitbucketServerSearchRequest = { query: query.trim(), entities: { code: { start: start, limit: limit } } }; // Make the API request (no query params needed, pagination is in payload) const response = await this.apiClient.makeRequest<BitbucketServerSearchResult>( 'post', `/rest/search/latest/search?avatarSize=64`, payload ); const searchResult = response; // Use simplified formatter for cleaner output const simplifiedOutput = formatCodeSearchOutput(searchResult); // Prepare pagination info const hasMore = searchResult.code?.isLastPage === false; const nextStart = hasMore ? (searchResult.code?.nextStart || start + limit) : undefined; const totalCount = searchResult.code?.count || 0; // Build a concise response with search context info let resultText = `Code search results for "${search_query}"`; if (search_context !== 'any') { resultText += ` (context: ${search_context})`; } resultText += ` in ${workspace}`; if (repository) { resultText += `/${repository}`; } // Show the actual search query used resultText += `\n\nSearch query: ${query.trim()}`; resultText += `\n\n${simplifiedOutput}`; if (totalCount > 0) { resultText += `\n\nTotal matches: ${totalCount}`; if (hasMore) { resultText += ` (showing ${start + 1}-${start + (searchResult.code?.values?.length || 0)})`; } } return { content: [{ type: 'text', text: resultText }] }; } catch (error: any) { const errorMessage = error.response?.data?.errors?.[0]?.message || error.message; return { content: [{ type: 'text', text: JSON.stringify({ error: `Failed to search code: ${errorMessage}`, details: error.response?.data }, null, 2) }], isError: true }; } } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { BitbucketApiClient } from './utils/api-client.js'; import { PullRequestHandlers } from './handlers/pull-request-handlers.js'; import { BranchHandlers } from './handlers/branch-handlers.js'; import { ReviewHandlers } from './handlers/review-handlers.js'; import { FileHandlers } from './handlers/file-handlers.js'; import { SearchHandlers } from './handlers/search-handlers.js'; import { ProjectHandlers } from './handlers/project-handlers.js'; import { toolDefinitions } from './tools/definitions.js'; // Get environment variables const BITBUCKET_USERNAME = process.env.BITBUCKET_USERNAME; const BITBUCKET_APP_PASSWORD = process.env.BITBUCKET_APP_PASSWORD; const BITBUCKET_TOKEN = process.env.BITBUCKET_TOKEN; // For Bitbucket Server const BITBUCKET_BASE_URL = process.env.BITBUCKET_BASE_URL || 'https://api.bitbucket.org/2.0'; // Check for either app password (Cloud) or token (Server) if (!BITBUCKET_USERNAME || (!BITBUCKET_APP_PASSWORD && !BITBUCKET_TOKEN)) { console.error('Error: BITBUCKET_USERNAME and either BITBUCKET_APP_PASSWORD (for Cloud) or BITBUCKET_TOKEN (for Server) are required'); console.error('Please set these in your MCP settings configuration'); process.exit(1); } class BitbucketMCPServer { private server: Server; private apiClient: BitbucketApiClient; private pullRequestHandlers: PullRequestHandlers; private branchHandlers: BranchHandlers; private reviewHandlers: ReviewHandlers; private fileHandlers: FileHandlers; private searchHandlers: SearchHandlers; private projectHandlers: ProjectHandlers; constructor() { this.server = new Server( { name: 'bitbucket-mcp-server', version: '1.1.2', }, { capabilities: { tools: {}, }, } ); // Initialize API client this.apiClient = new BitbucketApiClient( BITBUCKET_BASE_URL, BITBUCKET_USERNAME!, BITBUCKET_APP_PASSWORD, BITBUCKET_TOKEN ); // Initialize handlers this.pullRequestHandlers = new PullRequestHandlers( this.apiClient, BITBUCKET_BASE_URL, BITBUCKET_USERNAME! ); this.branchHandlers = new BranchHandlers(this.apiClient, BITBUCKET_BASE_URL); this.reviewHandlers = new ReviewHandlers(this.apiClient, BITBUCKET_USERNAME!); this.fileHandlers = new FileHandlers(this.apiClient, BITBUCKET_BASE_URL); this.searchHandlers = new SearchHandlers(this.apiClient, BITBUCKET_BASE_URL); this.projectHandlers = new ProjectHandlers(this.apiClient, BITBUCKET_BASE_URL); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions, })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { // Pull Request tools case 'get_pull_request': return this.pullRequestHandlers.handleGetPullRequest(request.params.arguments); case 'list_pull_requests': return this.pullRequestHandlers.handleListPullRequests(request.params.arguments); case 'create_pull_request': return this.pullRequestHandlers.handleCreatePullRequest(request.params.arguments); case 'update_pull_request': return this.pullRequestHandlers.handleUpdatePullRequest(request.params.arguments); case 'add_comment': return this.pullRequestHandlers.handleAddComment(request.params.arguments); case 'merge_pull_request': return this.pullRequestHandlers.handleMergePullRequest(request.params.arguments); case 'list_pr_commits': return this.pullRequestHandlers.handleListPrCommits(request.params.arguments); // Branch tools case 'list_branches': return this.branchHandlers.handleListBranches(request.params.arguments); case 'delete_branch': return this.branchHandlers.handleDeleteBranch(request.params.arguments); case 'get_branch': return this.branchHandlers.handleGetBranch(request.params.arguments); case 'list_branch_commits': return this.branchHandlers.handleListBranchCommits(request.params.arguments); // Code Review tools case 'get_pull_request_diff': return this.reviewHandlers.handleGetPullRequestDiff(request.params.arguments); case 'approve_pull_request': return this.reviewHandlers.handleApprovePullRequest(request.params.arguments); case 'unapprove_pull_request': return this.reviewHandlers.handleUnapprovePullRequest(request.params.arguments); case 'request_changes': return this.reviewHandlers.handleRequestChanges(request.params.arguments); case 'remove_requested_changes': return this.reviewHandlers.handleRemoveRequestedChanges(request.params.arguments); // File tools case 'list_directory_content': return this.fileHandlers.handleListDirectoryContent(request.params.arguments); case 'get_file_content': return this.fileHandlers.handleGetFileContent(request.params.arguments); // Search tools case 'search_code': return this.searchHandlers.handleSearchCode(request.params.arguments); // Project tools case 'list_projects': return this.projectHandlers.handleListProjects(request.params.arguments); case 'list_repositories': return this.projectHandlers.handleListRepositories(request.params.arguments); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error(`Bitbucket MCP server running on stdio (${this.apiClient.getIsServer() ? 'Server' : 'Cloud'} mode)`); } } const server = new BitbucketMCPServer(); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /src/handlers/project-handlers.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { BitbucketApiClient } from '../utils/api-client.js'; import { isListProjectsArgs, isListRepositoriesArgs } from '../types/guards.js'; import { BitbucketServerProject, BitbucketCloudProject, BitbucketServerRepository, BitbucketCloudRepository } from '../types/bitbucket.js'; export class ProjectHandlers { constructor( private apiClient: BitbucketApiClient, private baseUrl: string ) {} async handleListProjects(args: any) { if (!isListProjectsArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for list_projects' ); } const { name, permission, limit = 25, start = 0 } = args; try { let apiPath: string; let params: any = {}; let projects: any[] = []; let totalCount = 0; let nextPageStart: number | null = null; if (this.apiClient.getIsServer()) { // Bitbucket Server API apiPath = `/rest/api/1.0/projects`; params = { limit, start }; if (name) { params.name = name; } if (permission) { params.permission = permission; } const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); // Format projects projects = (response.values || []).map((project: BitbucketServerProject) => ({ key: project.key, id: project.id, name: project.name, description: project.description || '', is_public: project.public, type: project.type, url: `${this.baseUrl}/projects/${project.key}` })); totalCount = response.size || projects.length; if (!response.isLastPage && response.nextPageStart !== undefined) { nextPageStart = response.nextPageStart; } } else { // Bitbucket Cloud API apiPath = `/workspaces`; params = { pagelen: limit, page: Math.floor(start / limit) + 1 }; // Cloud uses workspaces, not projects exactly const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); projects = (response.values || []).map((workspace: any) => ({ key: workspace.slug, id: workspace.uuid, name: workspace.name, description: '', is_public: !workspace.is_private, type: 'WORKSPACE', url: workspace.links.html.href })); totalCount = response.size || projects.length; if (response.next) { nextPageStart = start + limit; } } return { content: [ { type: 'text', text: JSON.stringify({ projects, total_count: totalCount, start, limit, has_more: nextPageStart !== null, next_start: nextPageStart }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, 'listing projects'); } } async handleListRepositories(args: any) { if (!isListRepositoriesArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for list_repositories' ); } const { workspace, name, permission, limit = 25, start = 0 } = args; try { let apiPath: string; let params: any = {}; let repositories: any[] = []; let totalCount = 0; let nextPageStart: number | null = null; if (this.apiClient.getIsServer()) { // Bitbucket Server API if (workspace) { // List repos in a specific project apiPath = `/rest/api/1.0/projects/${workspace}/repos`; } else { // List all accessible repos apiPath = `/rest/api/1.0/repos`; } params = { limit, start }; if (name) { params.name = name; } if (permission) { params.permission = permission; } if (!workspace && name) { // When listing all repos and filtering by name params.projectname = name; } const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); // Format repositories repositories = (response.values || []).map((repo: BitbucketServerRepository) => ({ slug: repo.slug, id: repo.id, name: repo.name, description: repo.description || '', project_key: repo.project.key, project_name: repo.project.name, state: repo.state, is_public: repo.public, is_forkable: repo.forkable, clone_urls: { http: repo.links.clone.find(c => c.name === 'http')?.href || '', ssh: repo.links.clone.find(c => c.name === 'ssh')?.href || '' }, url: `${this.baseUrl}/projects/${repo.project.key}/repos/${repo.slug}` })); totalCount = response.size || repositories.length; if (!response.isLastPage && response.nextPageStart !== undefined) { nextPageStart = response.nextPageStart; } } else { // Bitbucket Cloud API if (workspace) { // List repos in a specific workspace apiPath = `/repositories/${workspace}`; } else { // Cloud doesn't support listing all repos without workspace // We'll return an error message return { content: [ { type: 'text', text: JSON.stringify({ error: 'Bitbucket Cloud requires a workspace parameter to list repositories. Please provide a workspace.' }, null, 2), }, ], isError: true, }; } params = { pagelen: limit, page: Math.floor(start / limit) + 1 }; const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); repositories = (response.values || []).map((repo: BitbucketCloudRepository) => ({ slug: repo.slug, id: repo.uuid, name: repo.name, description: repo.description || '', project_key: repo.project?.key || '', project_name: repo.project?.name || '', state: 'AVAILABLE', is_public: !repo.is_private, is_forkable: true, clone_urls: { http: repo.links.clone.find(c => c.name === 'https')?.href || '', ssh: repo.links.clone.find(c => c.name === 'ssh')?.href || '' }, url: repo.links.html.href })); totalCount = response.size || repositories.length; if (response.next) { nextPageStart = start + limit; } } return { content: [ { type: 'text', text: JSON.stringify({ repositories, total_count: totalCount, start, limit, has_more: nextPageStart !== null, next_start: nextPageStart, workspace: workspace || 'all' }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, workspace ? `listing repositories in ${workspace}` : 'listing repositories'); } } } ``` -------------------------------------------------------------------------------- /src/utils/formatters.ts: -------------------------------------------------------------------------------- ```typescript import { BitbucketServerPullRequest, BitbucketCloudPullRequest, MergeInfo, BitbucketServerCommit, BitbucketCloudCommit, FormattedCommit, BitbucketServerSearchResult, FormattedSearchResult } from '../types/bitbucket.js'; export function formatServerResponse( pr: BitbucketServerPullRequest, mergeInfo?: MergeInfo, baseUrl?: string ): any { const webUrl = `${baseUrl}/projects/${pr.toRef.repository.project.key}/repos/${pr.toRef.repository.slug}/pull-requests/${pr.id}`; return { id: pr.id, title: pr.title, description: pr.description || 'No description provided', state: pr.state, is_open: pr.open, is_closed: pr.closed, author: pr.author.user.displayName, author_username: pr.author.user.name, author_email: pr.author.user.emailAddress, source_branch: pr.fromRef.displayId, destination_branch: pr.toRef.displayId, source_commit: pr.fromRef.latestCommit, destination_commit: pr.toRef.latestCommit, reviewers: pr.reviewers.map(r => ({ name: r.user.displayName, approved: r.approved, status: r.status, })), participants: pr.participants.map(p => ({ name: p.user.displayName, role: p.role, approved: p.approved, status: p.status, })), created_on: new Date(pr.createdDate).toLocaleString(), updated_on: new Date(pr.updatedDate).toLocaleString(), web_url: webUrl, api_url: pr.links.self[0]?.href || '', is_locked: pr.locked, // Add merge commit details is_merged: pr.state === 'MERGED', merge_commit_hash: mergeInfo?.mergeCommitHash || pr.properties?.mergeCommit?.id || null, merged_by: mergeInfo?.mergedBy || null, merged_at: mergeInfo?.mergedAt || null, merge_commit_message: mergeInfo?.mergeCommitMessage || null, }; } export function formatCloudResponse(pr: BitbucketCloudPullRequest): any { return { id: pr.id, title: pr.title, description: pr.description || 'No description provided', state: pr.state, author: pr.author.display_name, source_branch: pr.source.branch.name, destination_branch: pr.destination.branch.name, reviewers: pr.reviewers.map(r => r.display_name), participants: pr.participants.map(p => ({ name: p.user.display_name, role: p.role, approved: p.approved, })), created_on: new Date(pr.created_on).toLocaleString(), updated_on: new Date(pr.updated_on).toLocaleString(), web_url: pr.links.html.href, api_url: pr.links.self.href, diff_url: pr.links.diff.href, is_merged: pr.state === 'MERGED', merge_commit_hash: pr.merge_commit?.hash || null, merged_by: pr.closed_by?.display_name || null, merged_at: pr.state === 'MERGED' ? pr.updated_on : null, merge_commit_message: null, // Would need additional API call to get this close_source_branch: pr.close_source_branch, }; } export function formatServerCommit(commit: BitbucketServerCommit): FormattedCommit { return { hash: commit.id, abbreviated_hash: commit.displayId, message: commit.message, author: { name: commit.author.name, email: commit.author.emailAddress, }, date: new Date(commit.authorTimestamp).toISOString(), parents: commit.parents.map(p => p.id), is_merge_commit: commit.parents.length > 1, }; } export function formatCloudCommit(commit: BitbucketCloudCommit): FormattedCommit { // Parse the author raw string which is in format "Name <email>" const authorMatch = commit.author.raw.match(/^(.+?)\s*<(.+?)>$/); const authorName = authorMatch ? authorMatch[1] : (commit.author.user?.display_name || commit.author.raw); const authorEmail = authorMatch ? authorMatch[2] : ''; return { hash: commit.hash, abbreviated_hash: commit.hash.substring(0, 7), message: commit.message, author: { name: authorName, email: authorEmail, }, date: commit.date, parents: commit.parents.map(p => p.hash), is_merge_commit: commit.parents.length > 1, }; } export function formatSearchResults(searchResult: BitbucketServerSearchResult): FormattedSearchResult[] { const results: FormattedSearchResult[] = []; if (!searchResult.code?.values) { return results; } for (const value of searchResult.code.values) { // Extract file name from path const fileName = value.file.split('/').pop() || value.file; const formattedResult: FormattedSearchResult = { file_path: value.file, file_name: fileName, repository: value.repository.slug, project: value.repository.project.key, matches: [] }; // Process hitContexts (array of arrays of line contexts) if (value.hitContexts && value.hitContexts.length > 0) { for (const contextGroup of value.hitContexts) { for (const lineContext of contextGroup) { // Parse HTML to extract text and highlight information const { text, segments } = parseHighlightedText(lineContext.text); formattedResult.matches.push({ line_number: lineContext.line, line_content: text, highlighted_segments: segments }); } } } results.push(formattedResult); } return results; } // Helper function to parse HTML-formatted text with <em> tags function parseHighlightedText(htmlText: string): { text: string; segments: Array<{ text: string; is_match: boolean }>; } { // Decode HTML entities const decodedText = htmlText .replace(/"/g, '"') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(///g, '/'); // Remove HTML tags and track highlighted segments const segments: Array<{ text: string; is_match: boolean }> = []; let plainText = ''; let currentPos = 0; // Match all <em> tags and their content const emRegex = /<em>(.*?)<\/em>/g; let lastEnd = 0; let match; while ((match = emRegex.exec(decodedText)) !== null) { // Add non-highlighted text before this match if (match.index > lastEnd) { const beforeText = decodedText.substring(lastEnd, match.index); segments.push({ text: beforeText, is_match: false }); plainText += beforeText; } // Add highlighted text const highlightedText = match[1]; segments.push({ text: highlightedText, is_match: true }); plainText += highlightedText; lastEnd = match.index + match[0].length; } // Add any remaining non-highlighted text if (lastEnd < decodedText.length) { const remainingText = decodedText.substring(lastEnd); segments.push({ text: remainingText, is_match: false }); plainText += remainingText; } // If no <em> tags were found, the entire text is non-highlighted if (segments.length === 0) { segments.push({ text: decodedText, is_match: false }); plainText = decodedText; } return { text: plainText, segments }; } // Simplified formatter for MCP tool output export function formatCodeSearchOutput(searchResult: BitbucketServerSearchResult): string { if (!searchResult.code?.values || searchResult.code.values.length === 0) { return 'No results found'; } const outputLines: string[] = []; for (const value of searchResult.code.values) { outputLines.push(`File: ${value.file}`); // Process all hit contexts if (value.hitContexts && value.hitContexts.length > 0) { for (const contextGroup of value.hitContexts) { for (const lineContext of contextGroup) { // Remove HTML tags and decode entities const cleanText = lineContext.text .replace(/<em>/g, '') .replace(/<\/em>/g, '') .replace(/"/g, '"') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(///g, '/') .replace(/'/g, "'"); outputLines.push(` Line ${lineContext.line}: ${cleanText}`); } } } outputLines.push(''); // Empty line between files } return outputLines.join('\n').trim(); } ``` -------------------------------------------------------------------------------- /src/handlers/review-handlers.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { BitbucketApiClient } from '../utils/api-client.js'; import { isGetPullRequestDiffArgs, isApprovePullRequestArgs, isRequestChangesArgs } from '../types/guards.js'; import { DiffParser } from '../utils/diff-parser.js'; export class ReviewHandlers { constructor( private apiClient: BitbucketApiClient, private username: string ) {} async handleGetPullRequestDiff(args: any) { if (!isGetPullRequestDiffArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for get_pull_request_diff' ); } const { workspace, repository, pull_request_id, context_lines = 3, include_patterns, exclude_patterns, file_path } = args; try { let apiPath: string; let config: any = {}; if (this.apiClient.getIsServer()) { // Bitbucket Server API apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/diff`; config.params = { contextLines: context_lines }; } else { // Bitbucket Cloud API apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/diff`; config.params = { context: context_lines }; } // For diff, we want the raw text response config.headers = { 'Accept': 'text/plain' }; const rawDiff = await this.apiClient.makeRequest<string>('get', apiPath, undefined, config); // Check if filtering is needed const needsFiltering = file_path || include_patterns || exclude_patterns; if (!needsFiltering) { // Return raw diff without filtering return { content: [ { type: 'text', text: JSON.stringify({ message: 'Pull request diff retrieved successfully', pull_request_id, diff: rawDiff }, null, 2), }, ], }; } // Apply filtering const diffParser = new DiffParser(); const sections = diffParser.parseDiffIntoSections(rawDiff); const filterOptions = { includePatterns: include_patterns, excludePatterns: exclude_patterns, filePath: file_path }; const filteredResult = diffParser.filterSections(sections, filterOptions); const filteredDiff = diffParser.reconstructDiff(filteredResult.sections); // Build response with filtering metadata const response: any = { message: 'Pull request diff retrieved successfully', pull_request_id, diff: filteredDiff }; // Add filter metadata if (filteredResult.metadata.excludedFiles > 0 || file_path || include_patterns || exclude_patterns) { response.filter_metadata = { total_files: filteredResult.metadata.totalFiles, included_files: filteredResult.metadata.includedFiles, excluded_files: filteredResult.metadata.excludedFiles }; if (filteredResult.metadata.excludedFileList.length > 0) { response.filter_metadata.excluded_file_list = filteredResult.metadata.excludedFileList; } response.filter_metadata.filters_applied = {}; if (file_path) { response.filter_metadata.filters_applied.file_path = file_path; } if (include_patterns) { response.filter_metadata.filters_applied.include_patterns = include_patterns; } if (exclude_patterns) { response.filter_metadata.filters_applied.exclude_patterns = exclude_patterns; } } return { content: [ { type: 'text', text: JSON.stringify(response, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, `getting diff for pull request ${pull_request_id} in ${workspace}/${repository}`); } } async handleApprovePullRequest(args: any) { if (!isApprovePullRequestArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for approve_pull_request' ); } const { workspace, repository, pull_request_id } = args; try { let apiPath: string; if (this.apiClient.getIsServer()) { // Bitbucket Server API - use participants endpoint // Convert email format: @ to _ for the API const username = this.username.replace('@', '_'); apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`; await this.apiClient.makeRequest<any>('put', apiPath, { status: 'APPROVED' }); } else { // Bitbucket Cloud API apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/approve`; await this.apiClient.makeRequest<any>('post', apiPath); } return { content: [ { type: 'text', text: JSON.stringify({ message: 'Pull request approved successfully', pull_request_id, approved_by: this.username }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, `approving pull request ${pull_request_id} in ${workspace}/${repository}`); } } async handleUnapprovePullRequest(args: any) { if (!isApprovePullRequestArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for unapprove_pull_request' ); } const { workspace, repository, pull_request_id } = args; try { let apiPath: string; if (this.apiClient.getIsServer()) { // Bitbucket Server API - use participants endpoint const username = this.username.replace('@', '_'); apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`; await this.apiClient.makeRequest<any>('put', apiPath, { status: 'UNAPPROVED' }); } else { // Bitbucket Cloud API apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/approve`; await this.apiClient.makeRequest<any>('delete', apiPath); } return { content: [ { type: 'text', text: JSON.stringify({ message: 'Pull request approval removed successfully', pull_request_id, unapproved_by: this.username }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, `removing approval from pull request ${pull_request_id} in ${workspace}/${repository}`); } } async handleRequestChanges(args: any) { if (!isRequestChangesArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for request_changes' ); } const { workspace, repository, pull_request_id, comment } = args; try { if (this.apiClient.getIsServer()) { // Bitbucket Server API - use needs-work status const username = this.username.replace('@', '_'); const apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`; await this.apiClient.makeRequest<any>('put', apiPath, { status: 'NEEDS_WORK' }); // Add comment if provided if (comment) { const commentPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/comments`; await this.apiClient.makeRequest<any>('post', commentPath, { text: comment }); } } else { // Bitbucket Cloud API - use request-changes status const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/request-changes`; await this.apiClient.makeRequest<any>('post', apiPath); // Add comment if provided if (comment) { const commentPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/comments`; await this.apiClient.makeRequest<any>('post', commentPath, { content: { raw: comment } }); } } return { content: [ { type: 'text', text: JSON.stringify({ message: 'Changes requested on pull request', pull_request_id, requested_by: this.username, comment: comment || 'No comment provided' }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, `requesting changes on pull request ${pull_request_id} in ${workspace}/${repository}`); } } async handleRemoveRequestedChanges(args: any) { if (!isApprovePullRequestArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for remove_requested_changes' ); } const { workspace, repository, pull_request_id } = args; try { if (this.apiClient.getIsServer()) { // Bitbucket Server API - remove needs-work status const username = this.username.replace('@', '_'); const apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/participants/${username}`; await this.apiClient.makeRequest<any>('put', apiPath, { status: 'UNAPPROVED' }); } else { // Bitbucket Cloud API const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/request-changes`; await this.apiClient.makeRequest<any>('delete', apiPath); } return { content: [ { type: 'text', text: JSON.stringify({ message: 'Change request removed from pull request', pull_request_id, removed_by: this.username }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, `removing change request from pull request ${pull_request_id} in ${workspace}/${repository}`); } } } ``` -------------------------------------------------------------------------------- /src/types/bitbucket.ts: -------------------------------------------------------------------------------- ```typescript // Bitbucket Server API response types export interface BitbucketServerPullRequest { id: number; version: number; title: string; description?: string; state: string; open: boolean; closed: boolean; createdDate: number; updatedDate: number; fromRef: { id: string; displayId: string; latestCommit: string; repository: { slug: string; name: string; project: { key: string; }; }; }; toRef: { id: string; displayId: string; latestCommit: string; repository: { slug: string; name: string; project: { key: string; }; }; }; locked: boolean; author: { user: { name: string; emailAddress: string; displayName: string; }; role: string; approved: boolean; status: string; }; reviewers: Array<{ user: { name: string; emailAddress: string; displayName: string; }; role: string; approved: boolean; status: string; }>; participants: Array<{ user: { name: string; emailAddress: string; displayName: string; }; role: string; approved: boolean; status: string; }>; links: { self: Array<{ href: string; }>; }; properties?: { mergeCommit?: { id: string; displayId: string; }; }; } // Bitbucket Server Activity types export interface BitbucketServerActivity { id: number; createdDate: number; user: { name: string; emailAddress: string; displayName: string; }; action: string; comment?: any; commit?: { id: string; displayId: string; message?: string; }; } // Bitbucket Server Branch types export interface BitbucketServerBranch { id: string; displayId: string; type: string; latestCommit: string; latestChangeset: string; isDefault: boolean; metadata?: { "com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata": { author: { name: string; emailAddress: string; }; authorTimestamp: number; message: string; }; }; } // Bitbucket Server Directory Entry export interface BitbucketServerDirectoryEntry { path: { name: string; toString: string; }; type: 'FILE' | 'DIRECTORY'; size?: number; contentId?: string; } // Bitbucket Cloud API response types export interface BitbucketCloudPullRequest { id: number; title: string; description: string; state: string; author: { display_name: string; account_id: string; }; source: { branch: { name: string; }; repository: { full_name: string; }; }; destination: { branch: { name: string; }; repository: { full_name: string; }; }; reviewers: Array<{ display_name: string; account_id: string; }>; participants: Array<{ user: { display_name: string; account_id: string; }; role: string; approved: boolean; }>; created_on: string; updated_on: string; links: { html: { href: string; }; self: { href: string; }; diff: { href: string; }; }; merge_commit?: { hash: string; }; close_source_branch: boolean; closed_by?: { display_name: string; account_id: string; }; } // Bitbucket Cloud Branch types export interface BitbucketCloudBranch { name: string; target: { hash: string; type: string; message: string; author: { raw: string; user?: { display_name: string; account_id: string; }; }; date: string; }; type: string; } // Bitbucket Cloud Directory Entry export interface BitbucketCloudDirectoryEntry { path: string; type: 'commit_file' | 'commit_directory'; size?: number; commit?: { hash: string; }; links?: { self: { href: string; }; html: { href: string; }; }; } // Bitbucket Cloud File Metadata export interface BitbucketCloudFileMetadata { path: string; size: number; encoding?: string; mimetype?: string; links: { self: { href: string; }; html: { href: string; }; download: { href: string; }; }; commit?: { hash: string; author?: { raw: string; user?: { display_name: string; account_id: string; }; }; date?: string; message?: string; }; } // Merge info type for enhanced PR details export interface MergeInfo { mergeCommitHash?: string; mergedBy?: string; mergedAt?: string; mergeCommitMessage?: string; } // Comment types export interface BitbucketServerComment { id: number; version: number; text: string; author: { name: string; emailAddress: string; displayName: string; }; createdDate: number; updatedDate: number; state?: 'OPEN' | 'RESOLVED'; anchor?: { line: number; lineType: string; fileType: string; path: string; }; } export interface BitbucketCloudComment { id: number; content: { raw: string; markup: string; html: string; }; user: { display_name: string; account_id: string; }; created_on: string; updated_on: string; deleted?: boolean; resolved?: boolean; inline?: { to: number; from?: number; path: string; }; } // File change types export interface BitbucketServerFileChange { path: { toString: string; }; executable: boolean; percentUnchanged: number; type: string; nodeType: string; srcPath?: { toString: string; }; linesAdded?: number; linesRemoved?: number; } export interface BitbucketCloudFileChange { path: string; type: 'added' | 'modified' | 'removed' | 'renamed'; lines_added: number; lines_removed: number; old?: { path: string; }; } // Formatted comment type for response export interface FormattedComment { id: number; author: string; text: string; created_on: string; is_inline: boolean; file_path?: string; line_number?: number; state?: 'OPEN' | 'RESOLVED'; parent_id?: number; // For Bitbucket Cloud style replies replies?: FormattedComment[]; // For Bitbucket Server nested replies } // Formatted file change type for response export interface FormattedFileChange { path: string; status: 'added' | 'modified' | 'removed' | 'renamed'; old_path?: string; } // Types for code snippet matching export interface CodeMatch { line_number: number; line_type: 'ADDED' | 'REMOVED' | 'CONTEXT'; exact_content: string; preview: string; confidence: number; context: { lines_before: string[]; lines_after: string[]; }; sequential_position?: number; // Position within diff (for ADDED lines) hunk_info?: { hunk_index: number; destination_start: number; line_in_hunk: number; }; } export interface MultipleMatchesError { code: 'MULTIPLE_MATCHES_FOUND'; message: string; occurrences: Array<{ line_number: number; file_path: string; preview: string; confidence: number; line_type: 'ADDED' | 'REMOVED' | 'CONTEXT'; }>; suggestion: string; } // Commit types export interface BitbucketServerCommit { id: string; displayId: string; message: string; author: { name: string; emailAddress: string; }; authorTimestamp: number; committer?: { name: string; emailAddress: string; }; committerTimestamp?: number; parents: Array<{ id: string; displayId: string; }>; } export interface BitbucketCloudCommit { hash: string; message: string; author: { raw: string; user?: { display_name: string; account_id: string; }; }; date: string; parents: Array<{ hash: string; type: string; }>; links?: { self: { href: string; }; html: { href: string; }; }; } export interface FormattedCommit { hash: string; abbreviated_hash: string; message: string; author: { name: string; email: string; }; date: string; parents: string[]; is_merge_commit: boolean; build_status?: BuildStatus; } // Search types export interface BitbucketServerSearchRequest { query: string; entities: { code?: { start?: number; limit?: number; }; commits?: { start?: number; limit?: number; }; pull_requests?: { start?: number; limit?: number; }; }; } export interface BitbucketServerSearchResult { scope?: { repository?: { slug: string; name: string; project: { key: string; name: string; }; }; type: string; }; code?: { category: string; isLastPage: boolean; count: number; start: number; nextStart?: number; values: Array<{ file: string; // Just the file path as string repository: { slug: string; name: string; project: { key: string; name: string; }; }; hitContexts: Array<Array<{ line: number; text: string; // HTML-formatted with <em> tags }>>; pathMatches: Array<any>; hitCount: number; }>; }; query?: { substituted: boolean; }; } export interface FormattedSearchResult { file_path: string; file_name: string; repository: string; project: string; matches: Array<{ line_number: number; line_content: string; highlighted_segments: Array<{ text: string; is_match: boolean; }>; }>; } // Build status types for Bitbucket Server export interface BitbucketServerBuildSummary { [commitId: string]: { failed?: number; inProgress?: number; successful?: number; unknown?: number; }; } export interface BuildStatus { successful: number; failed: number; in_progress: number; unknown: number; } // Project and Repository types export interface BitbucketServerProject { key: string; id: number; name: string; description?: string; public: boolean; type: 'NORMAL' | 'PERSONAL'; links: { self: Array<{ href: string; }>; }; } export interface BitbucketCloudProject { key: string; uuid: string; name: string; description?: string; is_private: boolean; links: { html: { href: string; }; }; } export interface BitbucketServerRepository { slug: string; id: number; name: string; description?: string; hierarchyId: string; scmId: string; state: 'AVAILABLE' | 'INITIALISING' | 'INITIALISATION_FAILED'; statusMessage: string; forkable: boolean; project: { key: string; id: number; name: string; public: boolean; type: string; }; public: boolean; links: { clone: Array<{ href: string; name: string; }>; self: Array<{ href: string; }>; }; } export interface BitbucketCloudRepository { slug: string; uuid: string; name: string; full_name: string; description?: string; scm: string; is_private: boolean; owner: { display_name: string; uuid: string; }; project: { key: string; name: string; }; mainbranch?: { name: string; type: string; }; links: { html: { href: string; }; clone: Array<{ href: string; name: string; }>; }; } ``` -------------------------------------------------------------------------------- /src/handlers/file-handlers.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { BitbucketApiClient } from '../utils/api-client.js'; import { isListDirectoryContentArgs, isGetFileContentArgs } from '../types/guards.js'; import { BitbucketServerDirectoryEntry, BitbucketCloudDirectoryEntry, BitbucketCloudFileMetadata } from '../types/bitbucket.js'; import * as path from 'path'; export class FileHandlers { // Default lines by file extension private readonly DEFAULT_LINES_BY_EXT: Record<string, number> = { '.yml': 200, '.yaml': 200, '.json': 200, // Config files '.md': 300, '.txt': 300, // Docs '.ts': 500, '.js': 500, '.py': 500, // Code '.tsx': 500, '.jsx': 500, '.java': 500, // More code '.log': -100 // Last 100 lines for logs }; constructor( private apiClient: BitbucketApiClient, private baseUrl: string ) {} async handleListDirectoryContent(args: any) { if (!isListDirectoryContentArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for list_directory_content' ); } const { workspace, repository, path: dirPath = '', branch } = args; try { let apiPath: string; let params: any = {}; let response: any; if (this.apiClient.getIsServer()) { // Bitbucket Server API apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/browse`; if (dirPath) { apiPath += `/${dirPath}`; } if (branch) { params.at = `refs/heads/${branch}`; } response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); } else { // Bitbucket Cloud API const branchOrDefault = branch || 'HEAD'; apiPath = `/repositories/${workspace}/${repository}/src/${branchOrDefault}`; if (dirPath) { apiPath += `/${dirPath}`; } response = await this.apiClient.makeRequest<any>('get', apiPath); } // Format the response let contents: any[] = []; let actualBranch = branch; if (this.apiClient.getIsServer()) { // Bitbucket Server response const entries = response.children?.values || []; contents = entries.map((entry: BitbucketServerDirectoryEntry) => ({ name: entry.path.name, type: entry.type === 'FILE' ? 'file' : 'directory', size: entry.size, path: dirPath ? `${dirPath}/${entry.path.name}` : entry.path.name })); // Get the actual branch from the response if available if (!branch && response.path?.components) { // Server returns default branch info in the response actualBranch = 'default'; } } else { // Bitbucket Cloud response const entries = response.values || []; contents = entries.map((entry: BitbucketCloudDirectoryEntry) => ({ name: entry.path.split('/').pop() || entry.path, type: entry.type === 'commit_file' ? 'file' : 'directory', size: entry.size, path: entry.path })); // Cloud returns the branch in the response actualBranch = branch || response.commit?.branch || 'main'; } return { content: [ { type: 'text', text: JSON.stringify({ path: dirPath || '/', branch: actualBranch, contents, total_items: contents.length }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, `listing directory '${dirPath}' in ${workspace}/${repository}`); } } async handleGetFileContent(args: any) { if (!isGetFileContentArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for get_file_content' ); } const { workspace, repository, file_path, branch, start_line, line_count, full_content = false } = args; try { let fileContent: string; let fileMetadata: any = {}; const fileSizeLimit = 1024 * 1024; // 1MB default limit if (this.apiClient.getIsServer()) { // Bitbucket Server - get file metadata first to check size const browsePath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/browse/${file_path}`; const browseParams: any = {}; if (branch) { browseParams.at = `refs/heads/${branch}`; } try { const metadataResponse = await this.apiClient.makeRequest<any>('get', browsePath, undefined, { params: browseParams }); fileMetadata = { size: metadataResponse.size || 0, path: file_path }; // Check file size if (!full_content && fileMetadata.size > fileSizeLimit) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'File too large', file_path, size: fileMetadata.size, size_mb: (fileMetadata.size / (1024 * 1024)).toFixed(2), message: `File exceeds size limit. Use full_content: true to force retrieval or use start_line/line_count for partial content.` }, null, 2), }, ], isError: true, }; } } catch (e) { // If browse fails, continue to try raw endpoint } // Get raw content const rawPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/raw/${file_path}`; const rawParams: any = {}; if (branch) { rawParams.at = `refs/heads/${branch}`; } const response = await this.apiClient.makeRequest<any>('get', rawPath, undefined, { params: rawParams, responseType: 'text', headers: { 'Accept': 'text/plain' } }); fileContent = response; } else { // Bitbucket Cloud - first get metadata const branchOrDefault = branch || 'HEAD'; const metaPath = `/repositories/${workspace}/${repository}/src/${branchOrDefault}/${file_path}`; const metadataResponse = await this.apiClient.makeRequest<BitbucketCloudFileMetadata>('get', metaPath); fileMetadata = { size: metadataResponse.size, encoding: metadataResponse.encoding, path: metadataResponse.path, commit: metadataResponse.commit }; // Check file size if (!full_content && fileMetadata.size > fileSizeLimit) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'File too large', file_path, size: fileMetadata.size, size_mb: (fileMetadata.size / (1024 * 1024)).toFixed(2), message: `File exceeds size limit. Use full_content: true to force retrieval or use start_line/line_count for partial content.` }, null, 2), }, ], isError: true, }; } // Follow the download link to get actual content const downloadUrl = metadataResponse.links.download.href; const downloadResponse = await this.apiClient.makeRequest<any>('get', downloadUrl, undefined, { baseURL: '', // Use full URL responseType: 'text', headers: { 'Accept': 'text/plain' } }); fileContent = downloadResponse; } // Apply line filtering if requested let processedContent = fileContent; let lineInfo: any = null; if (!full_content || start_line !== undefined || line_count !== undefined) { const lines = fileContent.split('\n'); const totalLines = lines.length; // Determine default line count based on file extension const ext = path.extname(file_path).toLowerCase(); const defaultLineCount = this.DEFAULT_LINES_BY_EXT[ext] || 500; const shouldUseTail = defaultLineCount < 0; // Calculate start and end indices let startIdx: number; let endIdx: number; if (start_line !== undefined) { if (start_line < 0) { // Negative start_line means from end startIdx = Math.max(0, totalLines + start_line); endIdx = totalLines; } else { // 1-based to 0-based index startIdx = Math.max(0, start_line - 1); endIdx = startIdx + (line_count || Math.abs(defaultLineCount)); } } else if (!full_content && fileMetadata.size > 50 * 1024) { // Auto-truncate large files if (shouldUseTail) { startIdx = Math.max(0, totalLines + defaultLineCount); endIdx = totalLines; } else { startIdx = 0; endIdx = Math.abs(defaultLineCount); } } else { // Return full content for small files startIdx = 0; endIdx = totalLines; } // Ensure indices are within bounds startIdx = Math.max(0, Math.min(startIdx, totalLines)); endIdx = Math.max(startIdx, Math.min(endIdx, totalLines)); // Extract the requested lines const selectedLines = lines.slice(startIdx, endIdx); processedContent = selectedLines.join('\n'); lineInfo = { total_lines: totalLines, returned_lines: { start: startIdx + 1, end: endIdx }, truncated: startIdx > 0 || endIdx < totalLines, message: endIdx < totalLines ? `Showing lines ${startIdx + 1}-${endIdx} of ${totalLines}. File size: ${(fileMetadata.size / 1024).toFixed(1)}KB` : null }; } // Build response const response: any = { file_path, branch: branch || (this.apiClient.getIsServer() ? 'default' : 'main'), size: fileMetadata.size || fileContent.length, encoding: fileMetadata.encoding || 'utf-8', content: processedContent }; if (lineInfo) { response.line_info = lineInfo; } if (fileMetadata.commit) { response.last_modified = { commit_id: fileMetadata.commit.hash, author: fileMetadata.commit.author?.user?.display_name || fileMetadata.commit.author?.raw, date: fileMetadata.commit.date, message: fileMetadata.commit.message }; } return { content: [ { type: 'text', text: JSON.stringify(response, null, 2), }, ], }; } catch (error: any) { // Handle specific not found error if (error.status === 404) { return { content: [ { type: 'text', text: `File '${file_path}' not found in ${workspace}/${repository}${branch ? ` on branch '${branch}'` : ''}`, }, ], isError: true, }; } return this.apiClient.handleApiError(error, `getting file content for '${file_path}' in ${workspace}/${repository}`); } } // Helper method to get default line count based on file extension private getDefaultLines(filePath: string, fileSize: number): { full: boolean } | { start: number; count: number } { // Small files: return full content if (fileSize < 50 * 1024) { // 50KB return { full: true }; } const ext = path.extname(filePath).toLowerCase(); const defaultLines = this.DEFAULT_LINES_BY_EXT[ext] || 500; return { start: defaultLines < 0 ? defaultLines : 1, count: Math.abs(defaultLines) }; } } ``` -------------------------------------------------------------------------------- /src/types/guards.ts: -------------------------------------------------------------------------------- ```typescript // Type guards for tool arguments export const isGetPullRequestArgs = ( args: any ): args is { workspace: string; repository: string; pull_request_id: number } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.pull_request_id === 'number'; export const isListPullRequestsArgs = ( args: any ): args is { workspace: string; repository: string; state?: string; author?: string; limit?: number; start?: number; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && (args.state === undefined || typeof args.state === 'string') && (args.author === undefined || typeof args.author === 'string') && (args.limit === undefined || typeof args.limit === 'number') && (args.start === undefined || typeof args.start === 'number'); export const isCreatePullRequestArgs = ( args: any ): args is { workspace: string; repository: string; title: string; source_branch: string; destination_branch: string; description?: string; reviewers?: string[]; close_source_branch?: boolean; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.title === 'string' && typeof args.source_branch === 'string' && typeof args.destination_branch === 'string' && (args.description === undefined || typeof args.description === 'string') && (args.reviewers === undefined || Array.isArray(args.reviewers)) && (args.close_source_branch === undefined || typeof args.close_source_branch === 'boolean'); export const isUpdatePullRequestArgs = ( args: any ): args is { workspace: string; repository: string; pull_request_id: number; title?: string; description?: string; destination_branch?: string; reviewers?: string[]; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.pull_request_id === 'number' && (args.title === undefined || typeof args.title === 'string') && (args.description === undefined || typeof args.description === 'string') && (args.destination_branch === undefined || typeof args.destination_branch === 'string') && (args.reviewers === undefined || Array.isArray(args.reviewers)); export const isAddCommentArgs = ( args: any ): args is { workspace: string; repository: string; pull_request_id: number; comment_text: string; parent_comment_id?: number; file_path?: string; line_number?: number; line_type?: 'ADDED' | 'REMOVED' | 'CONTEXT'; suggestion?: string; suggestion_end_line?: number; code_snippet?: string; search_context?: { before?: string[]; after?: string[]; }; match_strategy?: 'strict' | 'best'; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.pull_request_id === 'number' && typeof args.comment_text === 'string' && (args.parent_comment_id === undefined || typeof args.parent_comment_id === 'number') && (args.file_path === undefined || typeof args.file_path === 'string') && (args.line_number === undefined || typeof args.line_number === 'number') && (args.line_type === undefined || ['ADDED', 'REMOVED', 'CONTEXT'].includes(args.line_type)) && (args.suggestion === undefined || typeof args.suggestion === 'string') && (args.suggestion_end_line === undefined || typeof args.suggestion_end_line === 'number') && (args.code_snippet === undefined || typeof args.code_snippet === 'string') && (args.search_context === undefined || ( typeof args.search_context === 'object' && (args.search_context.before === undefined || Array.isArray(args.search_context.before)) && (args.search_context.after === undefined || Array.isArray(args.search_context.after)) )) && (args.match_strategy === undefined || ['strict', 'best'].includes(args.match_strategy)); export const isMergePullRequestArgs = ( args: any ): args is { workspace: string; repository: string; pull_request_id: number; merge_strategy?: string; close_source_branch?: boolean; commit_message?: string; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.pull_request_id === 'number' && (args.merge_strategy === undefined || typeof args.merge_strategy === 'string') && (args.close_source_branch === undefined || typeof args.close_source_branch === 'boolean') && (args.commit_message === undefined || typeof args.commit_message === 'string'); export const isDeleteBranchArgs = ( args: any ): args is { workspace: string; repository: string; branch_name: string; force?: boolean; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.branch_name === 'string' && (args.force === undefined || typeof args.force === 'boolean'); export const isListBranchesArgs = ( args: any ): args is { workspace: string; repository: string; filter?: string; limit?: number; start?: number; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && (args.filter === undefined || typeof args.filter === 'string') && (args.limit === undefined || typeof args.limit === 'number') && (args.start === undefined || typeof args.start === 'number'); export const isGetPullRequestDiffArgs = ( args: any ): args is { workspace: string; repository: string; pull_request_id: number; context_lines?: number; include_patterns?: string[]; exclude_patterns?: string[]; file_path?: string; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.pull_request_id === 'number' && (args.context_lines === undefined || typeof args.context_lines === 'number') && (args.include_patterns === undefined || (Array.isArray(args.include_patterns) && args.include_patterns.every((p: any) => typeof p === 'string'))) && (args.exclude_patterns === undefined || (Array.isArray(args.exclude_patterns) && args.exclude_patterns.every((p: any) => typeof p === 'string'))) && (args.file_path === undefined || typeof args.file_path === 'string'); export const isApprovePullRequestArgs = ( args: any ): args is { workspace: string; repository: string; pull_request_id: number; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.pull_request_id === 'number'; export const isRequestChangesArgs = ( args: any ): args is { workspace: string; repository: string; pull_request_id: number; comment?: string; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.pull_request_id === 'number' && (args.comment === undefined || typeof args.comment === 'string'); export const isGetBranchArgs = ( args: any ): args is { workspace: string; repository: string; branch_name: string; include_merged_prs?: boolean; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.branch_name === 'string' && (args.include_merged_prs === undefined || typeof args.include_merged_prs === 'boolean'); export const isListDirectoryContentArgs = ( args: any ): args is { workspace: string; repository: string; path?: string; branch?: string; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && (args.path === undefined || typeof args.path === 'string') && (args.branch === undefined || typeof args.branch === 'string'); export const isGetFileContentArgs = ( args: any ): args is { workspace: string; repository: string; file_path: string; branch?: string; start_line?: number; line_count?: number; full_content?: boolean; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.file_path === 'string' && (args.branch === undefined || typeof args.branch === 'string') && (args.start_line === undefined || typeof args.start_line === 'number') && (args.line_count === undefined || typeof args.line_count === 'number') && (args.full_content === undefined || typeof args.full_content === 'boolean'); export const isListBranchCommitsArgs = ( args: any ): args is { workspace: string; repository: string; branch_name: string; limit?: number; start?: number; since?: string; until?: string; author?: string; include_merge_commits?: boolean; search?: string; include_build_status?: boolean; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.branch_name === 'string' && (args.limit === undefined || typeof args.limit === 'number') && (args.start === undefined || typeof args.start === 'number') && (args.since === undefined || typeof args.since === 'string') && (args.until === undefined || typeof args.until === 'string') && (args.author === undefined || typeof args.author === 'string') && (args.include_merge_commits === undefined || typeof args.include_merge_commits === 'boolean') && (args.search === undefined || typeof args.search === 'string') && (args.include_build_status === undefined || typeof args.include_build_status === 'boolean'); export const isListPrCommitsArgs = ( args: any ): args is { workspace: string; repository: string; pull_request_id: number; limit?: number; start?: number; include_build_status?: boolean; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.repository === 'string' && typeof args.pull_request_id === 'number' && (args.limit === undefined || typeof args.limit === 'number') && (args.start === undefined || typeof args.start === 'number') && (args.include_build_status === undefined || typeof args.include_build_status === 'boolean'); export const isSearchCodeArgs = ( args: any ): args is { workspace: string; repository?: string; search_query: string; file_pattern?: string; limit?: number; start?: number; } => typeof args === 'object' && args !== null && typeof args.workspace === 'string' && typeof args.search_query === 'string' && (args.repository === undefined || typeof args.repository === 'string') && (args.file_pattern === undefined || typeof args.file_pattern === 'string') && (args.limit === undefined || typeof args.limit === 'number') && (args.start === undefined || typeof args.start === 'number'); export const isListProjectsArgs = ( args: any ): args is { name?: string; permission?: string; limit?: number; start?: number; } => typeof args === 'object' && args !== null && (args.name === undefined || typeof args.name === 'string') && (args.permission === undefined || typeof args.permission === 'string') && (args.limit === undefined || typeof args.limit === 'number') && (args.start === undefined || typeof args.start === 'number'); export const isListRepositoriesArgs = ( args: any ): args is { workspace?: string; name?: string; permission?: string; limit?: number; start?: number; } => typeof args === 'object' && args !== null && (args.workspace === undefined || typeof args.workspace === 'string') && (args.name === undefined || typeof args.name === 'string') && (args.permission === undefined || typeof args.permission === 'string') && (args.limit === undefined || typeof args.limit === 'number') && (args.start === undefined || typeof args.start === 'number'); ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.1.2] - 2025-10-14 ### Added - **CI/CD build status support in `list_pr_commits` tool**: - Added `include_build_status` optional parameter to fetch build/CI status for pull request commits - Returns build status with counts: successful, failed, in_progress, and unknown builds - Uses same Bitbucket Server UI API endpoint as `list_branch_commits` for consistency - Graceful degradation: failures in fetching build status don't break commit listing - Currently only supports Bitbucket Server (Cloud has different build status APIs) - Useful for tracking CI/CD pipeline status for all commits in a pull request ### Changed - Enhanced README.md with comprehensive documentation for new v1.1.0 features: - Added usage examples and documentation for `include_build_status` parameter in `list_branch_commits` tool - Added complete documentation for `list_projects` tool with examples, parameters, and response format - Added complete documentation for `list_repositories` tool with examples, parameters, and response format - Improved feature list to include new Project and Repository Discovery Tools section ## [1.1.0] - 2025-10-14 ### Added - **CI/CD build status support in `list_branch_commits` tool**: - Added `include_build_status` optional parameter to fetch build/CI status for commits - Returns build status with counts: successful, failed, in_progress, and unknown builds - Uses Bitbucket Server UI API endpoint for efficient batch fetching of build summaries - Graceful degradation: failures in fetching build status don't break commit listing - Currently only supports Bitbucket Server (Cloud has different build status APIs) - Useful for tracking CI/CD pipeline status alongside commit history - **New `list_projects` tool for project/workspace discovery**: - List all accessible Bitbucket projects (Server) or workspaces (Cloud) - Optional filtering by project name and permission level - Returns project metadata: key, ID, name, description, visibility, and type - Pagination support with `limit` and `start` parameters - Works with both Bitbucket Server and Cloud with unified response format - **New `list_repositories` tool for repository discovery**: - List repositories within a specific project/workspace or across all accessible repos - Optional filtering by repository name and permission level - Returns comprehensive repository details: - Basic info: slug, ID, name, description, state - Project association: project key and name - Clone URLs for both HTTP(S) and SSH - Repository settings: visibility, forkable status - Pagination support with `limit` and `start` parameters - Bitbucket Cloud requires workspace parameter (documented in response) - Bitbucket Server supports listing all accessible repos without workspace filter ### Changed - Added `ProjectHandlers` class following the modular architecture pattern - Enhanced TypeScript interfaces with project and repository types: - `BitbucketServerProject` and `BitbucketCloudProject` - `BitbucketServerRepository` and `BitbucketCloudRepository` - `BitbucketServerBuildSummary` and `BuildStatus` - Added custom params serializer for multiple `commitId` parameters in build status API - Enhanced `FormattedCommit` interface with optional `build_status` field - Updated API client with `getBuildSummaries` method for batch build status fetching ## [1.0.1] - 2025-08-08 ### Fixed - **Improved search_code tool response formatting**: - Added simplified `formatCodeSearchOutput` for cleaner AI consumption - Enhanced HTML entity decoding (handles ", <, >, &, /, ') - Improved response structure showing file paths and line numbers clearly - Removed HTML formatting tags for better readability ### Changed - Search results now use simplified formatter by default for better AI tool integration - Enhanced query display to show actual search patterns used ## [1.0.0] - 2025-07-25 ### Added - **New `search_code` tool for searching code across repositories**: - Search for code snippets, functions, or any text within Bitbucket repositories - Supports searching within a specific repository or across all repositories in a workspace - File path pattern filtering with glob patterns (e.g., `*.java`, `src/**/*.ts`) - Returns matched lines with highlighted segments showing exact matches - Pagination support for large result sets - Currently only supports Bitbucket Server (Cloud API support planned for future) - Added `SearchHandlers` class following the modular architecture pattern - Added TypeScript interfaces for search requests and responses - Added `formatSearchResults` formatter function for consistent output ### Changed - Major version bump to 1.0.0 indicating stable API with comprehensive feature set - Enhanced documentation with search examples ## [0.10.0] - 2025-07-03 ### Added - **New `list_branch_commits` tool for retrieving commit history**: - List all commits in a specific branch with detailed information - Advanced filtering options: - `since` and `until` parameters for date range filtering (ISO date strings) - `author` parameter to filter by author email/username - `include_merge_commits` parameter to include/exclude merge commits (default: true) - `search` parameter to search in commit messages - Returns branch head information and paginated commit list - Each commit includes hash, message, author details, date, parents, and merge status - Supports both Bitbucket Server and Cloud APIs with appropriate parameter mapping - Useful for reviewing commit history, tracking changes, and analyzing branch activity - **New `list_pr_commits` tool for pull request commits**: - List all commits that are part of a specific pull request - Returns PR title and paginated commit list - Simpler than branch commits - focused specifically on PR changes - Each commit includes same detailed information as branch commits - Supports pagination with `limit` and `start` parameters - Useful for reviewing all changes in a PR before merging ### Changed - Added new TypeScript interfaces for commit types: - `BitbucketServerCommit` and `BitbucketCloudCommit` for API responses - `FormattedCommit` for consistent commit representation - Added formatter functions `formatServerCommit` and `formatCloudCommit` for unified output - Enhanced type guards with `isListBranchCommitsArgs` and `isListPrCommitsArgs` ## [0.9.1] - 2025-01-27 ### Fixed - **Fixed `update_pull_request` reviewer preservation**: - When updating a PR without specifying reviewers, existing reviewers are now preserved - Previously, omitting the `reviewers` parameter would clear all reviewers - Now properly includes existing reviewers in the API request when not explicitly updating them - When updating reviewers, approval status is preserved for existing reviewers - This prevents accidentally removing reviewers when only updating PR title or description ### Changed - Updated tool documentation to clarify reviewer behavior in `update_pull_request` - Enhanced README with detailed explanation of reviewer handling ## [0.9.0] - 2025-01-26 ### Added - **Code snippet support in `add_comment` tool**: - Added `code_snippet` parameter to find line numbers automatically using code text - Added `search_context` parameter with `before` and `after` arrays to disambiguate multiple matches - Added `match_strategy` parameter with options: - `"strict"` (default): Fails with detailed error when multiple matches found - `"best"`: Auto-selects the highest confidence match - Returns detailed error with all occurrences when multiple matches found in strict mode - Particularly useful for AI-powered code review tools that analyze diffs - Created comprehensive line matching algorithm that: - Parses diffs to find exact code snippets - Calculates confidence scores based on context matching - Handles added, removed, and context lines appropriately ### Changed - Enhanced `add_comment` tool to resolve line numbers from code snippets when `line_number` is not provided - Improved error messages to include preview and suggestions for resolving ambiguous matches ## [0.8.0] - 2025-01-26 ### Added - **Code suggestions support in `add_comment` tool**: - Added `suggestion` parameter to add code suggestions in comments - Added `suggestion_end_line` parameter for multi-line suggestions - Suggestions are formatted using GitHub-style markdown ````suggestion` blocks - Works with both single-line and multi-line code replacements - Requires `file_path` and `line_number` to be specified when using suggestions - Compatible with both Bitbucket Cloud and Server - Created `suggestion-formatter.ts` utility for formatting suggestion comments ### Changed - Enhanced `add_comment` tool to validate suggestion requirements - Updated tool response to indicate when a comment contains a suggestion ## [0.7.0] - 2025-01-26 ### Added - **Enhanced `get_pull_request_diff` with filtering capabilities**: - Added `include_patterns` parameter to filter diff by file patterns (whitelist) - Added `exclude_patterns` parameter to exclude files from diff (blacklist) - Added `file_path` parameter to get diff for a specific file only - Patterns support standard glob syntax (e.g., `*.js`, `src/**/*.res`, `node_modules/**`) - Response includes filtering metadata showing total files, included/excluded counts, and excluded file list - Added `minimatch` dependency for glob pattern matching - Created `DiffParser` utility class for parsing and filtering unified diff format ### Changed - Modified `get_pull_request_diff` tool to support optional filtering without breaking existing usage - Updated tool definition and type guards to include new optional parameters - Enhanced documentation with comprehensive examples of filtering usage ## [0.6.1] - 2025-01-26 ### Added - Support for nested comment replies in Bitbucket Server - Added `replies` field to `FormattedComment` interface to support nested comment threads - Comments now include nested replies that are still relevant (not orphaned or resolved) - Total and active comment counts now include nested replies ### Changed - Updated comment fetching logic to handle Bitbucket Server's nested comment structure - Server uses `comments` array inside each comment object for replies - Cloud continues to use `parent` field for reply relationships - Improved comment filtering to exclude orphaned inline comments when code has changed ### Fixed - Fixed missing comment replies in PR details - replies are now properly included in the response ## [0.6.0] - 2025-01-26 ### Added - **Enhanced `get_pull_request` with active comments and file changes**: - Fetches and displays active (unresolved) comments that need attention - Shows up to 20 most recent active comments with: - Comment text, author, and creation date - Inline comment details (file path and line number) - Comment state (OPEN/RESOLVED for Server) - Provides comment counts: - `active_comment_count`: Total unresolved comments - `total_comment_count`: Total comments including resolved - Includes file change statistics: - List of all modified files with lines added/removed - File status (added, modified, removed, renamed) - Summary statistics (total files, lines added/removed) - Added new TypeScript interfaces for comments and file changes - Added `FormattedComment` and `FormattedFileChange` types for consistent response format ### Changed - Modified `handleGetPullRequest` to make parallel API calls for better performance - Enhanced error handling to gracefully continue if comment/file fetching fails ## [0.5.0] - 2025-01-21 ### Added - **New file and directory handling tools**: - `list_directory_content` - List files and directories in any repository path - Shows file/directory type, size, and full paths - Supports browsing specific branches - Works with both Bitbucket Server and Cloud APIs - `get_file_content` - Retrieve file content with smart truncation for large files - Automatic smart defaults by file type (config: 200 lines, docs: 300 lines, code: 500 lines) - Pagination support with `start_line` and `line_count` parameters - Tail functionality using negative `start_line` values (e.g., -50 for last 50 lines) - Automatic truncation for files >50KB to prevent token overload - Files >1MB require explicit `full_content: true` parameter - Returns metadata including file size, encoding, and last modified info - Added `FileHandlers` class following existing modular architecture patterns - Added TypeScript interfaces for file/directory entries and metadata - Added type guards `isListDirectoryContentArgs` and `isGetFileContentArgs` ### Changed - Enhanced documentation with comprehensive examples for file handling tools ## [0.4.0] - 2025-01-21 ### Added - **New `get_branch` tool for comprehensive branch information**: - Returns detailed branch information including name, ID, and latest commit details - Lists all open pull requests originating from the branch with approval status - Optionally includes merged pull requests when `include_merged_prs` is true - Provides useful statistics like PR counts and days since last commit - Supports both Bitbucket Server and Cloud APIs - Particularly useful for checking if a branch has open PRs before deletion - Added TypeScript interfaces for `BitbucketServerBranch` and `BitbucketCloudBranch` - Added type guard `isGetBranchArgs` for input validation ### Changed - Updated documentation to include the new `get_branch` tool with comprehensive examples ## [0.3.0] - 2025-01-06 ### Added - **Enhanced merge commit details in `get_pull_request`**: - Added `merge_commit_hash` field for both Cloud and Server - Added `merged_by` field showing who performed the merge - Added `merged_at` timestamp for when the merge occurred - Added `merge_commit_message` with the merge commit message - For Bitbucket Server: Fetches merge details from activities API when PR is merged - For Bitbucket Cloud: Extracts merge information from existing response fields ### Changed - **Major code refactoring for better maintainability**: - Split monolithic `index.ts` into modular architecture - Created separate handler classes for different tool categories: - `PullRequestHandlers` for PR lifecycle operations - `BranchHandlers` for branch management - `ReviewHandlers` for code review tools - Extracted types into dedicated files (`types/bitbucket.ts`, `types/guards.ts`) - Created utility modules (`utils/api-client.ts`, `utils/formatters.ts`) - Centralized tool definitions in `tools/definitions.ts` - Improved error handling and API client abstraction - Better separation of concerns between Cloud and Server implementations ### Fixed - Improved handling of merge commit information retrieval failures - Fixed API parameter passing for GET requests across all handlers (was passing config as third parameter instead of fourth) - Updated Bitbucket Server branch listing to use `/rest/api/latest/` endpoint with proper parameters - Branch filtering now works correctly with the `filterText` parameter for Bitbucket Server ## [0.2.0] - 2025-06-04 ### Added - Complete implementation of all Bitbucket MCP tools - Support for both Bitbucket Cloud and Server - Core PR lifecycle tools: - `create_pull_request` - Create new pull requests - `update_pull_request` - Update PR details - `merge_pull_request` - Merge pull requests - `list_branches` - List repository branches - `delete_branch` - Delete branches - Enhanced `add_comment` with inline comment support - Code review tools: - `get_pull_request_diff` - Get PR diff/changes - `approve_pull_request` - Approve PRs - `unapprove_pull_request` - Remove approval - `request_changes` - Request changes on PRs - `remove_requested_changes` - Remove change requests - npm package configuration for easy installation via npx ### Fixed - Author filter for Bitbucket Server (uses `role.1=AUTHOR` and `username.1=email`) - Branch deletion handling for 204 No Content responses ### Changed - Package name to `@nexus2520/bitbucket-mcp-server` for npm publishing ## [0.1.0] - 2025-06-03 ### Added - Initial implementation with basic tools: - `get_pull_request` - Get PR details - `list_pull_requests` - List PRs with filters - Support for Bitbucket Cloud with app passwords - Support for Bitbucket Server with HTTP access tokens - Authentication setup script - Comprehensive documentation ``` -------------------------------------------------------------------------------- /src/handlers/branch-handlers.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { BitbucketApiClient } from '../utils/api-client.js'; import { isListBranchesArgs, isDeleteBranchArgs, isGetBranchArgs, isListBranchCommitsArgs } from '../types/guards.js'; import { BitbucketServerBranch, BitbucketCloudBranch, BitbucketServerCommit, BitbucketCloudCommit, FormattedCommit } from '../types/bitbucket.js'; import { formatServerCommit, formatCloudCommit } from '../utils/formatters.js'; export class BranchHandlers { constructor( private apiClient: BitbucketApiClient, private baseUrl: string ) {} async handleListBranches(args: any) { if (!isListBranchesArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for list_branches' ); } const { workspace, repository, filter, limit = 25, start = 0 } = args; try { let apiPath: string; let params: any = {}; if (this.apiClient.getIsServer()) { // Bitbucket Server API - using latest version for better filtering support apiPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`; params = { limit, start, details: true, orderBy: 'MODIFICATION' }; if (filter) { params.filterText = filter; } } else { // Bitbucket Cloud API apiPath = `/repositories/${workspace}/${repository}/refs/branches`; params = { pagelen: limit, page: Math.floor(start / limit) + 1, }; if (filter) { params.q = `name ~ "${filter}"`; } } const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); // Format the response let branches: any[] = []; let totalCount = 0; let nextPageStart = null; if (this.apiClient.getIsServer()) { // Bitbucket Server response branches = (response.values || []).map((branch: any) => ({ name: branch.displayId, id: branch.id, latest_commit: branch.latestCommit, is_default: branch.isDefault || false })); totalCount = response.size || 0; if (!response.isLastPage && response.nextPageStart !== undefined) { nextPageStart = response.nextPageStart; } } else { // Bitbucket Cloud response branches = (response.values || []).map((branch: any) => ({ name: branch.name, target: branch.target.hash, is_default: branch.name === 'main' || branch.name === 'master' })); totalCount = response.size || 0; if (response.next) { nextPageStart = start + limit; } } return { content: [ { type: 'text', text: JSON.stringify({ branches, total_count: totalCount, start, limit, has_more: nextPageStart !== null, next_start: nextPageStart }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, `listing branches in ${workspace}/${repository}`); } } async handleDeleteBranch(args: any) { if (!isDeleteBranchArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for delete_branch' ); } const { workspace, repository, branch_name, force } = args; try { let apiPath: string; if (this.apiClient.getIsServer()) { // First, we need to get the branch details to find the latest commit const branchesPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`; const branchesResponse = await this.apiClient.makeRequest<any>('get', branchesPath, undefined, { params: { filterText: branch_name, limit: 100 } }); // Find the exact branch const branch = branchesResponse.values?.find((b: any) => b.displayId === branch_name); if (!branch) { throw new Error(`Branch '${branch_name}' not found`); } // Now delete using branch-utils endpoint with correct format apiPath = `/rest/branch-utils/latest/projects/${workspace}/repos/${repository}/branches`; try { await this.apiClient.makeRequest<any>('delete', apiPath, { name: branch_name, endPoint: branch.latestCommit }); } catch (deleteError: any) { // If the error is about empty response but status is 204 (No Content), it's successful if (deleteError.originalError?.response?.status === 204 || deleteError.message?.includes('No content to map')) { // Branch was deleted successfully } else { throw deleteError; } } } else { // Bitbucket Cloud API apiPath = `/repositories/${workspace}/${repository}/refs/branches/${branch_name}`; try { await this.apiClient.makeRequest<any>('delete', apiPath); } catch (deleteError: any) { // If the error is about empty response but status is 204 (No Content), it's successful if (deleteError.originalError?.response?.status === 204 || deleteError.message?.includes('No content to map')) { // Branch was deleted successfully } else { throw deleteError; } } } return { content: [ { type: 'text', text: JSON.stringify({ message: `Branch '${branch_name}' deleted successfully`, branch: branch_name, repository: `${workspace}/${repository}` }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, `deleting branch '${branch_name}' in ${workspace}/${repository}`); } } async handleGetBranch(args: any) { if (!isGetBranchArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for get_branch' ); } const { workspace, repository, branch_name, include_merged_prs = false } = args; try { // Step 1: Get branch details let branchInfo: any; let branchCommitInfo: any = {}; if (this.apiClient.getIsServer()) { // Bitbucket Server - get branch details const branchesPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`; const branchesResponse = await this.apiClient.makeRequest<any>('get', branchesPath, undefined, { params: { filterText: branch_name, limit: 100, details: true } }); // Find the exact branch const branch = branchesResponse.values?.find((b: BitbucketServerBranch) => b.displayId === branch_name); if (!branch) { throw new Error(`Branch '${branch_name}' not found`); } branchInfo = { name: branch.displayId, id: branch.id, latest_commit: { id: branch.latestCommit, message: branch.metadata?.['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata']?.message || null, author: branch.metadata?.['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata']?.author || null, date: branch.metadata?.['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata']?.authorTimestamp ? new Date(branch.metadata['com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata'].authorTimestamp).toISOString() : null }, is_default: branch.isDefault || false }; } else { // Bitbucket Cloud - get branch details const branchPath = `/repositories/${workspace}/${repository}/refs/branches/${encodeURIComponent(branch_name)}`; const branch = await this.apiClient.makeRequest<BitbucketCloudBranch>('get', branchPath); branchInfo = { name: branch.name, id: `refs/heads/${branch.name}`, latest_commit: { id: branch.target.hash, message: branch.target.message, author: branch.target.author.user?.display_name || branch.target.author.raw, date: branch.target.date }, is_default: false // Will check this with default branch info }; // Check if this is the default branch try { const repoPath = `/repositories/${workspace}/${repository}`; const repoInfo = await this.apiClient.makeRequest<any>('get', repoPath); branchInfo.is_default = branch.name === repoInfo.mainbranch?.name; } catch (e) { // Ignore error, just assume not default } } // Step 2: Get open PRs from this branch let openPRs: any[] = []; if (this.apiClient.getIsServer()) { // Bitbucket Server const prPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`; const prResponse = await this.apiClient.makeRequest<any>('get', prPath, undefined, { params: { state: 'OPEN', direction: 'OUTGOING', at: `refs/heads/${branch_name}`, limit: 100 } }); openPRs = (prResponse.values || []).map((pr: any) => ({ id: pr.id, title: pr.title, destination_branch: pr.toRef.displayId, author: pr.author.user.displayName, created_on: new Date(pr.createdDate).toISOString(), reviewers: pr.reviewers.map((r: any) => r.user.displayName), approval_status: { approved_by: pr.reviewers.filter((r: any) => r.approved).map((r: any) => r.user.displayName), changes_requested_by: pr.reviewers.filter((r: any) => r.status === 'NEEDS_WORK').map((r: any) => r.user.displayName), pending: pr.reviewers.filter((r: any) => !r.approved && r.status !== 'NEEDS_WORK').map((r: any) => r.user.displayName) }, url: `${this.baseUrl}/projects/${workspace}/repos/${repository}/pull-requests/${pr.id}` })); } else { // Bitbucket Cloud const prPath = `/repositories/${workspace}/${repository}/pullrequests`; const prResponse = await this.apiClient.makeRequest<any>('get', prPath, undefined, { params: { state: 'OPEN', q: `source.branch.name="${branch_name}"`, pagelen: 50 } }); openPRs = (prResponse.values || []).map((pr: any) => ({ id: pr.id, title: pr.title, destination_branch: pr.destination.branch.name, author: pr.author.display_name, created_on: pr.created_on, reviewers: pr.reviewers.map((r: any) => r.display_name), approval_status: { approved_by: pr.participants.filter((p: any) => p.approved).map((p: any) => p.user.display_name), changes_requested_by: [], // Cloud doesn't have explicit "changes requested" status pending: pr.reviewers.filter((r: any) => !pr.participants.find((p: any) => p.user.account_id === r.account_id && p.approved)) .map((r: any) => r.display_name) }, url: pr.links.html.href })); } // Step 3: Optionally get merged PRs let mergedPRs: any[] = []; if (include_merged_prs) { if (this.apiClient.getIsServer()) { // Bitbucket Server const mergedPrPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`; const mergedPrResponse = await this.apiClient.makeRequest<any>('get', mergedPrPath, undefined, { params: { state: 'MERGED', direction: 'OUTGOING', at: `refs/heads/${branch_name}`, limit: 25 } }); mergedPRs = (mergedPrResponse.values || []).map((pr: any) => ({ id: pr.id, title: pr.title, merged_at: new Date(pr.updatedDate).toISOString(), // Using updated date as merge date merged_by: pr.participants.find((p: any) => p.role === 'PARTICIPANT' && p.approved)?.user.displayName || 'Unknown' })); } else { // Bitbucket Cloud const mergedPrPath = `/repositories/${workspace}/${repository}/pullrequests`; const mergedPrResponse = await this.apiClient.makeRequest<any>('get', mergedPrPath, undefined, { params: { state: 'MERGED', q: `source.branch.name="${branch_name}"`, pagelen: 25 } }); mergedPRs = (mergedPrResponse.values || []).map((pr: any) => ({ id: pr.id, title: pr.title, merged_at: pr.updated_on, merged_by: pr.closed_by?.display_name || 'Unknown' })); } } // Step 4: Calculate statistics const daysSinceLastCommit = branchInfo.latest_commit.date ? Math.floor((Date.now() - new Date(branchInfo.latest_commit.date).getTime()) / (1000 * 60 * 60 * 24)) : null; // Step 5: Format and return combined response return { content: [ { type: 'text', text: JSON.stringify({ branch: branchInfo, open_pull_requests: openPRs, merged_pull_requests: mergedPRs, statistics: { total_open_prs: openPRs.length, total_merged_prs: mergedPRs.length, days_since_last_commit: daysSinceLastCommit } }, null, 2), }, ], }; } catch (error: any) { // Handle specific not found error if (error.message?.includes('not found')) { return { content: [ { type: 'text', text: `Branch '${branch_name}' not found in ${workspace}/${repository}`, }, ], isError: true, }; } return this.apiClient.handleApiError(error, `getting branch '${branch_name}' in ${workspace}/${repository}`); } } async handleListBranchCommits(args: any) { if (!isListBranchCommitsArgs(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for list_branch_commits' ); } const { workspace, repository, branch_name, limit = 25, start = 0, since, until, author, include_merge_commits = true, search, include_build_status = false } = args; try { let apiPath: string; let params: any = {}; let commits: FormattedCommit[] = []; let totalCount = 0; let nextPageStart: number | null = null; if (this.apiClient.getIsServer()) { // Bitbucket Server API apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/commits`; params = { until: `refs/heads/${branch_name}`, limit, start, withCounts: true }; // Add filters if (since) { params.since = since; } if (!include_merge_commits) { params.merges = 'exclude'; } const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); // Format commits commits = (response.values || []).map((commit: BitbucketServerCommit) => formatServerCommit(commit)); // Apply client-side filters for Server API if (author) { // Filter by author email or name commits = commits.filter(c => c.author.email === author || c.author.name === author || c.author.email.toLowerCase() === author.toLowerCase() || c.author.name.toLowerCase() === author.toLowerCase() ); } // Filter by date if 'until' is provided (Server API doesn't support 'until' param directly) if (until) { const untilDate = new Date(until).getTime(); commits = commits.filter(c => new Date(c.date).getTime() <= untilDate); } // Filter by message search if provided if (search) { const searchLower = search.toLowerCase(); commits = commits.filter(c => c.message.toLowerCase().includes(searchLower)); } // If we applied client-side filters, update the total count if (author || until || search) { totalCount = commits.length; // Can't determine if there are more results when filtering client-side nextPageStart = null; } else { totalCount = response.size || commits.length; if (!response.isLastPage && response.nextPageStart !== undefined) { nextPageStart = response.nextPageStart; } } } else { // Bitbucket Cloud API apiPath = `/repositories/${workspace}/${repository}/commits/${encodeURIComponent(branch_name)}`; params = { pagelen: limit, page: Math.floor(start / limit) + 1 }; // Build query string for filters const queryParts: string[] = []; if (author) { queryParts.push(`author.raw ~ "${author}"`); } if (!include_merge_commits) { // Cloud API doesn't have direct merge exclusion, we'll filter client-side } if (queryParts.length > 0) { params.q = queryParts.join(' AND '); } const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params }); // Format commits let cloudCommits = (response.values || []).map((commit: BitbucketCloudCommit) => formatCloudCommit(commit)); // Apply client-side filters if (!include_merge_commits) { cloudCommits = cloudCommits.filter((c: FormattedCommit) => !c.is_merge_commit); } if (since) { const sinceDate = new Date(since).getTime(); cloudCommits = cloudCommits.filter((c: FormattedCommit) => new Date(c.date).getTime() >= sinceDate); } if (until) { const untilDate = new Date(until).getTime(); cloudCommits = cloudCommits.filter((c: FormattedCommit) => new Date(c.date).getTime() <= untilDate); } if (search) { const searchLower = search.toLowerCase(); cloudCommits = cloudCommits.filter((c: FormattedCommit) => c.message.toLowerCase().includes(searchLower)); } commits = cloudCommits; totalCount = response.size || commits.length; if (response.next) { nextPageStart = start + limit; } } // Fetch build status if requested (Server only) if (include_build_status && this.apiClient.getIsServer() && commits.length > 0) { try { // Extract commit hashes (use full hash, not abbreviated) const commitIds = commits.map(c => c.hash); // Fetch build summaries for all commits const buildSummaries = await this.apiClient.getBuildSummaries( workspace, repository, commitIds ); // Merge build status into commits commits = commits.map(commit => { const buildData = buildSummaries[commit.hash]; if (buildData) { return { ...commit, build_status: { successful: buildData.successful || 0, failed: buildData.failed || 0, in_progress: buildData.inProgress || 0, unknown: buildData.unknown || 0 } }; } return commit; }); } catch (error) { // Gracefully degrade - log error but don't fail the entire request console.error('Failed to fetch build status:', error); } } // Get branch head info let branchHead: string | null = null; try { if (this.apiClient.getIsServer()) { const branchesPath = `/rest/api/latest/projects/${workspace}/repos/${repository}/branches`; const branchesResponse = await this.apiClient.makeRequest<any>('get', branchesPath, undefined, { params: { filterText: branch_name, limit: 1 } }); const branch = branchesResponse.values?.find((b: any) => b.displayId === branch_name); branchHead = branch?.latestCommit || null; } else { const branchPath = `/repositories/${workspace}/${repository}/refs/branches/${encodeURIComponent(branch_name)}`; const branch = await this.apiClient.makeRequest<any>('get', branchPath); branchHead = branch.target?.hash || null; } } catch (e) { // Ignore error, branch head is optional } // Build filters applied summary const filtersApplied: any = {}; if (author) filtersApplied.author = author; if (since) filtersApplied.since = since; if (until) filtersApplied.until = until; if (include_merge_commits !== undefined) filtersApplied.include_merge_commits = include_merge_commits; if (search) filtersApplied.search = search; if (include_build_status) filtersApplied.include_build_status = include_build_status; return { content: [ { type: 'text', text: JSON.stringify({ branch_name, branch_head: branchHead, commits, total_count: totalCount, start, limit, has_more: nextPageStart !== null, next_start: nextPageStart, filters_applied: filtersApplied }, null, 2), }, ], }; } catch (error) { return this.apiClient.handleApiError(error, `listing commits for branch '${branch_name}' in ${workspace}/${repository}`); } } } ```