Unified Tool Server
This directory contains an alternative Bitbucket MCP server implementation that exposes all functionality through a single unified tool backed by comprehensive server instructions, following the patterns outlined in the MCP Server Instructions blog post.
Overview
Instead of providing 13 separate tools (pr_list, pr_overview, pr_review, etc.), the unified server exposes a single bitbucket_operation tool with an action parameter. The server instructions (~200 lines) provide:
- Recommended workflows for common tasks (PR review, creation, pipeline debugging, workspace discovery)
- Action-specific guidance with parameter requirements and usage patterns
- Performance optimization tips (batching, pagination, auto-reviewers)
- Rate limits and constraints to prevent API abuse
- Error handling guidance for common issues
- Security notes for credential management
Architecture
server.py # Main entry point
├─ src/modules/tools/unified.py # Single tool implementation
└─ Uses same infrastructure as tools_server.py:
├─ src/modules/resources/ # MCP resources (unchanged)
├─ src/modules/prompts/ # MCP prompts (unchanged)
├─ src/modules/middleware/ # Auth middleware (unchanged)
└─ src/utils/ # Shared utilities (unchanged)
Running the Unified Server
# Stdio (default) -- for Claude Desktop and other MCP clients.
# --host/--port are ignored unless --transport http is also passed.
python server.py
# HTTP -- --transport http is REQUIRED; without it the server starts in stdio
# and silently ignores --host/--port.
python server.py --transport http --host 0.0.0.0 --port 8000
# With environment variables (still needs --transport http; the env vars set the
# bind address, they do not select the transport)
FASTMCP_HOST=localhost FASTMCP_PORT=8000 python server.py --transport http
Tool Interface
Single Tool: bitbucket_operation
All Bitbucket operations are performed through this one tool by specifying the action parameter.
Available Actions:
pr_list- List pull requests with filteringpr_overview- Get PR digest with blockers, comments, diffstatpr_review- Generate structured review summarypr_comment_add- Add PR comments (inline or general, with verified @mentions)pr_comments_list- List comments on a pull requestpr_tasks_list- List PR tasks with detailspr_tasks_sync- Create/resolve PR tasks in batchpr_upsert- Create or update pull requestspipe_fail_summary- Analyze failing pipeline stepspipe_branch_heads- List branches whose head pipeline run failedworkspace_list- List repos, members, projects, or reviewersrepo_get- Get repository basics with PR sampleme_whoami- Get authenticated user identity. Requires user-context auth; returns 403 with a repository/workspace access token (it callsGET /user). Userepo_getto verify a token-based setup.
Example Usage
# List open PRs for current user
{
"action": "pr_list",
"author": "me",
"state": "OPEN",
"limit": 10
}
# Get PR overview
{
"action": "pr_overview",
"pr": "123"
}
# Create PR with auto-reviewers
{
"action": "pr_upsert",
"title": "Add new feature",
"source": "feature/my-branch",
"destination": "main",
"auto_reviewers": "default"
}
# Add inline comment
{
"action": "pr_comment_add",
"pr": "123",
"text": "Consider extracting this to a helper function",
"file": "src/main.py",
"line": 42
}
# Add a comment that @mentions someone by display name
{
"action": "pr_comment_add",
"pr": "123",
"text": "PTAL",
"mentions": ["Jason Schulz"]
}
# Or write the mention directly where it belongs in the text
{
"action": "pr_comment_add",
"pr": "123",
"text": "cc @[Jason Schulz] — the fix is on main."
}
@Mentions
Bitbucket accepts a comment containing a mention token for an account that
doesn't exist and posts it happily -- @{some-bogus-id} renders as inert
text and notifies nobody, with no error to signal the miss. pr_comment_add
closes that gap:
mentionsaccepts a display name, nickname, or account id per entry. Each is resolved to a verified account id and prepended to the comment as Bitbucket's@{account_id}markup.- Any
@{...}token already typed intotextis verified the same way, even without usingmentions. - If any mention doesn't resolve -- unknown, ambiguous between multiple members, or a fabricated id -- nothing is posted. The error lists each failed mention and, for an ambiguous name, the candidate members.
- The response includes
mentions_resolved(query,account_id,display_nameper mention) so you can confirm what was actually stored without re-fetching the comment. - Resolution matches against workspace members by exact display name or nickname (Bitbucket's members API doesn't expose email addresses).
You can also write the mention directly into the body, where you want it to appear:
pr_comment_add(
pr="123",
text="cc @[Jason Schulz] — the fix is on main.",
)
Body references are handled by intent, same as the mentions parameter:
| Written in the body | Behavior |
|---|---|
@[Full Name] |
Explicit — must resolve, or the comment is refused |
@Name, @Name Surname |
Converted to a real mention when it matches exactly one workspace member |
| Anything that doesn't resolve | Left as the literal text you wrote, and returned in mentions_unmatched |
An incomplete @[... with no closing bracket |
Also returned in mentions_unmatched instead of being read as plain text — you meant to tag someone, so it's surfaced, not swallowed. It never blocks the post either, since there's nothing complete to refuse over. |
| Code spans, fenced code blocks | Never touched |
mentions_unmatched is the whole point: it turns "posted silently to nobody"
into "named in the response," so a bare guess like @Sprint never fails a
comment but also never notifies someone who doesn't exist without you
knowing. Unlike the Jira MCP server, there's no email form here — Bitbucket's
workspace members API doesn't expose email addresses, so bare-name
resolution matches display name or nickname only.
Verification also checks account status: Bitbucket's API documents
account_status as "active" today (its own schema notes more values may
be added later), so a present-but-non-"active" status is treated as
unresolvable rather than trusted — a future status change fails closed
instead of posting a mention nobody receives.
Command-Line Interface (bb-cli)
The same action-based surface is available from the terminal via bb-cli. It runs a
single action and prints the JSON result to stdout — clean to pipe into jq or consume
from an agent with bash access. Diagnostic logs go to stderr (silence with --quiet).
Exit code is 0 on success, 1 on an action/API error, 2 on a usage error (bad
action, bad flag value, missing positional) — stdout is always JSON
({"error": "...", "type": "..."} on failure) for all three, with one exception:
-h/--help prints human-readable text and exits 0.
# After `pip install -e .` the console script is on PATH:
bb-cli pr_list --author me --state OPEN --limit 5 --quiet | jq '.items'
# Or run the module directly without installing:
python cli.py pr_list --author me --state OPEN --limit 5 --quiet
Examples
# List open PRs for the current user
bb-cli pr_list --author me --state OPEN --limit 10
# PR overview / review
bb-cli pr_overview --pr 123
bb-cli pr_review --pr 123 --repo workspace/repo
# Add an inline comment (writes to Bitbucket)
bb-cli pr_comment_add --pr 123 --text "Extract this to a helper" --file src/main.py --line 42
# Add a comment that @mentions someone by display name (writes to Bitbucket)
bb-cli pr_comment_add --pr 123 --text "PTAL" --mentions "Jason Schulz"
# Batch create / resolve PR tasks
bb-cli pr_tasks_sync --pr 123 \
--create '[{"text":"fix typo","file":"a.py","line":3}]' \
--resolve 456,789
# Create a PR with auto-assigned reviewers
bb-cli pr_upsert --title "Add feature" --source feature/my-branch \
--destination main --auto-reviewers default
# Workspace discovery and repo basics
bb-cli workspace_list --kind repos --limit 20
bb-cli repo_get --slug workspace/repo --pretty
# Summarize the latest failing pipeline (with log excerpts)
bb-cli pipe_fail_summary --include-logs
# Which branches are currently broken? (one call, no logs)
bb-cli pipe_branch_heads --repo workspace/repo --limit 30 --days 14
List-valued flags (--create, --resolve, --reviewers) accept either a JSON array or
a comma-separated string. --state, --verbosity, --task-state, --kind, and
--auto-reviewers are restricted to the choices shown in --help.
Running many invocations in parallel
Each bb-cli run is one process doing one action, so a sweep across repos is a
fan-out. Size the pool by the number of jobs, not by CPU count.
# 15 repos, one worker each
printf '%s\n' "${REPOS[@]}" | xargs -P 15 -I{} bb-cli pipe_branch_heads --repo {} --quiet
Measured over 15 repositories: one worker per repo finished in 1.42s against 2.20s for a CPU-count-sized pool -- roughly a third faster. The work is network latency plus a fixed interpreter-and-import startup, and neither is CPU-bound, so workers past the core count still overlap usefully instead of contending. (Measurement contributed by a parallel session driving the same CLI.)
Two limits to respect:
- Bitbucket rate-limits per credential. A fan-out wide enough to trip it turns a fast sweep into a slow one with 429s, so keep the pool proportional to the job list rather than raising it indefinitely.
- Interpreter startup is paid per invocation. For a sweep large enough that startup dominates, call the tools in-process instead of spawning a CLI per item.
-h, --help
usage: bb-cli [-h] [--quiet] [--pretty] [--repo REPO] [--limit LIMIT]
[--cursor CURSOR] [--verbosity {ids,summary,full}]
[--author AUTHOR] [--reviewer REVIEWER] [--involving INVOLVING]
[--state {OPEN,MERGED,DECLINED,SUPERSEDED,ALL}] [--pr PR]
[--goal {quick,thorough}] [--policy {risk,style,both}]
[--text TEXT] [--file FILE] [--line LINE] [--mentions MENTIONS]
[--task-state {OPEN,RESOLVED,ALL}] [--create CREATE]
[--resolve RESOLVE] [--title TITLE] [--source SOURCE]
[--destination DESTINATION] [--summary SUMMARY]
[--reviewers REVIEWERS] [--auto-reviewers {default,all,none}]
[--close-source] [--draft]
[--kind {repos,members,projects,reviewers}]
[--workspace WORKSPACE] [--slug SLUG] [--pipeline PIPELINE]
[--include-logs] [--days DAYS]
{pr_list,pr_overview,pr_review,pr_comment_add,pr_comments_list,pr_tasks_list,pr_tasks_sync,pr_upsert,pipe_fail_summary,pipe_branch_heads,workspace_list,repo_get,me_whoami}
Run a single Bitbucket action and print its JSON result.
positional arguments:
{pr_list,pr_overview,pr_review,pr_comment_add,pr_comments_list,pr_tasks_list,pr_tasks_sync,pr_upsert,pipe_fail_summary,pipe_branch_heads,workspace_list,repo_get,me_whoami}
Operation to perform.
options:
-h, --help show this help message and exit
--quiet, -q Suppress diagnostic logs on stderr.
--pretty Pretty-print the JSON result.
--repo REPO 'workspace/repo' slug; falls back to configured
default.
--limit LIMIT Max items to return (1-50; pipe_branch_heads scans
runs, clamped 1-100).
--cursor CURSOR Pagination cursor.
--verbosity {ids,summary,full}
Response detail level.
--author AUTHOR Filter PRs by author: 'me', a display name/nickname,
or an account id. Unset by default (lists the repo's
PRs from all authors) -- 'me' fails under
repository/workspace access-token auth, so pass it
explicitly.
--reviewer REVIEWER Filter PRs by reviewer: 'me', a display
name/nickname, or an account id.
--involving INVOLVING
Filter PRs to where this person is author OR
reviewer: 'me', a display name/nickname, or an
account id. Combines with --author/--reviewer (if
also given) using AND, not OR.
--state {OPEN,MERGED,DECLINED,SUPERSEDED,ALL}
PR state filter.
--pr PR PR identifier, e.g. '123' or '#123'.
--goal {quick,thorough}
Review depth.
--policy {risk,style,both}
Finding categories to return.
--text TEXT Comment text.
--file FILE File path for inline anchoring.
--line LINE Line number for inline anchoring.
--mentions MENTIONS People to @mention: JSON array or comma-separated
display names/nicknames/account ids.
--task-state {OPEN,RESOLVED,ALL}
Filter tasks by state.
--create CREATE Tasks to create: JSON array of {text,file,line}.
--resolve RESOLVE Task IDs to resolve: JSON array or comma-separated.
--title TITLE PR title.
--source SOURCE Source branch.
--destination DESTINATION
Destination branch.
--summary SUMMARY PR description/summary.
--reviewers REVIEWERS
Reviewer identifiers: JSON array or comma-separated.
--auto-reviewers {default,all,none}
Auto-assign reviewers.
--close-source Close source branch on merge.
--draft Create/update as draft.
--kind {repos,members,projects,reviewers}
Item type to list.
--workspace WORKSPACE
Workspace slug.
--slug SLUG Repository slug or 'workspace/repo'.
--pipeline PIPELINE Specific pipeline ID/number to analyze.
--include-logs Include full log excerpts.
--days DAYS Activity window in days for pipe_branch_heads
(default 30).
Each action uses a subset of the flags below; unused flags are ignored. Run a
destructive action (pr_comment_add, pr_tasks_sync, pr_upsert) only when you
mean it — they write to Bitbucket.
Server Instructions
The unified server includes comprehensive instructions that are automatically injected into the LLM's context. These instructions guide the LLM to:
-
Follow optimal workflows
- Example: Use
pr_overviewbeforepr_reviewto gather context - Example: Use
pr_tasks_syncfor batch task operations instead of individual calls
- Example: Use
-
Understand cross-action relationships
- How
pr_upsertwithauto_reviewers="default"automatically fetches and assigns reviewers - When to use
pipe_fail_summaryafterpr_overviewshows failing checks
- How
-
Optimize performance
- Prefer
pr_overviewover multiple separate API calls - Use batching with
pr_tasks_syncfor multiple tasks - Leverage pagination with appropriate limits
- Prefer
-
Handle edge cases
- Required vs optional parameters for each action
- Parameter format variations (e.g., repo as "workspace/slug" or just "slug")
- Error messages and resolutions
Comparison: Multi-Tool vs Unified
Multi-Tool Server (tools_server.py)
# Three separate tool calls
response1 = call_tool("pr_overview", {"pr": "123"})
response2 = call_tool("pr_review", {"pr": "123"})
response3 = call_tool("pr_comment_add", {
"pr": "123",
"text": "LGTM",
})
Pros:
- Fine-grained tool selection
- Familiar pattern for traditional MCP clients
- Can filter specific tools via
ALLOWED_TOOLS
Cons:
- Limited guidance on workflows
- LLM must infer optimal sequences
- Tool descriptions have space constraints
Unified Tool Server (server.py)
# Same functionality, one tool interface
response1 = call_tool("bitbucket", {"action": "pr_overview", "pr": "123"})
response2 = call_tool("bitbucket", {"action": "pr_review", "pr": "123"})
response3 = call_tool("bitbucket", {
"action": "pr_comment_add",
"pr": "123",
"text": "LGTM"
})
Pros:
- Comprehensive server instructions guide optimal usage
- Documented workflows (PR review, creation, debugging)
- Performance optimization tips built-in
- Cross-action relationships explained
- Better for LLMs that benefit from detailed guidance
Cons:
- Single tool interface (action-based)
- Cannot selectively disable specific actions
- Server instructions increase context size (~200 lines)
When to Use Unified Server
Choose the unified server when:
- ✅ Your LLM client benefits from detailed contextual guidance
- ✅ You want optimized workflows documented in server instructions
- ✅ You prefer action-based tool invocation
- ✅ You want to follow MCP server instructions best practices
- ✅ You're working with LLMs that struggle with complex multi-tool orchestration
Choose the multi-tool server when:
- ✅ You need fine-grained tool filtering
- ✅ Your client has strict context size limits
- ✅ You prefer traditional tool-per-operation pattern
- ✅ You're integrating with existing MCP tooling
Implementation Details
The unified tool (src/modules/tools/unified.py) is a thin wrapper that:
- Accepts an
actionparameter and action-specific arguments - Validates required parameters based on the action
- Routes to the underlying implementation functions
- Returns the same response format as the individual tools
Key point: The unified server doesn't duplicate code. It uses the exact same tool implementations as the multi-tool server, just exposed through a different interface.
# unified.py routes to existing implementations
match action:
case BitbucketAction.PR_LIST:
return await pr_list(...) # Same function as multi-tool server
case BitbucketAction.PR_OVERVIEW:
return await pr_overview(...)
# ... etc
Configuration
The unified server uses the same configuration system as the multi-tool server:
Environment Variables:
# Preferred: repository/workspace access token (Bearer auth). Best for
# headless/hosted deployments; takes precedence over username/app password.
# App passwords are deprecated by Atlassian.
BITBUCKET_ACCESS_TOKEN=your_access_token
BITBUCKET_USERNAME=your_username
BITBUCKET_APP_PASSWORD=your_app_password
BITBUCKET_WORKSPACE=workspace-name
BITBUCKET_REPO=repo-slug
FASTMCP_PORT=8000
FASTMCP_HOST=localhost
HTTP Headers (per-request):
X-BITBUCKET-WORKSPACE: override-workspace
X-BITBUCKET-REPO: override-repo
X-BITBUCKET-ACCESS-TOKEN: your_access_token
Authorization: Basic <base64 user:app_password>
X-BITBUCKET-ACCESS-TOKEN is the way to supply a Bitbucket access token
per-request. An inbound Authorization: Bearer header is not accepted as a
Bitbucket credential and is never forwarded to api.bitbucket.org -- a bearer is
ambiguous between the MCP transport token (MCP_AUTH_TOKEN) and a Bitbucket
token, so forwarding it risked sending a platform credential to Atlassian.
Authorization: Basic is still forwarded, being unambiguous.
Credential scope: what an access token cannot reach
The access token above is the recommended credential, and it is not equivalent to a user login. A repository or workspace access token authenticates as the resource, not as a person, and Bitbucket refuses several endpoints outright for that authentication mechanism. This is not a permission you can grant on the token -- there is no scope that turns it on.
| Blocked for a repository-scoped token | Affects |
|---|---|
GET /user |
me_whoami; --author me / --reviewer me / --involving me |
GET /user/workspaces |
me_whoami's workspace list |
GET /workspaces/{ws}/members |
workspace_list --kind members; @mention resolution by display name |
| Any repository outside the token's own scope | pr_list / repo_get / pipe_* against another repo |
GET /user returns 403 with "This API is not accessible by this authentication
mechanism"; the workspace endpoints return 403 with "Your credentials lack one
or more required privilege scopes".
Practical consequences when running with a repository token:
- Use
repo_get-- notme_whoami-- to confirm a token-based setup works. - Pass
--author/--reviewer/--involvingan explicit display name or account id rather thanme.meneedsGET /userand cannot resolve. --mentionsby display name needs the members endpoint. Pass account ids directly, or use a user-context credential, if mention resolution is required.- A workspace-scoped token lifts the last row but not the first three.
This matters beyond configuration: it shapes what a test can observe. Four
silently-wrong behaviours in pr_list survived a green suite partly because the
me paths that would have exposed them returned 403 in the development
environment and were skipped rather than exercised. If your credential cannot
reach an endpoint, a passing run says nothing about the code behind it.
Resources and Prompts
The unified server mounts the same resources and prompts as the multi-tool server:
Resources:
- Repository information
- Recent pipeline runs
- Open pull requests
- Workspace members
- Available branches
- Workspace projects
Prompts:
- All prompts from the resources server are available
Testing
# Syntax validation
python -m py_compile server.py
python -m py_compile src/modules/tools/unified.py
# Type checking (with mypy installed)
mypy server.py --ignore-missing-imports
# Runtime test (requires dependencies)
python server.py --help
Migration from Multi-Tool Server
If you're currently using the multi-tool server and want to migrate:
-
Update your tool calls:
# Before call_tool("pr_list", {"author": "me"}) # After call_tool("bitbucket", {"action": "pr_list", "author": "me"})
-
Update your server startup:
# Before python tools_server.py --transport http --port 8000 # After python server.py --transport http --port 8000
-
No changes needed for:
- Environment variables
- HTTP headers
- Resources
- Prompts
- Authentication
Benefits of Server Instructions Pattern
Following the MCP blog post recommendations:
✅ Cross-feature relationships - Documents how actions interact ✅ Operational patterns - Specifies performance-optimized sequences ✅ Constraints and limitations - Clarifies rate limits and boundaries ✅ Model-agnostic language - Factual guidance without assumptions
The blog post testing showed 60% improvement in GPT models following optimal workflows when instructions were present.
Changelog
1.9.2
pr_listreportsitems_partial. Deduping OR-clause matches in 1.9.1 was necessary but traded a visible bug for an invisible one: Bitbucket's fan-out is consumed BEFORE the pagelen clamp, returning one row per reviewer (exactlymax(1, len(reviewers)), measured live), solimit=10against PRs with two reviewers each yields 10 rows and 5 distinct PRs. Collapsing them silently left a caller who asked for 10 holding 5, unable to tell whether the repo ran out or the window did.nextsignals that more exist but not why the page is short;items_partialsays it, mirroringpipe_branch_heads'heads_partial.- Correct the mechanism described in 1.9.1's entry below. It attributed the duplication to Bitbucket matching the reviewer clause through participants. That was wrong: it is a join fan-out on the reviewers array, which is why an account that reviews nothing duplicates identically. Reported and measured by the same downstream consumer.
1.9.1
pr_listdeduplicates OR-clause matches. Bitbucket returns one row per matching clause, soinvolving-- which builds(author.account_id=X OR reviewers.account_id=X)-- returned the same PR twice. Measured live: 20 rows for 11 distinct PRs. The mechanism is a join fan-out on the reviewers array, not clause matching: rows come back as exactlymax(1, len(reviewers))per PR, so an account that reviews nothing fans out identically and only the OR combination triggers it. Wrong sinceinvolvingshipped in 1.7.0. Reported by a downstream consumer whose own REST implementation hit it the same day.workspace_list --kind repossorts by recency. Bitbucket's default ordering is not recency -- live, page 1 led with a repo last updated in May 2025 while the workspace's newest had changed minutes earlier -- so "the N most recently active repos" required fetching every page and sorting locally. Now one call.- Malformed entries degrade in every tool, not just
pr_list. 1.9.0 said a bare string where an object belongs is dropped rather than crashing. That was true ofpr_listand of nothing else:workspace_list(all four kinds),pr_tasks_list,pr_tasks_sync,me_whoami,pipe_fail_summary,pr_overviewandpr_reviewall still raisedAttributeError, reaching the caller asError calling tool .... The 1.9.0 changelog entry overstated its scope; this is the fix that makes it true. - Fix two crashes on a present-and-null string:
pr_reviewon a null PR title (wherepr_overviewon the identical payload correctly returned a schema violation -- the two disagreed), andpr_tasks_syncwithcreate=[{"text": null}], which is caller-controlled. pr_list'sreviewersno longer emits an empty-string uuid. Tightening the filter to reject a non-string uuid in 1.9.0 accidentally admitted"", which the previous truthy check had excluded.- Docs named a tool that does not exist. The unified server registers
bitbucket_operation; README and CLAUDE.md both saidbitbucket, so a client following the documentation got "Unknown tool". A test now asserts every documented tool name is actually registered on one of the two surfaces. - Test coverage for the ~42-site null-safety rewrite, which had none.
Mutation testing showed six of those guards -- including the
mainbranchcrash fix -- could be reverted with the whole suite still green. A present-and-null fixture direction now exercises every tool. - Remove a vacuous test case (
pr_tasks_listin the boolean-hazard set produced byte-identical output with and without the poison) and correct two fixture assertions that were false against live data: Bitbucket nullsoldon an added file, andtarget.ref_nameis absent from most pipelines.
1.9.0
-
Breaking:
pr_overview'sdiffstat_topentries renamedeltoremove.delis a Python keyword, so a client building a model from the declared response schema cannot create a field for it and logs a parse error on everypr_overviewcall. To be precise about what this does and does not fix: the response data itself was never wrong -- structured content is present and correct either way -- so this removes client-side error noise, it does not repair broken output. A caller readingdiffstat_top[].delmust read[].removeinstead. -
pr_listsummary items now carrycomments(Bitbucket'scomment_count) andreviewers(requested reviewers by uuid). Neither was reachable before at any verbosity: the request's ownfields=projection filtered them out, soraw.comment_countwasNoneeven atverbosity="full". A downstream tool badging PR comment activity read every PR as zero comments, with no error anywhere -- verified against live data where 9 open PRs carried comments and all would have reported 0. -
pr_listresponses includeresolved_actors, echoing what eachauthor/reviewer/involvingfilter resolved to (uuidfor"me",account_idfor a name). A caller filtering by"me"can now read its own identity from the same response instead of issuing aGET /user-- the call that 403s under access-token auth.{}when no actor filter was given and on cursor requests; the key is always present. -
"me"is resolved at most once perpr_listcall.--author me --reviewer me --involving mepreviously issued three separateGET /userrequests for the same identity. -
Fix
pr_listandrepo_geton thetools_server.pysurface. Every tool there declares anoutput_schemawithadditionalProperties: false, which FastMCP enforces -- so addingcomments/reviewers/resolved_actorsbroke both tools for every call (isError: Output validation error), including empty result sets that had validated before.repo_getwas hit becauserecent_prsreuses the samePRItemdefinition. The unifiedserver.pysurface was never affected, which is why it went unnoticed. -
Fix five further output-schema violations that were already there and had never been reported, all on the same surface:
pr_listwithverbosity="ids"(emits integers, schema allowed only strings) andverbosity="full"(emitsraw, whichPRItemforbids);pr_overview(pr.description,comments_recent[].text);pr_review(goal,policy,findings[].category);pr_tasks_list(Bitbucket'sUNRESOLVEDstate, and nullcreated/updated/creator/url);repo_get(name,description). -
Fix
pr_tasks_syncandpr_upsert, which returnedisErroron EVERY call on thetools_server.pysurface:created_tasksanddescriptionare unconditional members of their return dicts and neither was declared (partial/failureswere undeclared too). Both were pre-existing. -
Fix
pipe_fail_summary, which failed validation on live payloads whenever a log excerpt exceeded 20 lines -- the normal case for the tool's purpose. It addstop_lines_truncated(andlogs_partial/logs_erroron a failed log fetch); none were declared. -
Declare nullability where the impl can genuinely emit null, and only there. Bitbucket omits optional fields routinely and the impls pass the resulting
Nonethrough, so a task without a timestamp or a repository without a main branch must validate. But fields the API always returns -- a PR'sstate, a repository'sslug, thetitle/stateechoed back from a write you just performed -- stay non-nullable: a null there means the response was not what was asked for, and that should fail loudly rather than validate. No field is now bothrequiredand nullable, which is not a contract but a contradiction ("must be present, may be meaningless"); there were ten. -
Fix a whole crash class, not one instance of it.
X.get("key", {}).get("sub")is unsafe because aget()default applies only when the key is ABSENT, and Bitbucket returns keys present-and-null (mainbranch: nullfor a repo with no commits,links: {"html": null}). 42 such chains acrosstools.py,meta.pyand the resources modules now use(X.get("key") or {}). Two were proven to crash and reach the caller asError calling tool ...; the rest were unaudited and are now moot. The first attempt at this fixed a single site and left an identical one 300 lines away in the same file. -
Degrade instead of crashing on malformed values. A bare string where a PR object belongs is dropped rather than becoming a null-filled item, and garbage
participants/reviewers/linksentries no longer raise. A wrong-typed scalar inside a real PR object is deliberately NOT coerced: it fails validation, because it means the response was not what was asked for. -
Reject booleans in integer fields.
isinstance(True, int)is True in Python (bool subclasses int) while JSON Schema treats them as disjoint types, so a booleancomment_countpassed every guard and then failed output validation. Guards now use an explicit int-not-bool check. -
Tighten
$defs.StepfromadditionalProperties: true, which made pipeline step content structurally unvalidatable -- nothing about a step could fail, mocked or live. -
New
tests/test_output_schema_contract.pydrives every tool's impl and validates the result against its own declared schema. Nothing did this before -- the existing schema test checks input parameter descriptions only, so the declared output contracts were never exercised against real returns. It runs every tool against both a rich and a near-empty fixture, because those ask different questions: a rich fixture asks "is every emitted field declared?", and only a thin one asks "is every declared type honest when the source data is missing?". The thin pass found 11 further violations. All 13 tools are covered with no exemptions. -
Documentation defaults are now checked against the live schema rather than by searching for wordings: the bundle's structured
(optional, default: X)entries must match, and any line in the server instructions that asserts a default must agree with it. This closes theauthor="me"claim, which turned out to exist in a third place (the bundle's SKILL.md) after being fixed in two others. -
Correct the unified server's
pr_listguidance, which told the modelauthor="me"was "default behavior". It has not been since 1.7.0, when the default was removed for 403ing under access-token auth -- the same defect already fixed in the skill bundle in 1.8.1, in a second place.
1.8.1
- Fix the
--quietauthlib deprecation warning for the second time. The previous fix matched on the message instead ofmodule=(correct, and necessary), but was still defeated by ordering:authlib/deprecate.pyrunswarnings.simplefilter("always", AuthlibDeprecationWarning)at import time, andsimplefilterinserts at the front ofwarnings.filters. Imported lazily through fastmcp, it landed ahead of our filter and re-armed the warning.cli.pynow importsauthlib.deprecatefirst so our filter goes in front. This is also whyPYTHONWARNINGSandwarnings.catch_warnings()both failed to suppress it. Verified 394 bytes of stderr to 0 against authlib 1.7.2; the import is load-bearing and must not be removed as unused. - Document credential scope: what a repository-scoped access token cannot
reach (
GET /user,/user/workspaces, workspace members, other repos), and which flags that rules out. - Document sizing for parallel
bb-clifan-out (one worker per job, not per core) with the measurement behind it. - Regenerate the README
--helpreference, which was missingpipe_branch_heads,--reviewer,--involving, and--days. - Add
pipe_branch_headsto the unified server's instruction block, the skill bundle, and the documented action lists. It shipped in 1.8.0 registered and routable but unmentioned in all ~200 lines of instructions, so a model driving thebitbuckettool had no way to discover it. A new test asserts every registered action appears in the instructions. - Backfill this changelog, which stopped at 1.2.4 while six releases shipped.
- Correct the skill bundle against the live schema. It documented
pr_list'sauthoras defaulting to"me"-- a default removed in 1.7.0 precisely because resolving it costs aGET /userthat 403s under access-token auth -- and omittedpr_list'sreviewer/involvingandpr_comment_add'smentionsentirely. Also drops an unmodified scaffolding placeholder (references/api_reference.md) that pointed at skills not present here. - Correct
.claude/PROGRESS.md, which had claimed 11 tools since 2025-11 and listed neitherpr_tasks_listnorpipe_branch_heads. - Tests now pin the documentation surface against the registry rather than against wording: every action is named in each doc and has a bullet in the bundle's SKILL.md and an entry in its Action Index; every live parameter appears in its own action's section; total counts must match the registry while group subtotals must sum to it.
1.8.0
pipe_branch_heads: report the head pipeline of every branch with recent activity, so a failing branch is visible without one call per branch. Returnsheads_partialwhen the scan window cut off before every branch was seen, rather than implying completeness.- Stop the resources mount re-fetching on every tool call.
resources_serverdeclared alifespan=, which made FastMCP mount it as a proxy; tool routing then enumerated every mounted server and re-entered that lifespan per call. Seven HTTP requests and ~1.9s per tool call became one and ~0.31s. - First
--quietauthlib fix (see 1.8.1 -- it was necessary but not sufficient).
1.7.0
- Breaking:
pr_listbuilds a singleqexpression from AND-ed predicates. Bitbucket ignores thestateparameter wheneverqis present, so--statewas silently dropped for any filtered query.stateis now folded intoqand never sent separately. Adds--reviewerand--involving.
1.6.0
- Stop forwarding an inbound
Authorization: Bearerheader to api.bitbucket.org. A bearer is ambiguous between the MCP transport token (MCP_AUTH_TOKEN) and a Bitbucket token, so forwarding it risked handing a platform credential to Atlassian. UseX-BITBUCKET-ACCESS-TOKENinstead. MCP_AUTH_TOKENnow guards the HTTP transport only, keyed on the absence of an HTTP request rather than an empty header dict -- setting it no longer bricks stdio, and an HTTP request sending no headers is still rejected.REQUIRE_CLIENT_CREDENTIALS=truealso blocks env fallback for a request that omits allX-BITBUCKET-*headers, which previously bypassed it.- Constant-time comparison (
hmac.compare_digest) for the transport token; stop logging credential length; close the set of loggable auth schemes so a schemeless header cannot leak its value into a DEBUG log. - Gate
initialize, honourNO_TOOLS, and report the real package version. - Fix error classification (
timed outwas unmatched), raw-payload mispairing inworkspace_list, and an N+1 issuingGET /userper reviewer. - Make the CLI's documented exit-code and JSON contract true: argparse usage errors now emit JSON on stdout and exit 2 instead of writing to stderr with empty stdout.
- Correct documented behaviour the code never had.
1.5.1
- Declare
pytest-asyncioso the suite runs on a clean checkout.
1.5.0
pr_comment_addverifies every @mention (via a newmentionsparameter, and any@{...}token already typed intotext) against Bitbucket before posting. Bitbucket accepts a mention token for an account that doesn't exist and posts it as inert, silent text -- an unresolved or ambiguous mention now blocks the whole comment instead. The response echoes what was actually stored viamentions_resolved. Seesrc/utils/mentions.py.pr_comment_addnow also recognizes@[Full Name]and bare@Name/@Name Surnamewritten directly into the comment body (not just thementionsparameter or a raw@{...}token). Explicit@[...]must resolve or the comment is refused; a bare guess that doesn't resolve is left as the literal text and reported in the newmentions_unmatchedresponse field instead of failing the post. Code spans and fenced code blocks are never touched.
1.4.0
- Add
BITBUCKET_ACCESS_TOKENfor non-interactive Bearer auth. - Complete the wrapper/impl split so the CLI and MCP share one code path.
- Register
pr_comments_listandpr_tasks_list; correct the tool count. - Stop discarding response content in the formatter; stop rebinding
unified.bitbucketto itsToolwrapper. pr_tasks_syncreports partial failures and echoes created tasks.- Keep the no-eligible-reviewers log at info, not warning.
1.3.1
- Drop deferred annotations so fastmcp 2.14 can build tool schemas.
1.3.0
- Add the
bb-cliterminal wrapper; remove hardcoded credentials. - Add
pr_comments_listandpr_tasks_listactions. - Migrate off the removed
/2.0/workspacesendpoint (CHANGE-2770). - Bound fastmcp to
<3to avoid an async Context API break.
1.2.4
- Fix
me_whoamiand workspace auto-discovery hittingGET /2.0/workspaces, which Atlassian removed in CHANGE-2770 (returns410 Gone). Both now use the user-scoped replacementGET /2.0/user/workspaces.
1.2.3
- Add
bb-cli, a command-line wrapper over the unified tool surface. - Remove hardcoded credentials from
config.py.
License
Same as the main project.
Metadata
Release files for bb-mcp-server 1.9.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bb_mcp_server-1.9.2.tar.gz | 179.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bb_mcp_server-1.9.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 279.4 kB
Release files / bb_mcp_server-1.9.2.tar.gz
| Download URL | bb_mcp_server-1.9.2.tar.gz |
|---|---|
| Size | 179.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bb3137af422423d13c86717cda640e59551dcc3489209c751cfb249133d7524a
|
|
BLAKE2b-256 checksum How to use checksums |
c444af8a06a4f5137493cb751cff7db2bdfb52cd8a0f7b5df9f68149fd1109fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.
Transparency logRelease files / bb_mcp_server-1.9.2-py3-none-any.whl
| Download URL | bb_mcp_server-1.9.2-py3-none-any.whl |
|---|---|
| Size | 99.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0271390e5b732ab79d8aa8edaced415eda9667079f5a0f5f2ebb92c48c361192
|
|
BLAKE2b-256 checksum How to use checksums |
e82c706c445c0162625e1bdfbafc760e8328041a19bd7a80f8e32539b567e917
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.
Transparency log