Compare commits

..

3 Commits

Author SHA1 Message Date
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
4 changed files with 100 additions and 43 deletions

11
.env.template Normal file
View File

@@ -0,0 +1,11 @@
BITBUCKET_MCP_EMAIL=your_email@example.com
BITBUCKET_MCP_TOKEN=your_api_token
DEFAULT_WORKSPACE=your-workspace
DEFAULT_REPO=your-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 tsconfig.tsbuildinfo
# Environment # Environment
.env *.env
.env.local *.env.*
.env.*.local !*.env.template
# IDE # IDE
.idea/ .idea/

View File

@@ -15,16 +15,19 @@
## Overview ## 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 ### Capabilities
| Category | Operations | | 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 | | **Code Review** | Diff, patch, file changes, commits |
| **Collaboration** | Comments, activities, participants, reviewers | | **Collaboration** | Comments (read/add/edit/delete), activities, participants, reviewers |
| **Workflow** | Status, tasks, task count | | **Review Workflow** | Approve, unapprove, request changes, remove request-changes |
| **Tasks** | Get tasks, task count, create/update/delete tasks |
| **Auth** | Token validation | | **Auth** | Token validation |
--- ---
@@ -108,6 +111,15 @@ Restart Claude (or reload MCP servers), then ask Claude to run the `validate_tok
|------|-------------| |------|-------------|
| `validate_token` | Verify credentials are valid | | `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 ### Pull Request Discovery
| Tool | Description | Key Parameters | | Tool | Description | Key Parameters |
@@ -117,6 +129,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_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` | | `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 ### Code Changes
| Tool | Description | Key Parameters | | Tool | Description | Key Parameters |
@@ -126,12 +156,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_changes` | List modified files | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_commits` | List commits in PR | `workspace`, `repository`, `pullRequestId` | | `get_pull_request_commits` | List commits in PR | `workspace`, `repository`, `pullRequestId` |
### Collaboration ### Comments
| Tool | Description | Key Parameters | | Tool | Description | Key Parameters |
|------|-------------|----------------| |------|-------------|----------------|
| `get_pull_request_comments` | Get all comments | `workspace`, `repository`, `pullRequestId` | | `get_pull_request_comments` | Get all comments | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_comment` | Get a specific comment | `workspace`, `repository`, `pullRequestId`, `commentId` | | `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_activities` | Get activity feed | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_participants` | Get all participants | `workspace`, `repository`, `pullRequestId` | | `get_pull_request_participants` | Get all participants | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_reviewers` | Get assigned reviewers | `workspace`, `repository`, `pullRequestId` | | `get_pull_request_reviewers` | Get assigned reviewers | `workspace`, `repository`, `pullRequestId` |
@@ -142,8 +180,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_tasks` | Get PR tasks | `workspace`, `repository`, `pullRequestId` |
| `get_pull_request_task_count` | Get task count | `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.
--- ---
@@ -204,13 +245,13 @@ npm start
### Testing ### Testing
```bash ```bash
# Run tests # Run all tests (unit only by default)
npm test npm test
# Run tests in watch mode # Run tests in watch mode
npm run test:watch npm run test:watch
# Integration tests (requires valid credentials) # Integration tests — read-only, requires valid credentials in .env
npm run test:integration npm run test:integration
# Type check # Type check
@@ -220,6 +261,15 @@ npx tsc --noEmit
npm run test:coverage npm run test:coverage
``` ```
**Write operation tests** (approve/unapprove, comments, tasks) are disabled by default. To enable them, set two variables in your `.env`:
```env
RUN_WRITE_TESTS=true
TEST_PR_ID=123 # ID of a PR you own and can safely modify
```
`TEST_PR_ID` is required — write tests will not fall back to a random PR. If it is not set, each write test is skipped with a warning. This prevents accidental modifications to production PRs.
### Project Structure ### Project Structure
``` ```

View File

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