diff --git a/docs/superpowers/specs/2026-05-20-bitbucket-mcp-features-design.md b/docs/superpowers/specs/2026-05-20-bitbucket-mcp-features-design.md new file mode 100644 index 0000000..ba3f519 --- /dev/null +++ b/docs/superpowers/specs/2026-05-20-bitbucket-mcp-features-design.md @@ -0,0 +1,253 @@ +# 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.