Metadata-Version: 2.4
Name: bb-mcp-server
Version: 1.10.2
Summary: Bitbucket MCP (Model Context Protocol) server built with FastMCP that provides programmatic access to Bitbucket API v2.0
Author-email: Jason Schulz <jason@schulz.studio>
License: MIT
Project-URL: Homepage, https://github.com/your-username/bb-mcp-server
Project-URL: Repository, https://github.com/your-username/bb-mcp-server
Project-URL: Issues, https://github.com/your-username/bb-mcp-server/issues
Keywords: mcp,bitbucket,api,fastmcp,model-context-protocol
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastmcp<3,>=2.12.1
Requires-Dist: httpx>=0.25.0
Requires-Dist: pydantic>=2.11.10
Requires-Dist: pytest>=8.4.2
Requires-Dist: pytest-asyncio>=1.0
Requires-Dist: python-dotenv>=1.0.0
Provides-Extra: dev
Requires-Dist: ruff>=0.16; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Requires-Dist: black>=24.0; extra == "dev"
Dynamic: license-file

# 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](https://blog.modelcontextprotocol.io/posts/2025-11-03-using-server-instructions/).

## 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

```bash
# 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 filtering
- `pr_overview` - Get PR digest with blockers, comments, diffstat
- `pr_review` - Generate structured review summary
- `pr_comment_add` - Add PR comments (inline or general, with verified @mentions)
- `pr_comments_list` - List comments on a pull request
- `pr_tasks_list` - List PR tasks with details
- `pr_tasks_sync` - Create/resolve PR tasks in batch
- `pr_upsert` - Create or update pull requests
- `pipe_fail_summary` - Analyze failing pipeline steps
- `pipe_branch_heads` - List branches whose head pipeline run failed
- `workspace_list` - List repos, members, projects, or reviewers
- `repo_get` - Get repository basics with PR sample
- `me_whoami` - Get authenticated user identity. **Requires user-context auth**; returns 403 with a repository/workspace access token (it calls `GET /user`). Use `repo_get` to verify a token-based setup.

### Example Usage

```python
# 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:

- `mentions` accepts 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 into `text` is verified the same way,
  even without using `mentions`.
- 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_name` per 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:

```python
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`.

```bash
# 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

```bash
# 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.

```bash
# 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`

```text
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:

1. **Follow optimal workflows**
   - Example: Use `pr_overview` before `pr_review` to gather context
   - Example: Use `pr_tasks_sync` for batch task operations instead of individual calls

2. **Understand cross-action relationships**
   - How `pr_upsert` with `auto_reviewers="default"` automatically fetches and assigns reviewers
   - When to use `pipe_fail_summary` after `pr_overview` shows failing checks

3. **Optimize performance**
   - Prefer `pr_overview` over multiple separate API calls
   - Use batching with `pr_tasks_sync` for multiple tasks
   - Leverage pagination with appropriate limits

4. **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`)

```python
# 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`)

```python
# 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:

1. Accepts an `action` parameter and action-specific arguments
2. Validates required parameters based on the action
3. Routes to the underlying implementation functions
4. 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.

```python
# 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:**
```env
# 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
X-BITBUCKET-USERNAME: your_username
X-BITBUCKET-APP-PASSWORD: your_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.
No inbound `Authorization` scheme is forwarded, Basic included -- see the
`CLAUDE.md` auth notes for why "Basic is unambiguous" does not survive a
gateway performing basic-auth in front of this server. Supply Bitbucket
credentials per-request with `X-BITBUCKET-USERNAME` + `X-BITBUCKET-APP-PASSWORD`
or `X-BITBUCKET-ACCESS-TOKEN`.

### Installing a fresh release

`uv` will report `no version of bb-mcp-server==<version>` for a release that is
already on PyPI, and `--refresh` is not enough to clear it. Use `--no-cache`:

```bash
uv tool install --force --no-cache bb-mcp-server==1.10.0
```

This has bitten three releases in a row, in two different environments, and it
fails in the most misleading way available -- the resolver says the version
does not exist while the simple index lists it and a direct wheel download
succeeds. Confirm the installed version before trusting any measurement taken
through the tool:

```bash
python -c "import importlib.metadata as m; print(m.version('bb-mcp-server'))"
```

Separately, `https://pypi.org/pypi/<pkg>/json` is CDN-cached and can report a
just-published version as absent for a minute or so. `https://pypi.org/simple/<pkg>/`
is what installers actually read and is authoritative.

### 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` -- not `me_whoami` -- to confirm a token-based setup works.
- Pass `--author` / `--reviewer` / `--involving` an explicit display name or
  account id rather than `me`. `me` needs `GET /user` and cannot resolve.
