Skip to main content
Glama
README.md
# BB-DC-MCP — Bitbucket Data Center MCP Server

A [Model Context Protocol](https://modelcontextprotocol.io) server for
**Bitbucket Data Center**, built with the official **mcp** SDK v2
(`MCPServer`, Python 3.12).

| Server | Product | Version | Tools |
|--------|---------|---------|-------|
| [Bitbucket](#-bitbucket-data-center) | Bitbucket Data Center | v9.4+ | 20 |

Every REST call runs as the **calling user**: the server is **broker-only**
— each caller links their Bitbucket account once (OAuth 2.0 authorization
code + PKCE against Bitbucket's native
[OAuth 2.0 Provider API](https://confluence.atlassian.com/bitbucketserver/bitbucket-oauth-2-0-provider-api-1108483661.html)),
and their tokens live encrypted in the server's Postgres token store. There
is no bot fallback, no PAT passthrough, and no unsigned-header identity mode.

## Architecture

```
User ──► MCP client (LibreChat / Claude Code / …), Okta PKCE
              │
              ▼
       agentgateway         ← validates the Okta JWT, applies tool RBAC
              │  POST /mcp    (streamable HTTP, stateless)
              │  Authorization: Bearer <okta access token>
              │  X-MCP-Gateway-Secret: <shared secret>
              ▼
       BB-DC-MCP            ← re-verifies the token (defense in depth),
              │               maps sub → the caller's own Bitbucket token
              ▼
       Bitbucket DC         ← native per-user permissions and audit
```

* **Transport**: MCP streamable HTTP, **stateless**, endpoint `/mcp` — one
  process serves both the 2026-07-28 protocol revision and legacy
  (`initialize`) clients.
* **Identity**: derived exclusively from the verified Okta access token
  (`MCP_OKTA_ISSUER` / `MCP_OKTA_AUDIENCE`); the immutable `sub` claim keys
  the token broker.
* **First link**: when the broker holds no token for the caller, the tool
  returns a URL elicitation (2026-07-28 clients) or a consent-link error
  text (legacy clients); the user approves once in the browser and retries.
* **Scope tiers**: two incoming Application Links — read tools use the
  `PROJECT_READ REPO_READ` grant, write tools the separate `REPO_WRITE`
  grant — so read-only usage never holds a write-capable token.

Design details and operational notes: [docs/broker-mode.md](docs/broker-mode.md).

## Deploying

Step-by-step (namespace, secrets, the two Application Links, gateway wiring,
verification, troubleshooting): **[docs/deploy-bitbucket.md](docs/deploy-bitbucket.md)**.

* [`k8s/`](k8s/) — kustomize base: the server Deployment (broker env
  included), token-store Postgres, NetworkPolicy, token GC CronJob.
* [`k8s/argocd/application.yaml`](k8s/argocd/application.yaml) — ArgoCD
  Application deploying the base (or your per-cluster overlay of it).

Local development:

```bash
uv sync --all-extras          # install
uv run pytest -q              # tests (all Bitbucket/Okta HTTP is faked)
uv run ruff check .           # lint
uv run mypy servers/          # strict type check

MCP_DEV_USER=jane.doe uv run bitbucket-server     # local stdio run
MCP_TRANSPORT=http uv run bitbucket-server        # streamable HTTP at :8000/mcp
```

Container image: `ghcr.io/betasystems/bb-dc-mcp/bitbucket`
(`linux/amd64` + `linux/arm64`), built by CI from [Dockerfile](Dockerfile).

### Releasing

Releases are semver git tags; deployments pin the matching image tag
instead of `:latest`.

**Every merge to `main` releases automatically**, sized by
[semantic-release](https://github.com/semantic-release/semantic-release)
([.releaserc.json](.releaserc.json)) from the commit messages since the
last tag:

| Commits since last release contain | Bump |
|---|---|
| a `BREAKING CHANGE:` footer (or `feat!:`) | major |
| a `feat:` commit | minor |
| anything else (catch-all rule — every merge releases) | patch |

CI dry-runs semantic-release to compute the version, builds + scans the
multi-arch image, publishes `:X.Y.Z`, `:X.Y`, and `:latest`, then
semantic-release creates the tag and the GitHub Release with
conventional-commit notes.

Manual tag pushes (`git tag v1.0.0 && git push origin v1.0.0`) remain
as an escape hatch — the pipeline releases them unchanged and
semantic-release continues from the new baseline. Bump `version` in
[pyproject.toml](pyproject.toml) only for such manual milestones
(automated releases don't rewrite it; the image's OCI `version` label
always carries the released version).

## Trust boundary

A captured Okta token is replayable for its validity window, so deployments
must combine:

1. **Okta re-verification** (`MCP_OKTA_ISSUER`/`MCP_OKTA_AUDIENCE`, required;
   optional `MCP_OKTA_AUTHORIZED_PARTY` pins `azp`) — independent of the
   gateway's own JWT validation.
2. **`MCP_GATEWAY_SECRET`** — constant-time-checked shared secret the gateway
   sends as `X-MCP-Gateway-Secret`; blocks direct token replay at the pods.
3. **NetworkPolicy** ([k8s/networkpolicy.yaml](k8s/networkpolicy.yaml)) —
   ingress restricted to the gateway data plane.
4. **`MCP_STRICT_AUTH=1`** (production) — fail closed unless the gateway
   secret, Okta verification, and shared request-state keys are configured.
   An Okta JWKS outage returns 503 (not a misleading 401).

Product tokens never travel through the gateway: they are minted at consent
by Bitbucket's OAuth 2.0 provider and stored **encrypted at rest** (AES-256-GCM
envelope; KMS-wrapped data key in production).

## 📁 Bitbucket Data Center

### MCP Apps views

On clients that negotiate the `io.modelcontextprotocol/ui` extension,
`list_pull_requests` renders an interactive **PR health** table and
`get_pull_request` a **PR lifecycle** view (open → review → merge-ready) —
pure renderers of the tool result, served as `ui://bitbucket/*` resources
([servers/bitbucket/ui.py](servers/bitbucket/ui.py)). Other clients get the
same markdown text; no extra tools or RBAC surface exist.

For hosts that speak **MCP-UI** instead of MCP Apps (e.g. Goose), set
`BITBUCKET_UI_INLINE_FALLBACK=true`: clients that did not negotiate the
extension then additionally get a self-contained rendering of the view
(data baked in, no handshake) embedded as a `text/html` resource in the
tool result. Off by default — plain-text clients would only see a large
HTML block. Every PR-view call logs the negotiation outcome, so the pod
log shows which path a client took.

**PR health** (`list_pull_requests`) — state/age/review chips with two
composable client-side filter rows (state × review hygiene: unassigned,
awaiting approval, needs work, ready to merge, draft, stale >30d), each
chip carrying its count; clicking a row opens the PR in Bitbucket:

![PR health view — pull-request table with state, age, and review chips](docs/images/pr-health.png)

**PR lifecycle** (`get_pull_request`) — the PR's gate pipeline plus
reviewers and description:

![PR lifecycle view — open → review → merge gate pipeline for one PR](docs/images/pr-lifecycle.png)

(Screenshots are the real `ui://bitbucket/*` views rendered against sample
data, in the host's light theme.)

### Tools (20)

| Tool | Description | R/W |
|------|-------------|-----|
| `list_bitbucket_projects` | List all accessible Bitbucket projects | R |
| `get_bitbucket_project` | Get details of a Bitbucket project | R |
| `list_repos` | List repositories in a project | R |
| `get_repo` | Get details of a repository | R |
| `list_repo_files` | List files/directories in a repo | R |
| `list_pull_requests` | List pull requests (OPEN, MERGED, DECLINED) | R |
| `get_pull_request` | Get detailed information about a PR | R |
| `create_pull_request` | Create a pull request | **W** |
| `decline_pull_request` | Decline a pull request | **W** |
| `add_pr_comment` | Add a comment to a pull request | **W** |
| `list_branches` | List branches in a repository | R |
| `create_branch` | Create a new branch | **W** |
| `list_tags` | List tags in a repository | R |
| `compare_branches` | Compare two branches | R |
| `list_commits` | List commits on a branch | R |
| `get_commit` | Get details of a specific commit | R |
| `get_commit_diff` | Get the diff for a commit | R |
| `get_commit_changes` | List files changed in a commit | R |
| `get_file_content` | Get raw file content | R |
| `get_build_status` | Get build statuses for a commit | R |

**Env prefix**: `BITBUCKET_` (product settings) and `MCP_` (shared server
settings). The full configuration surface is documented in
[k8s/configmap.yaml](k8s/configmap.yaml) and
[k8s/secret.yaml.example](k8s/secret.yaml.example).

Write tools mutate Bitbucket state **as the caller** (separate `REPO_WRITE`
grant, native audit attribution).

## License

MIT