diff --git a/.github/workflows/update-mcp-registry.yml b/.github/workflows/update-mcp-registry.yml new file mode 100644 index 0000000..1de6234 --- /dev/null +++ b/.github/workflows/update-mcp-registry.yml @@ -0,0 +1,145 @@ +name: Update MCP Registry + +on: + workflow_run: + workflows: ["Build and Push Docker Image"] + types: + - completed + branches: + - main + +jobs: + check-and-update-registry: + # Only run on tag pushes and if the docker workflow succeeded + if: | + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + startsWith(github.event.workflow_run.head_branch, 'v') + + runs-on: ubuntu-latest + permissions: + id-token: write # Required for GitHub OIDC authentication with MCP Registry + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_branch }} + + - name: Wait for PyPI release to complete + uses: fountainhead/action-wait-for-check@v1.2.0 + id: wait-for-pypi + with: + token: ${{ secrets.GITHUB_TOKEN }} + checkName: "Build and publish Python distributions to PyPI" + ref: ${{ github.event.workflow_run.head_sha }} + timeoutSeconds: 600 + intervalSeconds: 10 + + - name: Check PyPI release status + if: steps.wait-for-pypi.outputs.conclusion != 'success' + run: | + echo "PyPI release did not succeed. Status: ${{ steps.wait-for-pypi.outputs.conclusion }}" + exit 1 + + - name: Validate version consistency + id: version + run: | + # Read versions from files + ROOT_VERSION=$(jq -r '.version' server.json) + # Only get versions from packages that have a version field (exclude null/empty) + PACKAGE_VERSIONS=$(jq -r '.packages[] | select(.version != null) | .version' server.json | sort -u) + PYPROJECT_VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') + TAG_VERSION=${GITHUB_REF_NAME#v} + + echo "Version Check:" + echo " server.json root: $ROOT_VERSION" + echo " server.json packages:" + jq -r '.packages[] | + if .version != null then + " - \(.registryType):\(.identifier) = \(.version)" + else + " - \(.registryType):\(.identifier) (no version field)" + end' server.json + echo " pyproject.toml: $PYPROJECT_VERSION" + echo " Git tag: $TAG_VERSION" + echo "" + + # Check if all package versions match root version (only for packages with version field) + MISMATCH=false + if [ -n "$PACKAGE_VERSIONS" ]; then + while IFS= read -r pkg_ver; do + if [ "$pkg_ver" != "$ROOT_VERSION" ]; then + echo "ERROR: Package version ($pkg_ver) doesn't match root version ($ROOT_VERSION)" + MISMATCH=true + fi + done <<< "$PACKAGE_VERSIONS" + fi + + if [ "$MISMATCH" = true ]; then + echo "" + echo "Fix: Update server.json so all package versions match the root version" + exit 1 + fi + + # Check if root version matches tag + if [ "$ROOT_VERSION" != "$TAG_VERSION" ]; then + echo "ERROR: server.json version ($ROOT_VERSION) doesn't match tag ($TAG_VERSION)" + echo "Fix: Update server.json root version to match the tag" + exit 1 + fi + + # Warn if pyproject.toml doesn't match + if [ "$PYPROJECT_VERSION" != "$TAG_VERSION" ]; then + echo "WARNING: pyproject.toml version ($PYPROJECT_VERSION) doesn't match tag ($TAG_VERSION)" + echo "This may cause PyPI issues" + fi + + # Check Docker image tags in OCI identifiers + echo "" + echo "Checking Docker image tags..." + OCI_PACKAGES=$(jq -r '.packages[] | select(.registryType == "oci") | .identifier' server.json) + if [ -n "$OCI_PACKAGES" ]; then + OCI_TAG_MISMATCH=false + while IFS= read -r oci_id; do + # Extract tag from identifier (everything after last :) + IMAGE_TAG="${oci_id##*:}" + echo " - Image: $oci_id" + echo " Tag: $IMAGE_TAG" + + if [ "$IMAGE_TAG" != "$ROOT_VERSION" ]; then + echo " ERROR: Docker image tag ($IMAGE_TAG) doesn't match version ($ROOT_VERSION)" + OCI_TAG_MISMATCH=true + else + echo " Tag matches" + fi + done <<< "$OCI_PACKAGES" + + if [ "$OCI_TAG_MISMATCH" = true ]; then + echo "" + echo "Fix: Update Docker image tags in server.json to match the version" + exit 1 + fi + fi + + echo "" + echo "All version checks passed!" + echo "version=$ROOT_VERSION" >> $GITHUB_OUTPUT + + - name: Install MCP Publisher + run: | + curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher + + - name: Login to MCP Registry + run: ./mcp-publisher login github-oidc + + - name: Publish to MCP Registry + run: ./mcp-publisher publish + + - name: Notify completion + if: success() + run: | + echo "MCP Registry updated successfully for version ${{ steps.version.outputs.version }}" + echo "Docker image: docker.io/couchbaseecosystem/mcp-server-couchbase:${{ steps.version.outputs.version }}" + echo "PyPI package: couchbase-mcp-server==${{ steps.version.outputs.version }}" diff --git a/README.md b/README.md index 9fae243..5c1b6bb 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ An [MCP](https://modelcontextprotocol.io/) server implementation of Couchbase th Couchbase Server MCP server + + ## Features - Get a list of all the buckets in the cluster diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..151eca8 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,264 @@ +# Release Process + +This document describes how to create a new release of `mcp-server-couchbase`. + +## Release Process + +### Update Version Numbers + +**Option A: Use the helper script (Recommended):** + +```bash +./scripts/update_version.sh 0.5.2 +``` + +This automatically updates: + +- `pyproject.toml` version +- `server.json` root version +- `server.json` all package versions +- `server.json` Docker image tags (OCI identifiers) +- `uv.lock` + +**Option B: Manual update:** + +Update the version in all locations: + +1. **`pyproject.toml`:** + + ```toml + version = "0.5.2" + ``` + +2. **`server.json`** (root version): + + ```json + { + "version": "0.5.2", + ... + } + ``` + +3. **`server.json`** (each package version): + + ```json + { + "packages": [ + { + "version": "0.5.2", + ... + } + ] + } + ``` + +4. **`server.json`** (Docker image tags in OCI packages): + + ```json + { + "packages": [ + { + "registryType": "oci", + "identifier": "docker.io/couchbaseecosystem/mcp-server-couchbase:0.5.2", + ... + } + ] + } + ``` + +5. Update lock file: + ```bash + uv lock + ``` + +> **Important:** All versions and Docker image tags must match the root version. The CI/CD pipeline validates this and will fail if versions are inconsistent. + +### 2. Validate Versions + +Before pushing, verify all versions match: + +```bash +# Check versions +echo "Checking version consistency..." +echo "pyproject.toml: $(grep '^version = ' pyproject.toml)" +echo "server.json root: $(jq -r '.version' server.json)" +echo "server.json packages:" +jq -r '.packages[] | + if .registryType == "oci" then + " - \(.registryType):\(.identifier) (tag: \(.identifier | split(":")[1]))" + else + " - \(.registryType):\(.identifier) (version: \(.version))" + end' server.json +``` + +**Expected output:** + +- All versions should be `0.5.2` +- Docker image tag should be `:0.5.2` + +If versions don't match, run `./scripts/update_version.sh 0.5.2` again. + +### 3. Commit and Tag + +```bash +git add pyproject.toml server.json uv.lock +git commit -m "Bump version to 0.5.2" +git tag v0.5.2 +git push origin main +git push origin v0.5.2 +``` + +> **Important:** Once you push the tag, **all workflows start immediately** and PyPI publishes within ~3 minutes. PyPI versions are **immutable** - if anything fails later, you'll need a new version number. There's no going back! + +### 4. Automated Pipeline + +Once the tag is pushed, three GitHub Actions workflows run **in parallel/sequence**: + +1. **PyPI Release** + + - Builds Python package + - Publishes to PyPI as `couchbase-mcp-server` + - Creates GitHub Release with changelog + +2. **Docker Build** + + - Builds multi-architecture images (amd64, arm64) + - Pushes to Docker Hub as `couchbaseecosystem/mcp-server-couchbase` + - Updates Docker Hub description + +3. **MCP Registry Update** (runs after Docker completes) + - Waits for both PyPI and Docker to complete + - Validates version consistency + - Publishes to MCP Registry + +> **Note:** Version validation happens in the MCP Registry workflow, which runs **after** PyPI and Docker have already published. This is why local validation (step 2) is critical! + +### 5. Verify Release + +Check that all three workflows succeeded: + +- https://github.com/Couchbase-Ecosystem/mcp-server-couchbase/actions + +Verify the release is available: + +- PyPI: https://pypi.org/project/couchbase-mcp-server/ +- Docker Hub: https://hub.docker.com/r/couchbaseecosystem/mcp-server-couchbase +- MCP Registry + +## Release Candidates + +**Recommended for first-time releases or major changes.** + +Release candidates let you test the full release pipeline without committing to a final version number. If something fails, you can fix it and release the final version without version conflicts. + +**Create an RC release:** + +```bash +# Update version to RC +./scripts/update_version.sh 0.5.2rc1 + +# Or manually update pyproject.toml and server.json (root + all packages) + +# Commit and tag +git add pyproject.toml server.json uv.lock +git commit -m "Bump version to 0.5.2rc1" +git tag v0.5.2rc1 +git push origin main +git push origin v0.5.2rc1 +``` + +**What gets published:** + +- PyPI: `couchbase-mcp-server==0.5.2rc1` +- Docker Hub: `couchbaseecosystem/mcp-server-couchbase:0.5.2rc1` +- MCP Registry: version `0.5.2rc1` + +**If RC succeeds, release the final version:** + +```bash +./scripts/update_version.sh 0.5.2 +git add pyproject.toml server.json uv.lock +git commit -m "Bump version to 0.5.2" +git tag v0.5.2 +git push origin main +git push origin v0.5.2 +``` + +**If RC fails:** + +- Fix the issues +- Create `0.5.2rc2` and test again +- No version conflicts since the final `0.5.2` wasn't published yet! + +## Troubleshooting + +### Release Failed + +**IMPORTANT:** Once PyPI publishes a version, it **cannot be reused**. PyPI versions are immutable. + +If a release fails after PyPI has published (e.g., Docker build fails, MCP Registry update fails): + +**Skip to next patch version:** + +```bash +# If v0.5.2 was published but release incomplete +./scripts/update_version.sh 0.5.3 + +git add pyproject.toml server.json uv.lock +git commit -m "Bump version to 0.5.3" +git tag v0.5.3 +git push origin main +git push origin v0.5.3 +``` + +**Why this happens:** + +- PyPI, Docker, and MCP Registry workflows all start when you push the tag +- Version validation only happens in the MCP Registry workflow (which runs last) +- By that time, PyPI and Docker have already published +- If validation or MCP Registry publish fails, you can't reuse the version number + +**Prevention:** + +- **Always test with RC releases first** (e.g., `0.5.2rc1`) +- **Use the helper script** (`./scripts/update_version.sh`) to ensure all versions match +- **Review changes carefully** before pushing the tag (`git diff`) +- Verify all workflows succeeded before announcing release + +## How Versioning Works + +### Version Files + +All version numbers must be **manually synchronized** across: + +- **`pyproject.toml`**: Python package version +- **`server.json` root `version`**: MCP Registry metadata version +- **`server.json` package `version`**: Must match root version +- **`server.json` OCI identifiers**: Docker image tags must match root version +- **Git tag**: Must match all versions + +### Why All Versions Must Match + +The CI/CD pipeline validates version consistency and will **fail the build** if: + +- Package versions in `server.json` don't match the root version +- Docker image tags in OCI identifiers don't match the root version +- Root version in `server.json` doesn't match the git tag +- (Warning only) `pyproject.toml` doesn't match the git tag + +This ensures: + +- No accidental version mismatches +- Consistent versioning across PyPI, Docker, and MCP Registry +- Valid JSON files that can be tested locally +- Clear version history in git + +### Helper Script + +The `scripts/update_version.sh` script keeps all versions synchronized automatically: + +```bash +./scripts/update_version.sh 0.5.2 +``` + +This updates all three locations and runs `uv lock` in one command. diff --git a/pyproject.toml b/pyproject.toml index dd24dba..33f7749 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "couchbase-mcp-server" -version = "0.5.1" +version = "0.5.2rc1" description = "Couchbase MCP Server - The Developer Data Platform for Critical Applications in Our AI World" readme = "README.md" requires-python = ">=3.10,<3.14" diff --git a/scripts/update_version.sh b/scripts/update_version.sh new file mode 100755 index 0000000..6cca4c6 --- /dev/null +++ b/scripts/update_version.sh @@ -0,0 +1,79 @@ +#!/bin/bash +set -e + +# Simple script to update version in all necessary files +# Usage: ./scripts/update_version.sh 0.5.2 + +if [ -z "$1" ]; then + echo "Usage: $0 " + echo "Example: $0 0.5.2" + echo "Example: $0 0.5.2rc1" + exit 1 +fi + +NEW_VERSION="$1" + +echo "Updating version to $NEW_VERSION" +echo "" + +# Update pyproject.toml +echo "Updating pyproject.toml..." +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + sed -i '' "s/^version = \".*\"/version = \"$NEW_VERSION\"/" pyproject.toml +else + # Linux + sed -i "s/^version = \".*\"/version = \"$NEW_VERSION\"/" pyproject.toml +fi + +# Update server.json (root version, package versions, and Docker image tags) +echo "Updating server.json..." +if command -v jq &> /dev/null; then + # Use jq for safer JSON manipulation + jq --arg ver "$NEW_VERSION" ' + .version = $ver | + .packages = [ + .packages[] | + if .registryType == "oci" then + # Update Docker image tag (everything after last :) + # OCI packages should NOT have a separate version field + .identifier = (.identifier | sub(":[^:]*$"; ":" + $ver)) + else + # Update version field for non-OCI packages (e.g., PyPI) + .version = $ver + end + ] + ' server.json > server.json.tmp + mv server.json.tmp server.json +else + echo "Error: jq is required but not installed" + echo "Install with: brew install jq (macOS) or apt install jq (Linux)" + exit 1 +fi + +# Update lock file +echo "Updating uv.lock" +uv lock + +echo "" +echo "Version updated to $NEW_VERSION in:" +echo " - pyproject.toml" +echo " - server.json (root, packages, and Docker image tags)" +echo " - uv.lock" +echo "" +echo "Verification:" +echo " pyproject.toml: $(grep '^version = ' pyproject.toml)" +echo " server.json root: $(jq -r '.version' server.json)" +echo " server.json packages:" +jq -r '.packages[] | + if .registryType == "oci" then + " - \(.registryType):\(.identifier) (tag: \(.identifier | split(":")[1]))" + else + " - \(.registryType):\(.identifier) (version: \(.version))" + end' server.json +echo "" +echo "Next steps:" +echo " 1. Review changes: git diff" +echo " 2. Commit: git add pyproject.toml server.json uv.lock && git commit -m 'Bump version to $NEW_VERSION'" +echo " 3. Tag: git tag v$NEW_VERSION" +echo " 4. Push: git push origin main && git push origin v$NEW_VERSION" diff --git a/server.json b/server.json index d33ca58..6c24a5c 100644 --- a/server.json +++ b/server.json @@ -6,12 +6,12 @@ "url": "https://github.com/Couchbase-Ecosystem/mcp-server-couchbase", "source": "github" }, - "version": "0.5.1", + "version": "0.5.2rc1", "packages": [ { "registryType": "pypi", "identifier": "couchbase-mcp-server", - "version": "0.5.1", + "version": "0.5.2rc1", "transport": { "type": "stdio" }, @@ -172,7 +172,7 @@ }, { "registryType": "oci", - "identifier": "docker.io/couchbaseecosystem/mcp-server-couchbase:0.5.1", + "identifier": "docker.io/couchbaseecosystem/mcp-server-couchbase:0.5.2rc1", "transport": { "type": "stdio" }, diff --git a/uv.lock b/uv.lock index 20b547d..43d7022 100644 --- a/uv.lock +++ b/uv.lock @@ -168,7 +168,7 @@ wheels = [ [[package]] name = "couchbase-mcp-server" -version = "0.5.1" +version = "0.5.2rc1" source = { editable = "." } dependencies = [ { name = "click" },