- `--mentions` by 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

```bash
# 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:

1. **Update your tool calls:**
   ```python
   # Before
   call_tool("pr_list", {"author": "me"})

   # After
   call_tool("bitbucket", {"action": "pr_list", "author": "me"})
   ```

2. **Update your server startup:**
   ```bash
   # Before
   python tools_server.py --transport http --port 8000

   # After
   python server.py --transport http --port 8000
   ```

3. **No changes needed for:**
   - Environment variables
   - HTTP headers
   - Resources
   - Prompts
   - Authentication

## Benefits of Server Instructions Pattern

Following the [MCP blog post recommendations](https://blog.modelcontextprotocol.io/posts/2025-11-03-using-server-instructions/):

✅ **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.10.2

- **`pr_tasks_list --task-state OPEN` returned zero tasks, always.** Bitbucket's
  task vocabulary is `UNRESOLVED`/`RESOLVED`; this tool's filter is spelled
  `OPEN`/`RESOLVED`/`ALL`, and the caller's spelling went into the query
  verbatim. `state="OPEN"` is a query Bitbucket accepts and that matches
  nothing, so the filter answered "no tasks" whether or not any existed.
  `RESOLVED` worked only because that literal coincides with Bitbucket's.
- The failure direction is what makes it serious: `--task-state OPEN` answers
  "what tasks are blocking this PR?", and a caller could not distinguish a
  clean PR from a broken filter. The unfiltered call and `--task-state ALL`
  were correct throughout, including the computed `open` count.
- Only the query is translated. The emitted `state` still passes through as
  Bitbucket sends it (`UNRESOLVED`), and a test pins that asymmetry so it is
  not "fixed" later -- rewriting the response would break callers reading it.
- Found by exercising the one path this project could not reach on its own:
  no pull request in the workspace had a single task, so the filter had only
  ever been tested against mocks. A downstream consumer created one on a draft
  PR with explicit authorization, measured the failure, and removed it.

### 1.10.1

- **An inbound `Authorization: Basic` header is no longer forwarded to
  Bitbucket, and the documentation that said it was has been corrected.** The
  docs were wrong in both directions: they described behaviour the code did not
  have on the real HTTP path -- `get_config()` discarded the request config
  before `auth.py` could see the header -- and the behaviour they described was
  not one worth having. "Basic is unambiguous, Bitbucket is its only consumer
  here" is true of this server's own consumers and false of the deployment
  chain: an operator fronting a transport-auth-less MCP server with gateway
  basic-auth would have every request carry the *gateway* password, which this
  server would forward to Atlassian. That is the same confused-credential class
  that got `Bearer` forwarding removed in 1.6.0, and it appears in exactly the
  deployment where Basic forwarding was reachable at all.
- Nothing is lost: `X-BITBUCKET-USERNAME` + `X-BITBUCKET-APP-PASSWORD` is the
  exact equivalent and is unambiguous by construction. Basic forwarding was
  also only ever reachable on an HTTP server with no `MCP_AUTH_TOKEN`, since
  otherwise `Authorization` carries the transport bearer.
- The credential-leak test now covers the refusal path as well as the log path:
  a refused header's value must not appear in logs *or* in the resulting
  `ValueError`. `Basic` joins the refused-scheme cases.

### 1.10.0

- **`bb-cli` starts ~3x faster: 0.267s to 0.084s.** The CLI was importing
  `fastmcp` on every invocation for symbols its own code path never uses --
  `Context` appears only in the decorated MCP wrapper signatures, and
  `get_logger` is a stdlib logger with extra steps. Measured with
  `-X importtime`, the import was ~200ms of a ~267ms startup; config init,
  `load_dotenv` and logging setup were never the cost. After the split,
  `import cli` pulls 374 modules instead of 926, with **zero** `fastmcp` and
  zero `pydantic` among them, and startup sits on the httpx+dotenv floor of
  ~0.075s.
- The 13 context-taking tool wrappers and the unified tool moved to
  `src/modules/tools/wrappers.py`; implementations, helpers and enums stayed
  where they were. That direction was chosen deliberately over moving the
  impls: it preserves every `patch("src.modules.tools.tools.make_request")`
  target and the impl imports in nine test files. The v1.4.0 wrapper/impl
  split made this separation logical; this makes it physical.
- `config.py`'s `get_context` / `get_http_headers` / `get_http_request` are
  now lazy shims that delegate to fastmcp when it is loaded and otherwise
  reproduce its no-context behaviour, so the server path is unchanged.
- A regression test asserts the property directly rather than its symptom: a
  child interpreter imports `cli` and fails if any `fastmcp*` module appears
  in `sys.modules`. A timing measurement alone would not prove the import is
  gone.
- The header shim's delegation is covered by a test that drives fastmcp's real
  request context through httpx's ASGITransport. It was not: making the shim
  return `{}` unconditionally left all 350 tests green, because every other
  test passes headers explicitly. `Config.__init__` calls that shim whenever
  headers are not passed, so an unnoticed change there would have silently
  ignored per-request `X-BITBUCKET-*` configuration on the HTTP transport.
- Three behaviour deltas, none contract-breaking: CLI stderr diagnostics are
  plain-formatted rather than rich (same 13 lines, same order; server mode
  unaffected), submodules are no longer bound as attributes after a bare
  `import src.modules.tools`, and `config.get_http_headers` drops fastmcp's
  unused `include_all` parameter.

### 1.9.2

- `pr_list` reports `items_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
  (exactly `max(1, len(reviewers))`, measured live), so `limit=10` against 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. `next` signals that more exist but not
  why the page is short; `items_partial` says it, mirroring
  `pipe_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_list` deduplicates OR-clause matches.** Bitbucket returns one row per
  matching clause, so `involving` -- 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
  exactly `max(1, len(reviewers))` per PR, so an account that reviews nothing
  fans out identically and only the OR combination triggers it. Wrong since
  `involving` shipped in 1.7.0. Reported by a downstream consumer whose own
  REST implementation hit it the same day.
