pmcp 2.4.0

High-quality Rust SDK for Model Context Protocol (MCP) with full TypeScript SDK compatibility
Documentation
name: Release

on:
  push:
    tags:
      - 'v*'
  # Allow manual re-runs (e.g., after a workflow fix when a publish step
  # failed and we want to re-fire against an existing tag). All publish
  # steps already skip gracefully when the version is already on crates.io.
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: never

jobs:
  create-release:
    name: Create Release
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get_version.outputs.VERSION }}
    steps:
    - uses: actions/checkout@v6

    - name: Get version from tag
      id: get_version
      run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT

    - name: Read changelog
      id: changelog
      run: |
        # Extract the latest version's changelog
        VERSION="${{ steps.get_version.outputs.VERSION }}"
        VERSION_NO_V="${VERSION#v}"
        CHANGELOG=$(awk -v ver="## \\[$VERSION_NO_V\\]" '
          $0 ~ ver {p=1; next}
          /^## \[/ && p {exit}
          p {print}
        ' CHANGELOG.md)
        echo "CHANGELOG<<EOF" >> $GITHUB_OUTPUT
        echo "$CHANGELOG" >> $GITHUB_OUTPUT
        echo "EOF" >> $GITHUB_OUTPUT

    - name: Create Release
      id: create_release
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        VERSION: ${{ steps.get_version.outputs.VERSION }}
        RELEASE_NOTES: ${{ steps.changelog.outputs.CHANGELOG }}
      run: |
        # Check if release already exists
        if gh release view "$VERSION" &>/dev/null; then
          echo "Release $VERSION already exists, skipping creation"
          echo "RELEASE_EXISTS=true" >> $GITHUB_OUTPUT
        else
          gh release create "$VERSION" \
            --title "$VERSION" \
            --notes "$RELEASE_NOTES" \
            --verify-tag
          echo "RELEASE_EXISTS=false" >> $GITHUB_OUTPUT
        fi

  publish-crates:
    name: Publish to crates.io
    runs-on: ubuntu-latest
    needs: create-release
    steps:
    - uses: actions/checkout@v6

    - name: Install Rust
      uses: dtolnay/rust-toolchain@stable

    - name: Cache cargo
      uses: actions/cache@v5
      with:
        path: |
          ~/.cargo/bin/
          ~/.cargo/registry/index/
          ~/.cargo/registry/cache/
          ~/.cargo/git/db/
          target/
        key: ${{ runner.os }}-cargo-publish-${{ hashFiles('**/Cargo.lock') }}

    # Publish order: leaf dependencies first, then dependents.
    # pmcp-widget-utils (no internal deps)
    # -> pmcp-macros-support (no internal deps; consumed by pmcp-macros)
    # -> pmcp-macros (depends on pmcp-macros-support)
    # -> pmcp-code-mode (depends on pmcp)
    # -> pmcp-code-mode-derive (depends on pmcp-code-mode)
    # -> pmcp (depends on widget-utils + macros)
    # -> mcp-tester (depends on pmcp)
    # -> mcp-preview (depends on widget-utils)
    # -> cargo-pmcp (depends on pmcp, mcp-tester, mcp-preview)
    # -> pmcp-server (depends on pmcp, mcp-tester)

    - name: Publish pmcp-widget-utils
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: |
        echo "Publishing pmcp-widget-utils..."
        OUTPUT=$(cargo publish -p pmcp-widget-utils 2>&1) && echo "$OUTPUT" || {
          echo "$OUTPUT"
          if echo "$OUTPUT" | grep -q "already exists"; then
            echo "pmcp-widget-utils already published, continuing..."
          else
            echo "::error::Failed to publish pmcp-widget-utils"
            exit 1
          fi
        }

    - name: Wait for crates.io to index pmcp-widget-utils
      run: sleep 30

    - name: Publish pmcp-macros-support
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: |
        echo "Publishing pmcp-macros-support..."
        OUTPUT=$(cargo publish -p pmcp-macros-support 2>&1) && echo "$OUTPUT" || {
          echo "$OUTPUT"
          if echo "$OUTPUT" | grep -q "already exists"; then
            echo "pmcp-macros-support already published, continuing..."
          else
            echo "::error::Failed to publish pmcp-macros-support"
            exit 1
          fi
        }

    - name: Wait for crates.io to index pmcp-macros-support
      run: sleep 30

    - name: Publish pmcp-macros
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: |
        echo "Publishing pmcp-macros..."
        OUTPUT=$(cargo publish -p pmcp-macros 2>&1) && echo "$OUTPUT" || {
          echo "$OUTPUT"
          if echo "$OUTPUT" | grep -q "already exists"; then
            echo "pmcp-macros already published, continuing..."
          else
            echo "::error::Failed to publish pmcp-macros"
            exit 1
          fi
        }

    - name: Wait for crates.io to index pmcp-macros
      run: sleep 30

    - name: Publish pmcp-code-mode
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: |
        echo "Publishing pmcp-code-mode..."
        OUTPUT=$(cargo publish -p pmcp-code-mode 2>&1) && echo "$OUTPUT" || {
          echo "$OUTPUT"
          if echo "$OUTPUT" | grep -q "already exists"; then
            echo "pmcp-code-mode already published, continuing..."
          else
            echo "::error::Failed to publish pmcp-code-mode"
            exit 1
          fi
        }

    - name: Wait for crates.io to index pmcp-code-mode
      run: sleep 30

    - name: Publish pmcp-code-mode-derive
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: |
        echo "Publishing pmcp-code-mode-derive..."
        OUTPUT=$(cargo publish -p pmcp-code-mode-derive 2>&1) && echo "$OUTPUT" || {
          echo "$OUTPUT"
          if echo "$OUTPUT" | grep -q "already exists"; then
            echo "pmcp-code-mode-derive already published, continuing..."
          else
            echo "::error::Failed to publish pmcp-code-mode-derive"
            exit 1
          fi
        }

    - name: Wait for crates.io to index pmcp-code-mode-derive
      run: sleep 30

    - name: Publish pmcp (core SDK)
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: |
        echo "Publishing pmcp..."
        OUTPUT=$(cargo publish -p pmcp 2>&1) && echo "$OUTPUT" || {
          echo "$OUTPUT"
          if echo "$OUTPUT" | grep -q "already exists"; then
            echo "pmcp already published, continuing..."
          else
            echo "::error::Failed to publish pmcp"
            exit 1
          fi
        }

    - name: Wait for crates.io to index pmcp
      run: sleep 30

    - name: Publish mcp-tester
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: |
        echo "Publishing mcp-tester..."
        OUTPUT=$(cargo publish -p mcp-tester 2>&1) && echo "$OUTPUT" || {
          echo "$OUTPUT"
          if echo "$OUTPUT" | grep -q "already exists"; then
            echo "mcp-tester already published, continuing..."
          else
            echo "::error::Failed to publish mcp-tester"
            exit 1
          fi
        }

    - name: Publish mcp-preview
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: |
        echo "Publishing mcp-preview..."
        OUTPUT=$(cargo publish -p mcp-preview 2>&1) && echo "$OUTPUT" || {
          echo "$OUTPUT"
          if echo "$OUTPUT" | grep -q "already exists"; then
            echo "mcp-preview already published, continuing..."
          else
            echo "::error::Failed to publish mcp-preview"
            exit 1
          fi
        }

    # cargo-pmcp (the user-facing CLI) publishes BEFORE pmcp-server so that
    # any future pmcp-server packaging issues cannot block the main CLI from
    # shipping. cargo-pmcp depends on pmcp, mcp-tester, and mcp-preview — NOT
    # on pmcp-server — so no dependency ordering is violated by this swap.
    - name: Publish cargo-pmcp
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: |
        echo "Publishing cargo-pmcp..."
        OUTPUT=$(cargo publish -p cargo-pmcp 2>&1) && echo "$OUTPUT" || {
          echo "$OUTPUT"
          if echo "$OUTPUT" | grep -q "already exists"; then
            echo "cargo-pmcp already published, continuing..."
          else
            echo "::error::Failed to publish cargo-pmcp"
            exit 1
          fi
        }

    - name: Wait for crates.io to index cargo-pmcp
      run: sleep 30

    - name: Publish pmcp-server
      env:
        CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      run: |
        echo "Publishing pmcp-server..."
        OUTPUT=$(cargo publish -p pmcp-server 2>&1) && echo "$OUTPUT" || {
          echo "$OUTPUT"
          if echo "$OUTPUT" | grep -q "already exists"; then
            echo "pmcp-server already published, continuing..."
          else
            echo "::error::Failed to publish pmcp-server"
            exit 1
          fi
        }

  publish-mcp:
    name: Publish to MCP Registry
    runs-on: ubuntu-latest
    needs: publish-crates
    permissions:
      id-token: write  # Required for OIDC authentication
      contents: read
    steps:
    - uses: actions/checkout@v6

    - name: Wait for crates.io availability
      run: |
        echo "Waiting 60 seconds for crates.io to index all packages..."
        sleep 60

    - name: Install MCP Publisher
      run: |
        PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')
        ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
        curl -sL "https://github.com/modelcontextprotocol/registry/releases/download/v1.5.0/mcp-publisher_${PLATFORM}_${ARCH}.tar.gz" | tar xz
        chmod +x mcp-publisher

    - name: Login to MCP Registry
      run: ./mcp-publisher login github-oidc

    - name: Publish to MCP Registry
      run: ./mcp-publisher publish

  build-tester:
    name: Build Tester Binaries
    needs: create-release
    uses: ./.github/workflows/release-binary.yml
    with:
      tag_name: ${{ needs.create-release.outputs.version }}
      package_name: mcp-tester
    secrets: inherit

  build-pmcp-server:
    name: Build PMCP Server Binaries
    needs: create-release
    uses: ./.github/workflows/release-binary.yml
    with:
      tag_name: ${{ needs.create-release.outputs.version }}
      package_name: pmcp-server
    secrets: inherit