Init the server.json files and add helper CLI tools for building and migrating #715
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update MCP Server Tool Lists | |
| on: | |
| pull_request: | |
| paths: | |
| - 'registry/**/spec.yaml' | |
| - 'registry/**/spec.yml' | |
| workflow_dispatch: | |
| inputs: | |
| server: | |
| description: 'Specific server to update (leave empty for all changed)' | |
| required: false | |
| type: string | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| # Security check for fork PRs - prevents untrusted code execution with write permissions | |
| security-check: | |
| name: Security Check for Fork PRs | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| outputs: | |
| is-fork: ${{ steps.check-fork.outputs.is-fork }} | |
| has-label: ${{ steps.check-label.outputs.has-label }} | |
| should-run: ${{ steps.check-fork.outputs.should-run }} | |
| steps: | |
| - name: Check if PR is from a fork | |
| id: check-fork | |
| run: | | |
| # Get PR details from GitHub API | |
| PR_DATA=$(gh pr view ${{ github.event.pull_request.number }} --json isCrossRepository,labels --repo ${{ github.repository }}) | |
| IS_FORK=$(echo "$PR_DATA" | jq -r '.isCrossRepository') | |
| echo "is-fork=$IS_FORK" >> $GITHUB_OUTPUT | |
| if [ "$IS_FORK" = "false" ]; then | |
| echo "PR is from the same repository - workflow will run automatically" | |
| echo "should-run=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "PR is from a fork - checking for 'safe-to-update' label" | |
| # Check if PR has the safe-to-update label | |
| HAS_LABEL=$(echo "$PR_DATA" | jq -r '.labels | map(select(.name == "safe-to-update")) | length > 0') | |
| if [ "$HAS_LABEL" = "true" ]; then | |
| echo "PR has 'safe-to-update' label - workflow will run" | |
| echo "should-run=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "PR does not have 'safe-to-update' label - workflow will be skipped" | |
| echo "should-run=false" >> $GITHUB_OUTPUT | |
| fi | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check label status | |
| id: check-label | |
| if: steps.check-fork.outputs.is-fork == 'true' | |
| run: | | |
| PR_DATA=$(gh pr view ${{ github.event.pull_request.number }} --json labels --repo ${{ github.repository }}) | |
| HAS_LABEL=$(echo "$PR_DATA" | jq -r '.labels | map(select(.name == "safe-to-update")) | length > 0') | |
| echo "has-label=$HAS_LABEL" >> $GITHUB_OUTPUT | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Post instructions for fork PRs without label | |
| if: steps.check-fork.outputs.is-fork == 'true' && steps.check-fork.outputs.should-run == 'false' | |
| uses: peter-evans/create-or-update-comment@v5 | |
| with: | |
| issue-number: ${{ github.event.pull_request.number }} | |
| body: | | |
| ## Security Notice: Fork PR Tool Update Workflow | |
| This PR is from a forked repository. For security reasons, the automatic tool list update workflow requires a maintainer to add the `safe-to-update` label before it will run. | |
| A maintainer will review this PR and add the label if appropriate. The workflow will then automatically update the tool lists. | |
| --- | |
| **Why is this needed?** This workflow executes code and connects to MCP servers specified in spec files. To prevent potential security issues, we require manual verification for fork PRs. | |
| detect-changes: | |
| name: Detect Changed Specs | |
| runs-on: ubuntu-latest | |
| needs: [security-check] | |
| if: | | |
| always() && | |
| (github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'pull_request' && needs.security-check.outputs.should-run == 'true')) | |
| outputs: | |
| matrix: ${{ steps.set-matrix.outputs.matrix }} | |
| has-changes: ${{ steps.set-matrix.outputs.has-changes }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get changed files | |
| id: changed-files | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| # For PRs, get changed files between base and head | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | |
| # Get changed spec files | |
| CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -E '^registry/.*/spec\.ya?ml$' || true) | |
| else | |
| # For workflow_dispatch, get files changed in the last commit or all files if no recent changes | |
| CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD | grep -E '^registry/.*/spec\.ya?ml$' || true) | |
| # If no files in last commit, we'll handle this in the matrix step | |
| fi | |
| # Convert to JSON array | |
| if [ -z "$CHANGED_FILES" ]; then | |
| echo "all_changed_files=[]" >> $GITHUB_OUTPUT | |
| else | |
| # Convert newline-separated list to JSON array | |
| JSON_ARRAY=$(echo "$CHANGED_FILES" | jq -R -s -c 'split("\n") | map(select(length > 0))') | |
| echo "all_changed_files=$JSON_ARRAY" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Set matrix for changed specs | |
| id: set-matrix | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.server }}" ]; then | |
| # Manual run with specific server | |
| SPEC_FILE="registry/${{ inputs.server }}/spec.yaml" | |
| if [ ! -f "$SPEC_FILE" ]; then | |
| SPEC_FILE="registry/${{ inputs.server }}/spec.yml" | |
| fi | |
| if [ -f "$SPEC_FILE" ]; then | |
| echo "matrix={\"spec\":[\"$SPEC_FILE\"]}" >> $GITHUB_OUTPUT | |
| echo "has-changes=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "Error: Server ${{ inputs.server }} not found" | |
| echo "has-changes=false" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| # Manual run without specific server - process all spec files | |
| echo "Manual run without specific server - processing all spec files" | |
| ALL_SPECS=$(find registry -name "spec.yaml" -o -name "spec.yml" | sort) | |
| if [ -z "$ALL_SPECS" ]; then | |
| echo "No spec files found" | |
| echo "has-changes=false" >> $GITHUB_OUTPUT | |
| else | |
| # Convert to JSON array | |
| JSON_ARRAY=$(echo "$ALL_SPECS" | jq -R -s -c 'split("\n") | map(select(length > 0))') | |
| MATRIX=$(echo "$JSON_ARRAY" | jq -c '{spec: .}') | |
| echo "matrix=$MATRIX" >> $GITHUB_OUTPUT | |
| echo "has-changes=true" >> $GITHUB_OUTPUT | |
| # Log the files that will be processed | |
| echo "Files to process:" | |
| echo "$JSON_ARRAY" | jq -r '.[]' | |
| fi | |
| else | |
| # PR - use changed files | |
| CHANGED_FILES='${{ steps.changed-files.outputs.all_changed_files }}' | |
| if [ "$CHANGED_FILES" = "[]" ]; then | |
| echo "No spec files changed" | |
| echo "has-changes=false" >> $GITHUB_OUTPUT | |
| else | |
| # Convert the JSON array to matrix format | |
| MATRIX=$(echo "$CHANGED_FILES" | jq -c '{spec: .}') | |
| echo "matrix=$MATRIX" >> $GITHUB_OUTPUT | |
| echo "has-changes=true" >> $GITHUB_OUTPUT | |
| # Log the files that will be processed | |
| echo "Files to process:" | |
| echo "$CHANGED_FILES" | jq -r '.[]' | |
| fi | |
| fi | |
| update-tools: | |
| name: Update Tools for ${{ matrix.spec }} | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.has-changes == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }} | |
| fail-fast: false | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| ref: ${{ github.head_ref || github.ref }} | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: 'go.mod' | |
| cache: true | |
| - name: Install ToolHive | |
| uses: StacklokLabs/toolhive-actions/install@v0 | |
| with: | |
| version: 'v0.2.9' | |
| - name: Build update-tools | |
| run: | | |
| echo "Building update-tools..." | |
| go build -o update-tools ./cmd/update-tools | |
| - name: Extract server name | |
| id: server-info | |
| run: | | |
| SERVER_DIR=$(dirname "${{ matrix.spec }}") | |
| SERVER_NAME=$(basename "$SERVER_DIR") | |
| echo "server-name=$SERVER_NAME" >> $GITHUB_OUTPUT | |
| echo "Processing server: $SERVER_NAME" | |
| - name: Update tool list | |
| id: update | |
| run: | | |
| echo "Updating tools for ${{ steps.server-info.outputs.server-name }}..." | |
| # Run the update tool | |
| if ./update-tools "${{ matrix.spec }}" -v; then | |
| echo "update-status=success" >> $GITHUB_OUTPUT | |
| # Check if file was modified | |
| if git diff --quiet "${{ matrix.spec }}"; then | |
| echo "No changes needed for ${{ matrix.spec }}" | |
| echo "changed=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "Tools updated for ${{ matrix.spec }}" | |
| echo "changed=true" >> $GITHUB_OUTPUT | |
| # Get the diff for the comment | |
| DIFF=$(git diff "${{ matrix.spec }}" | head -100) | |
| echo "diff<<EOF" >> $GITHUB_OUTPUT | |
| echo "$DIFF" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| fi | |
| else | |
| echo "update-status=failed" >> $GITHUB_OUTPUT | |
| echo "changed=false" >> $GITHUB_OUTPUT | |
| # Check if warning was added | |
| if git diff "${{ matrix.spec }}" | grep -q "WARNING"; then | |
| echo "Warning comment added to spec file" | |
| echo "warning-added=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "warning-added=false" >> $GITHUB_OUTPUT | |
| fi | |
| fi | |
| - name: Prepare changes for commit | |
| if: (steps.update.outputs.changed == 'true' || steps.update.outputs.warning-added == 'true') && github.event_name == 'pull_request' | |
| run: | | |
| # Create artifact directory with both metadata and the actual changed file | |
| SERVER_NAME="${{ steps.server-info.outputs.server-name }}" | |
| mkdir -p /tmp/commit-info | |
| if [ "${{ steps.update.outputs.changed }}" = "true" ]; then | |
| echo "update" > "/tmp/commit-info/${SERVER_NAME}.type" | |
| echo "$SERVER_NAME" > "/tmp/commit-info/${SERVER_NAME}.name" | |
| echo "${{ matrix.spec }}" > "/tmp/commit-info/${SERVER_NAME}.spec" | |
| else | |
| echo "warning" > "/tmp/commit-info/${SERVER_NAME}.type" | |
| echo "$SERVER_NAME" > "/tmp/commit-info/${SERVER_NAME}.name" | |
| echo "${{ matrix.spec }}" > "/tmp/commit-info/${SERVER_NAME}.spec" | |
| fi | |
| # Copy the modified spec file to the artifact directory | |
| cp "${{ matrix.spec }}" "/tmp/commit-info/${SERVER_NAME}.spec.yaml" | |
| - name: Upload commit info | |
| if: (steps.update.outputs.changed == 'true' || steps.update.outputs.warning-added == 'true') && github.event_name == 'pull_request' | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: commit-info-${{ steps.server-info.outputs.server-name }} | |
| path: /tmp/commit-info/ | |
| - name: Output diff for workflow dispatch | |
| if: github.event_name == 'workflow_dispatch' && (steps.update.outputs.changed == 'true' || steps.update.outputs.warning-added == 'true') | |
| run: | | |
| SERVER_NAME="${{ steps.server-info.outputs.server-name }}" | |
| echo "## 🔧 Tool List Update for $SERVER_NAME" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ steps.update.outputs.changed }}" = "true" ]; then | |
| echo "✅ **Status**: Successfully updated tool list" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Changes Made:" >> $GITHUB_STEP_SUMMARY | |
| echo '```diff' >> $GITHUB_STEP_SUMMARY | |
| git diff "${{ matrix.spec }}" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "⚠️ **Note**: Changes were not committed because this is a workflow dispatch run. To apply these changes:" >> $GITHUB_STEP_SUMMARY | |
| echo "1. Create a new branch" >> $GITHUB_STEP_SUMMARY | |
| echo "2. Apply the changes shown above" >> $GITHUB_STEP_SUMMARY | |
| echo "3. Open a pull request" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ **Status**: Tool list update failed, warning added" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Warning Added:" >> $GITHUB_STEP_SUMMARY | |
| echo '```diff' >> $GITHUB_STEP_SUMMARY | |
| git diff "${{ matrix.spec }}" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Server**: $SERVER_NAME" >> $GITHUB_STEP_SUMMARY | |
| echo "**Spec File**: \`${{ matrix.spec }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "**Triggered by**: @${{ github.actor }}" >> $GITHUB_STEP_SUMMARY | |
| commit-all-changes: | |
| name: Commit All Tool Updates | |
| needs: update-tools | |
| if: always() && needs.detect-changes.outputs.has-changes == 'true' && github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| ref: ${{ github.head_ref || github.ref }} | |
| - name: Download all commit info artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| path: /tmp/artifacts/ | |
| pattern: commit-info-* | |
| merge-multiple: true | |
| - name: Check if any changes were made | |
| id: check_changes | |
| run: | | |
| if [ -d "/tmp/artifacts" ] && [ "$(ls -A /tmp/artifacts 2>/dev/null)" ]; then | |
| echo "changes=true" >> $GITHUB_OUTPUT | |
| echo "Found commit info files:" | |
| ls -la /tmp/artifacts/ | |
| else | |
| echo "changes=false" >> $GITHUB_OUTPUT | |
| echo "No changes to commit" | |
| fi | |
| - name: Commit and push all changes | |
| if: steps.check_changes.outputs.changes == 'true' | |
| run: | | |
| git config --local user.email "action@github.com" | |
| git config --local user.name "GitHub Action" | |
| # Pull any remote changes first to avoid conflicts | |
| git pull origin ${{ github.head_ref || github.ref_name }} --rebase | |
| # Collect all the server names that were updated | |
| UPDATED_SERVERS="" | |
| WARNING_SERVERS="" | |
| for file in /tmp/artifacts/*.type; do | |
| if [ -f "$file" ]; then | |
| SERVER_NAME=$(basename "$file" .type) | |
| TYPE=$(cat "$file") | |
| if [ "$TYPE" = "update" ]; then | |
| UPDATED_SERVERS="$UPDATED_SERVERS $SERVER_NAME" | |
| else | |
| WARNING_SERVERS="$WARNING_SERVERS $SERVER_NAME" | |
| fi | |
| # Restore the modified spec file from the artifact | |
| SPEC_FILE=$(cat "/tmp/artifacts/${SERVER_NAME}.spec") | |
| if [ -f "/tmp/artifacts/${SERVER_NAME}.spec.yaml" ]; then | |
| cp "/tmp/artifacts/${SERVER_NAME}.spec.yaml" "$SPEC_FILE" | |
| git add "$SPEC_FILE" | |
| else | |
| echo "Warning: Modified spec file not found in artifacts for $SERVER_NAME" | |
| fi | |
| fi | |
| done | |
| # Create commit message | |
| COMMIT_MSG="chore: update tool lists for MCP servers" | |
| if [ -n "$UPDATED_SERVERS" ]; then | |
| COMMIT_MSG="$COMMIT_MSG\n\nUpdated servers:$(echo $UPDATED_SERVERS | sed 's/ /\\n- /g' | sed 's/^/\\n- /')" | |
| fi | |
| if [ -n "$WARNING_SERVERS" ]; then | |
| COMMIT_MSG="$COMMIT_MSG\n\nWarning added for servers:$(echo $WARNING_SERVERS | sed 's/ /\\n- /g' | sed 's/^/\\n- /')" | |
| fi | |
| COMMIT_MSG="$COMMIT_MSG\n\nAutomatically updated using 'thv mcp list' command.\n\nCo-authored-by: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>" | |
| # Check if there are actually any changes to commit | |
| if ! git diff --cached --quiet; then | |
| git commit -m "$COMMIT_MSG" | |
| git push | |
| echo "✅ Successfully committed and pushed all tool updates" | |
| else | |
| echo "ℹ️ No changes to commit (files may have been identical)" | |
| fi | |
| comment-summary: | |
| name: Post Summary Comment | |
| needs: [detect-changes, update-tools] | |
| if: always() && needs.detect-changes.outputs.has-changes == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Find existing comment | |
| if: github.event_name == 'pull_request' | |
| uses: peter-evans/find-comment@v4 | |
| id: find-comment | |
| with: | |
| issue-number: ${{ github.event.pull_request.number }} | |
| comment-author: 'github-actions[bot]' | |
| body-includes: '## 🔧 MCP Server Tool List Updates' | |
| - name: Download all commit info artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| path: /tmp/comment-artifacts/ | |
| pattern: commit-info-* | |
| merge-multiple: true | |
| - name: Generate summary from artifacts | |
| id: generate-summary | |
| run: | | |
| SUMMARY="" | |
| if [ -d "/tmp/comment-artifacts" ] && [ "$(ls -A /tmp/comment-artifacts 2>/dev/null)" ]; then | |
| for file in /tmp/comment-artifacts/*.type; do | |
| if [ -f "$file" ]; then | |
| SERVER_NAME=$(basename "$file" .type) | |
| TYPE=$(cat "$file") | |
| if [ "$TYPE" = "update" ]; then | |
| SUMMARY="$SUMMARY| $SERVER_NAME | ✅ Updated | Tool list refreshed |\n" | |
| else | |
| SUMMARY="$SUMMARY| $SERVER_NAME | ⚠️ Warning | Could not fetch tools, added warning comment |\n" | |
| fi | |
| fi | |
| done | |
| fi | |
| if [ -z "$SUMMARY" ]; then | |
| SUMMARY="| _No changes detected_ | | |" | |
| fi | |
| echo "summary<<EOF" >> $GITHUB_OUTPUT | |
| echo -e "$SUMMARY" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Create or update comment | |
| if: github.event_name == 'pull_request' | |
| uses: peter-evans/create-or-update-comment@v5 | |
| with: | |
| comment-id: ${{ steps.find-comment.outputs.comment-id }} | |
| issue-number: ${{ github.event.pull_request.number }} | |
| body: | | |
| ## 🔧 MCP Server Tool List Updates | |
| The tool lists for modified MCP server specs have been automatically updated using `thv mcp list`. | |
| ### Summary | |
| | Server | Status | Details | | |
| |--------|--------|---------| | |
| ${{ steps.generate-summary.outputs.summary }} | |
| --- | |
| _This comment is automatically generated and will be updated as the workflow progresses._ | |
| edit-mode: replace | |
| validate-after-update: | |
| name: Validate Updated Specs | |
| needs: update-tools | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.head_ref || github.ref }} | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: 'go.mod' | |
| cache: true | |
| - name: Build registry-builder | |
| run: go build -o registry-builder ./cmd/registry-builder | |
| - name: Validate all specs | |
| run: | | |
| echo "Validating all registry entries after updates..." | |
| ./registry-builder validate -v |