- **`workspace_list --kind repos` sorts 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 of `pr_list` and of nothing else: `workspace_list` (all four kinds),
  `pr_tasks_list`, `pr_tasks_sync`, `me_whoami`, `pipe_fail_summary`,
  `pr_overview` and `pr_review` all still raised `AttributeError`, reaching the
  caller as `Error 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_review` on a null PR
  title (where `pr_overview` on the identical payload correctly returned a
  schema violation -- the two disagreed), and `pr_tasks_sync` with
  `create=[{"text": null}]`, which is caller-controlled.
- `pr_list`'s `reviewers` no 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 said `bitbucket`, 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 `mainbranch`
  crash 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_list` in 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 nulls `old`
  on an added file, and `target.ref_name` is absent from most pipelines.

### 1.9.0

- **Breaking:** `pr_overview`'s `diffstat_top` entries rename `del` to
  `remove`. `del` is 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 every `pr_overview` call. 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 reading `diffstat_top[].del`
  must read `[].remove` instead.

- `pr_list` summary items now carry `comments` (Bitbucket's `comment_count`)
  and `reviewers` (requested reviewers by uuid). Neither was reachable before
  at any verbosity: the request's own `fields=` projection filtered them out,
  so `raw.comment_count` was `None` even at `verbosity="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_list` responses include `resolved_actors`, echoing what each
  `author`/`reviewer`/`involving` filter resolved to (`uuid` for `"me"`,
  `account_id` for a name). A caller filtering by `"me"` can now read its own
  identity from the same response instead of issuing a `GET /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 per `pr_list` call. `--author me --reviewer
  me --involving me` previously issued three separate `GET /user` requests for
  the same identity.
- **Fix `pr_list` and `repo_get` on the `tools_server.py` surface.** Every tool
  there declares an `output_schema` with `additionalProperties: false`, which
  FastMCP enforces -- so adding `comments`/`reviewers`/`resolved_actors` broke
  both tools for every call (`isError: Output validation error`), including
  empty result sets that had validated before. `repo_get` was hit because
  `recent_prs` reuses the same `PRItem` definition. The unified `server.py`
  surface 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_list` with
  `verbosity="ids"` (emits integers, schema allowed only strings) and
  `verbosity="full"` (emits `raw`, which `PRItem` forbids); `pr_overview`
  (`pr.description`, `comments_recent[].text`); `pr_review` (`goal`, `policy`,
  `findings[].category`); `pr_tasks_list` (Bitbucket's `UNRESOLVED` state, and
  null `created`/`updated`/`creator`/`url`); `repo_get` (`name`,
  `description`).
