bitbucket-pr-review-mcp
Allows reading Bitbucket Cloud pull requests and posting line-anchored comments plus a summary comment on the pull request.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@bitbucket-pr-review-mcpReview pull request #7 in streamstech/lent-manager"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
bitbucket-pr-review-mcp
An MCP server that lets a language model read a Bitbucket Cloud pull request and leave comments on it — anchored to the lines they are about, plus one summary at the top.
The server supplies material and posts words. It does not form opinions: the calling model does the reviewing, and nothing here holds a review prompt or a model credential.
How it works and why: docs/architecture.md
The decisions behind it: docs/adr/
The language it uses: CONTEXT.md
What it cannot do
It can create and update comments. It cannot approve, decline or merge a pull request, cannot write to branches or files, and never deletes a comment.
Bitbucket does not sell a permission that separates commenting from merging. The credential you give this server is capable of merging your pull requests; nothing about tokens or scopes prevents it. What prevents it is that no tool asks for it, that a single chokepoint in the HTTP client refuses it however the request is constructed, and — the part that does not depend on this code being right — branch restrictions you configure on the repository yourself. See ADR-0002, and read Before you start.
Related MCP server: Bitbucket MCP Server
Before you start
Configure branch restrictions on any repository you allowlist. In Bitbucket: Repository settings → Branch restrictions. Restrict who may merge to your default branch, and require the approvals your team expects. Bitbucket enforces that regardless of what this server does, which makes it the only guarantee here that survives a bug in this repository. It takes a minute and it is the difference between "we believe this code is careful" and "it does not matter if it isn't".
Install
Requires uv and Python 3.13. The same three commands work on Windows, macOS and Linux:
git clone <this repository>
cd bitbucket-pr-review-mcp
uv syncThen tell it which repositories it may touch:
cp config/repositories.yaml.example config/repositories.yaml...and edit it. The file lists workspace/repo entries, holds no secrets, and is meant to
be committed:
repositories:
- streamstech/db-explorer
- streamstech/lent-managerA whole workspace can be written as jantrik/*. It is the widest entry here — it covers
repositories created after you write it — so the server says so at every startup:
repositories:
- jantrik/*The server refuses to start without a list. An absent one is indistinguishable from
permission to touch every repository your credential can reach. Patterns narrower than a
whole workspace (*/db-explorer, streamstech/db-*) are refused for the same reason:
they are guesses about naming, and they take in whatever gets named that way next.
Connecting your Bitbucket account
Run setup and open the link it prints:
uv run bb-pr-mcp --setupIt serves a page on your own machine — loopback only, on a random port, single-use link, gone after five minutes — asking for your Atlassian account email (not your Bitbucket username, and not the name you gave the token) and an API token. It verifies the pair against Bitbucket and shows your display name back before storing anything, then puts the credential in your OS keychain and closes itself.
Create the token at https://id.atlassian.com/manage-profile/security/api-tokens with exactly these four scopes:
Scope | Why |
| So the server knows whose comments are its own — without it, every re-review stacks duplicates |
| Reading files and commits around the change |
| Reading the pull request itself — granular scopes do not nest, so the write scope below does not cover this |
| Posting and updating comments |
Nothing wider. A token that can also write to a repository, administer one or run pipelines is refused at the form and refused again at startup.
Enter the token's expiry date when you set it up and you will be warned a week before it lapses instead of hitting a 401 mid-review.
You never have to run --setup explicitly: with no credential stored, every tool answers
with the setup URL instead of an error.
Check it whenever you like:
uv run bb-pr-mcp --checkThat validates the allowlist, the credential and its scopes, prints who you are posting
as, and exits with a shell status — 0 fine, 1 no usable credential, 2 a token whose
scopes are wrong.
Removing it
uv run bb-pr-mcp --forgetThat deletes the credential from this device's keychain and nothing else — the token still exists at Atlassian until you revoke it there, and the command says so. The next tool call will hand you a fresh setup link.
There is deliberately no tool for this. A tool that deletes the credential is a tool a pull request description can talk a model into calling, and nothing is gained: whoever wants it gone is at a terminal already.
The model never sees the token
The token goes from your browser into the keychain, and from there into an
Authorization header. It is never an argument to a tool, never in a tool's answer,
never in an error message, and not in the setup URL — that carries a different
single-use token, which grants nothing but the right to fill in one form on this machine.
tests/test_the_token_never_reaches_the_model.py goes looking for it in all of those
places.
If the keychain is unavailable
The credential goes in the OS keychain and nowhere else — never a file. On macOS and Windows that works out of the box. On Linux you need a Secret Service (gnome-keyring or KWallet) running and unlocked; if there is none, the server stops rather than falling back to a file (ADR-0003).
Running it
The server speaks MCP over stdio. Point your client at it:
{
"mcpServers": {
"bitbucket-pr-review": {
"command": "uv",
"args": ["run", "--directory", "/path/to/bitbucket-pr-review-mcp", "bb-pr-mcp"]
}
}
}On Windows use the same shape with a Windows path
("C:\\path\\to\\bitbucket-pr-review-mcp"). Nothing else differs between platforms.
For Claude Code:
claude mcp add bitbucket-pr-review -- uv run --directory /path/to/bitbucket-pr-review-mcp bb-pr-mcpRunning it in Docker
The image speaks MCP over stdio like everything else, so there is no port and nothing to
up. Build it, then run it with -i and talk to it.
docker build -t bitbucket-pr-review-mcp:local .A container has no keychain, and the setup page cannot help: it binds a loopback port
inside the container, which your browser cannot reach. So a containerised run is
given its credential instead of storing one. That is a real downgrade — an environment
variable is visible to docker inspect and to anything that can read the process — and it
is a decision rather than a fallback: nothing degrades into it, both variables must be
set, and startup says so every time. See
ADR-0007.
Put the credential in a file that is not in this repository:
BB_MCP_EMAIL=you@yourcompany.com
BB_MCP_API_TOKEN=ATATT...
BB_MCP_TOKEN_EXPIRES_ON=2027-08-24Then check it, and wire it into a client:
docker run --rm \
--env-file /path/to/env.docker \
-v /path/to/repositories.yaml:/config/repositories.yaml:ro \
bitbucket-pr-review-mcp:local --check{
"mcpServers": {
"bitbucket-pr-review": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"--env-file", "/path/to/env.docker",
"-v", "/path/to/repositories.yaml:/config/repositories.yaml:ro",
"bitbucket-pr-review-mcp:local"
]
}
}
}docker-compose.yaml writes the same flags down once: docker compose run --rm bitbucket-pr-review, reading .env.docker (gitignored) from this directory.
A few things worth knowing:
On Windows, use a Windows-style path in
-v(d:/path/to/repositories.yaml:/config/...). Under Git Bash, prefix the command withMSYS_NO_PATHCONV=1or the path is rewritten.The allowlist is mounted, not baked in. It names the repositories the server may touch; that list belongs to whoever runs the image, not to the image.
--setupexits 2 in a container, saying where setup can be run instead. Rotating a token means restarting with a new one.The container runs as a non-root user, read-only, with every capability dropped.
Connecting to the shared server
The shared deployment — several people, one server, each with their own Bitbucket account — runs behind a single origin. Both the MCP endpoint and Keycloak are served from it, because the issuer is a string that has to mean the same thing in the token, the discovery document, the browser and the configuration.
Neither Claude Desktop nor Claude Code reaches the server directly over localhost.
A Claude custom connector is fetched by Anthropic's infrastructure rather than by your
machine, so a server on your laptop is unreachable however it is configured. The local
loop goes through mcp-remote: a stdio bridge
that runs on your machine, performs the OAuth flow in your browser, and speaks HTTP to the
server. Once the server has a public https address, a custom connector reaches it
directly and the bridge is no longer needed.
Which client id goes where
The realm holds three OAuth clients because three different things authenticate, and
they are not interchangeable. Using the wrong one fails at the first step, with
Invalid parameter: redirect_uri on a Keycloak error page — which names the parameter
rather than the client, and is the same message for every cause.
You are configuring | Client id | Secret |
A custom connector, in Claude's settings |
|
|
|
| none — it is a public client |
Nothing. This server uses it itself, for |
|
|
The split follows where the callback lands. A connector's callback is
https://claude.ai/api/mcp/auth_callback, on Anthropic's infrastructure, so that client
is confidential and its secret lives there. The bridge's callback is a loopback port on
somebody's laptop, so that client holds no secret at all — a secret in a config file on a
laptop is not a secret, and PKCE is what protects a loopback flow. Registering the
hosted callback on the public client would hand the confidential flow to a client that
cannot keep anything, so it is not registered, and Keycloak refuses it.
Copy .env.example to .env, then start it:
cp .env.example .env # fill in the secrets; the defaults are the loopback stack
docker compose --profile shared up -dThat brings up Postgres, Keycloak, nginx and the review server. docker compose --profile shared ps should show four healthy containers, and http://localhost:8080/mcp should
answer 401 with a WWW-Authenticate header naming the bitbucket:review scope — an
unauthenticated request being refused is the system working.
Claude Code
Register the bridge once, for every project, with -s user:
claude mcp add bitbucket-pr-review -s user -- npx -y mcp-remote http://localhost:8080/mcp 3334 --allow-http --static-oauth-client-info "{\"client_id\":\"bitbucket-pr-review-cli\"}"-s user writes it to the top-level mcpServers key in ~/.claude.json, which applies
in every directory. The alternatives are -s local (this project only, also in
~/.claude.json, under projects) and -s project (a committed .mcp.json). Do not
register it at more than one scope: the configs are separate, user scope wins, and the
one you edit later may not be the one being used.
Check it:
claude mcp get bitbucket-pr-reviewwhich should report Scope: User config and Status: ✔ Connected. A session picks up
MCP servers when it starts, so an already-running Claude Code will not see a
newly-registered server until it is restarted.
Giving somebody an account
The realm ships one user, dev / dev-only-not-for-production, which is a development
credential and says so. Everybody else needs an account in Keycloak before they can sign
in — that is a separate thing from connecting their Bitbucket account afterwards, which
they do themselves at /connect.
In the admin console. Open http://localhost:8080/admin, sign in as the bootstrap
admin (KC_BOOTSTRAP_ADMIN_USERNAME / KC_BOOTSTRAP_ADMIN_PASSWORD from your .env),
switch the realm picker from master to streamstech, then Users → Add user. Fill in
username, email, first name and last name, tick Email verified, and create. Then
Credentials → Set password, and turn Temporary off unless you want them prompted to
change it at first login.
Or from the command line, which is easier to repeat:
docker exec bitbucket-pr-review-mcp-keycloak-1 /opt/keycloak/bin/kcadm.sh \
config credentials --server http://localhost:8080 --realm master \
--user admin --password "$KC_BOOTSTRAP_ADMIN_PASSWORD"
docker exec bitbucket-pr-review-mcp-keycloak-1 /opt/keycloak/bin/kcadm.sh \
create users -r streamstech \
-s username=somebody -s email=somebody@example.com -s emailVerified=true \
-s firstName=Some -s lastName=Body -s enabled=true
docker exec bitbucket-pr-review-mcp-keycloak-1 /opt/keycloak/bin/kcadm.sh \
set-password -r streamstech --username somebody --new-password 'their-password'Under Git Bash, prefix each of these with MSYS_NO_PATHCONV=1 or /opt/keycloak/... is
rewritten into a Windows path and docker exec reports that the file does not exist.
Two things that are easy to get wrong, both of which fail at login rather than at creation, with a message that does not point at the cause:
First and last name are required. Keycloak's user profile treats them as mandatory, so an account created without them authenticates with
invalid_grant: Account is not fully set up. Nothing warns you when the account is made.kcadm.sh set-passwordwithout-tis already permanent; passing-tmakes it temporary and leaves the same required action pending.
No roles or group memberships are needed. A new account gets default-roles-streamstech
automatically, and that is enough — this server authorizes on the bitbucket:review
scope, which is requested during the OAuth flow and consented to, not granted in advance.
Accounts live in Keycloak's database, which is a volume. They survive restarts, and they
do not survive docker volume rm bitbucket-pr-review-mcp_keycloak-db.
Claude Desktop
Claude Desktop has no mcp add command; you edit claude_desktop_config.json by hand.
Where it lives depends on how Claude was installed:
Install | Path |
Windows |
|
Windows, Microsoft Store |
|
macOS |
|
The Store path is the one that catches people out — an installation from the Store ignores
the %APPDATA% file entirely, and editing the wrong one changes nothing with no error.
{
"mcpServers": {
"bitbucket-pr-review": {
"command": "cmd",
"args": [
"/c", "npx",
"-y", "mcp-remote",
"http://localhost:8080/mcp",
"3335",
"--allow-http",
"--static-oauth-client-info", "{\"client_id\":\"bitbucket-pr-review-cli\"}"
]
}
}
}deploy/claude_desktop_config.example.json holds the same thing. Two details in there are
doing real work:
cmd /cin front ofnpx, on Windows. Claude Desktop does not spawn through a shell, so a bare"command": "npx"resolves to a batch file whose own path contains a space, and the whole thing dies with'C:\Program' is not recognized as an internal or external commandin the log below. On macOS, dropcmdand/cand use"command": "npx".Port
3335rather than3334. That number is the bridge's own loopback port, and Claude Code's registration already uses3334. Two bridges on one port means whichever starts second cannot receive its OAuth callback. The realm registershttp://127.0.0.1:*/oauth/callback, so any free port works.
Restart Claude Desktop — fully, from the tray, since closing the window leaves it running — and check it came up:
tail -f "$LOCALAPPDATA/Packages/Claude_*/LocalCache/Roaming/Claude/logs/mcp-server-bitbucket-pr-review.log"Proxy established successfully between local STDIO and remote StreamableHTTPClientTransport is the line that means the bridge is talking to the server.
Server transport closed unexpectedly means it exited instead — the reason is a few lines
above it.
The first tool call then opens a Keycloak login (dev / dev-only-not-for-production in
the development realm, or an account you made above). After signing in, a tool call
answers with a link to /connect, where you connect your Atlassian account — that page
makes the browser sign in too, which is why the link is safe to see in a transcript.
A few things worth knowing:
The bridge is a public OAuth client, with no secret. A client secret in a config file on a laptop is not a secret; PKCE is what protects a loopback flow.
Its callback is
http://127.0.0.1:3334/oauth/callback— the IP literal rather thanlocalhost, and/oauth/callbackrather than Claude Code's/callback. The realm registers all of them, because getting it wrong fails at the last step of the flow.--allow-httpis required while the server is on plain http. A real deployment is https, and this server refuses to describe itself over http anywhere but loopback.The
3334argument is the bridge's own port, and the realm registers the callback on it. Two Claude clients bridging at once want different ports.The
sharedprofile's defaults are development configuration — a bootstrap admin whose password is indocker-compose.yaml, a realm carrying a user whose password is in the realm file, and plain http on loopback. Every one of those is a variable with a default, so a real deployment overrides them in.envrather than editing either file. See docs/deploying-the-shared-server.md.Changing the origin needs the realm imported again. The realm is imported once, into Keycloak's database;
--import-realmleaves an existing realm alone. Drop that volume by name —docker volume rm bitbucket-pr-review-mcp_keycloak-db— and neverdown -v, which would take the credential vault with it.
Operating it
uv run bb-pr-mcp --health
uv run bb-pr-mcp --rotate-key /path/to/new.key--health says whether the deployment is fit to run — TLS, the vault key, the store, the
allowlist, whether the authorization server is reachable, and how many people are
connected — and exits 0 healthy, 1 something to look at, 2 this will not start.
--rotate-key re-seals every stored credential under a new key without anybody
re-enrolling, then tells you the order to do the rest in.
Read docs/deploying-the-shared-server.md before running this anywhere real. It states what one compromise of the host costs, which is larger than it looks, and what to do about it.
Who is connected, and taking somebody off
uv run bb-pr-mcp --who
uv run bb-pr-mcp --revoke alice@streamstech.com--who lists everybody who has connected a Bitbucket account: the opaque id, the
Atlassian email, when they connected, and when their token expires. It decrypts the vault
to answer, and then prints everything except the one field worth decrypting for.
--revoke deletes one person's stored credential. It takes an email or enough of the
opaque id to be unambiguous, and refuses rather than guessing when a name matches two
people. Revoking takes effect on the next tool call, including on a server that is
already running — the shared server reads the credential through rather than holding it,
precisely so that an operator in another terminal is not waiting for a restart.
What it does not do is the part worth reading. Three places hold something after somebody leaves, and this command owns one of them:
Here. The stored credential is gone.
Keycloak. They can still sign in and connect a new token. Disable their account there to stop that.
Atlassian. Their API token still exists and still works everywhere else. Only they, or an Atlassian admin, can revoke it.
The command says all three every time, because an offboarding checklist that gets ticked after step one is worse than no checklist.
Neither is a tool, and that is deliberate: a pull request description must not be able to talk a Caller into disconnecting a colleague.
The tools
Tool | What it does |
| Title, state, author, branches, and the Review Basis |
| Every changed file with counts, flagging binary, generated and lockfile entries |
| The whole diff, or one file — with an anchor gutter showing each line's number |
| The existing conversation, with anchors, stale flags, and which comments are the server's own |
| Post one finding or a whole review; validated entirely before any of it is sent |
| Post or update the one summary comment |
| Metadata and the default branch |
| Any file in an allowlisted repository, at a ref |
| A directory listing, at a ref |
| History for a ref or a pull request |
| Code search inside one allowlisted repository |
A pull request is named by one string: either a Bitbucket URL or the shorthand
workspace/repo/id.
Configuration
Everything below has a working default. Set them in the environment or a .env file, all
prefixed BB_MCP_:
Setting | Default | What it does |
|
| Where the allowlist lives |
|
| Logs go to stderr, never stdout |
|
| One JSON object per line, for shipping to an aggregator |
|
| Per-request timeout |
|
| Diff response ceiling |
|
| File response ceiling |
|
| Manifest rows |
|
| Directory rows |
|
| Commit rows |
|
| Search matches |
|
| Comments read per pull request |
Two more exist and are empty until the shared deployment is finished — the per-device install described above needs neither, because stdio has exactly one caller:
Setting | What it does |
| The address Claude connects to, exactly as it is typed into the connector. It is what tokens must name as their audience |
| The Keycloak realm that issues those tokens. It must match the issuer in the realm's discovery document exactly — a trailing slash is a difference |
| Where the per-person credentials are kept. Encrypted under |
| The Keycloak client this server signs people in with, so the page that collects an API token can ask who they are |
| That client's secret. Required for the connect page; without it a caller with no credential is told setup is unavailable rather than sent somewhere useless |
Every ceiling is stated in the response when it bites. Truncation is never silent: a Caller that cannot tell a truncated diff from a complete one will review the missing half by assuming it was fine.
Development
uv run pytest # the suite
uv run pytest --cov=src # with coverage
uv run ruff check src tests # lint
uv run ruff format src tests # formatTests never touch the network. The seam is the HTTP transport and nothing else, so the
guard, the allowlist and every response reader are exercised as production code.
tests/recorded/ holds responses captured from a real pull request — see
docs/architecture.md for why that matters more than it
sounds.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceEnables AI assistants to interact with Bitbucket Cloud repositories, allowing users to manage pull requests, comments, tasks, and branches through natural language commands.4,1381MIT
- AlicenseAqualityDmaintenanceEnables management of Bitbucket Cloud pull requests through natural language, including creating, reviewing, approving, and commenting on PRs with automatic default reviewer support.791MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to programmatically manage Bitbucket Cloud resources, including pull requests, repositories, and branches, automating code review workflows.12MIT
- AlicenseAqualityDmaintenanceEnables LLMs to review Bitbucket pull requests with custom checklists and API token authentication.51MIT
Related MCP Connectors
A Model Context Protocol (MCP) application for automated GitHub PR analysis and issue management.…
Screens public GitHub repos and PRs to generate risk maps, findings, and merge-readiness signals.
Risk-scan a diff, flag AI-generated-code tells, find secrets. 5 of 7 tools need no account.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/6shihab/bitbucket-pr-review-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server