Bitbucket MCP Server
by bobmaertz
README.md
# Bitbucket MCP Server
Read-only [MCP](https://modelcontextprotocol.io/) server for **Bitbucket Cloud** — lets Claude and other LLM clients browse repositories, source files, commits, pull requests, comments, tasks, branches, tags, CI statuses, pipelines, deployments, projects, and workspaces. Token-sparse output, no write access, credential never leaves the client layer.
## Quickstart
You need a Bitbucket Cloud (Atlassian) **API token with repository read scopes** (`read:repository:bitbucket`, plus `read:pullrequest:bitbucket` for PRs/comments/tasks) — no write scopes. Mint one at <https://id.atlassian.com/manage-profile/security/api-tokens>, then add to your MCP client (e.g. `claude_desktop_config.json`):
```json
{
"mcpServers": {
"bitbucket": {
"command": "npx",
"args": ["-y", "@bobmaertz/bitbucket-mcp"],
"env": {
"BITBUCKET_WORKSPACE": "your-workspace",
"BITBUCKET_EMAIL": "you@example.com",
"BITBUCKET_API_TOKEN": "your-api-token"
}
}
}
}
```
Then ask: _"List the open PRs in acme/widgets"_ · _"Show the comments on PR #42"_.
## Configuration
Environment variables. `LOG_LEVEL` (default `info`) and `BITBUCKET_ALLOW_WRITES` (reserved, off) are optional. See [`.env.example`](./.env.example).
Auth is resolved in precedence order: an **access token** (Bearer) wins, then the **API-token** pair (Basic), then the deprecated **app-password** pair (Basic). Supplying `BITBUCKET_ACCESS_TOKEN` is recommended for shared/automated use — workspace, project, and repository [Access Tokens](https://support.atlassian.com/bitbucket-cloud/docs/access-tokens/) get Bitbucket's _scaled_ rate limits (up to 10,000 req/hr vs 1,000 for a user API token). The server also surfaces a warning to stderr when Bitbucket signals the quota is nearly exhausted (`X-RateLimit-NearLimit`).
| Variable | Notes |
| ----------------------------------------------- | -------------------------------------------------------------------------------------- |
| `BITBUCKET_WORKSPACE` | Default workspace ID (required). |
| `BITBUCKET_ACCESS_TOKEN` | Workspace/project/repo Access Token (Bearer). Preferred; scaled rate limits. |
| `BITBUCKET_EMAIL` + `BITBUCKET_API_TOKEN` | User API token (Basic). Minted at id.atlassian.com. Used when no access token is set. |
| `BITBUCKET_USERNAME` + `BITBUCKET_APP_PASSWORD` | Deprecated app-password fallback. App Passwords are being removed by Atlassian (2026). |
## Logging
All logs go to **stderr** — stdout is reserved for the MCP protocol, so the server never writes log lines there and never writes its own log files. Where stderr ends up depends on how the server is run:
- **Claude Desktop** captures each MCP server's stderr to a log file: `~/Library/Logs/Claude/mcp-server-bitbucket.log` on macOS, `%APPDATA%\Claude\logs\mcp-server-bitbucket.log` on Windows (the filename uses the server's key from your config).
- **Claude Code** shows MCP server stderr when run with `claude --debug`, or via `/mcp` for server status.
- **Run directly** (e.g. `npx @bobmaertz/bitbucket-mcp` in a terminal), log lines appear in the terminal.
Lines are prefixed with their level (`[debug]`, `[info]`, `[warn]`, `[error]`); `LOG_LEVEL` (default `info`) sets the minimum level emitted. What gets logged: startup configuration at `debug` (useful for troubleshooting auth/workspace resolution), deprecation and rate-limit warnings at `warn`, and failed tool calls at `error`. Every line is scrubbed for credential-looking substrings before it is written, so tokens and passwords never appear in logs — even at `debug`.
## Tools
Read-only. `workspace` defaults to `BITBUCKET_WORKSPACE`; repo-scoped tools require `repo`.
Every list tool additionally takes `sort?`, plus `since?` / `until?` wherever its items carry a date — see [Sorting and date filtering](#sorting-and-date-filtering) for the fields each one accepts.
| Tool | Key inputs |
| ------------------------------------- | ----------------------------------------------------------------------------------- |
| `bitbucket_list_repositories` | `workspace?`, `query?`, `sort?`, `page?` |
| `bitbucket_get_repository` | `workspace?`, `repo` |
| `bitbucket_list_pull_requests` | `repo`, `state?` (default `OPEN`), `query?`, `page?` |
| `bitbucket_list_user_pull_requests` | `workspace?`, `user?` (default: me), `state?`, `sort?`, `pagelen?`, `max_pages?` |
| `bitbucket_whois` | `workspace?`, `users` (one id or an array of ids) |
| `bitbucket_get_pull_request` | `repo`, `id` |
| `bitbucket_get_pr_commits` | `repo`, `id`, `page?` |
| `bitbucket_get_pr_diff` | `repo`, `id`, `max_lines?` (default 200), `path?`, `context?` |
| `bitbucket_list_pr_comments` | `repo`, `id`, `page?` |
| `bitbucket_get_comment` | `repo`, `pr_id`, `comment_id` |
| `bitbucket_list_pr_tasks` | `repo`, `id`, `page?` |
| `bitbucket_get_task` | `repo`, `pr_id`, `task_id` |
| `bitbucket_list_branches` | `repo`, `query?`, `sort?`, `page?` |
| `bitbucket_get_branch` | `repo`, `name` |
| `bitbucket_list_pipelines` | `repo`, `branch?`, `pull_request_id?`, `status?`, `sort?`, `page?` |
| `bitbucket_get_pipeline` | `repo`, `pipeline` (build number or UUID) |
| `bitbucket_list_pipeline_steps` | `repo`, `pipeline`, `page?` |
| `bitbucket_get_step_log` | `repo`, `pipeline`, `step`, `tail?`, `grep?`, `max_bytes?` |
| `bitbucket_list_schedules` | `repo`, `page?` |
| `bitbucket_list_directory` | `repo`, `commit?` (default main), `path?`, `max_depth?`, `query?`, `sort?`, `page?` |
| `bitbucket_get_file` | `repo`, `path`, `commit?` (default main), `max_lines?`, `max_bytes?` |
| `bitbucket_get_file_history` | `repo`, `path`, `commit?` (default main), `renames?`, `page?` |
| `bitbucket_list_commits` | `repo`, `at?`, `path?`, `page?` |
| `bitbucket_get_commit` | `repo`, `commit` |
| `bitbucket_get_commit_diff` | `repo`, `spec` (hash or `a..b`), `max_lines?`, `path?`, `context?` |
| `bitbucket_get_diffstat` | `repo`, `spec` (hash or `a..b`), `path?`, `page?` |
| `bitbucket_list_tags` | `repo`, `query?`, `sort?`, `page?` |
| `bitbucket_get_tag` | `repo`, `name` |
| `bitbucket_get_pr_diffstat` | `repo`, `id`, `page?` |
| `bitbucket_list_pr_statuses` | `repo`, `id`, `query?`, `sort?`, `page?` |
| `bitbucket_list_commit_statuses` | `repo`, `commit`, `page?` |
| `bitbucket_get_pr_activity` | `repo`, `id`, `page?` |
| `bitbucket_list_commit_pull_requests` | `repo`, `commit`, `page?` |
| `bitbucket_get_test_reports` | `repo`, `pipeline` (build number or UUID), `step` |
| `bitbucket_list_workspaces` | `page?` |
| `bitbucket_list_projects` | `workspace?`, `query?`, `sort?`, `page?` |
| `bitbucket_get_project` | `workspace?`, `key` |
| `bitbucket_list_deployments` | `repo`, `page?` |
| `bitbucket_list_environments` | `repo`, `page?` |
| `bitbucket_get_branching_model` | `repo` |
| `bitbucket_list_workspace_members` | `workspace?`, `page?` |
`bitbucket_list_repositories` needs no args: omit `workspace` to list the configured `BITBUCKET_WORKSPACE`, or pass `workspace` to scope to another. There is no cross-workspace listing — Atlassian retired both `GET /repositories` and `GET /workspaces` under CHANGE-2770.
`bitbucket_list_user_pull_requests` lists **all pull requests authored by a user across a whole workspace in one aggregated call** — no more listing every repo and querying each. It auto-follows pagination (up to `max_pages`, default 10) and sorts newest-updated first (`-updated_on`). Omit `user` for the authenticated account ("my" PRs); otherwise `user` may be an account UUID (`{…}`), an Atlassian `account_id`, **or a natural display name / nickname** — the latter is resolved against the workspace's members and must match exactly one of them. It covers **authored** PRs only; reviewer-only involvement isn't included. Backed by `GET /workspaces/{workspace}/pullrequests/{selected_user}`.
`bitbucket_whois` goes the other way — hand it **one or many** account UUIDs / `account_id`s (as `users`, a single string or an array) and it returns each one's natural name (`display_name`, `nickname`) plus both ids, resolved via the workspace's members. Unknown ids come back as `{ query, error }` rather than failing the batch, so an author UUID seen in a PR listing can always be turned back into a human name. IDs only here; to look someone up _by_ name, pass the name straight to `bitbucket_list_user_pull_requests`. Backed by `GET /workspaces/{workspace}/members/{member}`.
`bitbucket_get_pr_diff` scopes the diff server-side: pass `path` (one or more files) and/or `context` (lines around each hunk) to fetch just what you need instead of downloading the whole diff and trimming it.
The source and commit tools give an agent eyes on the repository. `bitbucket_list_directory` and `bitbucket_get_file` default `commit` to the repo's main branch (and echo the resolved `ref`), so you can browse without knowing the default branch; use `max_depth` to pull a recursive tree in one call. `bitbucket_get_file` caps content by `max_bytes`/`max_lines` and returns `binary: true` (no content) for binary files. For "what changed", prefer `bitbucket_get_diffstat` (a cheap per-file summary) before pulling `bitbucket_get_commit_diff`, which supports the same `path`/`context` scoping as the PR diff.
For CI, `bitbucket_list_pr_statuses` and `bitbucket_list_commit_statuses` surface build/check results from any reporting system (not just Bitbucket Pipelines), and `bitbucket_get_test_reports` turns a failed pipeline step into a summary of pass/fail counts plus the named failing tests and their failure reasons — no log grepping. `bitbucket_get_pr_activity` gives a normalized timeline of updates, approvals, change requests, and comments, and `bitbucket_list_commit_pull_requests` answers "which PR introduced this commit?".
For discovery and governance, `bitbucket_list_workspaces` finds the workspace IDs you can pass elsewhere, `bitbucket_list_projects` / `bitbucket_get_project` and `bitbucket_list_workspace_members` cover project and membership metadata, `bitbucket_list_deployments` / `bitbucket_list_environments` give release visibility, and `bitbucket_get_branching_model` reports the repo's development/production branch conventions.
### Sorting and date filtering
Every list tool takes the same three optional inputs:
- **`sort`** — a field name from the table below, `-` prefixed for descending (`sort: "-updated_on"` = most recently updated first). An unrecognized field fails immediately with the list of valid ones, rather than reaching Bitbucket as an opaque `400`.
- **`since` / `until`** — an inclusive date window over the collection's date field. Accepts `YYYY-MM-DD` or a full ISO-8601 timestamp; a bare date covers the whole day, so `until: "2026-07-01"` includes July 1st.
- **`date_field`** — which date `since`/`until` apply to, for the collections that have more than one (pull requests, repositories, projects, statuses, tasks). Defaults to the first listed.
Where the work happens depends on the endpoint, and the response says which:
| Scope | What it means |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`server`** | Bitbucket sorts/filters the whole collection (`sort=` / `q=`). Ordering holds across pages. |
| **`page`** | The endpoint has no server-side sort (git-backed listings, PR activity, deployments…), so the fetched page is sorted/filtered locally. `total` and `has_more` still describe the unfiltered collection — page through for more. |
A list that was sorted or filtered echoes what happened, so a page-local ordering is never mistaken for a global one:
```json
{
"items": [],
"page": 1,
"has_more": false,
"sort": { "by": "-date", "applied": "page" },
"filter": { "field": "date", "since": "2026-05-01T00:00:00+00:00", "applied": "page" }
}
```
Untouched lists are byte-for-byte what they were before — the `sort`/`filter` keys appear only when you ask for them.
| Tool | Scope | Sort fields | `since`/`until` on | Default |
| ------------------------------------- | ------ | -------------------------------------------------- | -------------------------- | ------------- |
| `bitbucket_list_repositories` | server | `created_on`, `updated_on`, `name`, `size` | `updated_on`, `created_on` | — |
| `bitbucket_list_pull_requests` | server | `created_on`, `updated_on`, `id`, `title`, `state` | `updated_on`, `created_on` | — |
| `bitbucket_list_user_pull_requests` | server | `created_on`, `updated_on`, `id`, `title`, `state` | `updated_on`, `created_on` | `-updated_on` |
| `bitbucket_list_projects` | server | `created_on`, `updated_on`, `name`, `key` | `updated_on`, `created_on` | — |
| `bitbucket_list_branches` | server | `name`, `date`, `target.date` | `date` | — |
| `bitbucket_list_tags` | server | `name`, `date`, `target.date` | `date` | — |
| `bitbucket_list_pr_tasks` | server | `id`, `created_on`, `updated_on`, `state` | `created_on`, `updated_on` | — |
| `bitbucket_list_pr_statuses` | server | `created_on`, `updated_on`, `key`, `name`, `state` | `updated_on`, `created_on` | — |
| `bitbucket_list_commit_statuses` | server | `created_on`, `updated_on`, `key`, `name`, `state` | `updated_on`, `created_on` | — |
| `bitbucket_list_directory` | server | `path`, `size`, `type` | — | — |
| `bitbucket_get_file_history` | server | `date`, `commit.date`, `path` | `date` | — |
| `bitbucket_list_pipelines` | server | `created_on`, `build_number` | `created_on` | `-created_on` |
| `bitbucket_list_pr_comments` | page | `created_on`, `id`, `author` | `created_on` | — |
| `bitbucket_get_pr_commits` | page | `date`, `hash`, `author` | `date` | — |
| `bitbucket_get_pr_activity` | page | `date`, `kind`, `user` | `date` | — |
| `bitbucket_get_pr_diffstat` | page | `path`, `status`, `lines_added`, `lines_removed` | — | — |
| `bitbucket_list_commits` | page | `date`, `hash`, `author` | `date` | — |
| `bitbucket_get_diffstat` | page | `path`, `status`, `lines_added`, `lines_removed` | — | — |
| `bitbucket_list_commit_pull_requests` | page | `updated_on`, `id`, `title`, `state` | `updated_on` | — |
| `bitbucket_list_pipeline_steps` | page | `started_on`, `name`, `duration_in_seconds` | `started_on` | — |
| `bitbucket_list_schedules` | page | `cron_pattern`, `enabled`, `type` | — | — |
| `bitbucket_list_deployments` | page | `updated_on`, `environment`, `state` | `updated_on` | — |
| `bitbucket_list_environments` | page | `name`, `type`, `category` | — | — |
| `bitbucket_list_workspaces` | page | `name`, `slug` | — | — |
| `bitbucket_list_workspace_members` | page | `name`, `nickname` | — | — |
A collection is only `server` when Bitbucket's own OpenAPI spec declares `q`/`sort` on that path (or the client already depended on it), so the split tracks what the API actually supports rather than what we'd like it to. The table is generated from a single source of truth — `LISTINGS` in `bitbucket-core/src/listing.ts` — which also generates the tool schemas, so the advertised fields can't drift from the accepted ones.
### Efficiency
Every list/get request asks Bitbucket for a [partial response](https://developer.atlassian.com/cloud/bitbucket/rest/intro/#partial-responses) (`fields=`) matching exactly what the presenter keeps, so the payload is trimmed at the source rather than after download. Step logs are tailed with an HTTP `Range` request (only the last window of bytes is transferred unless you `grep`, which scans the whole log). Combined with per-request field selection and `q`/`sort` filtering, this keeps both token usage and API round-trips low — important against Bitbucket's hourly [rate limits](https://support.atlassian.com/bitbucket-cloud/docs/api-request-limits/).
## Development
Monorepo: **`bitbucket-api`** (REST client) → **`bitbucket-core`** (config, presenters, operations) → **`bitbucket-mcp-server`** (MCP adapter).
```bash
npm install && npm run build
npm test # unit + hermetic HTTP integration
npm run test:contract # validate against the vendored Bitbucket OpenAPI spec
```
API audit and roadmap: [docs/API_AUDIT_AND_ROADMAP.md](./docs/API_AUDIT_AND_ROADMAP.md).
## License
MIT
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues