bitbucket-mcp
1.1.0
{ "inputs": [ { "type": "promptString", "id": "BITBUCKET_OAUTH_KEY", "description": "OAuth consumer key of your own Bitbucket Cloud OAuth consumer. Together with BITBUCKET_OAUTH_SECRET this enables the browser login flow." }, { "type": "promptString", "id": "BITBUCKET_OAUTH_SECRET", "description": "OAuth consumer secret matching BITBUCKET_OAUTH_KEY.", "password": true }, { "type": "promptString", "id": "BITBUCKET_ACCESS_TOKEN", "description": "Bitbucket Cloud access token sent as a Bearer credential. Takes precedence over every other credential and skips the OAuth flow.", "password": true }, { "type": "promptString", "id": "BITBUCKET_EMAIL", "description": "Atlassian account e-mail, used with BITBUCKET_API_TOKEN as Basic credentials." }, { "type": "promptString", "id": "BITBUCKET_API_TOKEN", "description": "Atlassian API token, used with BITBUCKET_EMAIL as Basic credentials.", "password": true }, { "type": "promptString", "id": "BITBUCKET_DEFAULT_WORKSPACE", "description": "Workspace slug used when a tool call omits the workspace argument." } ], "servers": { "bitbucket-mcp": { "type": "stdio", "command": "dnx", "args": ["bitbucket-mcp@1.1.0", "--yes"], "env": { "BITBUCKET_OAUTH_KEY": "${input:BITBUCKET_OAUTH_KEY}", "BITBUCKET_OAUTH_SECRET": "${input:BITBUCKET_OAUTH_SECRET}", "BITBUCKET_ACCESS_TOKEN": "${input:BITBUCKET_ACCESS_TOKEN}", "BITBUCKET_EMAIL": "${input:BITBUCKET_EMAIL}", "BITBUCKET_API_TOKEN": "${input:BITBUCKET_API_TOKEN}", "BITBUCKET_DEFAULT_WORKSPACE": "${input:BITBUCKET_DEFAULT_WORKSPACE}" } } } }
.vscode/mcp.json settings file.
dotnet tool install --global bitbucket-mcp --version 1.1.0
dotnet new tool-manifest
dotnet tool install --local bitbucket-mcp --version 1.1.0
#tool dotnet:?package=bitbucket-mcp&version=1.1.0
nuke :add-package bitbucket-mcp --version 1.1.0
bitbucket-mcp
A self-owned Model Context Protocol server for Bitbucket
Cloud, covering the full pull-request lifecycle in sixteen tools — list, read, diff, comment,
resolve, task, review, create, update, merge, decline — including the gaps in Atlassian's own
Bitbucket tools (update, decline, request changes, unapprove, diffstat, inline comments, thread
resolution, tasks, build statuses, default reviewers). It is written in C# on
.NET 10 and ships as a Native AOT binary per platform: one self-contained executable, about 18 MB
on win-x64 (6 MB in the release archive), starting in roughly ten milliseconds. The point of the
project is a supply chain one person can actually audit — the whole runtime dependency tree is
four packages, all from Microsoft or the official MCP organisation:
ModelContextProtocol (pinned exactly to
2.1.0), Microsoft.Extensions.DependencyInjection, Microsoft.Extensions.Logging.Console and
System.Security.Cryptography.ProtectedData. Tool names follow Atlassian's camelCase verbNoun
convention, so it sits alongside the official Atlassian MCP server without a naming clash. MIT
licensed.
Bitbucket Data Center is explicitly out of scope.
Tools
| Tool | What it does | Annotations |
|---|---|---|
listPullRequests |
Lists a repository's pull requests, most recently updated first — a summary per pull request (id, title, state, author, branches, closeSourceBranch, web URL). Defaults to open ones only; sourceBranch answers "does this branch already have a pull request?". |
read-only, idempotent |
getPullRequest |
Reads one pull request in full: title, description, state, branches, web URL, reviewers and participants with their approvals. | read-only, idempotent |
getPullRequestDiff |
Fetches a pull request's changes: with no arguments it lists the changed files, and passing paths returns those files' unified diff. Truncation is always marked inline. |
read-only, idempotent |
getPullRequestComments |
Lists a pull request's comments — general and inline — oldest first, with deleted comments filtered out. | read-only, idempotent |
listDefaultReviewers |
Lists the repository's effective default reviewers with their account UUIDs, including the ones inherited from its project. Where reviewer UUIDs come from when there is no pull request to read them off. | read-only, idempotent |
listPullRequestStatuses |
Lists the build statuses reported against a pull request — the merge-readiness check to run before mergePullRequest. |
read-only, idempotent |
listPullRequestTasks |
Lists a pull request's tasks, RESOLVED or UNRESOLVED — the outstanding work, which the comments alone do not tell you. |
read-only, idempotent |
createPullRequest |
Opens a new pull request. Title and source branch are required; reviewers are account UUIDs. | write, not destructive |
updatePullRequest |
Changes an existing pull request's title, description, destination branch, reviewer list, closeSourceBranch flag or draft status. reviewers replaces the whole list; the two flags are only reachable here once the pull request is open. |
write, destructive |
addPullRequestComment |
Posts a comment: general, a reply, or inline on a line of the diff — anchored by codeSnippet copied verbatim out of the diff, or by line plus lineType. |
write, not destructive |
resolvePullRequestComment |
Marks a comment thread resolved, or reopens it — any top-level comment, inline or on the pull request as a whole. Asking for the state it is already in is not an error. | write, not destructive, idempotent |
addPullRequestTask |
Adds a task — one tracked item of work, optionally hung off a comment. Bitbucket counts these, and a repository can require them resolved before merging. | write, not destructive |
updatePullRequestTask |
Ticks a task off, reopens it, or rewrites its text. | write, not destructive, idempotent |
setPullRequestReviewStatus |
Sets the authenticated user's own review state: APPROVED, CHANGES_REQUESTED or UNAPPROVED (clears both flags). |
write, not destructive, idempotent |
mergePullRequest |
Merges a pull request into its destination branch, with an optional merge strategy. | write, destructive |
declinePullRequest |
Declines a pull request, closing it without merging. | write, destructive |
Every tool is annotated open-world (it talks to a live Bitbucket workspace) and returns structured
content. Destructive defaults to true in the MCP SDK, so the six write tools that do not
destroy anything say otherwise explicitly — a client should prompt before a merge, not before a
comment. The full annotation table, and the two rows that are judgement calls rather than readings
of the API, are in AGENTS.md.
Install
Two channels. The Native AOT binary is the recommended one — self-contained, nothing to install, roughly ten milliseconds to start. The NuGet package is the convenience one: no download step, at the cost of needing the .NET 10 SDK and a JIT startup.
Native AOT binary (recommended)
Download the archive for your platform from
GitHub Releases and extract it. Each archive is
named bitbucket-mcp-{version}-{rid} and contains the executable, LICENSE and this README.md.
| Platform | RID | Archive |
|---|---|---|
| Windows x64 | win-x64 |
bitbucket-mcp-{version}-win-x64.zip |
| Windows ARM64 | win-arm64 |
bitbucket-mcp-{version}-win-arm64.zip |
| Linux x64 | linux-x64 |
bitbucket-mcp-{version}-linux-x64.tar.gz |
| Linux ARM64 | linux-arm64 |
bitbucket-mcp-{version}-linux-arm64.tar.gz |
| macOS Apple silicon | osx-arm64 |
bitbucket-mcp-{version}-osx-arm64.tar.gz |
tar -xzf bitbucket-mcp-1.0.0-linux-x64.tar.gz
chmod +x bitbucket-mcp
./bitbucket-mcp --version
There is no runtime to install: the binary is self-contained. Put it wherever you like and note the absolute path — that is what the MCP client configuration needs.
With no arguments the binary speaks MCP over stdio. It also has three CLI modes:
bitbucket-mcp login Authenticate with Bitbucket via the OAuth browser flow.
bitbucket-mcp logout Delete the cached OAuth tokens.
bitbucket-mcp status Show the current authentication status.
NuGet package (dnx)
The same server is published to nuget.org as
bitbucket-mcp, a .NET tool package carrying the
McpServer package type. dnx — part of the .NET 10 SDK — downloads and runs it in one step, so
there is nothing to install and nothing to keep up to date by hand:
dnx bitbucket-mcp@1.0.0 --yes status
--yes accepts the download prompt and is consumed by dnx itself; everything after it is passed
to the server, so login, logout and status work exactly as they do on the binary. With no
trailing verb the server speaks MCP over stdio, which is how a client should launch it:
{
"servers": {
"bitbucket": {
"type": "stdio",
"command": "dnx",
"args": ["bitbucket-mcp@1.0.0", "--yes"],
"env": {
"BITBUCKET_OAUTH_KEY": "...",
"BITBUCKET_OAUTH_SECRET": "..."
}
}
}
}
Pin the version (@1.0.0) rather than floating: an MCP server is something an agent runs on your
behalf, and a pinned version is one you decided to run. This half is framework-dependent, so it
needs the .NET 10 SDK — if a client reports the command "dnx" was not found, that is what is
missing. Cold start is tens of milliseconds rather than the AOT binary's ten, and the first run
also has to download the package.
Releases are pushed to nuget.org by .github/workflows/publish.yml, a workflow only a v*.*.* tag
can start, using trusted publishing:
the workflow exchanges its GitHub OIDC token for an API key that lives minutes. There is no NuGet
API key stored in this repository, so there is none to leak.
Authentication
Three mechanisms, in this precedence order. The first one that is configured wins, and the rest are ignored:
BITBUCKET_ACCESS_TOKEN— sent asBearer.BITBUCKET_EMAIL+BITBUCKET_API_TOKEN— sent asBasic base64(email:token).BITBUCKET_OAUTH_KEY+BITBUCKET_OAUTH_SECRET— the OAuth 2.0 browser flow.
bitbucket-mcp status prints which one is in effect without printing any of the values.
Bitbucket app passwords were removed on 2026-07-28 and are not implemented here. If a guide tells you to create one, it is out of date.
OAuth consumer setup
Recommended: the tokens it produces are refreshed silently, so nothing has to be rotated by hand.
OAuth consumers live in workspace settings. Not in your personal or account settings, and not on admin.atlassian.com — those are the two places people look first, and the page is in neither. Every plan has it, Free included. The direct URL:
https://bitbucket.org/{workspace}/workspace/settings/api
{workspace} is the workspace slug — the first segment of a repository URL,
bitbucket.org/{workspace}/{repository} — not the workspace's display name.
To navigate there instead: click your avatar and pick the workspace the repositories live in, under Recent workspaces or All workspaces → the Settings cog in the top navigation → Workspace settings, which is the entry under the Bitbucket Administration heading in that menu, not the personal settings above it → Apps and features in the sidebar → OAuth consumers.
Then:
Add consumer.
Fill in:
- Name: anything, for example
bitbucket-mcp. - Callback URL:
http://127.0.0.1:33418/callback— exactly this string. Bitbucket compares it character for character against the redirect the server sends, and a mismatch is the most common first-run failure. (If port 33418 is taken, seeBITBUCKET_OAUTH_CALLBACK_PORTbelow and register the port you pick.) - Tick This is a private consumer. The server authenticates with the consumer secret at the token endpoint, which is what a private consumer means.
- Name: anything, for example
Under Permissions, tick:
- Account: Read
- Repositories: Read and Write
- Pull requests: Read and Write
Save, then expand the new consumer to read its Key and Secret.
Put them in the environment:
export BITBUCKET_OAUTH_KEY=... export BITBUCKET_OAUTH_SECRET=...$env:BITBUCKET_OAUTH_KEY = '...' $env:BITBUCKET_OAUTH_SECRET = '...'Sign in once:
bitbucket-mcp loginA browser opens on Bitbucket's authorization page; one click sends the code back to the loopback listener, and the tokens are cached on disk. The command prints the granted scopes, the access-token expiry and the cache location.
After that, bitbucket-mcp status reports the cached grant and the server keeps it alive on its
own: the access token is renewed before the expiry Bitbucket reports for it, and Bitbucket's
single-use refresh tokens are rotated and persisted atomically, serialised across concurrent
processes. bitbucket-mcp logout deletes the cache (it does not revoke the grant at Bitbucket's
end — delete the consumer for that).
If an MCP client starts the server before anyone has signed in, the first tool call opens the
browser itself and blocks up to BITBUCKET_MCP_AUTH_TIMEOUT_SECONDS. Running login up front is
what keeps that from happening mid-conversation.
Can't see OAuth consumers?
Three things account for almost every case, in the order they are worth checking:
You are in personal settings, not workspace settings. The avatar menu offers both, one under each heading, and OAuth consumers have not lived under the personal one for years. Apps and features → OAuth consumers only exists under Workspace settings. Going straight to
https://bitbucket.org/{workspace}/workspace/settings/apisidesteps the choice.The workspace is a legacy personal workspace — one that was auto-created for you and is named after your username. In those, Apps and features is shown only to the workspace owner: a user with the Admin role does not see it either, which is what makes this one confusing on a shared workspace. Sign in as the owner, or create the consumer in a regular workspace. Atlassian documents it as Apps and features settings are missing for admins in personal workspace (BCLOUD-20342).
You have no workspace at all. Personal workspaces are no longer created automatically at signup, and an account without one has no workspace settings to open. Create a workspace at admin.atlassian.com → Atlassian apps → Add app → Bitbucket, which since early 2026 is where Bitbucket workspaces come from.
If none of that helps, skip OAuth: an Atlassian API token supports every operation this server performs, writes included, and needs no consumer.
Tokens
For headless machines, CI, or when you would rather not create a consumer.
Workspace, project or repository access token — created under the workspace's, project's or repository's Settings → Access tokens, with the Pull requests: Write and Repositories: Write scopes:
export BITBUCKET_ACCESS_TOKEN=...
Atlassian API token — created at id.atlassian.com/manage-profile/security/api-tokens. Use the Create API token with scopes button. The plain Create API token button next to it produces an unscoped token, and an unscoped token does not work against the Bitbucket API at all. Choose Bitbucket as the app, then grant all four of these scopes:
read:repository:bitbucketwrite:repository:bitbucketread:pullrequest:bitbucketwrite:pullrequest:bitbucket
The :bitbucket suffix is part of the scope id, not a description of it. The scopes do not
imply one another: creating, updating, commenting on, approving, merging or declining a pull
request needs read:pullrequest:bitbucket and write:pullrequest:bitbucket, and a write scope
granted on its own answers 403. Scopes cannot be edited afterwards — a token with the wrong set has
to be replaced.
Pair the token with the account's email. It is a Basic credential —
base64(email:token) — which is exactly what these two variables produce:
export BITBUCKET_EMAIL=you@example.com
export BITBUCKET_API_TOKEN=...
Do not put an API token in BITBUCKET_ACCESS_TOKEN: that variable is sent as Bearer, and
Bitbucket answers 401 "Token is invalid, expired, or not supported for this endpoint". Bearer is
for OAuth access tokens and for workspace, project and repository access tokens. With the four
scopes above and Basic auth, an API token drives every operation this server performs, writes
included.
Tokens are read from the environment only. Nothing is cached, and bitbucket-mcp login is neither
needed nor used in this mode.
Client configuration
MCP clients launch the binary with an environment block and talk to it over stdio. Use the absolute path to the executable.
Claude Code
The shortest path is the plugin — it wires the server and the workflow skill in one step, and prompts for the credentials. See Agent skill. To register only the server:
claude mcp add bitbucket \
--env BITBUCKET_OAUTH_KEY=... \
--env BITBUCKET_OAUTH_SECRET=... \
--env BITBUCKET_DEFAULT_WORKSPACE=my-workspace \
-- /usr/local/bin/bitbucket-mcp
claude mcp add bitbucket --env BITBUCKET_OAUTH_KEY=... --env BITBUCKET_OAUTH_SECRET=... -- C:\tools\bitbucket-mcp\bitbucket-mcp.exe
VS Code
.vscode/mcp.json in the workspace (or the user-level mcp.json):
{
"servers": {
"bitbucket": {
"type": "stdio",
"command": "C:\\tools\\bitbucket-mcp\\bitbucket-mcp.exe",
"env": {
"BITBUCKET_OAUTH_KEY": "...",
"BITBUCKET_OAUTH_SECRET": "...",
"BITBUCKET_DEFAULT_WORKSPACE": "my-workspace"
}
}
}
}
Claude Desktop
claude_desktop_config.json (%APPDATA%\Claude\ on Windows,
~/Library/Application Support/Claude/ on macOS):
{
"mcpServers": {
"bitbucket": {
"command": "/usr/local/bin/bitbucket-mcp",
"env": {
"BITBUCKET_OAUTH_KEY": "...",
"BITBUCKET_OAUTH_SECRET": "..."
}
}
}
}
Setting BITBUCKET_DEFAULT_WORKSPACE is worth it if you mostly work in one workspace: the
workspace parameter becomes optional on every tool, which is one less thing for the model to get
wrong. It is the workspace slug — the first URL segment of
bitbucket.org/{workspace}/{repository} — not its display name.
Environment variables
Configuration is environment variables only; there are no config files and no configuration providers. Every value is read once at startup, and a malformed or out-of-range value falls back to the documented default rather than failing the process (in server mode there would be nowhere to report it — stdout is the protocol channel).
| Variable | Default | Meaning |
|---|---|---|
BITBUCKET_ACCESS_TOKEN |
— | Bearer token (workspace, project or repository access token). Highest precedence; bypasses OAuth entirely. |
BITBUCKET_EMAIL |
— | Atlassian account email, paired with BITBUCKET_API_TOKEN. |
BITBUCKET_API_TOKEN |
— | Atlassian API token. With the email, sent as Basic base64(email:token). Second precedence. |
BITBUCKET_OAUTH_KEY |
— | OAuth consumer key (browser flow). |
BITBUCKET_OAUTH_SECRET |
— | OAuth consumer secret (browser flow). |
BITBUCKET_OAUTH_CALLBACK_HOST |
127.0.0.1 |
Host in the redirect URI. Must match the consumer's registered callback URL. |
BITBUCKET_OAUTH_CALLBACK_PORT |
33418 |
Port in the redirect URI, 1–65535. Must match the consumer's registered callback URL. |
BITBUCKET_DEFAULT_WORKSPACE |
— | Default for the workspace tool parameter. The URL slug, not the display name. |
BITBUCKET_MCP_TOKEN_FILE |
per-OS (see Security) | Full path of the OAuth token cache file, overriding the per-OS default. |
BITBUCKET_MCP_NO_BROWSER |
0 |
1 never launches a browser; the authorize URL is logged instead. Accepts 1/true/yes/on and 0/false/no/off. |
BITBUCKET_MCP_AUTH_TIMEOUT_SECONDS |
180 |
Bound, in seconds (1–3600), on a browser sign-in started from inside a tool call. |
BITBUCKET_MCP_LOG_LEVEL |
Information |
Minimum level for the stderr logger: Trace, Debug, Information, Warning, Error, Critical, None. |
BITBUCKET_MCP_MAX_LINES_PER_FILE |
400 |
Diff lines returned per file before truncation (1–100000). Overridable per call with maxLinesPerFile. |
BITBUCKET_MCP_MAX_DIFF_LINES |
4000 |
Diff lines returned per response before truncation (1–1000000). |
Usage
The workflow the server is designed around is diffstat first. A whole-pull-request diff is both what Bitbucket refuses to build on large pull requests (HTTP 555) and what burns the model's context when it succeeds, so the diff tool defaults to listing files and fetches content only for the paths you name.
A review, end to end:
Find the pull request.
listPullRequests { "repository": "my-repo", "state": "OPEN" }Returns a summary per pull request — including its
urlon bitbucket.org — plus anextCursorwhen there are more pages. Add"sourceBranch": "feature/clamp"to ask whether one branch already has a pull request, which is the check to make before opening another one.Read it.
getPullRequest { "repository": "my-repo", "pullRequestId": 42 }Description, state, branches, reviewers and their approvals. This is also one place reviewer UUIDs come from —
{01234567-89ab-cdef-0123-456789abcdef}, in braces. Bitbucket rejects names, nicknames and email addresses as reviewers, so they are never guessed here. On a repository with no pull requests yet,listDefaultReviewersis where to get them instead.See what changed.
getPullRequestDiff { "repository": "my-repo", "pullRequestId": 42 }A list of files with per-file added/removed counts, paginated. This is what the tool does with no
pathsand nomode— the whole workflow's default.Fetch only the files worth reading.
getPullRequestDiff { "repository": "my-repo", "pullRequestId": 42, "paths": ["src/Api/Client.cs", "src/Api/Retry.cs"] }Naming
pathsis what selects the diff —modedoes not have to be set, and setting it to"diffstat"alongsidepathsis refused rather than silently ignored. Paths are spelled exactly as diffstat reported them. The response carriestruncatedand ahintnaming the call that shows the rest; every cut also leaves a visible marker inside the diff text. A truncated diff is never presented as a whole one.Comment on a line.
addPullRequestComment { "repository": "my-repo", "pullRequestId": 42, "path": "src/Api/Retry.cs", "codeSnippet": " await Task.Delay(delay, cancellationToken);", "content": "This ignores Retry-After." }codeSnippetis the line's text copied verbatim out of the diff. It is resolved against the file's diff to derive the anchor, which is far more reliable than counting lines; an ambiguous or missing snippet comes back as an error listing the candidate lines rather than a comment on the wrong line.linepluslineType(ADDED,REMOVED,CONTEXT) is the fallback.Turn a remark into something that has to be dealt with.
addPullRequestTask { "repository": "my-repo", "pullRequestId": 42, "content": "Honour Retry-After here.", "commentId": 987654321 }A comment can be read and forgotten; a task is counted, and a repository can require every task resolved before it will merge.
listPullRequestTasksshows what is stillUNRESOLVED, andupdatePullRequestTaskticks one off. Once a thread has been dealt with,resolvePullRequestCommentmarks it resolved — the same tick, on the comment side.Approve.
setPullRequestReviewStatus { "repository": "my-repo", "pullRequestId": 42, "status": "APPROVED", "comment": "Looks good." }Affects only the authenticated user's own stance.
UNAPPROVEDwithdraws both an approval and a change request.Check the builds, then merge.
listPullRequestStatuses { "repository": "my-repo", "pullRequestId": 42 }Every CI result, deployment and external check, each
SUCCESSFUL,FAILED,INPROGRESSorSTOPPED, with the URL of the run. Bitbucket will merge over a failing build if the repository does not require it, so this is the check to make beforemergePullRequest. An empty list means nothing has reported yet — which is not the same as passing.
Two conventions worth repeating, because they are what the server's initialize instructions
spend their budget on:
- Cursors are opaque. Pass a result's
nextCursorback ascursorverbatim. It is a base64url-encoded, validated Bitbucket URL, not something to edit or construct. Every filter is already encoded in it, so the other arguments are ignored when a cursor is passed. - Reviewers are UUIDs, in braced form, read from
listDefaultReviewersorgetPullRequest.
Agent skill
The workflow above is also shipped as an Agent Skill — the open
SKILL.md format most coding agents now read — at
.claude/skills/bitbucket-pull-requests/SKILL.md.
The server already teaches its own conventions: the tool schemas and the initialize instructions
arrive in every session. What they cannot teach is the order — diffstat before diff content,
build statuses before a merge, the duplicate check before opening a pull request — because each
schema only describes its own tool. That, plus the recovery moves (555, an ambiguous codeSnippet,
403, 429) and when to ask local git instead of spending an API call, is what the skill holds. Only
its one-paragraph description is always in context; the body loads when a Bitbucket task actually
starts.
There is exactly one copy of the file, and everything below installs that copy.
Claude Code: the plugin
This repository is also its own plugin marketplace, so one install delivers both the skill and the server:
/plugin marketplace add lahma/bitbucket-mcp
/plugin install bitbucket-mcp@bitbucket-mcp
or, without starting a session:
claude plugin marketplace add lahma/bitbucket-mcp
claude plugin install bitbucket-mcp@bitbucket-mcp
Enabling it asks for your credentials in a dialog — Atlassian e-mail plus API token is the shortest
way in, or the OAuth consumer key and secret for the browser flow, plus an optional default
workspace. Every field is optional and each maps to the environment variable of the same name in
Environment variables below; secrets go to the OS keychain rather than to settings.json. Nothing
has to be downloaded first: the plugin runs the server with dnx, so it needs the .NET 10 SDK,
and it is pinned to the release it shipped with rather than floating, so the skill and the
server it describes always move together. /plugin update picks up the next release.
Working inside a checkout of this repository needs none of that — .claude/skills/ is loaded as a
project skill automatically.
Any other tool
The skill installs from this repository with either of the two ecosystem CLIs, both of which read
.claude/skills/ out of the source repo and write to whatever location your agent expects:
npx skills add lahma/bitbucket-mcp --skill bitbucket-pull-requests -a codex -y
gh skill install lahma/bitbucket-mcp bitbucket-pull-requests --agent codex
Replace codex with claude-code, cursor, gemini-cli or github-copilot. Or copy the
directory by hand — it is one file:
| Tool | Where it looks |
|---|---|
| Claude Code | .claude/skills/ in the project, or ~/.claude/skills/ |
| Cursor | .agents/skills/, .cursor/skills/, and .claude/skills/ for compatibility |
| VS Code / GitHub Copilot | .github/skills/, .claude/skills/, .agents/skills/ |
| OpenAI Codex | .agents/skills/ only |
| Gemini CLI | .agents/skills/ or .gemini/skills/ |
The frontmatter uses only the six fields the Agent Skills spec defines, so the same file also uploads to claude.ai and the Skills API unchanged.
Both the skill and the plugin manifests are checked against the server on every build: one test
cross-references every tool the skill names against the real tool inventory in both directions, so
it can neither name a tool that does not exist nor quietly omit one that does; another asserts that
the plugin's skill path still resolves, that its version is the one in CHANGELOG.md, that the
dnx pin matches it, and that every credential it prompts for reaches the server.
Troubleshooting
Start with bitbucket-mcp status: it prints which credential would win, the exact callback URL
the server will use, the token cache path and what is in it — and none of the values.
403 Forbidden on a pull-request write. It is not an endpoint limitation. Every Bitbucket pull-request endpoint this server calls — create, update, comment, approve, request changes, merge, decline — accepts an Atlassian API token, and the old "this endpoint does not support token-based authentication" advice no longer applies. Check three things, in this order:
- Both scopes, not just the write one. The scopes do not imply each other, so a token holding
only
write:pullrequest:bitbucketis a 403 on every pull-request write. The set isread:repository:bitbucket,write:repository:bitbucket,read:pullrequest:bitbucketandwrite:pullrequest:bitbucket; a token's scopes cannot be changed after it is created, so a wrong set means a new token. The OAuth equivalent is the consumer's permissions — Account: Read, Repositories: Read and Write, Pull requests: Read and Write, which ispullrequest,pullrequest:write,repositoryandrepository:writeon the wire. After widening a consumer's permissions, runbitbucket-mcp logoutthenbitbucket-mcp login: the cached grant still carries the old scopes. - Basic, not Bearer. An API token goes in
BITBUCKET_EMAIL+BITBUCKET_API_TOKEN, which the server sends asBasic base64(email:token). The same token inBITBUCKET_ACCESS_TOKENis sent asBearerand Bitbucket rejects it.bitbucket-mcp statusprints which variable is in effect. - The account's own access. Scopes cannot grant more than the account has: the user the credential belongs to needs write access to the repository. A 403 that survives the first two checks is usually this one.
HTTP 555, "diff too large". Bitbucket refuses to build diffs beyond roughly 8,000 changed
lines or 200 files, and retrying never helps. Call getPullRequestDiff with no paths to list the
changed files, then again with paths=[...] naming the files you need. This is why listing the
files is what the tool does by default, and why paths selects the diff on its own.
404 on a repository you can see in the browser. workspace and repository are the two URL
slugs of bitbucket.org/{workspace}/{repository}, not display names. Bitbucket also answers 404
rather than 403 for a private repository the credential cannot reach, so check the token's access
if the slugs are right.
429 Too Many Requests. Bitbucket's limits are roughly 1,000–10,000 requests per hour. The
client already retries 429/408/502/503/504 with exponential backoff and honours Retry-After; a
429 that reaches you survived that. The error quotes the wait Bitbucket asked for when it sent one,
and otherwise says to wait about a minute. Ask for smaller pages, and fetch diffs per file rather
than per pull request.
Port 33418 is already in use. Set BITBUCKET_OAUTH_CALLBACK_PORT to a free port and
change the consumer's registered callback URL to match — Bitbucket compares the redirect URI as a
string. bitbucket-mcp status prints the URL the server will use, which is the one to register.
The browser does not open. The authorize URL is written to stderr; open it manually and the
listener still picks up the callback. On a headless or remote machine set
BITBUCKET_MCP_NO_BROWSER=1 so the server stops trying, and authenticate with
BITBUCKET_ACCESS_TOKEN instead — or run bitbucket-mcp login on a machine with a browser and
copy the token cache over (it is portable except on Windows, where it is encrypted to the user
account).
Signed in, but the server still asks for authentication. Either a token variable is set and
takes precedence over OAuth (status says which), or the cache was created with a different
consumer key — the cache records a fingerprint of the key it was obtained with and ignores itself
when it does not match. bitbucket-mcp logout followed by bitbucket-mcp login resets it.
OAuth stopped working after a few months away. A Bitbucket refresh token that goes three
months unused expires, and once it has, there is nothing left to renew silently — the grant has
to be established again. Run bitbucket-mcp login. Nothing else needs changing: the consumer, its
permissions and the environment variables are all still valid. Normal use never hits this, because
every silent renewal counts as a use.
Nothing works and you want to see why. BITBUCKET_MCP_LOG_LEVEL=Debug. All logging goes to
stderr; MCP clients usually surface it in a server log pane.
Security
The token cache is protected at rest. On Windows it is DPAPI-encrypted to the current user account; everywhere else the file is created
0600inside a0700directory, with the mode set before a single byte is written. Writes are atomic (temp file, flush to device, rename), and a cache that fails to decode is treated as absent rather than fatal. Locations:OS Path Windows %LOCALAPPDATA%\bitbucket-mcp\tokens.jsonmacOS ~/Library/Application Support/bitbucket-mcp/tokens.jsonLinux ${XDG_STATE_HOME:-~/.local/state}/bitbucket-mcp/tokens.jsonBITBUCKET_MCP_TOKEN_FILEoverrides it,bitbucket-mcp statusprints it, andbitbucket-mcp logoutdeletes it.Secrets are never logged or printed. Not the tokens, not the consumer secret, not even the consumer key —
statusreports which variables are set, never their contents, because a status readout has a habit of ending up in a bug report.The OAuth callback is CSRF-protected. The
stateparameter is 128 bits from the CSPRNG and is compared in constant time; anything can connect to a loopback port, and the state value is what distinguishes Bitbucket's redirect from a local process guessing at one. The listener binds before the browser is launched, caps the request it reads, and ignores stray connections.Pagination cursors are validated against SSRF. A cursor arrives as a tool argument from a model whose context is full of attacker-influenced text (pull request descriptions, comments, diff hunks). A decoded cursor must be
https, on exactlyapi.bitbucket.org, on the default port, with no embedded credentials, under/2.0/— otherwise it is not a cursor. Nothing else is ever fetched with a liveAuthorizationheader attached.Credentials are per-request, and a redirect only keeps them inside Bitbucket.
SocketsHttpHandlerstrips theAuthorizationheader on every automatic redirect — including a same-host one, which is exactly what Bitbucket's diff and diffstat endpoints answer with — so automatic redirects are switched off and the pipeline follows them itself:GET/HEADonly, at most five hops, and the credential is re-attached only when the target ishttpsonapi.bitbucket.org. A redirect anywhere else is still followed, but as an anonymous request. The header is never set on theHttpClientitself, so nothing can leak by default.The supply chain is four runtime packages, all from Microsoft or the official MCP organisation, centrally pinned in
Directory.Packages.propswith transitive pinning on, and the MCP SDK pinned to an exact version. Adding one requires a recorded decision in AGENTS.md. Builds are deterministic and SourceLink-enabled, so a release binary can be traced back to the commit it came from.
Building from source
Needs the .NET 10 SDK (the exact version is pinned in global.json).
git clone https://github.com/lahma/bitbucket-mcp.git
cd bitbucket-mcp
./build.sh Test # restore, compile, run the tests
./build.sh SmokeTest # AOT publish + a real stdio JSON-RPC handshake against the binary
./build.sh Pack # the NuGet tool package, into artifacts/packages
.\build.ps1 Test
.\build.ps1 SmokeTest
.\build.ps1 Pack
The orchestrator is Fallout; build.ps1 / build.sh bootstrap the CLI
from .config/dotnet-tools.json, so nothing needs installing globally. To publish a binary for a
specific platform:
dotnet fallout PublishAot --runtime linux-arm64
The executable lands in artifacts/publish/{rid}/ and the release archive in
artifacts/archives/. CHANGELOG.md is the version authority — the build parses its top section
and stamps that version into the binary, the package and .mcp/server.json (a test fails if the
manifest falls behind). Note that PublishAot applies only when a runtime identifier is given:
Pack deliberately produces the portable, framework-dependent tool half from the same project.
Contributing
Read AGENTS.md first. It records the design decisions, the package budget, and the
handful of hard rules that are easy to break by accident (no Console.Write outside Cli/, never
hand-edit the generated build.yml, never a bare build rule in .gitignore).
License
MIT. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.