Guide · CI/CD
MCP Servers with Bitbucket Pipelines — build, deploy, and verify the MCP protocol
Bitbucket Pipelines is Atlassian's native CI/CD service — fully integrated with Bitbucket repositories, Jira issue tracking, and the broader Atlassian toolchain. Every pipeline step runs inside a Docker container, which means no environment configuration beyond specifying an image: no plugins to install, no agents to provision for basic builds. For MCP server teams whose codebase already lives in Bitbucket, Pipelines offers the least-friction path to automated testing, container builds, and deployment with native environment tracking. This guide covers the complete bitbucket-pipelines.yml structure, Bitbucket Deployments, pipes for reusable deployment steps, parallel testing, and a post-deploy MCP protocol probe that verifies your server before alerting AliveMCP of a new deployment.
TL;DR
Structure your bitbucket-pipelines.yml with a default pipeline for pull request validation, a branches.main pipeline for staging deploys, and a branches.production pipeline for production deploys; use Bitbucket Deployment environments (deployment: staging / deployment: production) on deploy steps for environment-scoped secrets, Atlassian pipes for SSH and Kubernetes deployment, and finish every deploy step with a curl MCP protocol probe plus AliveMCP registration.
Pipeline structure: default, branches, pull-requests, and custom
A bitbucket-pipelines.yml file is organized under a top-level pipelines: key with four main sub-keys. The default: pipeline runs for every push to a branch that does not have a more specific rule. branches: lets you define per-branch pipelines using glob patterns (main:, feature/*:, 'release/**':). pull-requests: fires on pull request events and supports the special '**': key to match all PRs. custom: defines manual pipelines that can be triggered from the Bitbucket UI or API with optional variables — useful for production releases that should not auto-deploy.
For an MCP server the typical layout is: default: runs lint, unit tests, and type checking on every branch; branches.main: runs the full test suite, builds and pushes a Docker image, and deploys to staging; branches.production: deploys the pre-built image (or rebuilds) to production. Alternatively, you can use a custom: deploy-production: pipeline with a trigger: manual to require an explicit action to push to production rather than auto-deploying on every push to the production branch.
Each pipeline contains a list of steps:. A step is the unit of execution — it runs one Docker image and executes a list of script commands sequentially. Steps within the same pipeline run sequentially by default; to run steps in parallel you wrap them in a parallel: block. Between steps, Bitbucket does not automatically share files — you must use artifacts: to pass build outputs from one step to the next, and caches: to persist downloaded dependencies across pipeline runs.
A key difference from GitHub Actions and GitLab CI is that Bitbucket Pipelines has no concept of stages as a distinct YAML key — the sequence of steps in a pipeline is the stage order. Each step can be given a name, can specify its own Docker image, and can be marked as manual with trigger: manual. This is simpler and more explicit than a separate stages: declaration, but it also means you cannot define cross-step dependencies with a DAG — steps always run in the order they are listed.
Parallel steps for faster MCP server validation
The parallel: keyword wraps multiple steps and runs them simultaneously in separate containers. This is the primary mechanism for speeding up the test phase. For an MCP server that has a lint check, unit tests, and TypeScript type checking as separate npm scripts, you can run all three at once:
pipelines:
default:
- parallel:
- step:
name: Lint
image: node:20-alpine
caches:
- node
script:
- npm ci --prefer-offline
- npm run lint
- step:
name: Unit tests
image: node:20-alpine
caches:
- node
script:
- npm ci --prefer-offline
- npm test
- step:
name: Type check
image: node:20-alpine
caches:
- node
script:
- npm ci --prefer-offline
- npx tsc --noEmit
Each parallel step runs independently and Bitbucket waits for all three to succeed before moving to the next step in the pipeline. If any parallel step fails, the pipeline is marked as failed and subsequent steps do not run. The practical effect is that a three-minute lint + test + typecheck that previously ran sequentially now completes in roughly one minute — the time of the slowest step — at the cost of one extra minute of runner time. For MCP servers with even moderate test suites, the wall-clock savings justify the parallel step overhead.
Note that parallel steps cannot share files between each other (they run in separate containers with separate workspaces). If you need the test results from the unit test step in a later coverage reporting step, use artifacts: on the unit test step to export the coverage directory, then consume it in the later step. Artifacts persist within a single pipeline run but are not the same as caches — they are not shared across runs.
Bitbucket Deployments and environment-scoped variables
Bitbucket Deployments provide native environment tracking analogous to GitLab Environments or GitHub Environments. When a step includes a deployment: key, Bitbucket records that deploy in the Deployments view, showing which commit reached which environment and when. Supported environment types are test, staging, and production.
The most important feature of Bitbucket Deployments is deployment-scoped variables. In the Bitbucket repository settings under Deployments, you can configure variables that are only available to steps with a matching deployment: key. A variable named SSH_DEPLOY_KEY set in the production deployment configuration will only be exposed to steps with deployment: production. Repository-level variables (set in Repository Variables) are available to all pipeline steps regardless of environment. This separation means your production SSH key or database URL cannot leak into a staging or PR build.
Deployment variables are also distinct from repository variables in how they interact with Bitbucket's secret masking. Both can be marked as secured (masked from logs), but deployment variables have the additional scoping benefit. For MCP server teams managing multiple environments, the pattern to follow is: repository variables for non-secret, cross-environment config (like DOCKER_REGISTRY or MCP_SERVER_PORT), and deployment variables for all secrets and environment-specific endpoints.
Pipes: Atlassian's reusable deployment steps
Bitbucket Pipes are Atlassian's analogue to GitHub Actions marketplace actions: pre-built, versioned, parameterized building blocks for common CI/CD tasks. A pipe is just a Docker image that the Bitbucket runner pulls and executes with the variables you pass. The key pipes for MCP server deployment are atlassian/ssh-run:0.4.0 for running commands on a remote VPS via SSH, and atlassian/kubectl-run:1.1.2 for applying Kubernetes manifests or running kubectl set image to update a deployment.
Using atlassian/ssh-run for a VPS deploy looks like this in YAML:
- step:
name: Deploy to production VPS
deployment: production
script:
- pipe: atlassian/ssh-run:0.4.0
variables:
SSH_USER: 'deploy'
SERVER: $DEPLOY_HOST
SSH_KEY: $SSH_DEPLOY_KEY
COMMAND: |
docker pull $DOCKER_REGISTRY/my-mcp-server:$BITBUCKET_COMMIT
docker stop mcp-server || true
docker rm mcp-server || true
docker run -d \
--name mcp-server \
--restart unless-stopped \
-p 3000:3000 \
-e NODE_ENV=production \
$DOCKER_REGISTRY/my-mcp-server:$BITBUCKET_COMMIT
Pipes differ from GitHub Actions in a subtle but important way: pipes always run as a separate Docker container within the step, not as code within the runner's process. This means pipes have their own file system — they cannot read artifacts from the current step's workspace unless you explicitly mount them. For most deployment steps this is not a problem, but it means you cannot use a pipe to post-process test output files generated earlier in the same step.
For Kubernetes deployments, atlassian/kubectl-run requires a KUBE_CONFIG variable containing your kubeconfig as a base64-encoded string. Store this as a secured deployment variable in the production environment. The pipe decodes it, writes it to ~/.kube/config, and then runs the kubectl commands you specify in KUBECTL_COMMAND.
Service containers, caching, and artifacts
Bitbucket Pipelines supports service containers — additional Docker containers that run alongside the main step container. The canonical use case for MCP server integration tests is running a database or Redis cache that the tests connect to:
- step:
name: Integration tests
image: node:20-alpine
services:
- postgres
caches:
- node
script:
- npm ci --prefer-offline
- npm run test:integration
definitions:
services:
postgres:
image: postgres:16-alpine
variables:
POSTGRES_DB: mcp_test
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
The service container is accessible from the step container via the service name as a hostname (i.e., postgres:5432 from the step's perspective). This pattern enables testing MCP tool handlers that read from or write to a database without mocking the database layer.
Bitbucket has several built-in cache definitions: node caches ~/.npm, pip caches ~/.cache/pip, maven caches ~/.m2/repository, and so on. You can also define custom caches under definitions.caches:. Caches are invalidated when the cache hash changes — Bitbucket tracks a hash of the cached directory content and will upload a new cache if it has changed at the end of the step. Caches are shared across pipeline runs for the same branch, but not across branches by default.
Artifacts bridge steps within a single pipeline. Declare artifacts: at the end of a step to export specific files or glob patterns. A subsequent step in the same pipeline will find those files in the workspace automatically — no download command needed. For MCP server pipelines, the build step typically produces a compiled dist/ directory that the Docker build step picks up via artifact, avoiding a redundant npm run build in the image layer.
Complete bitbucket-pipelines.yml example
The following example implements a production-ready pipeline with parallel testing, Docker build, staging deploy, and production deploy with MCP protocol verification.
image: node:20-alpine
definitions:
caches:
node: node_modules
services:
postgres:
image: postgres:16-alpine
variables:
POSTGRES_DB: mcp_test
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
pipelines:
# ── Pull request validation ────────────────────────────────
pull-requests:
'**':
- parallel:
- step:
name: Lint & type-check
caches:
- node
script:
- npm ci --prefer-offline
- npm run lint
- npx tsc --noEmit
- step:
name: Unit tests
caches:
- node
script:
- npm ci --prefer-offline
- npm test
# ── Main branch: build + deploy to staging ─────────────────
branches:
main:
- step:
name: Test
caches:
- node
services:
- postgres
script:
- npm ci --prefer-offline
- npm test -- --coverage
artifacts:
- coverage/**
- step:
name: Build & push Docker image
image: atlassian/default-image:4
script:
- docker login -u $DOCKER_USER -p $DOCKER_PASSWORD $DOCKER_REGISTRY
- export IMAGE=$DOCKER_REGISTRY/my-mcp-server:$BITBUCKET_COMMIT
- docker build --tag $IMAGE --tag $DOCKER_REGISTRY/my-mcp-server:latest .
- docker push $IMAGE
- docker push $DOCKER_REGISTRY/my-mcp-server:latest
# Write image reference to artifact for later steps
- echo "IMAGE=$IMAGE" >> build.env
artifacts:
- build.env
services:
- docker
- step:
name: Deploy to staging
deployment: staging
script:
- source build.env
- pipe: atlassian/ssh-run:0.4.0
variables:
SSH_USER: 'deploy'
SERVER: $STAGING_HOST
SSH_KEY: $SSH_DEPLOY_KEY
COMMAND: >
docker pull $IMAGE &&
docker stop mcp-server-staging || true &&
docker rm mcp-server-staging || true &&
docker run -d
--name mcp-server-staging
--restart unless-stopped
-p 3001:3000
-e NODE_ENV=staging
$IMAGE
# MCP Protocol Verification
- apk add --no-cache curl jq
- sleep 8
- |
RESPONSE=$(curl -sf --max-time 15 \
-X POST "https://staging-mcp.example.com/mcp" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"bb-probe","version":"1.0"}}}')
echo "MCP response: $RESPONSE"
echo "$RESPONSE" | jq -e '.result.protocolVersion' \
|| (echo "MCP protocol verification FAILED" && exit 1)
echo "Staging MCP server verified OK"
# ── Production branch: deploy to production ────────────────
production:
- step:
name: Deploy to production
deployment: production
trigger: manual
script:
- apk add --no-cache curl jq
# The production environment variable DEPLOY_IMAGE is set in
# Bitbucket Deployments → production → variables
- pipe: atlassian/ssh-run:0.4.0
variables:
SSH_USER: 'deploy'
SERVER: $PROD_HOST
SSH_KEY: $SSH_DEPLOY_KEY
COMMAND: >
docker pull $DEPLOY_IMAGE &&
docker stop mcp-server || true &&
docker rm mcp-server || true &&
docker run -d
--name mcp-server
--restart unless-stopped
-p 3000:3000
-e NODE_ENV=production
-e DATABASE_URL=$DATABASE_URL
$DEPLOY_IMAGE
# MCP Protocol Verification
- sleep 8
- |
RESPONSE=$(curl -sf --max-time 15 \
-X POST "https://mcp.example.com/mcp" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"bb-probe","version":"1.0"}}}')
echo "$RESPONSE" | jq -e '.result.protocolVersion' \
|| (echo "ERROR: MCP protocol probe failed" && exit 1)
echo "Production MCP server verified. Protocol: $(echo $RESPONSE | jq -r '.result.protocolVersion')"
# AliveMCP Registration
- |
curl -sf \
-X POST "https://alivemcp.com/api/register" \
-H "Authorization: Bearer $ALIVEMCP_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"slug\":\"my-mcp-server\",\"url\":\"https://mcp.example.com/mcp\",\"version\":\"$BITBUCKET_COMMIT\"}" \
&& echo "AliveMCP registration OK" \
|| echo "WARNING: AliveMCP registration failed"
The trigger: manual on the production deploy step means Bitbucket will pause the pipeline at that step and display a "Run" button in the UI. A human must click it to proceed. The source build.env pattern passes the exact image tag built in the previous step into the current step's environment — an alternative to using a fixed :latest tag that could be overwritten by a concurrent pipeline run.
Frequently asked questions
What is the difference between repository variables and deployment variables in Bitbucket Pipelines?
Repository variables are available to every step in every pipeline for the repository, regardless of which branch or environment the pipeline is running against. They are the right place for values that need to be available everywhere, such as a Docker registry URL or a non-secret build configuration flag. Deployment variables are scoped to a specific Bitbucket Deployment environment (test, staging, or production) and are only injected into steps that have a matching deployment: key. This scoping is a security boundary: your production SSH_DEPLOY_KEY or DATABASE_URL will never be available in a pull request pipeline or a staging deploy step. When a variable with the same name exists at both the repository level and the deployment level, the deployment-level value takes precedence for steps in that environment. This is the correct pattern for environment-specific overrides: set a default at the repository level, override it per environment in the deployment settings.
How do Bitbucket Pipes differ from GitHub Actions actions?
Both are mechanisms for reusable CI/CD building blocks, but they work differently under the hood. A Bitbucket Pipe is a Docker image that Bitbucket pulls and runs as a separate container within your step's execution context. Variables you pass to the pipe become environment variables in that container. A GitHub Actions action is either a JavaScript module that runs in the runner's Node.js process, a Docker container action, or a composite of other actions; the Docker container variant is most analogous to a Bitbucket Pipe. The practical difference is that pipes are always containerized, which means they are fully isolated from your step's file system — a pipe cannot read files from your workspace unless the pipe explicitly mounts the workspace (some do, some don't). GitHub JavaScript actions can read and write workspace files directly because they run in the same process. For deployment tasks like SSH and kubectl, this isolation is usually not a problem because you are passing parameters rather than files.
How do I pass build artifacts between steps in Bitbucket Pipelines?
Use the artifacts: key on the step that produces the files, listing glob patterns for the files you want to preserve. Subsequent steps in the same pipeline will automatically have those files available in their workspace — you do not need to explicitly download them. The artifact upload happens at the end of the step (even if the step fails, if you set expire-in: never). Artifacts are scoped to a single pipeline run; they are not available to other pipeline runs. For passing large binary artifacts (like a compiled binary or a Docker image tarball), consider using an external storage location (S3, Artifactory) and passing the URL or digest as a small text artifact. Note that caches are different: caches persist across pipeline runs and are shared by steps using the same cache key on the same branch, but they are not guaranteed to be available — Bitbucket can evict caches, and a cache miss simply means the step runs without the cached data (it is not a failure).
Can I run integration tests against a real database in Bitbucket Pipelines?
Yes — this is the primary use case for service containers. Define your database (PostgreSQL, MySQL, Redis, MongoDB) under definitions.services: and reference it in the step's services: list. The service container starts before the step's script begins and is accessible via the service name as a hostname. Your MCP server's test configuration should connect to postgres:5432 (not localhost) because the step container and the service container are on the same Docker network but are separate containers. For MCP server integration tests that exercise tool handlers with real database operations, this approach is preferable to mocking because it catches SQL query errors, schema mismatches, and transaction bugs that mocks would silently absorb. The service container is started with the variables you define under the service's variables: key, which is where you set the test database name, user, and password.
How do I handle a failed MCP server deployment in Bitbucket Pipelines?
When the MCP protocol probe step fails (exits with a non-zero code), Bitbucket marks the pipeline as failed and stops subsequent steps. The failed deploy is visible in the Bitbucket Deployments view alongside the last successful deployment. To roll back, the cleanest approach is to re-run the pipeline from the last successful commit: open the commit in Bitbucket, click "Run pipeline", and select the branch pipeline or a custom deploy pipeline. Because the Docker image for that commit is still in your registry (tagged with its commit SHA), the deploy step will pull the exact same image that was running before the bad deploy. If you need to roll back urgently and cannot wait for a pipeline run, SSH to the host and manually run the previous image by its commit SHA tag. After rollback, investigate whether the MCP probe failure was a transient startup issue (the sleep before the probe was too short) or a genuine protocol regression (the new code has a bug in the initialize handler), and fix accordingly before re-deploying.
Further reading
- MCP Server CI/CD — general patterns and pipeline design principles
- MCP Servers with GitLab CI/CD — pipeline stages, container registry, and protocol verification
- MCP Servers with GitHub Actions — workflow files, secrets, and deployment
- MCP Servers with Tekton — cloud-native Kubernetes CI/CD
- MCP Servers with Jenkins — declarative pipeline and Docker builds
- MCP Servers with Azure DevOps — build, deploy to Azure, and protocol verification
- MCP Servers with Docker — building and running containers
- MCP Servers on Kubernetes — deployment, services, and health checks
- MCP Server Health Checks — implementing and monitoring protocol probes
- MCP Server Zero-Downtime Deployment — rolling updates and blue/green strategies
- MCP Servers with nginx — reverse proxy, TLS termination, and routing