Compare commits

6 Commits

Author SHA1 Message Date
2df53cbf10 fix: use correct Bitbucket API for user workspaces and clarify repo config
The `/workspaces` API endpoint was updated to `/user/workspaces` to correctly retrieve workspaces for the authenticated user.

Configuration guidance for `DEFAULT_WORKSPACE` and `DEFAULT_REPO` in documentation and templates now explicitly references deriving these values from the Bitbucket repository URL, simplifying initial setup.
2026-05-21 08:50:21 +02:00
8a410c7fd7 docs: update CLAUDE.md, README, and .env.template for write test requirements
Document RUN_WRITE_TESTS and TEST_PR_ID env vars across all three files.
Fix .env.template to keep write-test vars commented out. Reference .env.template
as the starting point for local development setup.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-20 23:35:05 +02:00
5d911d91a7 docs: update .env.template with write test environment variables 2026-05-20 23:31:01 +02:00
148fe7e51b chore: update .gitignore to ignore all .env files and allow templates 2026-05-20 23:27:26 +02:00
9c7d983df4 test: require TEST_PR_ID for write integration tests to prevent accidental mutations
Write tests no longer pick a random open PR. RUN_WRITE_TESTS=true alone is not
sufficient — TEST_PR_ID must also be set, otherwise each write test is skipped
with a warning. Added try/finally cleanup blocks so approval/comment/task state
is always restored even on assertion failures. Updated .env.example and README
to document the new requirement.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-20 23:25:40 +02:00
ab73c92e6d docs: update README with full capabilities — write ops, repo browsing, tasks
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-20 23:09:12 +02:00
6 changed files with 121 additions and 47 deletions

16
.env.template Normal file
View File

@@ -0,0 +1,16 @@
BITBUCKET_MCP_EMAIL=your_email@example.com
BITBUCKET_MCP_TOKEN=your_api_token
# Find these in your Bitbucket repo URL: https://bitbucket.org/my-workspace/my-repo/pull-requests
DEFAULT_WORKSPACE=my-workspace
DEFAULT_REPO=my-repo
# Integration test write operations
# ---------------------------------------------------------------------------------
# Set RUN_WRITE_TESTS=true to enable tests that create/modify/delete data.
#
# TEST_PR_ID must also be set — write tests will only run against this specific PR
# to avoid accidentally modifying unrelated pull requests.
# ---------------------------------------------------------------------------------
# RUN_WRITE_TESTS=true
# TEST_PR_ID=123
# ---------------------------------------------------------------------------------

6
.gitignore vendored
View File

@@ -6,9 +6,9 @@ dist/
tsconfig.tsbuildinfo
# Environment
.env
.env.local
.env.*.local
*.env
*.env.*
!*.env.template
# IDE
.idea/

View File

@@ -41,9 +41,15 @@ The server is a 3-layer pipeline: **MCP protocol** -> **Router** -> **Bitbucket
|----------|---------|
| `BITBUCKET_MCP_EMAIL` | Bitbucket account email (Basic Auth username) |
| `BITBUCKET_MCP_TOKEN` | Bitbucket API token (Basic Auth password) |
| `DEFAULT_WORKSPACE` | Optional default workspace slug |
| `DEFAULT_REPO` | Optional default repository slug |
| `DEFAULT_WORKSPACE` | Optional default workspace slug (from your repo URL: `https://bitbucket.org/{workspace}/{repo}/...`) |
| `DEFAULT_REPO` | Optional default repository slug (from your repo URL: `https://bitbucket.org/{workspace}/{repo}/...`) |
| `RUN_WRITE_TESTS` | Set to `true` to enable write integration tests |
| `TEST_PR_ID` | PR ID to use for write integration tests (required when `RUN_WRITE_TESTS=true`) |
Copy `.env.template` to `.env` and fill in your values for local development and testing.
## Testing
Tests use vitest with 30s timeouts. Unit tests mock the Bitbucket client; integration tests call the real API and require valid credentials in the environment.
Write integration tests (approve/unapprove, comments, tasks) are skipped by default. They require both `RUN_WRITE_TESTS=true` and `TEST_PR_ID` to be set. Write tests never pick a random PR — if `TEST_PR_ID` is missing the test is skipped with a warning.

View File