- Fix `pr_tasks_sync` and `pr_upsert`, which returned `isError` on EVERY call
  on the `tools_server.py` surface: `created_tasks` and `description` are
  unconditional members of their return dicts and neither was declared
  (`partial`/`failures` were 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
  adds `top_lines_truncated` (and `logs_partial`/`logs_error` on 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
  `None` through, so a task without a timestamp or a repository without a main
  branch must validate. But fields the API always returns -- a PR's `state`, a
  repository's `slug`, the `title`/`state` echoed 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 both `required` and 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 a `get()` default applies only when the key is ABSENT, and
  Bitbucket returns keys present-and-null (`mainbranch: null` for a repo with
  no commits, `links: {"html": null}`). 42 such chains across `tools.py`,
  `meta.py` and the resources modules now use `(X.get("key") or {})`. Two were
  proven to crash and reach the caller as `Error 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`/`links` entries 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
  boolean `comment_count` passed every guard and then failed output
  validation. Guards now use an explicit int-not-bool check.
- Tighten `$defs.Step` from `additionalProperties: true`, which made pipeline
  step content structurally unvalidatable -- nothing about a step could fail,
  mocked or live.
- New `tests/test_output_schema_contract.py` drives 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 the `author="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_list` guidance, which told the model
  `author="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 `--quiet` authlib deprecation warning for the second time. The
  previous fix matched on the message instead of `module=` (correct, and
  necessary), but was still defeated by ordering: `authlib/deprecate.py` runs
  `warnings.simplefilter("always", AuthlibDeprecationWarning)` at import time,
  and `simplefilter` inserts at the **front** of `warnings.filters`. Imported
  lazily through fastmcp, it landed ahead of our filter and re-armed the
  warning. `cli.py` now imports `authlib.deprecate` first so our filter goes in
  front. This is also why `PYTHONWARNINGS` and `warnings.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-cli` fan-out (one worker per job, not per
  core) with the measurement behind it.
- Regenerate the README `--help` reference, which was missing
  `pipe_branch_heads`, `--reviewer`, `--involving`, and `--days`.
- Add `pipe_branch_heads` to 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 the `bitbucket` tool 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`'s `author` as defaulting to `"me"` -- a default removed in 1.7.0
  precisely because resolving it costs a `GET /user` that 403s under
  access-token auth -- and omitted `pr_list`'s `reviewer`/`involving` and
  `pr_comment_add`'s `mentions` entirely. 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 neither `pr_tasks_list` nor `pipe_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.
  Returns `heads_partial` when 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_server`
  declared a `lifespan=`, 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 `--quiet` authlib fix (see 1.8.1 -- it was necessary but not
  sufficient).

### 1.7.0

- **Breaking:** `pr_list` builds a single `q` expression from AND-ed
  predicates. Bitbucket ignores the `state` parameter whenever `q` is present,
  so `--state` was silently dropped for any filtered query. `state` is now
  folded into `q` and never sent separately. Adds `--reviewer` and
  `--involving`.

### 1.6.0

- Stop forwarding an inbound `Authorization: Bearer` header 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. Use `X-BITBUCKET-ACCESS-TOKEN` instead.
- `MCP_AUTH_TOKEN` now 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=true` also blocks env fallback for a request
  that omits all `X-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`, honour `NO_TOOLS`, and report the real package version.
- Fix error classification (`timed out` was unmatched), raw-payload
  mispairing in `workspace_list`, and an N+1 issuing `GET /user` per 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-asyncio` so the suite runs on a clean checkout.

### 1.5.0

- `pr_comment_add` verifies every @mention (via a new `mentions` parameter,
  and any `@{...}` token already typed into `text`) 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 via `mentions_resolved`. See `src/utils/mentions.py`.
- `pr_comment_add` now also recognizes `@[Full Name]` and bare `@Name` /
  `@Name Surname` written directly into the comment body (not just the
  `mentions` parameter 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 new `mentions_unmatched`
  response field instead of failing the post. Code spans and fenced code
  blocks are never touched.

### 1.4.0

- Add `BITBUCKET_ACCESS_TOKEN` for non-interactive Bearer auth.
- Complete the wrapper/impl split so the CLI and MCP share one code path.
- Register `pr_comments_list` and `pr_tasks_list`; correct the tool count.
- Stop discarding response content in the formatter; stop rebinding
  `unified.bitbucket` to its `Tool` wrapper.
- `pr_tasks_sync` reports 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-cli` terminal wrapper; remove hardcoded credentials.
- Add `pr_comments_list` and `pr_tasks_list` actions.
- Migrate off the removed `/2.0/workspaces` endpoint (CHANGE-2770).
- Bound fastmcp to `<3` to avoid an async Context API break.

### 1.2.4

- Fix `me_whoami` and workspace auto-discovery hitting `GET /2.0/workspaces`,
  which Atlassian removed in [CHANGE-2770](https://developer.atlassian.com/cloud/bitbucket/changelog/)
  (returns `410 Gone`). Both now use the user-scoped replacement
  `GET /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.
