Add design spec for missing Bitbucket MCP features
Covers domain client split, write operations (PR, comment, task), and repository/workspace/branch browsing tools. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.<method>()` — 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~"<value>"`)
|
||||
|
||||
### `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.<method>()`
|
||||
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.
|
||||
Reference in New Issue
Block a user