@@ -15,16 +15,19 @@
## Overview
This MCP server exposes **read-only** Bitbucket Cloud Pull Request operations as tools that AI agents (Claude, etc.) can invoke over stdio. It connects your AI workflow directly to your Bitbucket repositories.
This MCP server exposes Bitbucket Cloud operations as tools that AI agents (Claude, etc.) can invoke over stdio. It connects your AI workflow directly to your Bitbucket repositories — covering everything from read-only PR inspection to full write operations.
### Capabilities
| Category | Operations |
|----------|-----------|
| **Pull Requests** | List, get details, get full expanded PR |
| **Workspaces & Repos** | List workspaces, list/get repositories, list branches |
| **Pull Requests (read)** | List, get details, get full expanded PR, status |
| **Pull Requests (write)** | Create, update, merge, decline |
| **Code Review** | Diff, patch, file changes, commits |
| **Collaboration** | Comments, activities, participants, reviewers |
| **Workflow** | Status, tasks, task count |
| **Collaboration** | Comments (read/add/edit/delete), activities, participants, reviewers |
| **Review Workflow** | Approve, unapprove, request changes, remove request-changes |
| **Tasks** | Get tasks, task count, create/update/delete tasks |
| **Auth** | Token validation |
---
@@ -92,6 +95,8 @@ Add the MCP server to your Claude configuration:
> **Important:**
> - Replace `/absolute/path/to/bitbucket-mcp` with the actual absolute path where you cloned the repo.
> - `DEFAULT_WORKSPACE` and `DEFAULT_REPO` are optional but convenient — they let you omit those parameters from every tool call.
> - Read them directly from your Bitbucket repo URL: `https://bitbucket.org/{workspace}/{repository}/pull-requests`
> For example, `https://bitbucket.org/my-workspace/my-repo/pull-requests` → `DEFAULT_WORKSPACE=my-workspace`, `DEFAULT_REPO=my-repo`
> - Get your API token from [Atlassian API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens).
### 4. Verify
@@ -108,6 +113,15 @@ Restart Claude (or reload MCP servers), then ask Claude to run the `validate_tok
|------|-------------|
| `validate_token` | Verify credentials are valid |
### Workspaces & Repositories
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `list_workspaces` | List all workspaces the authenticated user belongs to | `page?`, `pagelen?` |
| `list_repositories` | List repositories in a workspace | `workspace`, `role?`, `page?`, `pagelen?` |
| `get_repository` | Get metadata for a specific repository | `workspace`, `repository` |
| `list_branches` | List branches in a repository | `workspace`, `repository`, `filter_by_name?`, `page?`, `pagelen?` |
### Pull Request Discovery
| Tool | Description | Key Parameters |
@@ -117,6 +131,24 @@ Restart Claude (or reload MCP servers), then ask Claude to run the `validate_tok
| `get_full_pull_request` | Get PR with all fields expanded | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_status` | Get PR state (open/merged/declined) | `workspace`, `repository`, `pullRequestId` |
### Pull Request Write Operations
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `create_pull_request` | Create a new pull request | `workspace`, `repository`, `title`, `source_branch`, `destination_branch`, `description?`, `reviewers?`, `close_source_branch?` |
| `update_pull_request` | Update title, description, reviewers, or destination branch | `workspace`, `repository`, `pullRequestId`, `title?`, `description?`, `reviewers?`, `destination_branch?` |
| `merge_pull_request` | Merge an open pull request | `workspace`, `repository`, `pullRequestId`, `merge_strategy?`, `commit_message?`, `close_source_branch?` |
| `decline_pull_request` | Decline (close) an open pull request | `workspace`, `repository`, `pullRequestId` |
### Review Workflow
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `approve_pull_request` | Approve a pull request | `workspace`, `repository`, `pullRequestId` |
| `unapprove_pull_request` | Remove your approval | `workspace`, `repository`, `pullRequestId` |
| `request_changes_pull_request` | Request changes on a pull request | `workspace`, `repository`, `pullRequestId` |
| `remove_request_changes_pull_request` | Remove a request-changes vote | `workspace`, `repository`, `pullRequestId` |
### Code Changes
| Tool | Description | Key Parameters |
@@ -126,12 +158,20 @@ Restart Claude (or reload MCP servers), then ask Claude to run the `validate_tok
| `get_pull_request_changes` | List modified files | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_commits` | List commits in PR | `workspace`, `repository`, `pullRequestId` |
### Collaboration
### Comments
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `get_pull_request_comments` | Get all comments | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_comment` | Get a specific comment | `workspace`, `repository`, `pullRequestId`, `commentId` |
| `add_pull_request_comment` | Add a general or inline comment | `workspace`, `repository`, `pullRequestId`, `content`, `inline_path?`, `inline_line?`, `parent_comment_id?` |
| `update_pull_request_comment` | Edit an existing comment | `workspace`, `repository`, `pullRequestId`, `commentId`, `content` |
| `delete_pull_request_comment` | Delete a comment | `workspace`, `repository`, `pullRequestId`, `commentId` |
### Activities & Participants
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `get_pull_request_activities` | Get activity feed | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_participants` | Get all participants | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_reviewers` | Get assigned reviewers | `workspace`, `repository`, `pullRequestId` |
@@ -142,8 +182,11 @@ Restart Claude (or reload MCP servers), then ask Claude to run the `validate_tok
|------|-------------|----------------|
| `get_pull_request_tasks` | Get PR tasks | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_task_count` | Get task count | `workspace`, `repository`, `pullRequestId` |
| `create_pull_request_task` | Create a review task | `workspace`, `repository`, `pullRequestId`, `content`, `comment_id?` |
| `update_pull_request_task` | Update or resolve/unresolve a task | `workspace`, `repository`, `pullRequestId`, `taskId`, `content?`, `state?` |
| `delete_pull_request_task` | Delete a task | `workspace`, `repository`, `pullRequestId`, `taskId` |
> **Pagination:** Tools that return lists support `limit` and `start` parameters.
> **Pagination:** Tools that return lists support `page` and `pagelen` parameters.
---
@@ -184,13 +227,17 @@ Response:
### Running Locally
For development, you can use a `.env` file instead of configuring credentials in the MCP client:
For development, you can use a `.env` file instead of configuring credentials in the MCP client. Copy `.env.template` to `.env` and fill in your values:
```env
BITBUCKET_MCP_EMAIL=your_email@example.com
BITBUCKET_MCP_TOKEN=your_api_token
DEFAULT_WORKSPACE=my-workspace
DEFAULT_REPO=my-repo
# Only needed when running write integration tests (see Testing below)
# RUN_WRITE_TESTS=true
# TEST_PR_ID=123
```
```bash
@@ -204,13 +251,13 @@ npm start
### Testing
```bash
# Run tests
# Run all tests (unit only by default)
npm test
# Run tests in watch mode
npm run test:watch
# Integration tests (requires valid credentials)
# Integration tests — read-only, requires valid credentials in .env
npm run test:integration
# Type check
@@ -220,6 +267,15 @@ npx tsc --noEmit
npm run test:coverage
```
**Write operation tests** (approve/unapprove, comments, tasks) are disabled by default and require two additional variables in your `.env`:
| Variable | Description |
|----------|-------------|
| `RUN_WRITE_TESTS=true` | Opt in to running write tests |
| `TEST_PR_ID=<id>` | ID of a PR you own and can safely modify |
`TEST_PR_ID` is mandatory — write tests will never fall back to a random PR from the repository. If it is not set, each write test is skipped with a warning. This prevents accidental modifications to PRs you don't intend to touch.
### Project Structure
```

