# Bitbucket MCP Server — Missing Features Design **Date:** 2026-05-20 **Status:** Approved --- ## Overview Extend the existing read-only Bitbucket MCP server with: 1. **Repository/workspace browsing** — list workspaces, repos, branches; get repo metadata 2. **PR write operations** — create, update, merge, decline, approve, request-changes 3. **Comment write operations** — add, update, delete comments (general and inline) 4. **Task write operations** — create, update, delete PR tasks The server's public contract (tool names, parameter shapes, `ToolResult` response envelope) remains unchanged; new tools are additive. --- ## Architecture The current monolithic `bitbucket-client.ts` is split into domain modules. A shared `base-client.ts` owns the Axios instance, initialization guard, error interceptors, and `formatError`. Domain clients extend or compose the base. `BitbucketClient` becomes a composition root that wires all domain clients together and exposes a flat API to the router. ``` src/ ├── index.ts # MCP server — tool schemas added here ├── router.ts # Tool dispatcher — new switch cases added ├── config.ts # Unchanged ├── bitbucket-client.ts # Composition root: instantiates & delegates to domain clients └── clients/ ├── base-client.ts # Shared Axios instance, ensureInitialized, formatError, error interceptors ├── pull-request-client.ts # All PR methods (read + write) ├── repository-client.ts # Workspace, repo, branch methods └── comment-client.ts # Comment + task CRUD ``` The router continues calling `this.client.()` — the composition root makes this transparent. --- ## Domain Clients ### `base-client.ts` Extracted from the current `bitbucket-client.ts`: - Holds the `AxiosInstance` - `initializeClient(options)` — loads config, creates axios, attaches interceptors - `ensureInitialized()` — polling guard used by all domain clients - `handleResponseError(error)` — 401/403/429 logging and rate-limit wait - `formatError(error)` — HTTP status + message formatter - `sleep(ms)` — utility All domain clients receive a reference to the base client (or extend it) to share the axios instance and initialization state. ### `pull-request-client.ts` **Existing read methods** (migrated from current `bitbucket-client.ts`): - `listPullRequests`, `getPullRequest`, `getPullRequestActivities` - `getPullRequestChanges`, `getPullRequestComments`, `getPullRequestComment` - `getPullRequestCommits`, `getPullRequestDiff`, `getPullRequestPatch` - `getPullRequestParticipants`, `getPullRequestReviewers`, `getPullRequestStatus` - `getPullRequestTasks`, `getPullRequestTaskCount`, `getFullPullRequest` **New write methods:** | Method | HTTP | Endpoint | |--------|------|----------| | `createPullRequest(ws, repo, opts)` | POST | `/repositories/{ws}/{repo}/pullrequests` | | `updatePullRequest(ws, repo, id, opts)` | PUT | `/repositories/{ws}/{repo}/pullrequests/{id}` | | `mergePullRequest(ws, repo, id, opts)` | POST | `.../pullrequests/{id}/merge` | | `declinePullRequest(ws, repo, id)` | POST | `.../pullrequests/{id}/decline` | | `approvePullRequest(ws, repo, id)` | POST | `.../pullrequests/{id}/approve` | | `unapprovePullRequest(ws, repo, id)` | DELETE | `.../pullrequests/{id}/approve` | | `requestChangesPullRequest(ws, repo, id)` | POST | `.../pullrequests/{id}/request-changes` | | `removeRequestChangesPullRequest(ws, repo, id)` | DELETE | `.../pullrequests/{id}/request-changes` | `createPullRequest` options: ```ts { title: string; source_branch: string; destination_branch: string; description?: string; reviewers?: string[]; // account UUIDs or usernames close_source_branch?: boolean; } ``` `mergePullRequest` options: ```ts { merge_strategy?: 'merge_commit' | 'squash' | 'fast_forward'; commit_message?: string; close_source_branch?: boolean; } ``` `updatePullRequest` options (all optional, at least one required): ```ts { title?: string; description?: string; reviewers?: string[]; destination_branch?: string; } ``` ### `repository-client.ts` New domain client — no existing methods to migrate. | Method | HTTP | Endpoint | |--------|------|----------| | `listWorkspaces(opts)` | GET | `/workspaces` | | `listRepositories(ws, opts)` | GET | `/repositories/{workspace}` | | `getRepository(ws, repo)` | GET | `/repositories/{workspace}/{repo_slug}` | | `listBranches(ws, repo, opts)` | GET | `/repositories/{ws}/{repo}/refs/branches` | `listWorkspaces` options: `page`, `pagelen` `listRepositories` options: `role` (`member`|`contributor`|`owner`), `page`, `pagelen` `listBranches` options: `page`, `pagelen`, `filter_by_name` (maps to `q=name~""`) ### `comment-client.ts` **Existing read methods** (migrated): - `getPullRequestComments`, `getPullRequestComment` **New write methods:** | Method | HTTP | Endpoint | |--------|------|----------| | `addPullRequestComment(ws, repo, id, opts)` | POST | `.../pullrequests/{id}/comments` | | `updatePullRequestComment(ws, repo, id, commentId, opts)` | PUT | `.../comments/{commentId}` | | `deletePullRequestComment(ws, repo, id, commentId)` | DELETE | `.../comments/{commentId}` | | `createPullRequestTask(ws, repo, id, opts)` | POST | `.../pullrequests/{id}/tasks` | | `updatePullRequestTask(ws, repo, id, taskId, opts)` | PUT | `.../tasks/{taskId}` | | `deletePullRequestTask(ws, repo, id, taskId)` | DELETE | `.../tasks/{taskId}` | `addPullRequestComment` options: ```ts { content: string; inline?: { path: string; to: number }; // inline comment on file:line parent_id?: number; // reply to existing comment } ``` `updatePullRequestComment` options: ```ts { content: string } ``` `createPullRequestTask` options: ```ts { content: string; comment_id?: number; // anchor to a specific comment } ``` `updatePullRequestTask` options: ```ts { content?: string; state?: 'RESOLVED' | 'UNRESOLVED'; } ``` --- ## New MCP Tools (index.ts) ### Repository/Workspace | Tool name | Required params | Optional params | |-----------|----------------|-----------------| | `list_workspaces` | — | `page`, `pagelen` | | `list_repositories` | `workspace` | `role`, `page`, `pagelen` | | `get_repository` | `workspace`, `repository` | — | | `list_branches` | `workspace`, `repository` | `filter_by_name`, `page`, `pagelen` | ### PR Write | Tool name | Required params | Optional params | |-----------|----------------|-----------------| | `create_pull_request` | `workspace`, `repository`, `title`, `source_branch`, `destination_branch` | `description`, `reviewers`, `close_source_branch` | | `update_pull_request` | `workspace`, `repository`, `pullRequestId` | `title`, `description`, `reviewers`, `destination_branch` | | `merge_pull_request` | `workspace`, `repository`, `pullRequestId` | `merge_strategy`, `commit_message`, `close_source_branch` | | `decline_pull_request` | `workspace`, `repository`, `pullRequestId` | — | | `approve_pull_request` | `workspace`, `repository`, `pullRequestId` | — | | `unapprove_pull_request` | `workspace`, `repository`, `pullRequestId` | — | | `request_changes_pull_request` | `workspace`, `repository`, `pullRequestId` | — | | `remove_request_changes_pull_request` | `workspace`, `repository`, `pullRequestId` | — | ### Comment Write | Tool name | Required params | Optional params | |-----------|----------------|-----------------| | `add_pull_request_comment` | `workspace`, `repository`, `pullRequestId`, `content` | `inline_path`, `inline_line`, `parent_comment_id` | | `update_pull_request_comment` | `workspace`, `repository`, `pullRequestId`, `commentId`, `content` | — | | `delete_pull_request_comment` | `workspace`, `repository`, `pullRequestId`, `commentId` | — | ### Task Write | Tool name | Required params | Optional params | |-----------|----------------|-----------------| | `create_pull_request_task` | `workspace`, `repository`, `pullRequestId`, `content` | `comment_id` | | `update_pull_request_task` | `workspace`, `repository`, `pullRequestId`, `taskId` | `content`, `state` | | `delete_pull_request_task` | `workspace`, `repository`, `pullRequestId`, `taskId` | — | --- ## Router (router.ts) New `switch` cases added to `executeTool()` for every new tool. Same pattern as existing cases: 1. Extract `workspace`, `repoSlug` via `getDefaultParams()` 2. Parse integer IDs 3. Validate required params — return `{ success: false, error: '...' }` if missing 4. Delegate to `this.client.()` 5. Return `{ success: true, data: result }` No new helper is needed for write ops; the existing pattern is clear enough. --- ## Error Handling - Write operations do not retry on failure. - `base-client.ts` error interceptor handles 401/403/429 (existing behavior preserved). - All domain client methods catch errors and re-throw with `this.formatError(error)`. - Router catches and returns `{ success: false, error: '...' }`. --- ## Testing **Unit tests (`tests/unit/router.test.ts`):** - New cases added for every new tool, mocking `BitbucketClient` identically to existing tests. **Integration tests (`tests/integration/bitbucket-api.test.ts`):** - Read operations (workspaces, repos, branches) added unconditionally. - Write operations (merge, decline, approve, comment, task) gated on `RUN_WRITE_TESTS=true` env var to prevent accidental mutation of real PRs. --- ## Out of Scope - Webhooks, pipelines, deployments, issue tracker, snippets. - Any Bitbucket Server (self-hosted) support — this server targets Bitbucket Cloud only. - Streaming / SSE responses.