View File

@@ -11,7 +11,7 @@ export class RepositoryClient extends BaseClient {
const params: Record<string, any> = {};
if (options?.page) params.page = options.page;
if (options?.pagelen) params.pagelen = options.pagelen;
const response = await this.axiosInstance.get('/workspaces', { params });
const response = await this.axiosInstance.get('/user/workspaces', { params });
return response.data;
} catch (error) {
throw new Error(`Failed to list workspaces: ${this.formatError(error)}`);

View File

@@ -294,6 +294,7 @@ describe('Error Handling', () => {
});
const RUN_WRITES = process.env.RUN_WRITE_TESTS === 'true';
const TEST_PR_ID = process.env.TEST_PR_ID ? parseInt(process.env.TEST_PR_ID, 10) : undefined;
describe('Repository / Workspace (read)', () => {
let client: BitbucketClient;
@@ -340,31 +341,28 @@ describe('Repository / Workspace (read)', () => {
});
});
describe('PR Write Operations (requires RUN_WRITE_TESTS=true)', () => {
describe('PR Write Operations (requires RUN_WRITE_TESTS=true and TEST_PR_ID)', () => {
let client: BitbucketClient;
beforeAll(() => {
client = new BitbucketClient();
});
it('should approve a PR', async () => {
it('should approve then immediately unapprove a PR', async () => {
if (!HAS_TOKEN || !RUN_WRITES) return;
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'OPEN' });
if (prs.length === 0) { console.log('No open PRs, skipping'); return; }
const result = await client.approvePullRequest(WORKSPACE, REPO_SLUG, prs[0].id);
expect(result).toBeDefined();
});
if (!TEST_PR_ID) { console.log('⚠️ TEST_PR_ID not set, skipping approve/unapprove test'); return; }
it('should unapprove a PR', async () => {
if (!HAS_TOKEN || !RUN_WRITES) return;
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'OPEN' });
if (prs.length === 0) { console.log('No open PRs, skipping'); return; }
const result = await client.unapprovePullRequest(WORKSPACE, REPO_SLUG, prs[0].id);
expect(result).toHaveProperty('success', true);
await client.approvePullRequest(WORKSPACE, REPO_SLUG, TEST_PR_ID);
try {
const unapproved = await client.unapprovePullRequest(WORKSPACE, REPO_SLUG, TEST_PR_ID);
expect(unapproved).toHaveProperty('success', true);
} finally {
await client.unapprovePullRequest(WORKSPACE, REPO_SLUG, TEST_PR_ID).catch(() => {});
}
});
});
describe('Comment / Task Write Operations (requires RUN_WRITE_TESTS=true)', () => {
describe('Comment / Task Write Operations (requires RUN_WRITE_TESTS=true and TEST_PR_ID)', () => {
let client: BitbucketClient;
beforeAll(() => {
@@ -373,35 +371,33 @@ describe('Comment / Task Write Operations (requires RUN_WRITE_TESTS=true)', () =
it('should add, update, then delete a comment', async () => {
if (!HAS_TOKEN || !RUN_WRITES) return;
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'OPEN' });
if (prs.length === 0) { console.log('No open PRs, skipping'); return; }
const prId = prs[0].id;
if (!TEST_PR_ID) { console.log('⚠️ TEST_PR_ID not set, skipping comment test'); return; }
const added = await client.addPullRequestComment(WORKSPACE, REPO_SLUG, prId, { content: 'Integration test comment' });
const added = await client.addPullRequestComment(WORKSPACE, REPO_SLUG, TEST_PR_ID, { content: 'Integration test comment' });
expect(added).toHaveProperty('id');
const commentId = added.id;
const updated = await client.updatePullRequestComment(WORKSPACE, REPO_SLUG, prId, commentId, { content: 'Updated comment' });
try {
const updated = await client.updatePullRequestComment(WORKSPACE, REPO_SLUG, TEST_PR_ID, commentId, { content: 'Updated comment' });
expect(updated).toHaveProperty('id', commentId);
const deleted = await client.deletePullRequestComment(WORKSPACE, REPO_SLUG, prId, commentId);
expect(deleted).toHaveProperty('success', true);
} finally {
await client.deletePullRequestComment(WORKSPACE, REPO_SLUG, TEST_PR_ID, commentId).catch(() => {});
}
});
it('should create, resolve, then delete a task', async () => {
if (!HAS_TOKEN || !RUN_WRITES) return;
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'OPEN' });
if (prs.length === 0) { console.log('No open PRs, skipping'); return; }
const prId = prs[0].id;
if (!TEST_PR_ID) { console.log('⚠️ TEST_PR_ID not set, skipping task test'); return; }
const created = await client.createPullRequestTask(WORKSPACE, REPO_SLUG, prId, { content: 'Integration test task' });
const created = await client.createPullRequestTask(WORKSPACE, REPO_SLUG, TEST_PR_ID, { content: 'Integration test task' });
expect(created).toHaveProperty('id');
const taskId = created.id;
const resolved = await client.updatePullRequestTask(WORKSPACE, REPO_SLUG, prId, taskId, { state: 'RESOLVED' });
try {
const resolved = await client.updatePullRequestTask(WORKSPACE, REPO_SLUG, TEST_PR_ID, taskId, { state: 'RESOLVED' });
expect(resolved).toBeDefined();
const deleted = await client.deletePullRequestTask(WORKSPACE, REPO_SLUG, prId, taskId);
expect(deleted).toHaveProperty('success', true);
} finally {
await client.deletePullRequestTask(WORKSPACE, REPO_SLUG, TEST_PR_ID, taskId).catch(() => {});
}
});
});