modelcontextprotocol/registry/main 1.5M tokens More Tools
```
├── .env.example (omitted)
├── .githooks/
   ├── pre-commit (200 tokens)
├── .github/
   ├── copilot-instructions.md (100 tokens)
   ├── dependabot.yml (100 tokens)
   ├── workflows/
      ├── cancel-pulumi-lock.yml (500 tokens)
      ├── ci.yml (300 tokens)
      ├── claude.yml (300 tokens)
      ├── deploy-production.yml (300 tokens)
      ├── deploy-staging.yml (600 tokens)
      ├── release.yml (500 tokens)
      ├── sync-db.yml (3.5k tokens)
      ├── sync-schema.yml (500 tokens)
├── .gitignore
├── .golangci.yml (300 tokens)
├── .goreleaser.yaml (600 tokens)
├── .ko.dockerignore (100 tokens)
├── .ko.yaml (200 tokens)
├── .vscode/
   ├── launch.json (200 tokens)
├── CHANGES.md
├── CLAUDE.md (100 tokens)
├── CONTRIBUTING.md (300 tokens)
├── LICENSE (omitted)
├── Makefile (900 tokens)
├── README.md (1500 tokens)
├── SECURITY.md (200 tokens)
├── cmd/
   ├── publisher/
      ├── README.md (300 tokens)
      ├── auth/
         ├── azurekeyvault/
            ├── common.go (500 tokens)
         ├── common.go (1400 tokens)
         ├── dns.go (100 tokens)
         ├── github-at.go (2000 tokens)
         ├── github-oidc.go (1100 tokens)
         ├── github-oidc_internal_test.go (300 tokens)
         ├── github_at_test.go (1300 tokens)
         ├── googlekms/
            ├── common.go (800 tokens)
         ├── http.go (100 tokens)
         ├── interface.go (100 tokens)
         ├── none.go (300 tokens)
      ├── commands/
         ├── init.go (2.4k tokens)
         ├── init_test.go (600 tokens)
         ├── login.go (1800 tokens)
         ├── login_test.go (600 tokens)
         ├── logout.go (300 tokens)
         ├── logout_test.go (900 tokens)
         ├── publish.go (900 tokens)
         ├── publish_test.go (2.3k tokens)
         ├── status.go (2.2k tokens)
         ├── status_test.go (1800 tokens)
         ├── testutil_test.go (600 tokens)
         ├── validate.go (1800 tokens)
         ├── validate_test.go (1000 tokens)
      ├── main.go (1500 tokens)
   ├── registry/
      ├── main.go (900 tokens)
├── complete.md (20.7k tokens)
├── data/
   ├── seed.json (1100 tokens)
├── deploy/
   ├── .gitignore
   ├── Makefile (400 tokens)
   ├── Pulumi.gcpProd.yaml (1000 tokens)
   ├── Pulumi.gcpStaging.yaml (1000 tokens)
   ├── Pulumi.local.yaml (100 tokens)
   ├── Pulumi.yaml
   ├── README.md (2.3k tokens)
   ├── go.mod (1200 tokens)
   ├── go.sum (6.7k tokens)
   ├── main.go (300 tokens)
   ├── pkg/
      ├── k8s/
         ├── backup.go (900 tokens)
         ├── cert_manager.go (500 tokens)
         ├── deploy.go (300 tokens)
         ├── ingress.go (800 tokens)
         ├── monitoring.go (4.3k tokens)
         ├── postgres.go (800 tokens)
         ├── registry.go (2.8k tokens)
      ├── providers/
         ├── gcp/
            ├── provider.go (3.1k tokens)
         ├── local/
            ├── provider.go (1800 tokens)
         ├── types.go (200 tokens)
├── docker-compose.yml (500 tokens)
├── docs/
   ├── README.md (200 tokens)
   ├── administration/
      ├── admin-operations.md (1200 tokens)
      ├── maintainer-onboarding.md (200 tokens)
   ├── community-projects.md (700 tokens)
   ├── contributing/
      ├── add-package-registry.md (1000 tokens)
      ├── releasing.md (600 tokens)
   ├── design/
      ├── design-principles.md (400 tokens)
      ├── dev-summit-2025-05-registry-goals-presentation.pdf
      ├── dev-summit-2025-10-registry-status-presentation.pdf
      ├── ecosystem-diagram.excalidraw.svg (566.8k tokens)
      ├── ecosystem-vision.md (600 tokens)
      ├── proposed-enhanced-validation.md (9k tokens)
      ├── roadmap.md (300 tokens)
      ├── tech-architecture.md (1100 tokens)
   ├── modelcontextprotocol-io/
      ├── about.mdx (1300 tokens)
      ├── authentication.mdx (2.4k tokens)
      ├── ecosystem-diagram.excalidraw.svg (566.8k tokens)
      ├── faq.mdx (600 tokens)
      ├── github-actions.mdx (1400 tokens)
      ├── moderation-policy.mdx (600 tokens)
      ├── package-types.mdx (2.1k tokens)
      ├── quickstart.mdx (2000 tokens)
      ├── registry-aggregators.mdx (1000 tokens)
      ├── remote-servers.mdx (1000 tokens)
      ├── terms-of-service.mdx (1500 tokens)
      ├── versioning.mdx (1200 tokens)
   ├── reference/
      ├── README.md (100 tokens)
      ├── api/
         ├── CHANGELOG.md (1000 tokens)
         ├── extensions.md (500 tokens)
         ├── generic-registry-api.md (600 tokens)
         ├── official-registry-api.md (1000 tokens)
         ├── openapi.yaml (8.7k tokens)
         ├── registry-authorization.md (300 tokens)
      ├── cli/
         ├── commands.md (2.2k tokens)
      ├── server-json/
         ├── CHANGELOG.md (1800 tokens)
         ├── CONTRIBUTING.md (300 tokens)
         ├── draft/
            ├── server.schema.json (4.5k tokens)
         ├── generic-server-json.md (4.9k tokens)
         ├── official-registry-requirements.md (1200 tokens)
├── go.mod (800 tokens)
├── go.sum (3.8k tokens)
├── internal/
   ├── api/
      ├── cors_test.go (1100 tokens)
      ├── handlers/
         ├── v0/
            ├── auth/
               ├── common.go (2.6k tokens)
               ├── common_test.go (200 tokens)
               ├── dns.go (1100 tokens)
               ├── dns_test.go (7.3k tokens)
               ├── github_at.go (1200 tokens)
               ├── github_at_test.go (3.7k tokens)
               ├── github_oidc.go (1800 tokens)
               ├── github_oidc_test.go (2k tokens)
               ├── http.go (1900 tokens)
               ├── http_internal_test.go (500 tokens)
               ├── http_test.go (8.8k tokens)
               ├── main.go (200 tokens)
               ├── none.go (600 tokens)
               ├── none_test.go (400 tokens)
               ├── oidc.go (1700 tokens)
               ├── oidc_internal_test.go (300 tokens)
               ├── oidc_test.go (600 tokens)
            ├── edit.go (900 tokens)
            ├── edit_test.go (3.8k tokens)
            ├── health.go (400 tokens)
            ├── health_test.go (400 tokens)
            ├── list_errors.go (200 tokens)
            ├── list_errors_test.go (300 tokens)
            ├── ping.go (200 tokens)
            ├── ping_test.go (200 tokens)
            ├── publish.go (800 tokens)
            ├── publish_integration_test.go (1800 tokens)
            ├── publish_registry_validation_test.go (1600 tokens)
            ├── publish_test.go (3.5k tokens)
            ├── response.go (100 tokens)
            ├── servers.go (1900 tokens)
            ├── servers_test.go (4.6k tokens)
            ├── status.go (2.5k tokens)
            ├── status_test.go (6k tokens)
            ├── telemetry_test.go (600 tokens)
            ├── ui.go
            ├── ui_index.html (4.8k tokens)
            ├── validate.go (300 tokens)
            ├── validate_test.go (1300 tokens)
            ├── version.go (200 tokens)
            ├── version_test.go (400 tokens)
      ├── openapi_compliance_test.go (700 tokens)
      ├── router/
         ├── router.go (1700 tokens)
         ├── router_test.go (800 tokens)
         ├── v0.go (400 tokens)
      ├── server.go (1100 tokens)
      ├── server_test.go (1900 tokens)
   ├── auth/
      ├── blocks.go (100 tokens)
      ├── jwt.go (1000 tokens)
      ├── jwt_test.go (2.4k tokens)
      ├── types.go (100 tokens)
   ├── config/
      ├── config.go (300 tokens)
   ├── database/
      ├── database.go (1100 tokens)
      ├── migrate.go (1000 tokens)
      ├── migrations/
         ├── 001_initial_schema.sql (400 tokens)
         ├── 002_add_server_extensions.sql (500 tokens)
         ├── 003_simplify_to_key_value.sql (200 tokens)
         ├── 004_update_meta_field_format.sql (100 tokens)
         ├── 005_add_server_id_rename_version_id.sql (600 tokens)
         ├── 006_migrate_server_json_camelcase.sql (600 tokens)
         ├── 007_add_publish_constraints.sql (200 tokens)
         ├── 008_clean_invalid_data.sql (1600 tokens)
         ├── 009_separate_official_metadata.sql (1800 tokens)
         ├── 010_migrate_canonical_package_refs.sql (1200 tokens)
         ├── 011_fix_empty_schema_fields.sql (500 tokens)
         ├── 012_fix_nuget_registry_base_url.sql (400 tokens)
         ├── 013_add_status_fields.sql (200 tokens)
         ├── 014_heal_is_latest.sql (400 tokens)
      ├── postgres.go (6k tokens)
      ├── postgres_test.go (12.5k tokens)
      ├── testutil.go (800 tokens)
   ├── importer/
      ├── importer.go (1200 tokens)
      ├── importer_test.go (1500 tokens)
   ├── service/
      ├── registry_service.go (3.6k tokens)
      ├── registry_service_test.go (7k tokens)
      ├── service.go (400 tokens)
      ├── versioning.go (600 tokens)
      ├── versioning_test.go (1100 tokens)
   ├── telemetry/
      ├── metrics.go (800 tokens)
      ├── metrics_test.go (400 tokens)
   ├── validators/
      ├── constants.go (300 tokens)
      ├── package.go (200 tokens)
      ├── registries/
         ├── cargo.go (3.1k tokens)
         ├── cargo_internal_test.go (400 tokens)
         ├── cargo_test.go (3.9k tokens)
         ├── export_test.go (100 tokens)
         ├── mcpb.go (1200 tokens)
         ├── mcpb_test.go (1300 tokens)
         ├── mcpname.go (800 tokens)
         ├── mcpname_internal_test.go (900 tokens)
         ├── npm.go (600 tokens)
         ├── npm_test.go (800 tokens)
         ├── nuget.go (2k tokens)
         ├── nuget_test.go (700 tokens)
         ├── oci.go (1300 tokens)
         ├── oci_test.go (1900 tokens)
         ├── pypi.go (700 tokens)
         ├── pypi_test.go (500 tokens)
         ├── testutils_test.go (100 tokens)
      ├── schema.go (3.2k tokens)
      ├── schema_regex_test.go (700 tokens)
      ├── schema_test.go (800 tokens)
      ├── schemas/
         ├── 2025-07-09.json (3.5k tokens)
         ├── 2025-09-16.json (3.5k tokens)
         ├── 2025-09-29.json (3.4k tokens)
         ├── 2025-10-11.json (4.1k tokens)
         ├── 2025-10-17.json (4.1k tokens)
         ├── 2025-12-11.json (4.4k tokens)
         ├── README.md (200 tokens)
      ├── utils.go (1100 tokens)
      ├── validation_detailed_test.go (3k tokens)
      ├── validation_types.go (1200 tokens)
      ├── validation_types_test.go (1000 tokens)
      ├── validators.go (4.8k tokens)
      ├── validators_test.go (13.7k tokens)
├── pkg/
   ├── api/
      ├── v0/
         ├── types.go (800 tokens)
   ├── model/
      ├── constants.go (300 tokens)
      ├── types.go (2000 tokens)
├── scripts/
   ├── mirror_data/
      ├── .gitignore
      ├── README.md (1300 tokens)
      ├── fetch_production_data.go (400 tokens)
      ├── load_production_data.go (900 tokens)
   ├── test_endpoints.sh (1300 tokens)
   ├── test_publish.sh (1400 tokens)
├── tests/
   ├── integration/
      ├── README.md (300 tokens)
      ├── docker-compose.integration-test.yml (100 tokens)
      ├── main.go (2.1k tokens)
      ├── run.sh (400 tokens)
├── tools/
   ├── admin/
      ├── auth.sh (200 tokens)
      ├── takedown.sh (200 tokens)
   ├── extract-server-schema/
      ├── main.go (1200 tokens)
   ├── validate-examples.sh
   ├── validate-examples/
      ├── main.go (1400 tokens)
   ├── validate-schemas.sh
   ├── validate-schemas/
      ├── main.go (500 tokens)
```


## /.githooks/pre-commit

```githooks/pre-commit path="/.githooks/pre-commit" 
#!/bin/bash

# Pre-commit hook for MCP Registry
# Runs linting and formatting checks before allowing commits

set -e

echo "Running pre-commit checks..."

# Check if golangci-lint is installed
if ! command -v golangci-lint &> /dev/null; then
    echo "❌ golangci-lint is not installed!"
    echo "See README.md Prerequisites section for installation instructions."
    exit 1
fi

# Run golangci-lint
echo "Running golangci-lint..."
if ! golangci-lint run --timeout=5m; then
    echo "❌ Linting failed! Please fix the issues above."
    exit 1
fi

# Check formatting
echo "Checking Go formatting..."
UNFORMATTED=$(gofmt -s -l .)
if [ -n "$UNFORMATTED" ]; then
    echo "❌ The following files need formatting:"
    echo "$UNFORMATTED"
    echo ""
    echo "Run 'gofmt -s -w .' to fix formatting issues."
    exit 1
fi

echo "✅ All pre-commit checks passed!"

```

## /.github/copilot-instructions.md

# Copilot Instructions for MCP Registry

## Important: Publishing MCP servers

The `data/seed.json` file is seed data for local development only. Do NOT create pull requests that add or modify server entries in `data/seed.json` to publish a server.

To publish an MCP server to the registry, use the `mcp-publisher` CLI tool. See `docs/modelcontextprotocol-io/quickstart.mdx` for instructions.

## Development

- Use `make` targets where possible (run `make help` to see available targets)
- Run `make check` to run lint, unit tests, and integration tests
- Run `make dev-compose` to start the local development environment

## /.github/dependabot.yml

```yml path="/.github/dependabot.yml" 
version: 2
updates:
  - package-ecosystem: gomod
    directory: /
    groups:
      opentelemetry:
        patterns:
          - "go.opentelemetry.io/*"
    schedule:
      interval: weekly
  - package-ecosystem: gomod
    directory: /deploy
    groups:
      opentelemetry:
        patterns:
          - "go.opentelemetry.io/*"
    schedule:
      interval: weekly
  - package-ecosystem: github-actions
    directory: /
    groups:
      actions:
        patterns:
          - "*"
    schedule:
      interval: weekly
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
        interval: "weekly"

```

## /.github/workflows/cancel-pulumi-lock.yml

```yml path="/.github/workflows/cancel-pulumi-lock.yml" 
name: Cancel Pulumi Lock

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to cancel lock for'
        required: true
        type: choice
        options:
          - staging
          - production

permissions:
  contents: read

env:
  PULUMI_VERSION: "3.188.0"

jobs:
  cancel-lock:
    name: Cancel Pulumi Lock
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - name: Checkout code
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8

      - name: Setup Pulumi
        uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
        with:
          pulumi-version: ${{ env.PULUMI_VERSION }}

      - name: Authenticate to Google Cloud (Staging)
        if: inputs.environment == 'staging'
        uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
        with:
          credentials_json: ${{ secrets.GCP_STAGING_SERVICE_ACCOUNT_KEY }}

      - name: Authenticate to Google Cloud (Production)
        if: inputs.environment == 'production'
        uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
        with:
          credentials_json: ${{ secrets.GCP_PROD_SERVICE_ACCOUNT_KEY }}

      - name: Setup Google Cloud SDK (Staging)
        if: inputs.environment == 'staging'
        uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
        with:
          project_id: mcp-registry-staging

      - name: Setup Google Cloud SDK (Production)
        if: inputs.environment == 'production'
        uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
        with:
          project_id: mcp-registry-prod

      - name: Cancel Pulumi Lock (Staging)
        if: inputs.environment == 'staging'
        working-directory: ./deploy
        env:
          PULUMI_STAGING_PASSPHRASE: ${{ secrets.PULUMI_STAGING_PASSPHRASE }}
        run: |
          echo "$PULUMI_STAGING_PASSPHRASE" > passphrase.staging.txt
          pulumi login gs://mcp-registry-staging-pulumi-state
          PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.staging.txt pulumi cancel --stack gcpStaging --yes

      - name: Cancel Pulumi Lock (Production)
        if: inputs.environment == 'production'
        working-directory: ./deploy
        env:
          PULUMI_PROD_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
        run: |
          echo "$PULUMI_PROD_PASSPHRASE" > passphrase.prod.txt
          pulumi login gs://mcp-registry-prod-pulumi-state
          PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi cancel --stack gcpProd --yes

```

## /.github/workflows/ci.yml

```yml path="/.github/workflows/ci.yml" 
name: CI Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

permissions:
  contents: read

jobs:
  # Build, Lint, and Validate
  build-lint-validate:
    name: Build, Lint, and Validate
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8

    - name: Set up Go
      uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
      with:
        go-version: 'stable'
        cache: true

    - name: Run lint
      uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee
      with:
        version: v2.11.4

    - name: Validate schemas and examples
      run: make validate

    - name: Build application
      run: make build

    - name: Run govulncheck
      uses: golang/govulncheck-action@v1
      with:
        go-version-input: 'stable'
        go-package: ./...
        repo-checkout: false

  # All Tests
  tests:
    name: Tests
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8

    - name: Set up Go
      uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
      with:
        go-version: 'stable'
        cache: true

    - name: Set up ko
      uses: ko-build/setup-ko@v0.9

    - name: Run all tests
      run: make test-all

    - name: Upload coverage artifacts
      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
      with:
        name: coverage-report
        path: |
          coverage.out
          coverage.html

```

## /.github/workflows/claude.yml

```yml path="/.github/workflows/claude.yml" 
name: Claude Code

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]
  pull_request_review:
    types: [submitted]

jobs:
  claude:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write
      actions: read
    steps:
      - name: Checkout repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
        with:
          fetch-depth: 1

      - name: Run Claude Code
        id: claude
        uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

          # Allow Claude to read CI results on PRs
          additional_permissions: |
            actions: read

          # Trigger when assigned to an issue
          assignee_trigger: "claude"

          claude_args: |
            --allowedTools Bash
            --system-prompt "If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a <details> block."

```

## /.github/workflows/deploy-production.yml

```yml path="/.github/workflows/deploy-production.yml" 
name: Deploy to Production

on:
  push:
    branches:
      - main
    paths:
      - 'deploy/Pulumi.gcpProd.yaml'

permissions:
  contents: read

env:
  PULUMI_VERSION: "3.188.0"

jobs:
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    environment: production
    concurrency:
      group: deploy-production
      cancel-in-progress: false
    steps:
      - name: Checkout code
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8

      - name: Setup Go
        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
        with:
          go-version: 'stable'
          cache: true

      - name: Setup Pulumi
        uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
        with:
          pulumi-version: ${{ env.PULUMI_VERSION }}

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
        with:
          credentials_json: ${{ secrets.GCP_PROD_SERVICE_ACCOUNT_KEY }}

      - name: Setup Google Cloud SDK
        uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
        with:
          project_id: mcp-registry-prod
          install_components: gke-gcloud-auth-plugin

      - name: Deploy to Production
        working-directory: ./deploy
        env:
          PULUMI_PROD_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
        run: |
          echo "$PULUMI_PROD_PASSPHRASE" > passphrase.prod.txt
          make prod-up
```

## /.github/workflows/deploy-staging.yml

```yml path="/.github/workflows/deploy-staging.yml" 
name: Deploy to Staging

on:
  push:
    branches:
      - main
    paths-ignore:
      - 'deploy/Pulumi.gcpProd.yaml'

permissions:
  contents: read

env:
  PULUMI_VERSION: "3.188.0"

jobs:
  ko-push:
    name: Build and Push Image with ko
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8

      - name: Set up Go
        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
        with:
          go-version: 'stable'
          cache: true

      - name: Set up ko
        uses: ko-build/setup-ko@v0.9

      - name: Log in to Container Registry
        uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Get build timestamp and tags
        id: build-info
        run: |
          echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
          echo "date=$(date -u +%Y%m%d)" >> $GITHUB_OUTPUT
          echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

      - name: Build and push with ko
        env:
          KO_DOCKER_REPO: ghcr.io/${{ github.repository }}
          VERSION: main-${{ steps.build-info.outputs.date }}-${{ steps.build-info.outputs.short_sha }}
          GIT_COMMIT: ${{ github.sha }}
          BUILD_TIME: ${{ steps.build-info.outputs.timestamp }}
        run: |
          # Build and push multi-platform image
          ko build ./cmd/registry \
            --bare \
            --platform=linux/amd64,linux/arm64 \
            --tags=main-${{ steps.build-info.outputs.date }}-${{ steps.build-info.outputs.short_sha }},main

  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    environment: staging
    needs: ko-push
    concurrency:
      group: deploy-staging
      cancel-in-progress: false
    steps:
      - name: Checkout code
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8

      - name: Setup Go
        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
        with:
          go-version: 'stable'
          cache: true

      - name: Setup Pulumi
        uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
        with:
          pulumi-version: ${{ env.PULUMI_VERSION }}

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
        with:
          credentials_json: ${{ secrets.GCP_STAGING_SERVICE_ACCOUNT_KEY }}

      - name: Setup Google Cloud SDK
        uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
        with:
          project_id: mcp-registry-staging
          install_components: gke-gcloud-auth-plugin

      - name: Deploy to Staging
        working-directory: ./deploy
        env:
          PULUMI_STAGING_PASSPHRASE: ${{ secrets.PULUMI_STAGING_PASSPHRASE }}
        run: |
          echo "$PULUMI_STAGING_PASSPHRASE" > passphrase.staging.txt
          make staging-up

```

## /.github/workflows/release.yml

```yml path="/.github/workflows/release.yml" 
name: Release

on:
  release:
    types: [published]

permissions:
  contents: write
  packages: write
  id-token: write # needed for signing

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
        with:
          fetch-depth: 0

      - name: Set up Go
        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
        with:
          go-version: 'stable'
          cache: true

      - name: Install cosign
        uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

      - name: Install Syft
        uses: anchore/sbom-action/download-syft@v0.24.0

      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89
        with:
          distribution: goreleaser
          version: v2.12.0
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  ko-push:
    name: Build and Push Image with ko
    runs-on: ubuntu-latest
    needs: goreleaser
    steps:
      - name: Checkout
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8

      - name: Set up Go
        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
        with:
          go-version: 'stable'
          cache: true

      - name: Set up ko
        uses: ko-build/setup-ko@v0.9

      - name: Log in to Container Registry
        uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Get build timestamp and version
        id: build-info
        run: |
          echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
          # Extract version from tag and strip 'v' prefix (e.g., refs/tags/v1.2.3 -> 1.2.3)
          VERSION="${GITHUB_REF#refs/tags/}"
          VERSION="${VERSION#v}"
          echo "version=$VERSION" >> $GITHUB_OUTPUT

      - name: Build and push with ko
        env:
          KO_DOCKER_REPO: ghcr.io/${{ github.repository }}
          VERSION: ${{ steps.build-info.outputs.version }}
          GIT_COMMIT: ${{ github.sha }}
          BUILD_TIME: ${{ steps.build-info.outputs.timestamp }}
        run: |
          # Build and push multi-platform image with version tag and latest
          ko build ./cmd/registry \
            --bare \
            --platform=linux/amd64,linux/arm64 \
            --tags=${{ steps.build-info.outputs.version }},latest

```

## /.github/workflows/sync-db.yml

```yml path="/.github/workflows/sync-db.yml" 
name: Sync Production DB to Staging (from backups)

on:
  # schedule:
  #   # Run daily at 2 AM UTC (during low-traffic hours)
  #   - cron: '0 2 * * *'
  workflow_dispatch:  # Allow manual triggering

permissions:
  contents: read

jobs:
  sync-database:
    name: Sync Prod DB to Staging from k8up Backups
    runs-on: ubuntu-latest
    environment: staging
    concurrency:
      group: sync-staging-database
      cancel-in-progress: false
    steps:
      - name: Authenticate to Google Cloud (Production)
        uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
        with:
          credentials_json: ${{ secrets.GCP_PROD_SERVICE_ACCOUNT_KEY }}

      - name: Setup Google Cloud SDK
        uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
        with:
          project_id: mcp-registry-prod
          install_components: gke-gcloud-auth-plugin

      - name: Get backup credentials from prod cluster
        id: backup-creds
        run: |
          gcloud container clusters get-credentials mcp-registry-prod \
            --zone=us-central1-b \
            --project=mcp-registry-prod

          # Store in outputs (GitHub Actions encrypts these automatically)
          kubectl get secret k8up-backup-credentials -n default -o json | jq -r '
            "access_key=" + (.data.AWS_ACCESS_KEY_ID | @base64d),
            "secret_key=" + (.data.AWS_SECRET_ACCESS_KEY | @base64d)
          ' >> $GITHUB_OUTPUT

      - name: Remove all production access (SAFETY MEASURE)
        run: |
          # Remove production cluster from kubeconfig
          kubectl config delete-context gke_mcp-registry-prod_us-central1-b_mcp-registry-prod 2>/dev/null || true

          # Revoke gcloud credentials
          gcloud auth revoke --all 2>/dev/null || true

          # Clear gcloud configuration
          gcloud config unset project 2>/dev/null || true
          gcloud config unset account 2>/dev/null || true

          # Verify no contexts remain
          CONTEXT_COUNT=$(kubectl config get-contexts -o name 2>/dev/null | wc -l)
          if [ "$CONTEXT_COUNT" -gt 0 ]; then
            echo "❌ ERROR: $CONTEXT_COUNT context(s) still exist after cleanup!"
            kubectl config get-contexts
            exit 1
          fi

      - name: Switch to staging cluster
        uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
        with:
          credentials_json: ${{ secrets.GCP_STAGING_SERVICE_ACCOUNT_KEY }}

      - name: Configure staging cluster access
        run: |
          gcloud config set project mcp-registry-staging
          gcloud container clusters get-credentials mcp-registry-staging \
            --zone=us-central1-b \
            --project=mcp-registry-staging

      - name: Create secret for prod backup bucket access
        run: |
          # Create/update secret in staging with access to prod backups
          kubectl create secret generic prod-to-staging-sync-credentials \
            --from-literal=AWS_ACCESS_KEY_ID="${{ steps.backup-creds.outputs.access_key }}" \
            --from-literal=AWS_SECRET_ACCESS_KEY="${{ steps.backup-creds.outputs.secret_key }}" \
            --dry-run=client -o yaml | kubectl apply -f -

      - name: Create restore PVC
        run: |
          kubectl apply -f - <<EOF
          apiVersion: v1
          kind: PersistentVolumeClaim
          metadata:
            name: restore-data-pvc
            namespace: default
          spec:
            accessModes:
              - ReadWriteOnce
            resources:
              requests:
                storage: 50Gi
          EOF

      - name: Trigger k8up restore from prod backups
        id: restore
        run: |
          RESTORE_NAME="restore-from-prod-$(date +%Y%m%d-%H%M%S)"
          echo "restore_name=$RESTORE_NAME" >> $GITHUB_OUTPUT

          # Create a k8up Restore resource to restore from prod backups
          kubectl apply -f - <<EOF
          apiVersion: k8up.io/v1
          kind: Restore
          metadata:
            name: $RESTORE_NAME
            namespace: default
          spec:
            snapshot: latest
            restoreMethod:
              folder:
                claimName: restore-data-pvc
            backend:
              repoPasswordSecretRef:
                name: k8up-repo-password
                key: password
              s3:
                bucket: mcp-registry-prod-backups
                endpoint: https://storage.googleapis.com
                accessKeyIDSecretRef:
                  name: prod-to-staging-sync-credentials
                  key: AWS_ACCESS_KEY_ID
                secretAccessKeySecretRef:
                  name: prod-to-staging-sync-credentials
                  key: AWS_SECRET_ACCESS_KEY
          EOF

      - name: Wait for k8up restore to complete
        run: |
          RESTORE_NAME="${{ steps.restore.outputs.restore_name }}"

          echo "Waiting for restore job to start..."
          sleep 15

          # Find the job created by k8up for this restore
          # k8up creates jobs with name pattern "restore-<restore-name>"
          # Since our restore is named "restore-from-prod-*", the job will be "restore-restore-from-prod-*"
          for i in {1..60}; do
            JOB_NAME=$(kubectl get jobs -n default --no-headers 2>/dev/null | grep "^restore-$RESTORE_NAME" | awk '{print $1}' | head -1)
            if [ -n "$JOB_NAME" ]; then
              echo "Found restore job: $JOB_NAME"
              break
            fi
            echo "Waiting for job to be created... ($i/60)"
            sleep 2
          done

          if [ -z "$JOB_NAME" ]; then
            echo "ERROR: Restore job not found after 120 seconds"
            echo "Checking restore resource status:"
            kubectl get restore $RESTORE_NAME -n default
            kubectl describe restore $RESTORE_NAME -n default

            echo "Checking for any restore jobs:"
            kubectl get jobs -n default | grep restore || echo "No restore jobs found"

            echo "Checking k8up operator logs:"
            kubectl logs deployment/k8up -n default --tail=50 | grep -i restore || echo "No restore logs found"
            exit 1
          fi

          # Wait for the restore job to complete (max 15 minutes)
          echo "Waiting for restore job to complete..."
          kubectl wait --for=condition=complete \
            job/$JOB_NAME \
            --timeout=900s -n default || {
              echo "Restore job failed or timed out"
              echo "Job status:"
              kubectl get job/$JOB_NAME -n default
              echo "Job details:"
              kubectl describe job/$JOB_NAME -n default
              echo "Job logs (if available):"
              kubectl logs job/$JOB_NAME -n default --tail=100 || echo "No logs available"

              # Check if it's a credential issue
              echo "Checking if credentials exist:"
              kubectl get secret prod-to-staging-sync-credentials -n default || echo "Credentials secret missing!"
              exit 1
            }

      - name: Find staging PostgreSQL PVC
        id: pgdata-pvc
        run: |
          # Find the PVC used by the PostgreSQL cluster
          PVC_NAME=$(kubectl get pvc -n default -l cnpg.io/cluster=registry-pg -o jsonpath='{.items[0].metadata.name}')

          if [ -z "$PVC_NAME" ]; then
            echo "ERROR: Could not find PostgreSQL PVC"
            kubectl get pvc -n default -l cnpg.io/cluster=registry-pg
            exit 1
          fi

          echo "pvc_name=$PVC_NAME" >> $GITHUB_OUTPUT

      - name: Scale down staging PostgreSQL
        run: |
          echo "Scaling down PostgreSQL cluster..."
          kubectl patch cluster registry-pg -n default \
            --type merge \
            --patch '{"spec":{"instances":0}}'

          # Wait for pods to terminate
          echo "Waiting for pods to terminate..."
          kubectl wait --for=delete pod -l cnpg.io/cluster=registry-pg -n default --timeout=300s || true

      - name: Verify we are in staging cluster (SAFETY CHECK)
        run: |
          # Get current cluster context
          CURRENT_CONTEXT=$(kubectl config current-context)
          CURRENT_PROJECT=$(gcloud config get-value project)

          echo "Current kubectl context: $CURRENT_CONTEXT"
          echo "Current GCP project: $CURRENT_PROJECT"

          # Verify we're in staging
          if ! echo "$CURRENT_CONTEXT" | grep -qi "staging"; then
            echo "❌ SAFETY CHECK FAILED: Not in staging cluster"
            echo "Context: $CURRENT_CONTEXT"
            echo "Expected: staging cluster"
            exit 1
          fi

          if [ "$CURRENT_PROJECT" != "mcp-registry-staging" ]; then
            echo "❌ SAFETY CHECK FAILED: Not in staging project"
            echo "Project: $CURRENT_PROJECT"
            echo "Expected: mcp-registry-staging"
            exit 1
          fi

      - name: Replace staging database with restored backup
        id: copy-job
        run: |
          JOB_NAME="copy-pgdata-$(date +%Y%m%d-%H%M%S)"
          echo "job_name=$JOB_NAME" >> $GITHUB_OUTPUT

          # Create a job to copy the restored backup data to the staging PVC
          kubectl apply -f - <<EOF
          apiVersion: batch/v1
          kind: Job
          metadata:
            name: $JOB_NAME
            namespace: default
          spec:
            ttlSecondsAfterFinished: 600
            template:
              spec:
                restartPolicy: Never
                containers:
                - name: copy-data
                  image: busybox:latest
                  command:
                    - /bin/sh
                    - -c
                    - |
                      set -e
                      echo "Finding PostgreSQL data in backup..."
                      echo "Restore structure:"
                      find /restore -maxdepth 3 -type d 2>/dev/null | head -20

                      # Try different possible paths for pgdata
                      PGDATA_SOURCE=""
                      for path in \$(find /restore -type d -name "pgdata" 2>/dev/null); do
                        if [ -f "\$path/PG_VERSION" ]; then
                          PGDATA_SOURCE="\$path"
                          break
                        fi
                      done

                      if [ -z "\$PGDATA_SOURCE" ]; then
                        echo "ERROR: Could not find valid pgdata directory with PG_VERSION"
                        echo "Searched paths:"
                        find /restore -type d -name "pgdata" 2>/dev/null
                        exit 1
                      fi

                      echo "Found pgdata at: \$PGDATA_SOURCE"
                      echo "Contents:"
                      ls -lah \$PGDATA_SOURCE/ | head -10

                      echo "Backing up existing staging data..."
                      mkdir -p /pgdata-backup
                      if [ "\$(ls -A /pgdata)" ]; then
                        cp -a /pgdata/. /pgdata-backup/ || echo "Warning: Could not backup existing data"
                      fi

                      echo "Clearing existing data..."
                      rm -rf /pgdata/*

                      echo "Copying backup data to staging PVC..."
                      cp -a \$PGDATA_SOURCE/. /pgdata/

                      echo "Setting correct permissions..."
                      chmod 700 /pgdata

                      ls -lah /pgdata/ | head -20
                      echo "PostgreSQL version: \$(cat /pgdata/PG_VERSION)"
                  volumeMounts:
                  - name: restore-data
                    mountPath: /restore
                  - name: staging-pgdata
                    mountPath: /pgdata
                volumes:
                - name: restore-data
                  persistentVolumeClaim:
                    claimName: restore-data-pvc
                - name: staging-pgdata
                  persistentVolumeClaim:
                    claimName: ${{ steps.pgdata-pvc.outputs.pvc_name }}
          EOF

      - name: Wait for data copy to complete
        run: |
          JOB_NAME="${{ steps.copy-job.outputs.job_name }}"

          # Wait for copy to complete
          kubectl wait --for=condition=complete job/$JOB_NAME --timeout=600s -n default || {
            echo "Data copy job failed"
            kubectl describe job/$JOB_NAME -n default
            kubectl logs job/$JOB_NAME -n default --tail=100
            exit 1
          }

      - name: Scale up staging PostgreSQL
        run: |
          echo "Scaling up PostgreSQL cluster..."
          kubectl patch cluster registry-pg -n default \
            --type merge \
            --patch '{"spec":{"instances":1}}'

          # Wait for PostgreSQL pod to be created
          echo "Waiting for PostgreSQL pod to be created..."
          for i in {1..60}; do
            POD_COUNT=$(kubectl get pods -l cnpg.io/cluster=registry-pg -n default --no-headers 2>/dev/null | wc -l)
            if [ "$POD_COUNT" -gt 0 ]; then
              echo "Pod created"
              break
            fi
            echo "Waiting... ($i/60)"
            sleep 2
          done

          # Wait for PostgreSQL to be ready
          echo "Waiting for PostgreSQL to be ready..."
          kubectl wait --for=condition=ready pod -l cnpg.io/cluster=registry-pg -n default --timeout=300s

      - name: Verify staging DB is functional
        run: |
          # Create a verification pod
          kubectl run pg-verify-$(date +%s) \
            --image=postgres:15 \
            --rm -i --restart=Never \
            --env="PGPASSWORD=$(kubectl get secret registry-pg-superuser -n default -o jsonpath='{.data.password}' | base64 -d)" \
            -- bash -c '
              echo "Waiting for database to accept connections..."
              for i in {1..30}; do
                if pg_isready -h registry-pg-rw -U postgres 2>/dev/null; then
                  break
                fi
                echo "Waiting... ($i/30)"
                sleep 2
              done

              echo "Querying database..."
              TABLE_COUNT=$(psql -h registry-pg-rw -U postgres -d app -tAc "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '\''public'\'';" 2>&1)

              if [ $? -ne 0 ]; then
                echo "ERROR: Could not query database"
                echo "$TABLE_COUNT"
                exit 1
              fi

              if [ "$TABLE_COUNT" -lt 1 ]; then
                echo "ERROR: Staging DB has no tables!"
                exit 1
              fi

              echo "Staging DB has $TABLE_COUNT tables"
              echo "Top 10 tables by row count:"
              psql -h registry-pg-rw -U postgres -d app \
                -c "SELECT schemaname, tablename, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 10;" || true
            '

      - name: Cleanup
        if: always()
        run: |
          # Wait for any restore jobs to finish before cleanup (max 5 minutes for large database restores)
          echo "Checking for running restore jobs..."
          RESTORE_NAME="${{ steps.restore.outputs.restore_name }}"

          for i in {1..60}; do
            # Check specifically for our restore job in Running or Pending state
            RUNNING_JOBS=$(kubectl get jobs -n default --no-headers 2>/dev/null | grep "^restore-${RESTORE_NAME}" | grep -E "Running|Pending" | wc -l)
            if [ "$RUNNING_JOBS" -eq 0 ]; then
              echo "No running restore jobs found"
              break
            fi
            echo "Waiting for $RUNNING_JOBS restore job(s) to finish... ($i/60)"
            sleep 5
          done

          # Clean up jobs first
          if [ -n "${{ steps.copy-job.outputs.job_name }}" ]; then
            kubectl delete job ${{ steps.copy-job.outputs.job_name }} -n default || true
          fi

          # Clean up the restore job if it exists (with proper error handling)
          if [ -n "$RESTORE_NAME" ]; then
            if kubectl get job "restore-$RESTORE_NAME" -n default >/dev/null 2>&1; then
              echo "Deleting restore job: restore-$RESTORE_NAME"
              kubectl delete job "restore-$RESTORE_NAME" -n default || echo "Failed to delete job, may have already been cleaned up"
            else
              echo "Restore job restore-$RESTORE_NAME not found, skipping deletion"
            fi
          fi

          # Remove restore PVC (will wait for jobs to finish)
          kubectl delete pvc restore-data-pvc -n default || true

          # Remove prod backup credentials (for security)
          kubectl delete secret prod-to-staging-sync-credentials -n default || true

          # Clean up old restore resources (keep last 3)
          kubectl get restore -n default --sort-by=.metadata.creationTimestamp -o name | head -n -3 | xargs -r kubectl delete || true

          # Clean up old restore jobs (keep last 3)
          kubectl get jobs -n default --sort-by=.metadata.creationTimestamp -o name | grep '^job.batch/restore-' | head -n -3 | xargs -r kubectl delete -n default || true

          # Clean up old copy jobs (keep last 3)
          kubectl get jobs -n default --sort-by=.metadata.creationTimestamp -o name | grep 'copy-pgdata-' | head -n -3 | xargs -r kubectl delete -n default || true

```

## /.github/workflows/sync-schema.yml

```yml path="/.github/workflows/sync-schema.yml" 
name: Sync Schema

on:
  workflow_dispatch: # Manual trigger
  # TODO: Add daily schedule later
  # schedule:
  #   - cron: '0 2 * * *'  # Run daily at 2 AM UTC

permissions:
  contents: write

jobs:
  sync-schema:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Checkout static repo
        uses: actions/checkout@v6
        with:
          repository: modelcontextprotocol/static
          path: static-repo

      - name: Sync schemas from static repo
        run: |
          echo "🔍 Syncing schemas from modelcontextprotocol/static..."
          mkdir -p internal/validators/schemas
          
          # Copy all versioned schema files
          for dir in static-repo/schemas/*/; do
            if [ -f "$dir/server.schema.json" ]; then
              version=$(basename "$dir")
              # Skip draft directory if it exists
              if [ "$version" != "draft" ]; then
                output_file="internal/validators/schemas/${version}.json"
                if [ ! -f "$output_file" ] || ! cmp -s "$dir/server.schema.json" "$output_file"; then
                  echo "⬇ Adding/updating ${version}/server.schema.json -> ${version}.json"
                  cp "$dir/server.schema.json" "$output_file"
                else
                  echo "✓ ${version} is already up to date"
                fi
              fi
            fi
          done
          
          echo "✅ Schema sync complete"

      - name: Check for changes
        id: changes
        run: |
          # Check for both modified and untracked files
          if [ -n "$(git status --porcelain internal/validators/schemas/)" ]; then
            echo "changed=true" >> $GITHUB_OUTPUT
            git status --porcelain internal/validators/schemas/
          else
            echo "changed=false" >> $GITHUB_OUTPUT
            echo "No changes to schemas"
          fi

      - name: Commit and push changes
        if: steps.changes.outputs.changed == 'true'
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add internal/validators/schemas/
          git commit -m "Sync schemas from modelcontextprotocol/static [skip ci]"
          git push

```

## /.gitignore

```gitignore path="/.gitignore" 
build/
bin/
tmp/

.db
.env
.mcpregistry*
.DS_Store
validate-examples
validate-schemas
.idea/
coverage.out
coverage.html
deploy/infra/infra
registry

```

## /.golangci.yml

```yml path="/.golangci.yml" 
version: "2"
run:
  modules-download-mode: readonly
linters:
  enable:
    - asasalint
    - asciicheck
    - bidichk
    - bodyclose
    - containedctx
    - contextcheck
    - cyclop
    - dupl
    - durationcheck
    - errname
    - errorlint
    - exhaustive
    - forbidigo
    - gocognit
    - goconst
    - gocritic
    - gocyclo
    - godox
    - gomoddirectives
    - gomodguard
    - goprintffuncname
    - gosec
    - grouper
    - importas
    - makezero
    - misspell
    - nakedret
    - nestif
    - nilerr
    - nilnil
    - noctx
    - nolintlint
    - nosprintfhostport
    - predeclared
    - promlinter
    - reassign
    - revive
    - rowserrcheck
    - sqlclosecheck
    - testpackage
    - thelper
    - tparallel
    - unconvert
    - unparam
    - usestdlibvars
    - wastedassign
    - whitespace
  settings:
    cyclop:
      max-complexity: 20
    dupl:
      threshold: 300
    gocognit:
      min-complexity: 51
    nestif:
      min-complexity: 10
  exclusions:
    generated: lax
    presets:
      - comments
      - common-false-positives
      - legacy
      - std-error-handling
    rules:
      # httptest.NewRequest is idiomatic in tests; noctx is not useful there.
      - path: _test\.go
        linters:
          - noctx
    paths:
      - third_party$
      - builtin$
      - examples$
formatters:
  enable:
    - gofmt
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$

```

## /.goreleaser.yaml

```yaml path="/.goreleaser.yaml" 
# GoReleaser configuration for MCP Registry
version: 2

# Build configuration
builds:
  # Registry server binary
  - id: registry
    binary: registry
    main: ./cmd/registry
    env:
      - CGO_ENABLED=0
    goos:
      - linux
      - darwin
      - windows
    goarch:
      - amd64
      - arm64
    ldflags:
      - -s -w
      - -X main.Version={{.Version}}
      - -X main.GitCommit={{.FullCommit}}
      - -X main.BuildTime={{.Date}}
    
  # Publisher CLI tool binary
  - id: publisher
    binary: mcp-publisher
    main: ./cmd/publisher
    env:
      - CGO_ENABLED=0
    goos:
      - linux
      - darwin
      - windows
    goarch:
      - amd64
      - arm64
    ldflags:
      - -s -w
      - -X main.Version={{.Version}}
      - -X main.GitCommit={{.FullCommit}}
      - -X main.BuildTime={{.Date}}

# This section defines whether we want to release the source code too.
source:
  enabled: true

# This section defines how to generate the changelog
changelog:
  sort: asc
  use: github
  filters:
    exclude:
      - '^docs:'
      - '^test:'
      - '^ci:'
      - '^chore:'
      - '^style:'
      - 'merge conflict'
      - 'Merge pull request'
      - 'Merge remote-tracking branch'
      - 'Merge branch'
  groups:
    - title: 'New Features'
      regexp: '^.*?feat(\([[:word:]]+\))??!?:.+{{contextString}}#39;
      order: 0
    - title: 'Bug Fixes'
      regexp: '^.*?fix(\([[:word:]]+\))??!?:.+{{contextString}}#39;
      order: 1
    - title: 'Performance Improvements'
      regexp: '^.*?perf(\([[:word:]]+\))??!?:.+{{contextString}}#39;
      order: 2
    - title: 'Refactors'
      regexp: '^.*?refactor(\([[:word:]]+\))??!?:.+{{contextString}}#39;
      order: 3
    - title: 'Documentation'
      regexp: '^.*?docs?(\([[:word:]]+\))??!?:.+{{contextString}}#39;
      order: 4
    - title: 'Other Changes'
      order: 999

# This section defines for which artifact types to generate SBOMs.
sboms:
  - artifacts: archive

# This section defines the release policy.
release:
  github:
    owner: modelcontextprotocol
    name: registry

# This section defines how and which artifacts we want to sign for the release.
signs:
  - id: archives
    cmd: cosign
    args:
      - "sign-blob"
      - "--bundle=${signature}" # cosign v3+: bundles signature and certificate together
      - "${artifact}"
      - "--yes" # needed on cosign 2.0.0+
    artifacts: archive
    output: true
    signature: "${artifact}.sigstore.json"

  # Also sign checksums file for additional verification
  - id: checksums
    cmd: cosign
    args:
      - "sign-blob"
      - "--bundle=${signature}" # cosign v3+: bundles signature and certificate together
      - "${artifact}"
      - "--yes"
    artifacts: checksum
    output: true
    signature: "${artifact}.sigstore.json"

# This section defines the release format.
archives:
  # Registry server archive
  - id: registry
    name_template: "registry_{{ .Os }}_{{ .Arch }}"
    ids:
      - registry

  # Publisher CLI archive
  - id: publisher
    name_template: "mcp-publisher_{{ .Os }}_{{ .Arch }}"
    ids:
      - publisher

```

## /.ko.dockerignore

```dockerignore path="/.ko.dockerignore" 
# Build artifacts
bin/
*.exe
*.dll
*.so
*.dylib

# Test and coverage files
coverage.out
coverage.html
*.test

# Database files
*.db
.db/

# Deployment files
deploy/

# Documentation
docs/

# Tests
tests/

# Git
.git/
.gitignore

# GitHub Actions
.github/

# Docker files (no longer used)
Dockerfile
.dockerignore
docker-compose.yml

# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~

# Temporary files
tmp/
temp/
*.tmp

# OS files
.DS_Store
Thumbs.db
```

## /.ko.yaml

```yaml path="/.ko.yaml" 
# ko configuration for MCP Registry
# Documentation: https://ko.build/configuration/

# Default base image for all builds
# Using Chainguard's static image for minimal, secure containers (~2MB)
defaultBaseImage: cgr.dev/chainguard/static:latest

# Default platforms for multi-architecture builds
defaultPlatforms:
  - linux/amd64
  - linux/arm64

# Build configuration
builds:
  - id: registry
    # Main package to build
    main: ./cmd/registry
    # Inject version information at build time via ldflags
    ldflags:
      - -s -w
      - -X main.Version={{ .Env.VERSION }}
      - -X main.GitCommit={{ .Env.GIT_COMMIT }}
      - -X main.BuildTime={{ .Env.BUILD_TIME }}
    env:
      - CGO_ENABLED=0

# SBOM (Software Bill of Materials) configuration
sbom: spdx

# Base import path handling
# Set to false to preserve full import paths in image names
baseImportPaths: false

```

## /.vscode/launch.json

```json path="/.vscode/launch.json" 
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [

        {
            "name": "Launch server",
            "type": "go",
            "request": "launch",
            "mode": "auto",
            "program": "${workspaceFolder}/cmd/registry",
            "envFile": "${workspaceFolder}/.env",
        },
        {
            "name": "Launch publisher",
            "type": "go",
            "request": "launch",
            "mode": "auto",
            "program": "${workspaceFolder}/cmd/publisher/main.go",
            "args": [
                "-registry-url=http://localhost:8080",
                "-mcp-file=${workspaceFolder}/cmd/publisher/server.json",
            ],
        }
    ]
}
```

## /CHANGES.md



## /CLAUDE.md

# CLAUDE.md
_Guidance for Claude Code (claude.ai/code) when working in this repository. If it's also useful to humans (probably most things!), put the instructions in README.md instead._

Import @README.md

## Important: Publishing MCP servers

The `data/seed.json` file is seed data for local development only. Do NOT create pull requests or commits that add server entries to `data/seed.json` as a way to publish a server to the registry.

To publish an MCP server, use the `mcp-publisher` CLI tool. See `docs/modelcontextprotocol-io/quickstart.mdx` for instructions.


## /CONTRIBUTING.md

# Contributing to the MCP Registry

Thank you for your interest in contributing!

## Want to publish an MCP server?

**Do NOT open a pull request to add your server to `data/seed.json`.**

The `data/seed.json` file is seed data used only for local development. Modifying it will not publish your server to the registry.

To publish an MCP server, use the official `mcp-publisher` CLI tool. See the [publishing quickstart guide](docs/modelcontextprotocol-io/quickstart.mdx) for step-by-step instructions.

## Contributing to the registry codebase

We welcome contributions to the registry itself! Here's how to get started:

### Communication channels

We use multiple channels for collaboration - see [modelcontextprotocol.io/community/communication](https://modelcontextprotocol.io/community/communication).

Often (but not always) ideas flow through this pipeline:

- **[Discord](https://modelcontextprotocol.io/community/communication)** - Real-time community discussions
- **[Discussions](https://github.com/modelcontextprotocol/registry/discussions)** - Propose and discuss product/technical requirements
- **[Issues](https://github.com/modelcontextprotocol/registry/issues)** - Track well-scoped technical work
- **[Pull Requests](https://github.com/modelcontextprotocol/registry/pulls)** - Contribute work towards issues

### Development setup

See the [README](README.md#quick-start) for prerequisites and instructions on running the server locally.

### Running checks

```bash
# Run lint, unit tests and integration tests
make check
```

Run `make help` for more available commands.

## /Makefile

``` path="/Makefile" 
.PHONY: help build test test-unit test-integration test-endpoints test-publish test-all lint lint-fix validate validate-schemas validate-examples check ko-build ko-rebuild dev-compose dev-down clean publisher generate-schema check-schema

# Use bash for all commands to support pipefail
SHELL := /bin/bash

# Default target
help: ## Show this help message
	@echo "Available targets:"
	@grep -E '^[a-zA-Z_-]+:.*?## .*$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "  %-20s %s\n", $1, $2}'

# Build targets
build: ## Build the registry application with version info
	@mkdir -p bin
	go build -ldflags="-X main.Version=dev-$(shell git rev-parse --short HEAD) -X main.GitCommit=$(shell git rev-parse HEAD) -X main.BuildTime=$(shell date -u +%Y-%m-%dT%H:%M:%SZ)" -o bin/registry ./cmd/registry

publisher: ## Build the publisher tool with version info
	@mkdir -p bin
	go build -ldflags="-X main.Version=dev-$(shell git rev-parse --short HEAD) -X main.GitCommit=$(shell git rev-parse HEAD) -X main.BuildTime=$(shell date -u +%Y-%m-%dT%H:%M:%SZ)" -o bin/mcp-publisher ./cmd/publisher

# Schema generation targets
generate-schema: ## Generate server.schema.json from openapi.yaml
	@mkdir -p bin
	go build -o bin/extract-server-schema ./tools/extract-server-schema
	@./bin/extract-server-schema

check-schema: ## Check if server.schema.json is in sync with openapi.yaml
	@mkdir -p bin
	go build -o bin/extract-server-schema ./tools/extract-server-schema
	@./bin/extract-server-schema -check

# Test targets
test-unit: ## Run unit tests with coverage (requires PostgreSQL)
	@echo "Starting PostgreSQL for unit tests..."
	@docker compose up -d postgres 2>&1 | grep -v "Pulling\|Pulled\|Creating\|Created\|Starting\|Started" || true
	@echo "Waiting for PostgreSQL to be ready..."
	@sleep 3
	@echo ""
	@echo "Running unit tests..."
	@set -o pipefail; if command -v gotestsum >/dev/null 2>&1; then \
		gotestsum --format pkgname-and-test-fails -- -race -coverprofile=coverage.out -covermode=atomic ./internal/... ./cmd/... 2>&1 | grep -v "ld: warning:"; \
	else \
		go test -race -coverprofile=coverage.out -covermode=atomic ./internal/... ./cmd/... 2>&1 | grep -v "ld: warning:" | grep -v "^ld:"; \
	fi
	@echo ""
	@go tool cover -html=coverage.out -o coverage.html
	@echo "✅ Coverage report: coverage.html"
	@go tool cover -func=coverage.out | tail -1
	@echo ""
	@docker compose down postgres >/dev/null 2>&1
	@echo "✅ Tests complete"

test: ## Run unit tests (use 'make test-all' to run all tests)
	@echo "⚠️  Running unit tests only. Use 'make test-all' to run both unit and integration tests."
	@$(MAKE) test-unit

test-integration: ## Run integration tests
	./tests/integration/run.sh

test-endpoints: ## Test API endpoints (requires running server)
	./scripts/test_endpoints.sh

test-publish: ## Test publish endpoint (requires BEARER_TOKEN env var)
	./scripts/test_publish.sh

test-all: test-unit test-integration ## Run all tests (unit and integration)

# Validation targets
validate-schemas: ## Validate JSON schemas
	./tools/validate-schemas.sh
	@$(MAKE) check-schema

validate-examples: ## Validate examples against schemas
	./tools/validate-examples.sh

validate: validate-schemas validate-examples ## Run all validation checks

# Lint targets
lint: ## Run linter (includes formatting)
	golangci-lint run --timeout=5m

lint-fix: ## Run linter with auto-fix (includes formatting)
	golangci-lint run --fix --timeout=5m

# Combined targets
check: dev-down lint validate test-all ## Run all checks (lint, validate, unit tests) and ensure dev environment is down
	@echo "All checks passed!"

# Development targets
ko-build: ## Build registry image using ko (loads into local docker daemon)
	@echo "Building registry with ko..."
	VERSION=dev-$(git rev-parse --short HEAD) \
	GIT_COMMIT=$(git rev-parse HEAD) \
	BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
	KO_DOCKER_REPO=ko.local \
	ko build --preserve-import-paths --tags=dev --sbom=none ./cmd/registry
	@echo "Image built: ko.local/github.com/modelcontextprotocol/registry/cmd/registry:dev"

ko-rebuild: ## Rebuild with ko and restart registry container
	@$(MAKE) ko-build
	@echo "Restarting registry container..."
	@docker compose restart registry

dev-compose: ko-build ## Start development environment with Docker Compose (builds with ko first)
	@echo "Starting Docker Compose..."
	docker compose up

dev-down: ## Stop development environment
	docker compose down

# Cleanup
clean: ## Clean build artifacts and coverage files
	rm -rf bin
	rm -f coverage.out coverage.html


.DEFAULT_GOAL := help

```

## /README.md

# MCP Registry

The MCP registry provides MCP clients with a list of MCP servers, like an app store for MCP servers.

[**📤 Publish my MCP server**](docs/modelcontextprotocol-io/quickstart.mdx) | [**⚡️ Live API docs**](https://registry.modelcontextprotocol.io/docs) | [**👀 Ecosystem vision**](docs/design/ecosystem-vision.md) | 📖 **[Full documentation](./docs)**

## Development Status

**2025-10-24 update**: The Registry API has entered an **API freeze (v0.1)** 🎉. For the next month or more, the API will remain stable with no breaking changes, allowing integrators to confidently implement support. This freeze applies to v0.1 while development continues on v0. We'll use this period to validate the API in real-world integrations and gather feedback to shape v1 for general availability. Thank you to everyone for your contributions and patience—your involvement has been key to getting us here!

**2025-09-08 update**: The registry has launched in preview 🎉 ([announcement blog post](https://blog.modelcontextprotocol.io/posts/2025-09-08-mcp-registry-preview/)). While the system is now more stable, this is still a preview release and breaking changes or data resets may occur. A general availability (GA) release will follow later. We'd love your feedback in [GitHub discussions](https://github.com/modelcontextprotocol/registry/discussions/new?category=ideas) or in the [#registry-dev Discord](https://discord.com/channels/1358869848138059966/1369487942862504016) ([joining details here](https://modelcontextprotocol.io/community/communication)).

Registry Working Group:
- **Tadas Antanavicius** (PulseMCP) [@tadasant](https://github.com/tadasant)
- **Radoslav (Rado) Dimitrov** (Stacklok) [@rdimitrov](https://github.com/rdimitrov)
- **Bob Dickinson** (TeamSpark) [@BobDickinson](https://github.com/BobDickinson)
- **Preeti (Pree) Dewani** (Ravenmail) [@pree-dew](https://github.com/pree-dew)

## Contributing

We use multiple channels for collaboration - see [modelcontextprotocol.io/community/communication](https://modelcontextprotocol.io/community/communication).

Often (but not always) ideas flow through this pipeline:

- **[Discord](https://modelcontextprotocol.io/community/communication)** - Real-time community discussions
- **[Discussions](https://github.com/modelcontextprotocol/registry/discussions)** - Propose and discuss product/technical requirements
- **[Issues](https://github.com/modelcontextprotocol/registry/issues)** - Track well-scoped technical work  
- **[Pull Requests](https://github.com/modelcontextprotocol/registry/pulls)** - Contribute work towards issues

### Quick start:

#### Pre-requisites

- **Docker**
- **Go 1.24.x**
- **ko** - Container image builder for Go ([installation instructions](https://ko.build/install/))
- **golangci-lint v2.4.0**

#### Running the server

```bash
# Start full development environment
make dev-compose
```

This starts the registry at [`localhost:8080`](http://localhost:8080) with PostgreSQL. The database uses ephemeral storage and is reset each time you restart the containers, ensuring a clean state for development and testing.

**Note:** The registry uses [ko](https://ko.build) to build container images. The `make dev-compose` command automatically builds the registry image with ko and loads it into your local Docker daemon before starting the services.

By default, the registry seeds from the production API with a filtered subset of servers (to keep startup fast). This ensures your local environment mirrors production behavior and all seed data passes validation. For offline development you can seed from a file without validation with `MCP_REGISTRY_SEED_FROM=data/seed.json MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=false make dev-compose`.

The setup can be configured with environment variables in [docker-compose.yml](./docker-compose.yml) - see [.env.example](./.env.example) for a reference.

<details>
<summary>Alternative: Running a pre-built Docker image</summary>

Pre-built Docker images are automatically published to GitHub Container Registry. Note that the image does not bundle PostgreSQL, so you need to run your own and point the registry at it via `MCP_REGISTRY_DATABASE_URL` (see [docker-compose.yml](./docker-compose.yml) for a working example):

```bash
# Run latest stable release
docker run -p 8080:8080 ghcr.io/modelcontextprotocol/registry:latest

# Run latest from main branch (continuous deployment)
docker run -p 8080:8080 ghcr.io/modelcontextprotocol/registry:main

# Run specific release version
docker run -p 8080:8080 ghcr.io/modelcontextprotocol/registry:v1.0.0

# Run development build from main branch
docker run -p 8080:8080 ghcr.io/modelcontextprotocol/registry:main-20250906-abc123d
```

**Available tags:** 
- **Releases**: `latest`, `v1.0.0`, `v1.1.0`, etc.
- **Continuous**: `main` (latest main branch build)
- **Development**: `main-<date>-<sha>` (specific commit builds)

</details>

#### Publishing a server

To publish a server, we've built a simple CLI. You can use it with:

```bash
# Build the latest CLI
make publisher

# Use it!
./bin/mcp-publisher --help
```

See [the publisher guide](./docs/modelcontextprotocol-io/quickstart.mdx) for more details.

#### Other commands

```bash
# Run lint, unit tests and integration tests
make check
```

There are also a few more helpful commands for development. Run `make help` to learn more, or look in [Makefile](./Makefile).

<!--
For Claude and other AI tools: Always prefer make targets over custom commands where possible.
-->

## Architecture

### Project Structure

```
├── cmd/                     # Application entry points
│   └── publisher/           # Server publishing tool
├── data/                    # Seed data
├── deploy/                  # Deployment configuration (Pulumi)
├── docs/                    # Documentation
├── internal/                # Private application code
│   ├── api/                 # HTTP handlers and routing
│   ├── auth/                # Authentication (GitHub OAuth, JWT, namespace blocking)
│   ├── config/              # Configuration management
│   ├── database/            # Data persistence (PostgreSQL)
│   ├── service/             # Business logic
│   ├── telemetry/           # Metrics and monitoring
│   └── validators/          # Input validation
├── pkg/                     # Public packages
│   ├── api/                 # API types and structures
│   │   └── v0/              # Version 0 API types
│   └── model/               # Data models for server.json
├── scripts/                 # Development and testing scripts
├── tests/                   # Integration tests
└── tools/                   # CLI tools and utilities
    └── validate-*.sh        # Schema validation tools
```

### Authentication

Publishing supports multiple authentication methods:
- **GitHub OAuth** - For publishing by logging into GitHub
- **GitHub OIDC** - For publishing from GitHub Actions
- **DNS verification** - For proving ownership of a domain and its subdomains
- **HTTP verification** - For proving ownership of a domain

The registry validates namespace ownership when publishing. E.g. to publish...:
- `io.github.domdomegg/my-cool-mcp` you must login to GitHub as `domdomegg`, or be in a GitHub Action on domdomegg's repos
- `me.adamjones/my-cool-mcp` you must prove ownership of `adamjones.me` via DNS or HTTP challenge

## Community Projects

Check out [community projects](docs/community-projects.md) to explore notable registry-related work created by the community.

## More documentation

See the [documentation](./docs) for more details if your question has not been answered here!


## /SECURITY.md

# Security Policy

Thank you for helping keep the Model Context Protocol and its ecosystem secure.

## Reporting Security Issues

If you discover a security vulnerability in this repository, please report it through
the [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)
for this repository.

Please **do not** report security vulnerabilities through public GitHub issues, discussions,
or pull requests.

## What to Include

To help us triage and respond quickly, please include:

- A description of the vulnerability
- Steps to reproduce the issue
- The potential impact
- Any suggested fixes (optional)


## /cmd/publisher/README.md

# MCP Publisher Tool - Development

CLI tool for publishing MCP servers to the registry.

> These docs are for contributors. See the [Publisher User Guide](../../docs/modelcontextprotocol-io/quickstart.mdx) for end-user documentation.

## Quick Development Setup

```bash
# Build the tool
make publisher

# Test locally 
make dev-compose  # Start local registry
./bin/mcp-publisher init
./bin/mcp-publisher login none --registry=http://localhost:8080
./bin/mcp-publisher publish --registry=http://localhost:8080
```

## Architecture

### Commands
- **`init`** - Generate server.json templates with auto-detection
- **`login`** - Handle authentication (github, dns, http, none)
- **`publish`** - Validate and upload servers to registry
- **`status`** - Update server lifecycle status (active, deprecated, deleted)
- **`logout`** - Clear stored credentials

### Authentication Providers
- **`github`** - Interactive OAuth flow
- **`github-oidc`** - CI/CD with GitHub Actions
- **`dns`** - Domain verification via DNS TXT records
- **`http`** - Domain verification via HTTPS endpoints
- **`none`** - No auth (testing only)

### Signing Providers
Optional: enables `dns` and `http` methods to sign out-of-process without direct access to the private key.

- **`google-kms`** - Google KMS signing
- **`azure-key-vault`** - Azure Key Vault signing

## Key Files

- **`main.go`** - CLI setup and command routing
- **`commands/`** - Command implementations with auto-detection logic
- **`auth/`** - Authentication provider implementations
- **`build.sh`** - Cross-platform build script


## /cmd/publisher/auth/azurekeyvault/common.go

```go path="/cmd/publisher/auth/azurekeyvault/common.go" 
package azurekeyvault

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/sha512"
	"fmt"
	"math/big"
	"os"

	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
	"github.com/modelcontextprotocol/registry/cmd/publisher/auth"
)

func GetSignatureProvider(vaultName, keyName string) (auth.Signer, error) {
	if vaultName == "" {
		return nil, fmt.Errorf("--vault option (vault name) is required")
	}

	if keyName == "" {
		return nil, fmt.Errorf("--key option (key name) is required")
	}

	return Signer{
		vaultName: vaultName,
		keyName:   keyName,
	}, nil
}

type Signer struct {
	vaultName string
	keyName   string
}

func (d Signer) GetSignedTimestamp(ctx context.Context) (*string, []byte, error) {
	fmt.Fprintf(os.Stdout, "Signing using Azure Key Vault %s and key %s\n", d.vaultName, d.keyName)

	cred, err := azidentity.NewDefaultAzureCredential(nil)
	if err != nil {
		return nil, nil, fmt.Errorf("authentication to Azure failed: %w", err)
	}

	vaultURL := fmt.Sprintf("https://%s.vault.azure.net/", d.vaultName)
	client, err := azkeys.NewClient(vaultURL, cred, nil)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create Key Vault client: %w", err)
	}

	keyResp, err := client.GetKey(ctx, d.keyName, "", nil)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to retrieve key for public parameters: %w", err)
	}

	if *keyResp.Key.Kty != azkeys.KeyTypeEC && *keyResp.Key.Kty != azkeys.KeyTypeECHSM {
		return nil, nil, fmt.Errorf("unsupported key type: kty: %s (only EC or EC-HSM keys are supported)", *keyResp.Key.Kty)
	}

	if *keyResp.Key.Crv != azkeys.CurveNameP384 {
		return nil, nil, fmt.Errorf("unsupported curve: %s (only P-384 is supported)", *keyResp.Key.Crv)
	}

	fmt.Fprintln(os.Stdout, "Successfully read the public key from Key Vault.")
	auth.PrintEcdsaP384KeyInfo(ecdsa.PublicKey{
		Curve: elliptic.P384(),
		X:     new(big.Int).SetBytes(keyResp.Key.X),
		Y:     new(big.Int).SetBytes(keyResp.Key.Y),
	})

	timestamp := auth.GetTimestamp()
	digest := sha512.Sum384([]byte(timestamp))
	alg := azkeys.SignatureAlgorithmES384
	fmt.Fprintln(os.Stdout, "Executing the sign request...")
	signResp, err := client.Sign(ctx, d.keyName, "", azkeys.SignParameters{
		Algorithm: &alg,
		Value:     digest[:],
	}, nil)

	if err != nil {
		return nil, nil, fmt.Errorf("failed to sign message: %w", err)
	}

	return &timestamp, signResp.Result, nil
}

```

## /cmd/publisher/auth/common.go

```go path="/cmd/publisher/auth/common.go" 
package auth

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha512"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"math/big"
	"net/http"
	"os"
	"time"
)

type CryptoAlgorithm string

const (
	AlgorithmEd25519 CryptoAlgorithm = "ed25519"

	// ECDSA with NIST P-384 curve
	// public key is in compressed format
	// signature is in R || S format
	AlgorithmECDSAP384 CryptoAlgorithm = "ecdsap384"
)

// CryptoProvider provides common functionality for DNS and HTTP authentication
type CryptoProvider struct {
	registryURL string
	domain      string
	signer      Signer
	authMethod  string
}

type Signer interface {
	GetSignedTimestamp(ctx context.Context) (*string, []byte, error)
}

func GetTimestamp() string {
	return time.Now().UTC().Format(time.RFC3339)
}

func NewInProcessSigner(privateKey string, algorithm CryptoAlgorithm) (Signer, error) {
	if privateKey == "" {
		return nil, fmt.Errorf("%s private key (hex) is required", algorithm)
	}

	// Decode private key from hex
	privateKeyBytes, err := hex.DecodeString(privateKey)
	if err != nil {
		return nil, fmt.Errorf("invalid hex private key format: %w", err)
	}

	return &InProcessSigner{
		privateKey:      privateKeyBytes,
		cryptoAlgorithm: algorithm,
	}, nil
}

// GetToken retrieves the registry JWT token using cryptographic authentication
func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
	if c.domain == "" {
		return "", fmt.Errorf("%s domain is required", c.authMethod)
	}

	// Generate current timestamp
	timestamp, signedTimestamp, err := c.signer.GetSignedTimestamp(ctx)
	if err != nil {
		return "", fmt.Errorf("failed to sign timestamp: %w", err)
	}
	signedTimestampHex := hex.EncodeToString(signedTimestamp)

	// Exchange signature for registry token
	registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, *timestamp, signedTimestampHex)
	if err != nil {
		return "", fmt.Errorf("failed to exchange %s signature: %w", c.authMethod, err)
	}

	return registryToken, nil
}

type InProcessSigner struct {
	privateKey      []byte
	cryptoAlgorithm CryptoAlgorithm
}

func (c *InProcessSigner) GetSignedTimestamp(_ context.Context) (*string, []byte, error) {
	fmt.Fprintf(os.Stdout, "Signing in process using key algorithm %s\n", c.cryptoAlgorithm)

	timestamp := GetTimestamp()

	switch c.cryptoAlgorithm {
	case AlgorithmEd25519:
		if len(c.privateKey) != ed25519.SeedSize {
			return nil, nil, fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(c.privateKey))
		}

		privateKey := ed25519.NewKeyFromSeed(c.privateKey)

		PrintEd25519KeyInfo(privateKey.Public().(ed25519.PublicKey))

		signature := ed25519.Sign(privateKey, []byte(timestamp))
		return &timestamp, signature, nil
	case AlgorithmECDSAP384:
		if len(c.privateKey) != 48 {
			return nil, nil, fmt.Errorf("invalid seed length for ECDSA P-384: expected 48 bytes, got %d", len(c.privateKey))
		}

		digest := sha512.Sum384([]byte(timestamp))
		curve := elliptic.P384()

		// Parse the raw private key (compatible with Go 1.24)
		privateKey, err := parseRawPrivateKey(curve, c.privateKey)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to parse ECDSA private key: %w", err)
		}

		PrintEcdsaP384KeyInfo(privateKey.PublicKey)

		r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:])
		if err != nil {
			return nil, nil, fmt.Errorf("failed to sign message: %w", err)
		}
		signature := append(r.Bytes(), s.Bytes()...)
		return &timestamp, signature, nil
	default:
		return nil, nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm)
	}
}

// parseRawPrivateKey parses a raw ECDSA private key from bytes.
// This mimics crypto/ecdsa.ParseRawPrivateKey from Go 1.25+ for compatibility with Go 1.24.
func parseRawPrivateKey(curve elliptic.Curve, privateKeyBytes []byte) (*ecdsa.PrivateKey, error) {
	if curve == nil {
		return nil, fmt.Errorf("nil curve")
	}

	expectedBytes := (curve.Params().N.BitLen() + 7) / 8
	if len(privateKeyBytes) != expectedBytes {
		return nil, fmt.Errorf("invalid private key length: expected %d bytes, got %d", expectedBytes, len(privateKeyBytes))
	}

	// Only standard NIST curves supported
	switch curve {
	case elliptic.P224(), elliptic.P256(), elliptic.P384(), elliptic.P521():
		// ok
	default:
		return nil, fmt.Errorf("unsupported curve")
	}

	d := new(big.Int).SetBytes(privateKeyBytes)
	params := curve.Params()
	if d.Sign() <= 0 || d.Cmp(params.N) >= 0 {
		return nil, fmt.Errorf("invalid private scalar")
	}

	x, y := curve.ScalarBaseMult(d.Bytes()) //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
	return &ecdsa.PrivateKey{
		PublicKey: ecdsa.PublicKey{
			Curve: curve,
			X:     x,
			Y:     y,
		},
		D: d,
	}, nil
}

// Login is not needed for cryptographic auth since authentication is cryptographic
func (c *CryptoProvider) Login(_ context.Context) error {
	return nil
}

func PrintEd25519KeyInfo(pubKey ed25519.PublicKey) {
	pubKeyString := base64.StdEncoding.EncodeToString(pubKey)
	fmt.Fprint(os.Stdout, "Expected proof record:\n")
	fmt.Fprintf(os.Stdout, "v=MCPv1; k=ed25519; p=%s\n", pubKeyString)
}

func PrintEcdsaP384KeyInfo(pubKey ecdsa.PublicKey) {
	printEcdsaKeyInfo("ecdsap384", pubKey)
}

func printEcdsaKeyInfo(k string, pubKey ecdsa.PublicKey) {
	compressed := elliptic.MarshalCompressed(pubKey.Curve, pubKey.X, pubKey.Y) //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
	pubKeyString := base64.StdEncoding.EncodeToString(compressed)
	fmt.Fprint(os.Stdout, "Expected proof record:\n")
	fmt.Fprintf(os.Stdout, "v=MCPv1; k=%s; p=%s\n", k, pubKeyString)
}

// exchangeTokenForRegistry exchanges signature for a registry JWT token
func (c *CryptoProvider) exchangeTokenForRegistry(ctx context.Context, domain, timestamp, signedTimestamp string) (string, error) {
	if c.registryURL == "" {
		return "", fmt.Errorf("registry URL is required for token exchange")
	}

	// Prepare the request body
	payload := map[string]string{
		"domain":           domain,
		"timestamp":        timestamp,
		"signed_timestamp": signedTimestamp,
	}

	jsonData, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("failed to marshal request: %w", err)
	}

	// Make the token exchange request
	exchangeURL := fmt.Sprintf("%s/v0/auth/%s", c.registryURL, c.authMethod)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, exchangeURL, bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, body)
	}

	var tokenResp RegistryTokenResponse
	err = json.Unmarshal(body, &tokenResp)
	if err != nil {
		return "", fmt.Errorf("failed to unmarshal response: %w", err)
	}

	return tokenResp.RegistryToken, nil
}

```

## /cmd/publisher/auth/dns.go

```go path="/cmd/publisher/auth/dns.go" 
package auth

type DNSProvider struct {
	*CryptoProvider
}

// NewDNSProvider creates a new DNS-based auth provider
func NewDNSProvider(registryURL, domain string, signer *Signer) Provider {
	return &DNSProvider{
		CryptoProvider: &CryptoProvider{
			registryURL: registryURL,
			domain:      domain,
			signer:      *signer,
			authMethod:  "dns",
		},
	}
}

// Name returns the name of this auth provider
func (d *DNSProvider) Name() string {
	return "dns"
}

```

## /cmd/publisher/auth/github-at.go

```go path="/cmd/publisher/auth/github-at.go" 
package auth

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

const (
	// GitHub OAuth URLs
	GitHubDeviceCodeURL  = "https://github.com/login/device/code"        // #nosec:G101
	GitHubAccessTokenURL = "https://github.com/login/oauth/access_token" // #nosec:G101
)

// DeviceCodeResponse represents the response from GitHub's device code endpoint
type DeviceCodeResponse struct {
	DeviceCode      string `json:"device_code"`
	UserCode        string `json:"user_code"`
	VerificationURI string `json:"verification_uri"`
	ExpiresIn       int    `json:"expires_in"`
	Interval        int    `json:"interval"`
}

// AccessTokenResponse represents the response from GitHub's access token endpoint
type AccessTokenResponse struct {
	AccessToken string `json:"access_token"`
	TokenType   string `json:"token_type"`
	Scope       string `json:"scope"`
	Error       string `json:"error,omitempty"`
}

// RegistryTokenResponse represents the response from registry's token exchange endpoint
type RegistryTokenResponse struct {
	RegistryToken string `json:"registry_token"`
	ExpiresAt     int64  `json:"expires_at"`
}

// GitHubATProvider implements the Provider interface using GitHub's device flow
type GitHubATProvider struct {
	clientID      string
	registryURL   string
	providedToken string // Token provided via --token flag or MCP_GITHUB_TOKEN env var
	githubToken   string // In-memory GitHub token set by Login()
}

// ServerHealthResponse represents the response from the health endpoint
type ServerHealthResponse struct {
	Status         string `json:"status"`
	GitHubClientID string `json:"github_client_id"`
}

// NewGitHubATProvider creates a new GitHub OAuth provider
func NewGitHubATProvider(registryURL, token string) Provider {
	// Check for token from flag or environment variable
	if token == "" {
		token = os.Getenv("MCP_GITHUB_TOKEN")
	}

	return &GitHubATProvider{
		registryURL:   registryURL,
		providedToken: token,
	}
}

// GetToken retrieves the registry JWT token (exchanges GitHub token if needed)
func (g *GitHubATProvider) GetToken(ctx context.Context) (string, error) {
	if g.githubToken == "" {
		return "", fmt.Errorf("no GitHub token available; run Login() first")
	}

	// Exchange GitHub token for registry token
	registryToken, _, err := g.exchangeTokenForRegistry(ctx, g.githubToken)
	// Clear the GitHub token from memory after exchange
	g.githubToken = ""
	if err != nil {
		return "", fmt.Errorf("failed to exchange token: %w", err)
	}

	return registryToken, nil
}

// Login performs the GitHub device flow authentication
func (g *GitHubATProvider) Login(ctx context.Context) error {
	// If a token was provided via --token or MCP_GITHUB_TOKEN, store it in memory and skip device flow
	if g.providedToken != "" {
		g.githubToken = g.providedToken
		return nil
	}

	// If clientID is not set, try to retrieve it from the server's health endpoint
	if g.clientID == "" {
		clientID, err := getClientID(ctx, g.registryURL)
		if err != nil {
			return fmt.Errorf("error getting GitHub Client ID: %w", err)
		}
		g.clientID = clientID
	}

	// Device flow login logic using GitHub's device flow
	// First, request a device code
	deviceCode, userCode, verificationURI, err := g.requestDeviceCode(ctx)
	if err != nil {
		return fmt.Errorf("error requesting device code: %w", err)
	}

	// Display instructions to the user
	_, _ = fmt.Fprintln(os.Stdout, "\nTo authenticate, please:")
	_, _ = fmt.Fprintln(os.Stdout, "1. Go to:", verificationURI)
	_, _ = fmt.Fprintln(os.Stdout, "2. Enter code:", userCode)
	_, _ = fmt.Fprintln(os.Stdout, "3. Authorize this application")

	// Poll for the token
	_, _ = fmt.Fprintln(os.Stdout, "Waiting for authorization...")
	token, err := g.pollForToken(ctx, deviceCode)
	if err != nil {
		return fmt.Errorf("error polling for token: %w", err)
	}

	// Store the token in memory
	g.githubToken = token

	_, _ = fmt.Fprintln(os.Stdout, "Successfully authenticated!")
	return nil
}

// Name returns the name of this auth provider
func (g *GitHubATProvider) Name() string {
	return "github"
}

// requestDeviceCode initiates the device authorization flow
func (g *GitHubATProvider) requestDeviceCode(ctx context.Context) (string, string, string, error) {
	if g.clientID == "" {
		return "", "", "", fmt.Errorf("GitHub Client ID is required for device flow login")
	}

	payload := map[string]string{
		"client_id": g.clientID,
		"scope":     "read:org read:user",
	}

	jsonData, err := json.Marshal(payload)
	if err != nil {
		return "", "", "", err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, GitHubDeviceCodeURL, bytes.NewBuffer(jsonData))
	if err != nil {
		return "", "", "", err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", "", "", err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", "", "", err
	}

	if resp.StatusCode != http.StatusOK {
		return "", "", "", fmt.Errorf("request device code failed: %s", body)
	}

	var deviceCodeResp DeviceCodeResponse
	err = json.Unmarshal(body, &deviceCodeResp)
	if err != nil {
		return "", "", "", err
	}

	return deviceCodeResp.DeviceCode, deviceCodeResp.UserCode, deviceCodeResp.VerificationURI, nil
}

// pollForToken polls for access token after user completes authorization
func (g *GitHubATProvider) pollForToken(ctx context.Context, deviceCode string) (string, error) {
	if g.clientID == "" {
		return "", fmt.Errorf("GitHub Client ID is required for device flow login")
	}

	payload := map[string]string{
		"client_id":   g.clientID,
		"device_code": deviceCode,
		"grant_type":  "urn:ietf:params:oauth:grant-type:device_code",
	}

	jsonData, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}

	// Default polling interval and expiration time
	interval := 5    // seconds
	expiresIn := 900 // 15 minutes
	deadline := time.Now().Add(time.Duration(expiresIn) * time.Second)

	for time.Now().Before(deadline) {
		req, err := http.NewRequestWithContext(ctx, http.MethodPost, GitHubAccessTokenURL, bytes.NewBuffer(jsonData))
		if err != nil {
			return "", err
		}
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Accept", "application/json")

		client := &http.Client{}
		resp, err := client.Do(req)
		if err != nil {
			return "", err
		}

		body, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			return "", err
		}

		var tokenResp AccessTokenResponse
		err = json.Unmarshal(body, &tokenResp)
		if err != nil {
			return "", err
		}

		if tokenResp.Error == "authorization_pending" {
			// User hasn't authorized yet, wait and retry
			time.Sleep(time.Duration(interval) * time.Second)
			continue
		}

		if tokenResp.Error != "" {
			return "", fmt.Errorf("token request failed: %s", tokenResp.Error)
		}

		if tokenResp.AccessToken != "" {
			return tokenResp.AccessToken, nil
		}

		// If we reach here, something unexpected happened
		return "", fmt.Errorf("failed to obtain access token")
	}

	return "", fmt.Errorf("device code authorization timed out")
}

func getClientID(ctx context.Context, registryURL string) (string, error) {
	// This function should retrieve the GitHub Client ID from the registry URL
	// For now, we will return a placeholder value
	// In a real implementation, this would likely involve querying the registry or configuration
	if registryURL == "" {
		return "", fmt.Errorf("registry URL is required to get GitHub Client ID")
	}
	// get the clientID from the server's health endpoint
	healthURL := registryURL + "/v0/health"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
	if err != nil {
		return "", err
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return "", fmt.Errorf("health endpoint returned status %d: %s", resp.StatusCode, body)
	}

	var healthResponse ServerHealthResponse
	err = json.NewDecoder(resp.Body).Decode(&healthResponse)
	if err != nil {
		return "", err
	}
	if healthResponse.GitHubClientID == "" {
		return "", fmt.Errorf("GitHub Client ID is not set in the server's health response")
	}

	githubClientID := healthResponse.GitHubClientID

	return githubClientID, nil
}

// exchangeTokenForRegistry exchanges a GitHub token for a registry JWT token
func (g *GitHubATProvider) exchangeTokenForRegistry(ctx context.Context, githubToken string) (string, int64, error) {
	if g.registryURL == "" {
		return "", 0, fmt.Errorf("registry URL is required for token exchange")
	}

	// Prepare the request body
	payload := map[string]string{
		"github_token": githubToken,
	}

	jsonData, err := json.Marshal(payload)
	if err != nil {
		return "", 0, fmt.Errorf("failed to marshal request: %w", err)
	}

	// Make the token exchange request
	exchangeURL := g.registryURL + "/v0/auth/github-at"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, exchangeURL, bytes.NewBuffer(jsonData))
	if err != nil {
		return "", 0, fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", 0, fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", 0, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return "", 0, fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, body)
	}

	var tokenResp RegistryTokenResponse
	err = json.Unmarshal(body, &tokenResp)
	if err != nil {
		return "", 0, fmt.Errorf("failed to unmarshal response: %w", err)
	}

	return tokenResp.RegistryToken, tokenResp.ExpiresAt, nil
}

```

## /cmd/publisher/auth/github-oidc.go

```go path="/cmd/publisher/auth/github-oidc.go" 
package auth

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
)

type GitHubOIDCProvider struct {
	registryURL string
}

// NewGitHubOIDCProvider creates a new GitHub OIDC provider
func NewGitHubOIDCProvider(registryURL string) Provider {
	return &GitHubOIDCProvider{
		registryURL: registryURL,
	}
}

// GetToken retrieves the registry JWT token using GitHub Actions OIDC token
func (o *GitHubOIDCProvider) GetToken(ctx context.Context) (string, error) {
	audience, err := audienceFromRegistryURL(o.registryURL)
	if err != nil {
		return "", fmt.Errorf("invalid --registry URL: %w", err)
	}

	// Get OIDC token from GitHub Actions endpoint
	oidcToken, err := o.getOIDCTokenFromGitHub(ctx, audience)
	if err != nil {
		return "", fmt.Errorf("failed to get OIDC token from GitHub: %w", err)
	}

	// Exchange OIDC token for registry token
	registryToken, err := o.exchangeOIDCTokenForRegistry(ctx, oidcToken)
	if err != nil {
		return "", fmt.Errorf("failed to exchange OIDC token: %w", err)
	}

	return registryToken, nil
}

// Login is not needed for OIDC since tokens are provided by GitHub Actions
func (o *GitHubOIDCProvider) Login(_ context.Context) error {
	// No interactive login needed for OIDC
	return nil
}

// Name returns the name of this auth provider
func (o *GitHubOIDCProvider) Name() string {
	return "github-oidc"
}

// exchangeOIDCTokenForRegistry exchanges a GitHub OIDC token for a registry JWT token
func (o *GitHubOIDCProvider) exchangeOIDCTokenForRegistry(ctx context.Context, oidcToken string) (string, error) {
	if o.registryURL == "" {
		return "", fmt.Errorf("registry URL is required for token exchange")
	}

	// Prepare the request body
	payload := map[string]string{
		"oidc_token": oidcToken,
	}

	jsonData, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("failed to marshal request: %w", err)
	}

	// Make the token exchange request
	exchangeURL := o.registryURL + "/v0/auth/github-oidc"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, exchangeURL, bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, body)
	}

	var tokenResp RegistryTokenResponse
	err = json.Unmarshal(body, &tokenResp)
	if err != nil {
		return "", fmt.Errorf("failed to unmarshal response: %w", err)
	}

	return tokenResp.RegistryToken, nil
}

// getOIDCTokenFromGitHub fetches the OIDC token from GitHub Actions endpoint.
func (o *GitHubOIDCProvider) getOIDCTokenFromGitHub(ctx context.Context, audience string) (string, error) {
	// Check for required environment variables
	requestToken := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN")
	if requestToken == "" {
		return "", fmt.Errorf("ACTIONS_ID_TOKEN_REQUEST_TOKEN environment variable not found - are you running in GitHub Actions with id-token: write permissions?")
	}

	requestURL := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL")
	if requestURL == "" {
		return "", fmt.Errorf("ACTIONS_ID_TOKEN_REQUEST_URL environment variable not found - are you running in GitHub Actions with id-token: write permissions?")
	}

	// Build the full URL with audience parameter
	fullURL := requestURL + "&audience=" + url.QueryEscape(audience)

	// Create the request
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) //nolint:gosec // G704: URL is from GitHub Actions ACTIONS_ID_TOKEN_REQUEST_URL env var
	if err != nil {
		return "", fmt.Errorf("failed to create request: %w", err)
	}

	// Set the authorization header
	req.Header.Set("Authorization", "Bearer "+requestToken)
	req.Header.Set("Accept", "application/json")

	// Make the request
	client := &http.Client{}
	resp, err := client.Do(req) //nolint:gosec // G704: URL is from GitHub Actions env var
	if err != nil {
		return "", fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()

	// Read the response
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("GitHub OIDC token request failed with status %d: %s", resp.StatusCode, body)
	}

	// Parse the response to extract the token value
	var tokenResp struct {
		Value string `json:"value"`
	}
	err = json.Unmarshal(body, &tokenResp)
	if err != nil {
		return "", fmt.Errorf("failed to unmarshal OIDC token response: %w", err)
	}

	if tokenResp.Value == "" {
		return "", fmt.Errorf("OIDC token value is empty in response")
	}

	return tokenResp.Value, nil
}

// audienceFromRegistryURL returns scheme + lowercased host for the given URL.
func audienceFromRegistryURL(registryURL string) (string, error) {
	registryURL = strings.TrimSpace(registryURL)
	if registryURL == "" {
		return "", fmt.Errorf("registry URL is empty")
	}
	u, err := url.Parse(registryURL)
	if err != nil {
		return "", fmt.Errorf("parse %q: %w", registryURL, err)
	}
	if u.Scheme == "" || u.Host == "" {
		return "", fmt.Errorf("registry URL must include scheme and host: %q", registryURL)
	}
	return u.Scheme + "://" + strings.ToLower(u.Host), nil
}

```

## /cmd/publisher/auth/github-oidc_internal_test.go

```go path="/cmd/publisher/auth/github-oidc_internal_test.go" 
package auth

import "testing"

func TestAudienceFromRegistryURL(t *testing.T) {
	tests := []struct {
		input   string
		want    string
		wantErr bool
	}{
		// Canonical forms
		{"https://registry.modelcontextprotocol.io", "https://registry.modelcontextprotocol.io", false},
		{"https://staging.registry.modelcontextprotocol.io", "https://staging.registry.modelcontextprotocol.io", false},

		// Trailing slash and path are stripped
		{"https://registry.modelcontextprotocol.io/", "https://registry.modelcontextprotocol.io", false},
		{"https://registry.modelcontextprotocol.io/api/v0", "https://registry.modelcontextprotocol.io", false},

		// Host is lowercased
		{"https://Registry.Example.COM", "https://registry.example.com", false},

		// Whitespace is tolerated
		{"  https://registry.example  ", "https://registry.example", false},

		// Invalid inputs
		{"", "", true},
		{"registry.example", "", true}, // missing scheme
		{"https://", "", true},         // missing host
		{"://nothing", "", true},       // missing scheme
	}
	for _, tc := range tests {
		t.Run(tc.input, func(t *testing.T) {
			got, err := audienceFromRegistryURL(tc.input)
			if tc.wantErr {
				if err == nil {
					t.Fatalf("expected error for %q, got %q", tc.input, got)
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error for %q: %v", tc.input, err)
			}
			if got != tc.want {
				t.Errorf("audienceFromRegistryURL(%q) = %q, want %q", tc.input, got, tc.want)
			}
		})
	}
}

```

## /cmd/publisher/auth/github_at_test.go

```go path="/cmd/publisher/auth/github_at_test.go" 
package auth_test

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/modelcontextprotocol/registry/cmd/publisher/auth"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewGitHubATProvider_Name(t *testing.T) {
	p := auth.NewGitHubATProvider("https://registry.example.com", "")
	assert.Equal(t, "github", p.Name())
}

func TestNewGitHubATProvider_WithEnvToken(t *testing.T) {
	t.Setenv("MCP_GITHUB_TOKEN", "env-token")

	registry := newMockExchangeServer(t, "env-token")

	p := auth.NewGitHubATProvider(registry.URL, "")

	// Login should use the env var token
	err := p.Login(context.Background())
	require.NoError(t, err)

	// GetToken should exchange it successfully
	token, err := p.GetToken(context.Background())
	require.NoError(t, err)
	assert.Equal(t, "registry-jwt", token)
}

func TestNewGitHubATProvider_ExplicitTokenTakesPrecedence(t *testing.T) {
	t.Setenv("MCP_GITHUB_TOKEN", "env-token")

	registry := newMockExchangeServer(t, "explicit-token")

	p := auth.NewGitHubATProvider(registry.URL, "explicit-token")

	err := p.Login(context.Background())
	require.NoError(t, err)

	token, err := p.GetToken(context.Background())
	require.NoError(t, err)
	assert.Equal(t, "registry-jwt", token)
}

func TestGitHubATProvider_LoginWithProvidedToken(t *testing.T) {
	registry := newMockExchangeServer(t, "my-token")

	p := auth.NewGitHubATProvider(registry.URL, "my-token")

	err := p.Login(context.Background())
	require.NoError(t, err)

	// Verify the token exchange works (proves Login stored the token)
	token, err := p.GetToken(context.Background())
	require.NoError(t, err)
	assert.Equal(t, "registry-jwt", token)
}

func TestGitHubATProvider_LoginDoesNotWriteFiles(t *testing.T) {
	cwd, err := os.Getwd()
	require.NoError(t, err)

	p := auth.NewGitHubATProvider("https://registry.example.com", "my-token")

	err = p.Login(context.Background())
	require.NoError(t, err)

	// Verify no .mcpregistry_* files were created in cwd
	for _, name := range []string{".mcpregistry_github_token", ".mcpregistry_registry_token"} {
		_, statErr := os.Stat(filepath.Join(cwd, name))
		assert.True(t, os.IsNotExist(statErr), "Login should not create %s in cwd", name)
	}
}

func TestGitHubATProvider_GetTokenWithoutLogin(t *testing.T) {
	p := auth.NewGitHubATProvider("https://registry.example.com", "my-token")

	// GetToken without Login should fail
	_, err := p.GetToken(context.Background())
	require.Error(t, err)
	assert.Contains(t, err.Error(), "no GitHub token available")
}

func TestGitHubATProvider_GetTokenClearsTokenAfterExchange(t *testing.T) {
	registry := newMockExchangeServer(t, "my-token")

	p := auth.NewGitHubATProvider(registry.URL, "my-token")

	err := p.Login(context.Background())
	require.NoError(t, err)

	// First GetToken should succeed
	token, err := p.GetToken(context.Background())
	require.NoError(t, err)
	assert.Equal(t, "registry-jwt", token)

	// Second GetToken should fail (GitHub token was cleared after exchange)
	_, err = p.GetToken(context.Background())
	require.Error(t, err)
	assert.Contains(t, err.Error(), "no GitHub token available")
}

func TestGitHubATProvider_GetTokenClearsTokenOnError(t *testing.T) {
	// Mock registry that always returns an error
	registry := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		http.Error(w, "server error", http.StatusInternalServerError)
	}))
	t.Cleanup(registry.Close)

	p := auth.NewGitHubATProvider(registry.URL, "my-token")

	err := p.Login(context.Background())
	require.NoError(t, err)

	// GetToken should fail
	_, err = p.GetToken(context.Background())
	require.Error(t, err)

	// Token should be cleared — second call also fails with "no GitHub token"
	_, err = p.GetToken(context.Background())
	require.Error(t, err)
	assert.Contains(t, err.Error(), "no GitHub token available")
}

func TestGitHubATProvider_GetTokenExchangeFailure_NoURL(t *testing.T) {
	p := auth.NewGitHubATProvider("", "my-token")

	err := p.Login(context.Background())
	require.NoError(t, err)

	_, err = p.GetToken(context.Background())
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to exchange token")
}

func TestGitHubATProvider_LoginDeviceFlow_FetchesClientID(t *testing.T) {
	// Mock registry health endpoint
	registry := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/v0/health" {
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(map[string]string{
				"status":           "ok",
				"github_client_id": "test-client-id",
			})
			return
		}
		http.Error(w, "not found", http.StatusNotFound)
	}))
	t.Cleanup(registry.Close)

	p := auth.NewGitHubATProvider(registry.URL, "")

	// Login without a provided token triggers device flow, which calls
	// the real GitHub device code URL. This will fail, but only after
	// fetching the client ID from our mock health endpoint.
	err := p.Login(context.Background())
	require.Error(t, err)
	assert.Contains(t, err.Error(), "error requesting device code")
}

func TestGitHubATProvider_ImplementsProviderInterface(_ *testing.T) {
	// Compile-time check
	var _ = auth.NewGitHubATProvider("https://example.com", "token")
}

// newMockExchangeServer creates an httptest.Server that mocks the registry's
// token exchange endpoint. It accepts a specific GitHub token and returns
// "registry-jwt" as the registry token.
func newMockExchangeServer(t *testing.T, expectedGitHubToken string) *httptest.Server {
	t.Helper()
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/v0/auth/github-at" && r.Method == http.MethodPost {
			var body map[string]string
			if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
				http.Error(w, "bad request", http.StatusBadRequest)
				return
			}
			if body["github_token"] == expectedGitHubToken {
				w.Header().Set("Content-Type", "application/json")
				resp := map[string]interface{}{
					"registry_token": "registry-jwt",
					"expires_at":     9999999999,
				}
				_ = json.NewEncoder(w).Encode(resp)
				return
			}
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		http.Error(w, "not found", http.StatusNotFound)
	}))
	t.Cleanup(server.Close)
	return server
}

```

## /cmd/publisher/auth/googlekms/common.go

```go path="/cmd/publisher/auth/googlekms/common.go" 
package googlekms

import (
	"context"
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/elliptic"
	"crypto/sha512"
	"crypto/x509"
	"encoding/asn1"
	"encoding/pem"
	"errors"
	"fmt"
	"math/big"
	"os"

	kms "cloud.google.com/go/kms/apiv1"
	"cloud.google.com/go/kms/apiv1/kmspb"
	"github.com/modelcontextprotocol/registry/cmd/publisher/auth"
)

// GetSignatureProvider validates inputs and returns a GoogleKMSSigner implementing auth.Signer.
func GetSignatureProvider(resourceName string) (auth.Signer, error) {
	if resourceName == "" {
		return nil, fmt.Errorf("--resource is required, e.g. projects/my-project/locations/global/keyRings/fellowship/cryptoKeys/bilbo/cryptoKeyVersions/1")
	}

	return &Signer{
		resource: resourceName,
	}, nil
}

type Signer struct {
	resource string
}

type ecdsaSignature struct {
	R, S *big.Int
}

func derToRS(der []byte, curve elliptic.Curve) ([]byte, error) {
	var sig ecdsaSignature
	if _, err := asn1.Unmarshal(der, &sig); err != nil {
		return nil, fmt.Errorf("invalid DER ECDSA signature: %w", err)
	}
	if sig.R == nil || sig.S == nil || sig.R.Sign() <= 0 || sig.S.Sign() <= 0 {
		return nil, fmt.Errorf("invalid ECDSA signature components")
	}

	size := (curve.Params().BitSize + 7) / 8
	rBytes := sig.R.Bytes()
	sBytes := sig.S.Bytes()
	if len(rBytes) > size || len(sBytes) > size {
		return nil, fmt.Errorf("ECDSA signature component too large")
	}

	out := make([]byte, 2*size)
	copy(out[size-len(rBytes):size], rBytes)
	copy(out[2*size-len(sBytes):], sBytes)
	return out, nil
}

func (g *Signer) GetSignedTimestamp(ctx context.Context) (*string, []byte, error) {
	client, err := kms.NewKeyManagementClient(ctx)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create KMS client: %w", err)
	}
	defer client.Close()

	// Fetch public key (PEM) so we can output expected proof record.
	algo, err := g.showPublicKeyAndGetAlgorithm(ctx, client)
	if err != nil {
		return nil, nil, err
	}

	timestamp := auth.GetTimestamp()

	fmt.Fprintln(os.Stdout, "Executing the sign request...")
	switch algo {
	case auth.AlgorithmEd25519:
		signReq := &kmspb.AsymmetricSignRequest{
			Name: g.resource,
			Data: []byte(timestamp),
		}
		signResp, err := client.AsymmetricSign(ctx, signReq)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to sign with KMS: %w", err)
		}

		return &timestamp, signResp.Signature, nil
	case auth.AlgorithmECDSAP384:
		digest := sha512.Sum384([]byte(timestamp))
		signReq := &kmspb.AsymmetricSignRequest{
			Name:   g.resource,
			Digest: &kmspb.Digest{Digest: &kmspb.Digest_Sha384{Sha384: digest[:]}},
		}
		signResp, err := client.AsymmetricSign(ctx, signReq)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to sign with KMS: %w", err)
		}

		sigBytes, err := derToRS(signResp.Signature, elliptic.P384())
		if err != nil {
			return nil, nil, fmt.Errorf("failed to convert DER signature: %w", err)
		}

		return &timestamp, sigBytes, nil
	}

	return nil, nil, fmt.Errorf("unsupported algorithm: %s", algo)
}

func (g *Signer) showPublicKeyAndGetAlgorithm(ctx context.Context, client *kms.KeyManagementClient) (auth.CryptoAlgorithm, error) {
	pubResp, err := client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: g.resource})
	if err != nil {
		return "", fmt.Errorf("failed to get public key: %w", err)
	}

	block, _ := pem.Decode([]byte(pubResp.Pem))
	if block == nil {
		return "", errors.New("failed to decode PEM public key from KMS")
	}

	parsed, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return "", fmt.Errorf("failed to parse public key: %w", err)
	}

	switch pubResp.Algorithm { //nolint:exhaustive
	case kmspb.CryptoKeyVersion_EC_SIGN_ED25519:
		pk, ok := parsed.(ed25519.PublicKey)
		if !ok {
			return "", errors.New("KMS reported ED25519 but parsed key is different type")
		}
		auth.PrintEd25519KeyInfo(pk)
		return auth.AlgorithmEd25519, nil
	case kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384:
		pk, ok := parsed.(*ecdsa.PublicKey)
		if !ok || pk.Curve != elliptic.P384() {
			return "", errors.New("KMS reported P-384 but parsed key mismatch")
		}
		auth.PrintEcdsaP384KeyInfo(*pk)
		return auth.AlgorithmECDSAP384, nil
	default:
		return "", fmt.Errorf("unsupported KMS key algorithm: %s", pubResp.Algorithm.String())
	}
}

```

## /cmd/publisher/auth/http.go

```go path="/cmd/publisher/auth/http.go" 
package auth

type HTTPProvider struct {
	*CryptoProvider
}

// NewHTTPProvider creates a new HTTP-based auth provider
func NewHTTPProvider(registryURL, domain string, signer *Signer) Provider {
	return &HTTPProvider{
		CryptoProvider: &CryptoProvider{
			registryURL: registryURL,
			domain:      domain,
			signer:      *signer,
			authMethod:  "http",
		},
	}
}

// Name returns the name of this auth provider
func (h *HTTPProvider) Name() string {
	return "http"
}

```

## /cmd/publisher/auth/interface.go

```go path="/cmd/publisher/auth/interface.go" 
package auth

import "context"

// Provider defines the interface for authentication mechanisms
type Provider interface {
	// GetToken retrieves or generates an authentication token
	// It returns the token string and any error encountered
	GetToken(ctx context.Context) (string, error)

	// Login performs the authentication flow
	// This might involve user interaction, device flows, etc.
	Login(ctx context.Context) error

	// Name returns the name of the authentication provider
	Name() string
}

```

## /cmd/publisher/auth/none.go

```go path="/cmd/publisher/auth/none.go" 
package auth

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
)

type NoneProvider struct {
	registryURL string
	token       string
}

type TokenResponse struct {
	RegistryToken string `json:"registry_token"`
	ExpiresAt     int64  `json:"expires_at"`
}

func NewNoneProvider(registryURL string) Provider {
	return &NoneProvider{
		registryURL: registryURL,
	}
}

func (p *NoneProvider) GetToken(ctx context.Context) (string, error) {
	if p.token != "" {
		return p.token, nil
	}

	// Get anonymous token from registry
	if !strings.HasSuffix(p.registryURL, "/") {
		p.registryURL += "/"
	}
	tokenURL := p.registryURL + "v0/auth/none"

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil)
	if err != nil {
		return "", fmt.Errorf("error creating request: %w", err)
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("error getting anonymous token: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return "", fmt.Errorf("failed to get anonymous token (status %d): %s", resp.StatusCode, body)
	}

	var tokenResp TokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
		return "", fmt.Errorf("error decoding token response: %w", err)
	}

	p.token = tokenResp.RegistryToken
	return p.token, nil
}

func (p *NoneProvider) Login(_ context.Context) error {
	// No login needed for anonymous auth
	return nil
}

func (p *NoneProvider) Name() string {
	return "none"
}

```

## /cmd/publisher/commands/init.go

```go path="/cmd/publisher/commands/init.go" 
package commands

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
	"github.com/modelcontextprotocol/registry/pkg/model"
)

func InitCommand() error {
	// Check if server.json already exists
	if _, err := os.Stat("server.json"); err == nil {
		return errors.New("server.json already exists")
	}

	// Detect if we're in a subdirectory of the git repository
	subfolder := detectSubfolder()

	// Try to detect values from environment
	name := detectServerName(subfolder)
	description := detectDescription()
	version := getVersionFromPackageJSON()
	if version == "" {
		version = "1.0.0"
	}
	repoURL := detectRepoURL()
	repoSource := MethodGitHub
	if repoURL != "" && !strings.Contains(repoURL, "github.com") {
		if strings.Contains(repoURL, "gitlab.com") {
			repoSource = "gitlab"
		} else {
			repoSource = "git"
		}
	}

	packageType := detectPackageType()
	packageIdentifier := detectPackageIdentifier(name, packageType)

	// Create example environment variables
	envVars := []model.KeyValueInput{
		{
			Name: "YOUR_API_KEY",
			InputWithVariables: model.InputWithVariables{
				Input: model.Input{
					Description: "Your API key for the service",
					IsRequired:  true,
					IsSecret:    true,
					Format:      model.FormatString,
				},
			},
		},
	}

	// Create the server structure
	server := createServerJSON(
		model.CurrentSchemaURL, name, description, version, repoURL, repoSource, subfolder,
		packageType, packageIdentifier, version, envVars,
	)

	// Write to file
	jsonData, err := json.MarshalIndent(server, "", "  ")
	if err != nil {
		return fmt.Errorf("error marshaling JSON: %w", err)
	}

	err = os.WriteFile("server.json", jsonData, 0600)
	if err != nil {
		return fmt.Errorf("error writing file: %w", err)
	}

	_, _ = fmt.Fprintln(os.Stdout, "Created server.json")
	_, _ = fmt.Fprintln(os.Stdout, "\nEdit server.json to update:")
	_, _ = fmt.Fprintln(os.Stdout, "  • Server name and description")
	_, _ = fmt.Fprintln(os.Stdout, "  • Package details")
	_, _ = fmt.Fprintln(os.Stdout, "  • Environment variables")
	_, _ = fmt.Fprintln(os.Stdout, "\nThen publish with:")
	_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher login github  # or your preferred auth method")
	_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher publish")

	return nil
}

func detectSubfolder() string {
	// Get current working directory
	cwd, err := os.Getwd()
	if err != nil {
		return ""
	}

	// Find git repository root
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel")
	cmd.Dir = cwd
	output, err := cmd.Output()
	if err != nil {
		// Not in a git repository
		return ""
	}

	gitRoot := strings.TrimSpace(string(output))

	// Clean the paths to ensure proper comparison
	gitRoot = filepath.Clean(gitRoot)
	cwd = filepath.Clean(cwd)

	// If we're in the root, no subfolder
	if gitRoot == cwd {
		return ""
	}

	// Check if cwd is actually within gitRoot
	if !strings.HasPrefix(cwd, gitRoot) {
		return ""
	}

	// Calculate relative path from git root to current directory
	relPath, err := filepath.Rel(gitRoot, cwd)
	if err != nil {
		return ""
	}

	// Convert to forward slashes for consistency (important for cross-platform)
	return filepath.ToSlash(relPath)
}

// getMcpNameFromPackageJSON returns the `mcpName` field from package.json, or
// "" if not set. mcpName is the authoritative MCP server name when present —
// see docs/modelcontextprotocol-io/quickstart.mdx.
func getMcpNameFromPackageJSON() string {
	data, err := os.ReadFile("package.json")
	if err != nil {
		return ""
	}

	var pkg map[string]any
	if err := json.Unmarshal(data, &pkg); err != nil {
		return ""
	}

	if mcpName, ok := pkg["mcpName"].(string); ok && mcpName != "" {
		return mcpName
	}
	return ""
}

func getNameFromPackageJSON() string {
	data, err := os.ReadFile("package.json")
	if err != nil {
		return ""
	}

	var pkg map[string]any
	if err := json.Unmarshal(data, &pkg); err != nil {
		return ""
	}

	name, ok := pkg["name"].(string)
	if !ok || name == "" {
		return ""
	}

	// Convert npm package name to MCP server name
	// @org/package -> io.github.org/package
	if strings.HasPrefix(name, "@") {
		parts := strings.Split(name[1:], "/")
		if len(parts) == 2 {
			return fmt.Sprintf("io.github.%s/%s", parts[0], parts[1])
		}
	}
	return fmt.Sprintf("io.github.<your-username>/%s", name)
}

func getVersionFromPackageJSON() string {
	data, err := os.ReadFile("package.json")
	if err != nil {
		return ""
	}

	var pkg map[string]any
	if err := json.Unmarshal(data, &pkg); err != nil {
		return ""
	}

	version, ok := pkg["version"].(string)
	if !ok || version == "" {
		return ""
	}

	return version
}

func detectServerName(subfolder string) string {
	// mcpName in package.json is the authoritative server name when set, so it
	// takes precedence over names inferred from the git remote or the npm
	// `name` field.
	if name := getMcpNameFromPackageJSON(); name != "" {
		return name
	}

	// Try to get from git remote
	repoURL := detectRepoURL()
	if repoURL != "" && strings.Contains(repoURL, "github.com") {
		name := buildGitHubServerName(repoURL, subfolder)
		if name != "" {
			return name
		}
	}

	// Try to get from package.json
	name := getNameFromPackageJSON()
	if name != "" {
		return name
	}

	// Use current directory name as fallback
	if cwd, err := os.Getwd(); err == nil {
		return fmt.Sprintf("com.example/%s", filepath.Base(cwd))
	}

	return "com.example/my-mcp-server"
}

func buildGitHubServerName(repoURL, subfolder string) string {
	parts := strings.Split(repoURL, "/")
	if len(parts) < 5 {
		return ""
	}

	owner := parts[3]
	repo := strings.TrimSuffix(parts[4], ".git")

	// If we're in a subdirectory, use the current folder name
	if subfolder != "" {
		folderName := filepath.Base(subfolder)
		return fmt.Sprintf("io.github.%s/%s", owner, folderName)
	}

	return fmt.Sprintf("io.github.%s/%s", owner, repo)
}

func detectDescription() string {
	// Try to get from package.json
	if data, err := os.ReadFile("package.json"); err == nil {
		var pkg map[string]any
		if json.Unmarshal(data, &pkg) == nil {
			if desc, ok := pkg["description"].(string); ok && desc != "" {
				return desc
			}
		}
	}

	return "An MCP server that provides [describe what your server does]"
}

func detectRepoURL() string {
	sanitizeURL := func(url string) string {
		return strings.TrimPrefix(url, "git+")
	}

	// Try git remote
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, "git", "remote", "get-url", "origin")
	if output, err := cmd.Output(); err == nil {
		url := strings.TrimSpace(string(output))
		// Convert SSH URL to HTTPS if needed
		if strings.HasPrefix(url, "git@github.com:") {
			url = strings.Replace(url, "git@github.com:", "https://github.com/", 1)
		}
		url = strings.TrimSuffix(url, ".git")
		return url
	}

	// Try package.json repository field
	if data, err := os.ReadFile("package.json"); err == nil {
		var pkg map[string]any
		if json.Unmarshal(data, &pkg) == nil {
			if repo, ok := pkg["repository"].(map[string]any); ok {
				if url, ok := repo["url"].(string); ok {
					return sanitizeURL(strings.TrimSuffix(url, ".git"))
				}
			}
			if repo, ok := pkg["repository"].(string); ok {
				return sanitizeURL(strings.TrimSuffix(repo, ".git"))
			}
		}
	}

	return ""
}

func detectPackageType() string {
	// Check for package.json
	if _, err := os.Stat("package.json"); err == nil {
		return model.RegistryTypeNPM
	}

	// Check for pyproject.toml or setup.py
	if _, err := os.Stat("pyproject.toml"); err == nil {
		return model.RegistryTypePyPI
	}
	if _, err := os.Stat("setup.py"); err == nil {
		return model.RegistryTypePyPI
	}

	// Check for Dockerfile
	if _, err := os.Stat("Dockerfile"); err == nil {
		return model.RegistryTypeOCI
	}

	// Default to npm as most common
	return model.RegistryTypeNPM
}

func detectPackageIdentifier(serverName string, packageType string) string {
	switch packageType {
	case model.RegistryTypeNPM:
		// Try to get from package.json
		if data, err := os.ReadFile("package.json"); err == nil {
			var pkg map[string]any
			if json.Unmarshal(data, &pkg) == nil {
				if name, ok := pkg["name"].(string); ok && name != "" {
					return name
				}
			}
		}
		// Convert server name to npm package name
		if strings.HasPrefix(serverName, "io.github.") {
			parts := strings.Split(serverName, "/")
			if len(parts) == 2 {
				owner := strings.TrimPrefix(parts[0], "io.github.")
				return fmt.Sprintf("@%s/%s", owner, parts[1])
			}
		}
		return "@your-org/your-package"

	case model.RegistryTypePyPI:
		// Try to get from pyproject.toml or setup.py
		if data, err := os.ReadFile("pyproject.toml"); err == nil {
			// Simple extraction - could be improved with proper TOML parser
			lines := strings.Split(string(data), "\n")
			for _, line := range lines {
				if strings.HasPrefix(line, "name") && strings.Contains(line, "=") {
					parts := strings.Split(line, "=")
					if len(parts) >= 2 {
						name := strings.Trim(parts[1], " \"'")
						if name != "" {
							return name
						}
					}
				}
			}
		}
		return "your-package"

	case model.RegistryTypeOCI:
		// Use a sensible default
		if strings.Contains(serverName, "/") {
			parts := strings.Split(serverName, "/")
			return parts[len(parts)-1]
		}
		return "your-image"

	default:
		return "your-package"
	}
}

func createServerJSON(
	currentSchema, name, description, version, repoURL, repoSource, subfolder,
	packageType, packageIdentifier, packageVersion string,
	envVars []model.KeyValueInput,
) apiv0.ServerJSON {
	// Create package based on type
	var pkg model.Package

	switch packageType {
	case model.RegistryTypeNPM:
		pkg = model.Package{
			RegistryType:         model.RegistryTypeNPM,
			Identifier:           packageIdentifier,
			Version:              packageVersion,
			EnvironmentVariables: envVars,
			Transport: model.Transport{
				Type: model.TransportTypeStdio,
			},
		}
	case model.RegistryTypePyPI:
		pkg = model.Package{
			RegistryType:         model.RegistryTypePyPI,
			Identifier:           packageIdentifier,
			Version:              packageVersion,
			EnvironmentVariables: envVars,
			Transport: model.Transport{
				Type: model.TransportTypeStdio,
			},
		}
	case model.RegistryTypeOCI:
		// OCI packages use canonical references: registry/namespace/image:tag
		// Format: docker.io/username/image:version
		canonicalRef := fmt.Sprintf("docker.io/%s:%s", packageIdentifier, packageVersion)
		pkg = model.Package{
			RegistryType: model.RegistryTypeOCI,
			Identifier:   canonicalRef,
			// No Version field for OCI - it's embedded in the canonical reference
			EnvironmentVariables: envVars,
			Transport: model.Transport{
				Type: model.TransportTypeStdio,
			},
		}
	case "url":
		pkg = model.Package{
			RegistryType:         "url",
			Identifier:           packageIdentifier,
			Version:              packageVersion,
			EnvironmentVariables: envVars,
			Transport: model.Transport{
				Type: model.TransportTypeStdio,
			},
		}
	default:
		pkg = model.Package{
			RegistryType:         packageType,
			Identifier:           packageIdentifier,
			Version:              packageVersion,
			EnvironmentVariables: envVars,
			Transport: model.Transport{
				Type: model.TransportTypeStdio,
			},
		}
	}

	// Create repository with optional subfolder
	var repo *model.Repository
	if repoURL != "" && repoSource != "" {
		repo = &model.Repository{
			URL:    repoURL,
			Source: repoSource,
		}

		// Only set subfolder if we're actually in a subdirectory
		if subfolder != "" {
			repo.Subfolder = subfolder
		}
	}

	// Create server structure
	return apiv0.ServerJSON{
		Schema:      currentSchema,
		Name:        name,
		Description: description,
		Repository:  repo,
		Version:     version,
		Packages:    []model.Package{pkg},
	}
}

```

## /cmd/publisher/commands/init_test.go

```go path="/cmd/publisher/commands/init_test.go" 
package commands_test

import (
	"encoding/json"
	"os"
	"path/filepath"
	"testing"

	"github.com/modelcontextprotocol/registry/cmd/publisher/commands"
	apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestInitCommand_PackageJSON(t *testing.T) {
	tests := []struct {
		name            string
		pkgJSON         string
		expectedName    string
		expectedVersion string
	}{
		{
			name:            "mcpName is returned as-is without transformation",
			pkgJSON:         `{"name": "@acme/weather", "mcpName": "io.github.acme/weather-mcp", "version": "2.3.4", "repository": "https://gitlab.com/acme/weather"}`,
			expectedName:    "io.github.acme/weather-mcp",
			expectedVersion: "2.3.4",
		},
		{
			name:            "empty mcpName falls back to scoped name transformation",
			pkgJSON:         `{"name": "@acme/weather", "mcpName": "", "version": "1.2.3", "repository": "https://gitlab.com/acme/weather"}`,
			expectedName:    "io.github.acme/weather",
			expectedVersion: "1.2.3",
		},
		{
			name:            "missing mcpName falls back to scoped name transformation",
			pkgJSON:         `{"name": "@acme/weather", "version": "1.2.3", "repository": "https://gitlab.com/acme/weather"}`,
			expectedName:    "io.github.acme/weather",
			expectedVersion: "1.2.3",
		},
		{
			name:            "missing version falls back to 1.0.0",
			pkgJSON:         `{"name": "@acme/weather", "mcpName": "io.github.acme/weather", "repository": "https://gitlab.com/acme/weather"}`,
			expectedName:    "io.github.acme/weather",
			expectedVersion: "1.0.0",
		},
		{
			name:            "mcpName takes precedence over GitHub repository URL",
			pkgJSON:         `{"name": "weather", "mcpName": "io.github.acme/weather-mcp", "version": "1.0.0", "repository": "https://github.com/someone-else/some-repo"}`,
			expectedName:    "io.github.acme/weather-mcp",
			expectedVersion: "1.0.0",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			dir := withIsolatedPackageJSON(t, tt.pkgJSON)

			require.NoError(t, commands.InitCommand())

			data, err := os.ReadFile(filepath.Join(dir, "server.json"))
			require.NoError(t, err)

			var got apiv0.ServerJSON
			require.NoError(t, json.Unmarshal(data, &got))

			assert.Equal(t, tt.expectedName, got.Name)
			assert.Equal(t, tt.expectedVersion, got.Version)
			require.Len(t, got.Packages, 1)
			assert.Equal(t, tt.expectedVersion, got.Packages[0].Version,
				"package version should match server version")
		})
	}
}

// withIsolatedPackageJSON creates a temp dir outside any git repository,
// writes package.json with the given contents, chdirs into it, and returns
// the directory path. The HOME override prevents init from detecting the
// host repo's git state.
func withIsolatedPackageJSON(t *testing.T, contents string) string {
	t.Helper()

	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(contents), 0600))

	originalDir, err := os.Getwd()
	require.NoError(t, err)
	t.Cleanup(func() { _ = os.Chdir(originalDir) })

	require.NoError(t, os.Chdir(dir))

	return dir
}

```

## /cmd/publisher/commands/login.go

```go path="/cmd/publisher/commands/login.go" 
package commands

import (
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/modelcontextprotocol/registry/cmd/publisher/auth"
	"github.com/modelcontextprotocol/registry/cmd/publisher/auth/azurekeyvault"
	"github.com/modelcontextprotocol/registry/cmd/publisher/auth/googlekms"
)

const (
	DefaultRegistryURL  = "https://registry.modelcontextprotocol.io"
	LegacyTokenFileName = ".mcp_publisher_token" //nolint:gosec // Not a credential, just a filename
	MethodGitHub        = "github"
	MethodGitHubOIDC    = "github-oidc"
	MethodDNS           = "dns"
	MethodHTTP          = "http"
	MethodNone          = "none"
)

// tokenFilePath returns the path to the token file in the user's config directory
// (~/.config/mcp-publisher/token.json). It does not create the directory.
func tokenFilePath() (string, error) {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return "", fmt.Errorf("failed to get home directory: %w", err)
	}
	return filepath.Join(homeDir, ".config", "mcp-publisher", "token.json"), nil
}

// notAuthenticatedError returns an error guiding the user to log in,
// with a hint about the token location change for users upgrading.
func notAuthenticatedError() error {
	_, _ = fmt.Fprintln(os.Stderr, "hint: token storage moved to ~/.config/mcp-publisher/. If you recently upgraded, please re-login.")
	return errors.New("not authenticated, run 'mcp-publisher login <method>' first")
}

// ensureTokenDir creates the token directory (~/.config/mcp-publisher/) if needed.
func ensureTokenDir() error {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return fmt.Errorf("failed to get home directory: %w", err)
	}
	dir := filepath.Join(homeDir, ".config", "mcp-publisher")
	if err := os.MkdirAll(dir, 0700); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}
	return nil
}

type CryptoAlgorithm auth.CryptoAlgorithm

type SignerType string

type LoginFlags struct {
	Domain          string
	PrivateKey      string
	RegistryURL     string
	KvVault         string
	KvKeyName       string
	KmsResource     string
	Token           Token
	CryptoAlgorithm CryptoAlgorithm
	SignerType      SignerType
	ArgOffset       int
}

const (
	InProcessSignerType     SignerType = "in-process"
	AzureKeyVaultSignerType SignerType = "azure-key-vault"
	GoogleKMSSignerType     SignerType = "google-kms"
	NoSignerType            SignerType = "none"
)

func (c *CryptoAlgorithm) String() string {
	return string(*c)
}

func (c *CryptoAlgorithm) Set(v string) error {
	switch v {
	case string(auth.AlgorithmEd25519), string(auth.AlgorithmECDSAP384):
		*c = CryptoAlgorithm(v)
		return nil
	}
	return fmt.Errorf("invalid algorithm: %q (allowed: ed25519, ecdsap384)", v)
}

type Token string

func parseLoginFlags(method string, args []string) (LoginFlags, error) {
	var flags LoginFlags
	loginFlags := flag.NewFlagSet("login", flag.ExitOnError)
	flags.CryptoAlgorithm = CryptoAlgorithm(auth.AlgorithmEd25519)
	flags.SignerType = NoSignerType
	flags.ArgOffset = 1
	loginFlags.StringVar(&flags.RegistryURL, "registry", DefaultRegistryURL, "Registry URL")

	// Add --token flag for GitHub authentication
	var token string
	if method == MethodGitHub {
		loginFlags.StringVar(&token, "token", "", "GitHub Personal Access Token")
	}

	if method == "dns" || method == "http" {
		loginFlags.StringVar(&flags.Domain, "domain", "", "Domain name")
		if len(args) > 1 {
			switch args[1] {
			case string(AzureKeyVaultSignerType):
				flags.SignerType = AzureKeyVaultSignerType
				loginFlags.StringVar(&flags.KvVault, "vault", "", "The name of the Azure Key Vault resource")
				loginFlags.StringVar(&flags.KvKeyName, "key", "", "Name of the signing key in the Azure Key Vault")
				flags.ArgOffset = 2
			case string(GoogleKMSSignerType):
				flags.SignerType = GoogleKMSSignerType
				loginFlags.StringVar(&flags.KmsResource, "resource", "", "Google Cloud KMS resource name (e.g. projects/lotr/locations/global/keyRings/fellowship/cryptoKeys/frodo/cryptoKeyVersions/1)")
				flags.ArgOffset = 2
			}
		}
		if flags.SignerType == NoSignerType {
			flags.SignerType = InProcessSignerType
			loginFlags.StringVar(&flags.PrivateKey, "private-key", "", "Private key (hex)")
			loginFlags.Var(&flags.CryptoAlgorithm, "algorithm", "Cryptographic algorithm (ed25519, ecdsap384)")
		}
	}
	err := loginFlags.Parse(args[flags.ArgOffset:])
	if err == nil {
		flags.RegistryURL = strings.TrimRight(flags.RegistryURL, "/")
	}

	// Store the token in flags if it was provided
	if method == MethodGitHub {
		flags.Token = Token(token)
	}

	return flags, err
}

func createSigner(flags LoginFlags) (auth.Signer, error) {
	switch flags.SignerType {
	case AzureKeyVaultSignerType:
		return azurekeyvault.GetSignatureProvider(flags.KvVault, flags.KvKeyName)
	case GoogleKMSSignerType:
		return googlekms.GetSignatureProvider(flags.KmsResource)
	case InProcessSignerType:
		return auth.NewInProcessSigner(flags.PrivateKey, auth.CryptoAlgorithm(flags.CryptoAlgorithm))
	case NoSignerType:
		return nil, errors.New("no signing provider specified")
	default:
		return nil, errors.New("unknown signing provider specified")
	}
}

func createAuthProvider(method, registryURL, domain string, token Token, signer auth.Signer) (auth.Provider, error) {
	switch method {
	case MethodGitHub:
		return auth.NewGitHubATProvider(registryURL, string(token)), nil
	case MethodGitHubOIDC:
		return auth.NewGitHubOIDCProvider(registryURL), nil
	case MethodDNS:
		if domain == "" {
			return nil, errors.New("dns authentication requires --domain")
		}
		return auth.NewDNSProvider(registryURL, domain, &signer), nil
	case MethodHTTP:
		if domain == "" {
			return nil, errors.New("http authentication requires --domain")
		}
		return auth.NewHTTPProvider(registryURL, domain, &signer), nil
	case MethodNone:
		return auth.NewNoneProvider(registryURL), nil
	default:
		return nil, fmt.Errorf("unknown authentication method: %s\nFor a list of available methods, run: mcp-publisher login", method)
	}
}

func LoginCommand(args []string) error {
	if len(args) < 1 {
		return errors.New(`authentication method required

Usage: mcp-publisher login <method> [<signing provider>]

Methods:
  github            Interactive GitHub authentication
  github-oidc       GitHub Actions OIDC authentication
  dns               DNS-based authentication (requires --domain)
  http              HTTP-based authentication (requires --domain)
  none              Anonymous authentication (for testing)

Signing providers:
  azure-key-vault   Sign using Azure Key Vault
  google-kms        Sign using Google Cloud KMS

The dns and http methods require a --private-key for in-process signing. For
out-of-process signing, use one of the supported signing providers. Signing is
needed for an authentication challenge with the registry.

The github and github-oidc methods do not support signing providers and
authenticate using the GitHub as an identity provider.

Examples:

  # Interactive GitHub login, using device code flow
  mcp-publisher login github
  
  # Sign in using a specific Ed25519 private key for DNS authentication
  mcp-publisher login dns -algorithm ed25519 -domain example.com -private-key <64 hex chars>

  # Sign in using a specific ECDSA P-384 private key for DNS authentication
  mcp-publisher login dns -algorithm ecdsap384 -domain example.com -private-key <96 hex chars>
  
  # Sign in with gcloud CLI, use Google Cloud KMS for signing in DNS authentication
  gcloud auth application-default login
  mcp-publisher login dns google-kms -domain example.com -resource projects/lotr/locations/global/keyRings/fellowship/cryptoKeys/frodo/cryptoKeyVersions/1

  # Sign in with az CLI, use Azure Key Vault for signing in HTTP authentication
  az login
  mcp-publisher login http azure-key-vault -domain example.com -vault myvault -key mysigningkey

  `)
	}

	method := args[0]
	flags, err := parseLoginFlags(method, args)
	if err != nil {
		return err
	}

	var signer auth.Signer
	if flags.SignerType != NoSignerType {
		signer, err = createSigner(flags)
		if err != nil {
			return err
		}
	}

	authProvider, err := createAuthProvider(method, flags.RegistryURL, flags.Domain, flags.Token, signer)
	if err != nil {
		return err
	}
	ctx := context.Background()
	_, _ = fmt.Fprintf(os.Stdout, "Logging in with %s...\n", method)

	if err := authProvider.Login(ctx); err != nil {
		return fmt.Errorf("login failed: %w", err)
	}

	token, err := authProvider.GetToken(ctx)
	if err != nil {
		return fmt.Errorf("failed to get token: %w", err)
	}

	if err := ensureTokenDir(); err != nil {
		return err
	}

	tokenPath, err := tokenFilePath()
	if err != nil {
		return err
	}

	tokenData := map[string]string{
		"token":    token,
		"method":   method,
		"registry": flags.RegistryURL,
	}

	jsonData, err := json.Marshal(tokenData)
	if err != nil {
		return fmt.Errorf("failed to marshal token data: %w", err)
	}

	if err := os.WriteFile(tokenPath, jsonData, 0600); err != nil {
		return fmt.Errorf("failed to save token: %w", err)
	}

	_, _ = fmt.Fprintln(os.Stdout, "✓ Successfully logged in")
	return nil
}

```

## /cmd/publisher/commands/login_test.go

```go path="/cmd/publisher/commands/login_test.go" 
package commands_test

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/modelcontextprotocol/registry/cmd/publisher/commands"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestLoginCommand_WritesTokenToConfigDir(t *testing.T) {
	tempHome := t.TempDir()
	t.Setenv("HOME", tempHome)

	// Mock registry that returns a token for "none" auth
	server := setupNoneAuthServer(t)

	err := commands.LoginCommand([]string{"none", "--registry", server.URL})
	require.NoError(t, err)

	// Verify token written to new location
	tokenPath := filepath.Join(tempHome, ".config", "mcp-publisher", "token.json")
	data, err := os.ReadFile(tokenPath)
	require.NoError(t, err)

	var tokenInfo map[string]string
	require.NoError(t, json.Unmarshal(data, &tokenInfo))

	assert.Equal(t, "none", tokenInfo["method"])
	assert.Equal(t, server.URL, tokenInfo["registry"])
	assert.NotEmpty(t, tokenInfo["token"])
}

func TestLoginCommand_CreatesDirectoryWithCorrectPerms(t *testing.T) {
	tempHome := t.TempDir()
	t.Setenv("HOME", tempHome)

	server := setupNoneAuthServer(t)

	err := commands.LoginCommand([]string{"none", "--registry", server.URL})
	require.NoError(t, err)

	// Verify directory permissions
	dir := filepath.Join(tempHome, ".config", "mcp-publisher")
	info, err := os.Stat(dir)
	require.NoError(t, err)
	assert.Equal(t, os.FileMode(0700), info.Mode().Perm())

	// Verify file permissions
	tokenPath := filepath.Join(dir, "token.json")
	info, err = os.Stat(tokenPath)
	require.NoError(t, err)
	assert.Equal(t, os.FileMode(0600), info.Mode().Perm())
}

func TestLoginCommand_DoesNotWriteLegacyFiles(t *testing.T) {
	tempHome := t.TempDir()
	t.Setenv("HOME", tempHome)

	server := setupNoneAuthServer(t)

	err := commands.LoginCommand([]string{"none", "--registry", server.URL})
	require.NoError(t, err)

	// Verify no legacy file at $HOME
	_, err = os.Stat(filepath.Join(tempHome, ".mcp_publisher_token"))
	assert.True(t, os.IsNotExist(err), "should not create legacy ~/.mcp_publisher_token")

	// Verify no .mcpregistry_* files in cwd
	cwd, err := os.Getwd()
	require.NoError(t, err)
	for _, name := range []string{".mcpregistry_github_token", ".mcpregistry_registry_token"} {
		_, err := os.Stat(filepath.Join(cwd, name))
		assert.True(t, os.IsNotExist(err), "should not create %s in cwd", name)
	}
}

// setupNoneAuthServer creates a mock registry that handles the "none" auth flow.
func setupNoneAuthServer(t *testing.T) *httptest.Server {
	t.Helper()
	mux := http.NewServeMux()
	mux.HandleFunc("/v0/auth/none", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]interface{}{
			"registry_token": "test-registry-jwt",
			"expires_at":     9999999999,
		})
	})
	server := httptest.NewServer(mux)
	t.Cleanup(server.Close)
	return server
}

```

## /cmd/publisher/commands/logout.go

```go path="/cmd/publisher/commands/logout.go" 
package commands

import (
	"fmt"
	"os"
	"path/filepath"
)

func LogoutCommand() error {
	tokenPath, err := tokenFilePath()
	if err != nil {
		return err
	}

	// Check if token file exists at new location or legacy location
	homeDir, err := os.UserHomeDir()
	if err != nil {
		homeDir = "" // degrade gracefully for legacy cleanup
	}
	legacyTokenPath := ""
	if homeDir != "" {
		legacyTokenPath = filepath.Join(homeDir, LegacyTokenFileName)
	}

	newExists := fileExists(tokenPath)
	legacyExists := legacyTokenPath != "" && fileExists(legacyTokenPath)

	if !newExists && !legacyExists {
		_, _ = fmt.Fprintln(os.Stdout, "Not logged in")
		return nil
	}

	// Remove token file from new location
	os.Remove(tokenPath)

	// Remove from legacy location
	if legacyTokenPath != "" {
		os.Remove(legacyTokenPath)
	}

	// Clean up legacy intermediate token files from $HOME and cwd
	legacyIntermediateFiles := []string{
		".mcpregistry_github_token",
		".mcpregistry_registry_token",
	}

	for _, file := range legacyIntermediateFiles {
		// Clean from $HOME
		if homeDir != "" {
			os.Remove(filepath.Join(homeDir, file))
		}
		// Clean from cwd (the original bug location)
		os.Remove(file)
	}

	_, _ = fmt.Fprintln(os.Stdout, "✓ Successfully logged out")
	return nil
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

```

## /cmd/publisher/commands/logout_test.go

```go path="/cmd/publisher/commands/logout_test.go" 
package commands_test

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/modelcontextprotocol/registry/cmd/publisher/commands"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestLogoutCommand_RemovesNewToken(t *testing.T) {
	tempHome := t.TempDir()
	t.Setenv("HOME", tempHome)

	// Create token at new location
	dir := filepath.Join(tempHome, ".config", "mcp-publisher")
	require.NoError(t, os.MkdirAll(dir, 0700))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "token.json"), []byte(`{"token":"t"}`), 0600))

	err := commands.LogoutCommand()
	require.NoError(t, err)

	_, err = os.Stat(filepath.Join(dir, "token.json"))
	assert.True(t, os.IsNotExist(err))
}

func TestLogoutCommand_RemovesLegacyToken(t *testing.T) {
	tempHome := t.TempDir()
	t.Setenv("HOME", tempHome)

	// Create token at legacy location only
	require.NoError(t, os.WriteFile(filepath.Join(tempHome, ".mcp_publisher_token"), []byte(`{"token":"t"}`), 0600))

	err := commands.LogoutCommand()
	require.NoError(t, err)

	_, err = os.Stat(filepath.Join(tempHome, ".mcp_publisher_token"))
	assert.True(t, os.IsNotExist(err))
}

func TestLogoutCommand_RemovesBothOldAndNew(t *testing.T) {
	tempHome := t.TempDir()
	t.Setenv("HOME", tempHome)

	// Create tokens at both locations
	dir := filepath.Join(tempHome, ".config", "mcp-publisher")
	require.NoError(t, os.MkdirAll(dir, 0700))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "token.json"), []byte(`{"token":"new"}`), 0600))
	require.NoError(t, os.WriteFile(filepath.Join(tempHome, ".mcp_publisher_token"), []byte(`{"token":"old"}`), 0600))

	err := commands.LogoutCommand()
	require.NoError(t, err)

	_, err = os.Stat(filepath.Join(dir, "token.json"))
	assert.True(t, os.IsNotExist(err), "new token should be removed")

	_, err = os.Stat(filepath.Join(tempHome, ".mcp_publisher_token"))
	assert.True(t, os.IsNotExist(err), "legacy token should be removed")
}

func TestLogoutCommand_CleansUpCwdLegacyFiles(t *testing.T) {
	tempHome := t.TempDir()
	t.Setenv("HOME", tempHome)

	// Create a token so logout doesn't say "Not logged in"
	dir := filepath.Join(tempHome, ".config", "mcp-publisher")
	require.NoError(t, os.MkdirAll(dir, 0700))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "token.json"), []byte(`{"token":"t"}`), 0600))

	// Create legacy intermediate files in a temp cwd
	tempCwd := t.TempDir()
	origDir, err := os.Getwd()
	require.NoError(t, err)
	t.Cleanup(func() { _ = os.Chdir(origDir) })
	require.NoError(t, os.Chdir(tempCwd))

	require.NoError(t, os.WriteFile(".mcpregistry_github_token", []byte("gh-token"), 0600))
	require.NoError(t, os.WriteFile(".mcpregistry_registry_token", []byte("reg-token"), 0600))

	err = commands.LogoutCommand()
	require.NoError(t, err)

	_, err = os.Stat(filepath.Join(tempCwd, ".mcpregistry_github_token"))
	assert.True(t, os.IsNotExist(err), ".mcpregistry_github_token should be removed from cwd")

	_, err = os.Stat(filepath.Join(tempCwd, ".mcpregistry_registry_token"))
	assert.True(t, os.IsNotExist(err), ".mcpregistry_registry_token should be removed from cwd")
}

func TestLogoutCommand_CleansUpHomeLegacyFiles(t *testing.T) {
	tempHome := t.TempDir()
	t.Setenv("HOME", tempHome)

	// Create a token so logout doesn't say "Not logged in"
	dir := filepath.Join(tempHome, ".config", "mcp-publisher")
	require.NoError(t, os.MkdirAll(dir, 0700))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "token.json"), []byte(`{"token":"t"}`), 0600))

	// Create legacy intermediate files in $HOME
	require.NoError(t, os.WriteFile(filepath.Join(tempHome, ".mcpregistry_github_token"), []byte("gh-token"), 0600))
	require.NoError(t, os.WriteFile(filepath.Join(tempHome, ".mcpregistry_registry_token"), []byte("reg-token"), 0600))

	err := commands.LogoutCommand()
	require.NoError(t, err)

	_, err = os.Stat(filepath.Join(tempHome, ".mcpregistry_github_token"))
	assert.True(t, os.IsNotExist(err), ".mcpregistry_github_token should be removed from $HOME")

	_, err = os.Stat(filepath.Join(tempHome, ".mcpregistry_registry_token"))
	assert.True(t, os.IsNotExist(err), ".mcpregistry_registry_token should be removed from $HOME")
}

func TestLogoutCommand_NotLoggedIn(t *testing.T) {
	tempHome := t.TempDir()
	t.Setenv("HOME", tempHome)

	// No token files exist anywhere
	err := commands.LogoutCommand()
	// Should not error, just print "Not logged in"
	require.NoError(t, err)
}

```

## /cmd/publisher/commands/publish.go

```go path="/cmd/publisher/commands/publish.go" 
package commands

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
)

func PublishCommand(args []string) error {
	// Check for server.json file
	serverFile := "server.json"
	if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
		serverFile = args[0]
	}

	// Read server.json
	serverData, err := os.ReadFile(serverFile)
	if err != nil {
		if os.IsNotExist(err) {
			return fmt.Errorf("server.json not found. Run 'mcp-publisher init' to create one")
		}
		return fmt.Errorf("failed to read server.json: %w", err)
	}

	// Validate JSON
	var serverJSON apiv0.ServerJSON
	if err := json.Unmarshal(serverData, &serverJSON); err != nil {
		return fmt.Errorf("invalid server.json: %w", err)
	}

	// Load saved token
	tokenPath, err := tokenFilePath()
	if err != nil {
		return err
	}

	tokenData, err := os.ReadFile(tokenPath)
	if err != nil {
		if os.IsNotExist(err) {
			return notAuthenticatedError()
		}
		return fmt.Errorf("failed to read token: %w", err)
	}

	var tokenInfo map[string]string
	if err := json.Unmarshal(tokenData, &tokenInfo); err != nil {
		return fmt.Errorf("invalid token data: %w", err)
	}

	token := tokenInfo["token"]
	registryURL := tokenInfo["registry"]
	if registryURL == "" {
		registryURL = DefaultRegistryURL
	}

	// Publish to registry
	_, _ = fmt.Fprintf(os.Stdout, "Publishing to %s...\n", registryURL)
	response, statusCode, err := publishToRegistry(registryURL, serverData, token)
	if err != nil {
		// If publish failed with 422, call validate endpoint to show detailed errors
		if statusCode == http.StatusUnprocessableEntity {
			_, _ = fmt.Fprintln(os.Stdout, "Validation failed. Checking detailed validation errors...")
			_, _ = fmt.Fprintln(os.Stdout)

			// Call validate endpoint (same as validate command does)
			result, validateErr := validateViaAPI(registryURL, serverData)
			if validateErr != nil {
				// If validate also fails, return original publish error
				return fmt.Errorf("publish failed: %w", err)
			}

			// Print validation results using shared formatting logic
			formattedErrorMsg := printValidationIssues(result, &serverJSON)

			if !result.Valid {
				// Return error with formatted message if available
				if formattedErrorMsg != "" {
					return fmt.Errorf("%s", formattedErrorMsg)
				}
				return fmt.Errorf("validation failed")
			}
		}

		// For non-422 errors, return the original error
		return fmt.Errorf("publish failed: %w", err)
	}

	_, _ = fmt.Fprintln(os.Stdout, "✓ Successfully published")
	_, _ = fmt.Fprintf(os.Stdout, "✓ Server %s version %s\n", response.Server.Name, response.Server.Version)

	return nil
}

func publishToRegistry(registryURL string, serverData []byte, token string) (*apiv0.ServerResponse, int, error) {
	// Parse the server JSON data
	var serverJSON apiv0.ServerJSON
	err := json.Unmarshal(serverData, &serverJSON)
	if err != nil {
		return nil, 0, fmt.Errorf("error parsing server.json file: %w", err)
	}

	// Convert to JSON
	jsonData, err := json.Marshal(serverJSON)
	if err != nil {
		return nil, 0, fmt.Errorf("error serializing request: %w", err)
	}

	// Ensure URL ends with the publish endpoint
	if !strings.HasSuffix(registryURL, "/") {
		registryURL += "/"
	}
	publishURL := registryURL + "v0/publish"

	// Create and send request
	req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, publishURL, bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, 0, fmt.Errorf("error creating request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+token)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, 0, fmt.Errorf("error sending request: %w", err)
	}
	defer resp.Body.Close()

	// Read response
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, resp.StatusCode, fmt.Errorf("error reading response: %w", err)
	}

	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
		return nil, resp.StatusCode, fmt.Errorf("server returned status %d: %s", resp.StatusCode, body)
	}

	var serverResponse apiv0.ServerResponse
	if err := json.Unmarshal(body, &serverResponse); err != nil {
		return nil, resp.StatusCode, err
	}

	return &serverResponse, resp.StatusCode, nil
}

```

## /cmd/publisher/commands/publish_test.go

```go path="/cmd/publisher/commands/publish_test.go" 
package commands_test

import (
	"encoding/json"
	"io"
	"net/http"
	"testing"

	"github.com/modelcontextprotocol/registry/cmd/publisher/commands"
	"github.com/modelcontextprotocol/registry/internal/validators"
	apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
	"github.com/modelcontextprotocol/registry/pkg/model"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPublishCommand_Success(t *testing.T) {
	// Setup mock server that returns success
	server := SetupMockRegistryServer(t, nil, nil)

	// Setup token
	SetupTestToken(t, server.URL, "test-token")

	// Create valid server.json
	serverJSON := apiv0.ServerJSON{
		Schema:      model.CurrentSchemaURL,
		Name:        "com.example/test-server",
		Description: "A test server",
		Version:     "1.0.0",
	}
	CreateTestServerJSON(t, serverJSON)

	// Run publish command
	err := commands.PublishCommand([]string{})

	// Should succeed
	assert.NoError(t, err)
}

func TestPublishCommand_422ValidationFlow(t *testing.T) {
	validateCallCount := 0
	publishCallCount := 0

	// Setup mock server
	server := SetupMockRegistryServer(t,
		// Publish handler: return 422 for invalid schema
		func(w http.ResponseWriter, _ *http.Request) {
			publishCallCount++
			w.WriteHeader(http.StatusUnprocessableEntity)
			_, _ = w.Write([]byte(`{"message":"Failed to publish server, invalid schema: call /validate for details"}`))
		},
		// Validate handler: return validation errors
		func(w http.ResponseWriter, r *http.Request) {
			validateCallCount++
			w.Header().Set("Content-Type", "application/json")

			body, _ := io.ReadAll(r.Body)
			var req apiv0.ServerJSON
			_ = json.Unmarshal(body, &req)

			// Return validation result with deprecated schema error
			result := validators.ValidationResult{
				Valid: false,
				Issues: []validators.ValidationIssue{
					{
						Type:      validators.ValidationIssueTypeSemantic,
						Path:      "schema",
						Message:   "schema version 2025-07-09 is not the current version",
						Severity:  validators.ValidationIssueSeverityWarning,
						Reference: "schema-version-deprecated",
					},
				},
			}

			_ = json.NewEncoder(w).Encode(result)
		},
	)

	// Setup token
	SetupTestToken(t, server.URL, "test-token")

	// Create server.json with deprecated schema
	serverJSON := apiv0.ServerJSON{
		Schema:      "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
		Name:        "com.example/test-server",
		Description: "A test server",
		Version:     "1.0.0",
	}
	CreateTestServerJSON(t, serverJSON)

	// Run publish command
	err := commands.PublishCommand([]string{})

	// Should fail with validation error
	require.Error(t, err)
	assert.Contains(t, err.Error(), "schema version 2025-07-09")
	assert.Contains(t, err.Error(), "Migration checklist:")
	assert.Contains(t, err.Error(), "Full changelog with examples:")

	// Verify both endpoints were called
	assert.Equal(t, 1, publishCallCount, "publish endpoint should be called once")
	assert.Equal(t, 1, validateCallCount, "validate endpoint should be called once after 422")
}

func TestPublishCommand_422WithMultipleIssues(t *testing.T) {
	validateCallCount := 0

	// Setup mock server
	server := SetupMockRegistryServer(t,
		func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusUnprocessableEntity)
			_, _ = w.Write([]byte(`{"message":"Failed to publish server, invalid schema"}`))
		},
		func(w http.ResponseWriter, _ *http.Request) {
			validateCallCount++
			w.Header().Set("Content-Type", "application/json")

			result := validators.ValidationResult{
				Valid: false,
				Issues: []validators.ValidationIssue{
					{
						Type:      validators.ValidationIssueTypeSemantic,
						Path:      "version",
						Message:   "version must be a specific version, not a range",
						Severity:  validators.ValidationIssueSeverityError,
						Reference: "semantic-version-range",
					},
					{
						Type:      validators.ValidationIssueTypeSchema,
						Path:      "name",
						Message:   "name is required",
						Severity:  validators.ValidationIssueSeverityError,
						Reference: "schema-field-required",
					},
				},
			}

			_ = json.NewEncoder(w).Encode(result)
		},
	)

	SetupTestToken(t, server.URL, "test-token")

	serverJSON := apiv0.ServerJSON{
		Schema:      model.CurrentSchemaURL,
		Name:        "com.example/test-server",
		Description: "A test server",
		Version:     "^1.0.0", // Invalid version range
	}
	CreateTestServerJSON(t, serverJSON)

	err := commands.PublishCommand([]string{})

	require.Error(t, err)
	assert.Equal(t, 1, validateCallCount, "validate endpoint should be called")
}

func TestPublishCommand_NoToken(t *testing.T) {
	// Don't create a token file
	serverJSON := apiv0.ServerJSON{
		Schema:      model.CurrentSchemaURL,
		Name:        "com.example/test-server",
		Description: "A test server",
		Version:     "1.0.0",
	}
	CreateTestServerJSON(t, serverJSON)

	err := commands.PublishCommand([]string{})

	require.Error(t, err)
	assert.Contains(t, err.Error(), "not authenticated")
}

func TestPublishCommand_Non422Error(t *testing.T) {
	publishCallCount := 0

	server := SetupMockRegistryServer(t,
		func(w http.ResponseWriter, _ *http.Request) {
			publishCallCount++
			w.WriteHeader(http.StatusUnauthorized)
			_, _ = w.Write([]byte(`{"message":"Unauthorized"}`))
		},
		nil, // No validate handler needed
	)

	SetupTestToken(t, server.URL, "invalid-token")

	serverJSON := apiv0.ServerJSON{
		Schema:      model.CurrentSchemaURL,
		Name:        "com.example/test-server",
		Description: "A test server",
		Version:     "1.0.0",
	}
	CreateTestServerJSON(t, serverJSON)

	err := commands.PublishCommand([]string{})

	require.Error(t, err)
	assert.Contains(t, err.Error(), "publish failed")
	assert.Equal(t, 1, publishCallCount, "publish endpoint should be called")
}

func TestPublishCommand_DeprecatedSchema(t *testing.T) {
	tests := []struct {
		name           string
		schema         string
		publishStatus  int
		validationOpts func(req apiv0.ServerJSON) validators.ValidationResult
		expectError    bool
		errorSubstr    string
		checkLinks     bool
	}{
		{
			name:          "deprecated 2025-07-09 schema should show warning",
			schema:        "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
			publishStatus: http.StatusUnprocessableEntity,
			validationOpts: func(_ apiv0.ServerJSON) validators.ValidationResult {
				return validators.ValidationResult{
					Valid: false,
					Issues: []validators.ValidationIssue{
						{
							Type:      validators.ValidationIssueTypeSemantic,
							Path:      "schema",
							Message:   "schema version 2025-07-09 is not the current version",
							Severity:  validators.ValidationIssueSeverityWarning,
							Reference: "schema-version-deprecated",
						},
					},
				}
			},
			expectError: true,
			errorSubstr: "schema version 2025-07-09",
			checkLinks:  true,
		},
		{
			name:          "current 2025-12-11 schema should pass validation",
			schema:        "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
			publishStatus: http.StatusCreated,
			validationOpts: func(_ apiv0.ServerJSON) validators.ValidationResult {
				// Should not be called since publish succeeds
				return validators.ValidationResult{Valid: true}
			},
			expectError: false,
		},
		{
			name:          "empty schema should fail validation",
			schema:        "",
			publishStatus: http.StatusUnprocessableEntity,
			validationOpts: func(_ apiv0.ServerJSON) validators.ValidationResult {
				return validators.ValidationResult{
					Valid: false,
					Issues: []validators.ValidationIssue{
						{
							Type:      validators.ValidationIssueTypeSemantic,
							Path:      "schema",
							Message:   "$schema field is required",
							Severity:  validators.ValidationIssueSeverityError,
							Reference: "schema-field-required",
						},
					},
				}
			},
			expectError: true,
			errorSubstr: "$schema field is required",
			checkLinks:  true,
		},
		{
			name:          "custom schema without valid version should fail validation",
			schema:        "https://example.com/custom.schema.json",
			publishStatus: http.StatusUnprocessableEntity,
			validationOpts: func(_ apiv0.ServerJSON) validators.ValidationResult {
				return validators.ValidationResult{
					Valid: false,
					Issues: []validators.ValidationIssue{
						{
							Type:      validators.ValidationIssueTypeSchema,
							Path:      "schema",
							Message:   "failed to extract schema version from URL",
							Severity:  validators.ValidationIssueSeverityError,
							Reference: "schema-version-extraction-error",
						},
					},
				}
			},
			expectError: true,
			errorSubstr: "failed to extract schema version from URL",
			checkLinks:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			validateCallCount := 0
			publishCallCount := 0

			// Setup mock server
			server := SetupMockRegistryServer(t,
				// Publish handler
				func(w http.ResponseWriter, _ *http.Request) {
					publishCallCount++
					if tt.publishStatus == http.StatusCreated {
						w.WriteHeader(http.StatusCreated)
						response := apiv0.ServerResponse{
							Server: apiv0.ServerJSON{
								Name:    "com.example/test-server",
								Version: "1.0.0",
							},
						}
						_ = json.NewEncoder(w).Encode(response)
					} else {
						w.WriteHeader(tt.publishStatus)
						_, _ = w.Write([]byte(`{"message":"Failed to publish server, invalid schema: call /validate for details"}`))
					}
				},
				// Validate handler (only called on 422)
				func(w http.ResponseWriter, r *http.Request) {
					validateCallCount++
					w.Header().Set("Content-Type", "application/json")

					body, _ := io.ReadAll(r.Body)
					var req apiv0.ServerJSON
					_ = json.Unmarshal(body, &req)

					result := tt.validationOpts(req)
					_ = json.NewEncoder(w).Encode(result)
				},
			)

			SetupTestToken(t, server.URL, "test-token")

			// Create server.json with specific schema
			serverJSON := apiv0.ServerJSON{
				Schema:      tt.schema,
				Name:        "com.example/test-server",
				Description: "A test server",
				Version:     "1.0.0",
			}
			CreateTestServerJSON(t, serverJSON)

			err := commands.PublishCommand([]string{})

			if tt.expectError {
				require.Error(t, err, "Expected error for test case: %s", tt.name)
				if tt.errorSubstr != "" {
					assert.Contains(t, err.Error(), tt.errorSubstr, "Error should contain expected substring")
				}
				if tt.checkLinks {
					assert.Contains(t, err.Error(), "Migration checklist:", "Error should contain migration checklist link")
					assert.Contains(t, err.Error(), "Full changelog with examples:", "Error should contain changelog link")
				}
				if tt.publishStatus == http.StatusUnprocessableEntity {
					assert.Equal(t, 1, publishCallCount, "publish endpoint should be called once")
					assert.Equal(t, 1, validateCallCount, "validate endpoint should be called after 422")
				}
			} else {
				assert.NoError(t, err, "Expected success for test case: %s", tt.name)
				assert.Equal(t, 1, publishCallCount, "publish endpoint should be called once")
				assert.Equal(t, 0, validateCallCount, "validate endpoint should not be called on success")
			}
		})
	}
}

```

## /cmd/publisher/commands/status.go

```go path="/cmd/publisher/commands/status.go" 
package commands

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
)

// StatusUpdateRequest represents the request body for status update endpoints
type StatusUpdateRequest struct {
	Status        string  `json:"status"`
	StatusMessage *string `json:"statusMessage,omitempty"`
}

// AllVersionsStatusResponse represents the response from the all-versions status endpoint
type AllVersionsStatusResponse struct {
	UpdatedCount int `json:"updatedCount"`
}

// VersionInfo holds version and status for display
type VersionInfo struct {
	Version string
	Status  string
}

// ServerResponseMeta represents the _meta field in API responses
type ServerResponseMeta struct {
	Official *struct {
		Status string `json:"status"`
	} `json:"io.modelcontextprotocol.registry/official,omitempty"`
}

// SingleServerResponse represents the response from a single server version endpoint
type SingleServerResponse struct {
	Server struct {
		Version string `json:"version"`
	} `json:"server"`
	Meta ServerResponseMeta `json:"_meta"`
}

// ServerListResponse represents the response from the versions list endpoint
type ServerListResponse struct {
	Servers []SingleServerResponse `json:"servers"`
}

func StatusCommand(args []string) error {
	// Parse command flags
	fs := flag.NewFlagSet("status", flag.ExitOnError)
	status := fs.String("status", "", "New status: active, deprecated, or deleted (required)")
	message := fs.String("message", "", "Optional status message explaining the change")
	allVersions := fs.Bool("all-versions", false, "Apply status change to all versions of the server")
	yes := fs.Bool("yes", false, "Skip confirmation prompt for bulk operations")
	fs.BoolVar(yes, "y", false, "Skip confirmation prompt for bulk operations (shorthand)")

	if err := fs.Parse(args); err != nil {
		return err
	}

	// Validate required arguments
	if *status == "" {
		return errors.New("--status flag is required (active, deprecated, or deleted)")
	}

	// Validate status value
	validStatuses := map[string]bool{"active": true, "deprecated": true, "deleted": true}
	if !validStatuses[*status] {
		return fmt.Errorf("invalid status '%s'. Must be one of: active, deprecated, deleted", *status)
	}

	// Get server name from positional args
	remainingArgs := fs.Args()
	if len(remainingArgs) < 1 {
		return errors.New("server name is required\n\nUsage: mcp-publisher status --status <active|deprecated|deleted> [flags] <server-name> [version]")
	}

	serverName := remainingArgs[0]
	var version string

	// Get version if provided (required unless --all-versions is set)
	if !*allVersions {
		if len(remainingArgs) < 2 {
			return errors.New("version is required unless --all-versions flag is set\n\nUsage: mcp-publisher status --status <active|deprecated|deleted> [flags] <server-name> <version>")
		}
		version = remainingArgs[1]
	}

	// Load saved token
	tokenPath, err := tokenFilePath()
	if err != nil {
		return err
	}

	tokenData, err := os.ReadFile(tokenPath)
	if err != nil {
		if os.IsNotExist(err) {
			return notAuthenticatedError()
		}
		return fmt.Errorf("failed to read token: %w", err)
	}

	var tokenInfo map[string]string
	if err := json.Unmarshal(tokenData, &tokenInfo); err != nil {
		return fmt.Errorf("invalid token data: %w", err)
	}

	token := tokenInfo["token"]
	registryURL := tokenInfo["registry"]
	if registryURL == "" {
		registryURL = DefaultRegistryURL
	}

	// Update status
	if *allVersions {
		return updateAllVersionsStatus(registryURL, serverName, *status, *message, token, *yes)
	}
	return updateVersionStatus(registryURL, serverName, version, *status, *message, token)
}

func updateVersionStatus(registryURL, serverName, version, status, statusMessage, token string) error {
	// Fetch current status to show "from → to"
	currentStatus, err := fetchVersionStatus(registryURL, serverName, version, token)
	if err != nil {
		return fmt.Errorf("failed to fetch current status: %w", err)
	}

	_, _ = fmt.Fprintf(os.Stdout, "Updating %s version %s: %s → %s\n", serverName, version, currentStatus, status)

	if err := updateServerStatus(registryURL, serverName, version, status, statusMessage, token); err != nil {
		return fmt.Errorf("failed to update status: %w", err)
	}

	_, _ = fmt.Fprintln(os.Stdout, "✓ Successfully updated status")
	return nil
}

func updateAllVersionsStatus(registryURL, serverName, status, statusMessage, token string, skipConfirm bool) error {
	if !strings.HasSuffix(registryURL, "/") {
		registryURL += "/"
	}

	// Fetch all versions to show current statuses and get count for confirmation
	versions, err := fetchAllVersionsStatus(registryURL, serverName, token)
	if err != nil {
		return fmt.Errorf("failed to fetch current versions: %w", err)
	}

	if len(versions) == 0 {
		return errors.New("no versions found for this server")
	}

	// Show what will be updated
	_, _ = fmt.Fprintf(os.Stdout, "This will update %d version(s) of %s:\n", len(versions), serverName)
	for _, v := range versions {
		_, _ = fmt.Fprintf(os.Stdout, "  %s: %s → %s\n", v.Version, v.Status, status)
	}

	// Prompt for confirmation unless -y/--yes was provided
	if !skipConfirm {
		_, _ = fmt.Fprint(os.Stdout, "Continue? [y/N] ")
		reader := bufio.NewReader(os.Stdin)
		response, err := reader.ReadString('\n')
		if err != nil {
			return fmt.Errorf("failed to read response: %w", err)
		}
		response = strings.TrimSpace(strings.ToLower(response))
		if response != "y" && response != "yes" {
			return errors.New("operation cancelled")
		}
	}

	// Build the request body
	requestBody := StatusUpdateRequest{
		Status: status,
	}
	if statusMessage != "" {
		requestBody.StatusMessage = &statusMessage
	}

	jsonData, err := json.Marshal(requestBody)
	if err != nil {
		return fmt.Errorf("error serializing request: %w", err)
	}

	// URL encode the server name
	encodedServerName := url.PathEscape(serverName)
	statusURL := registryURL + "v0/servers/" + encodedServerName + "/status"

	req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, statusURL, bytes.NewBuffer(jsonData))
	if err != nil {
		return fmt.Errorf("error creating request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+token)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("error sending request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("error reading response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("server returned status %d: %s", resp.StatusCode, body)
	}

	// Parse response to get updated count
	var response AllVersionsStatusResponse
	if err := json.Unmarshal(body, &response); err != nil {
		// If we can't parse the response, just report success
		_, _ = fmt.Fprintln(os.Stdout, "✓ Successfully updated all versions")
		return nil
	}

	_, _ = fmt.Fprintf(os.Stdout, "✓ Successfully updated %d version(s)\n", response.UpdatedCount)
	return nil
}

func updateServerStatus(registryURL, serverName, version, status, statusMessage, token string) error {
	if !strings.HasSuffix(registryURL, "/") {
		registryURL += "/"
	}

	// Build the request body
	requestBody := StatusUpdateRequest{
		Status: status,
	}
	if statusMessage != "" {
		requestBody.StatusMessage = &statusMessage
	}

	jsonData, err := json.Marshal(requestBody)
	if err != nil {
		return fmt.Errorf("error serializing request: %w", err)
	}

	// URL encode the server name and version
	encodedServerName := url.PathEscape(serverName)
	encodedVersion := url.PathEscape(version)
	statusURL := registryURL + "v0/servers/" + encodedServerName + "/versions/" + encodedVersion + "/status"

	req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, statusURL, bytes.NewBuffer(jsonData))
	if err != nil {
		return fmt.Errorf("error creating request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+token)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("error sending request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("error reading response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("server returned status %d: %s", resp.StatusCode, body)
	}

	return nil
}

func fetchVersionStatus(registryURL, serverName, version, token string) (string, error) {
	if !strings.HasSuffix(registryURL, "/") {
		registryURL += "/"
	}

	encodedServerName := url.PathEscape(serverName)
	encodedVersion := url.PathEscape(version)
	fetchURL := registryURL + "v0/servers/" + encodedServerName + "/versions/" + encodedVersion + "?include_deleted=true"

	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fetchURL, nil)
	if err != nil {
		return "", fmt.Errorf("error creating request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+token)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("error sending request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("error reading response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("server returned status %d: %s", resp.StatusCode, body)
	}

	// Parse the response to extract status
	var response SingleServerResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return "", fmt.Errorf("error parsing response: %w", err)
	}

	if response.Meta.Official == nil {
		return "", errors.New("server response missing status information")
	}

	return response.Meta.Official.Status, nil
}

func fetchAllVersionsStatus(registryURL, serverName, token string) ([]VersionInfo, error) {
	if !strings.HasSuffix(registryURL, "/") {
		registryURL += "/"
	}

	encodedServerName := url.PathEscape(serverName)
	fetchURL := registryURL + "v0/servers/" + encodedServerName + "/versions?include_deleted=true"

	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fetchURL, nil)
	if err != nil {
		return nil, fmt.Errorf("error creating request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+token)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("error sending request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("error reading response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("server returned status %d: %s", resp.StatusCode, body)
	}

	var response ServerListResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return nil, fmt.Errorf("error parsing response: %w", err)
	}

	var versions []VersionInfo
	for _, s := range response.Servers {
		status := "unknown"
		if s.Meta.Official != nil {
			status = s.Meta.Official.Status
		}
		versions = append(versions, VersionInfo{
			Version: s.Server.Version,
			Status:  status,
		})
	}

	return versions, nil
}

```

## /cmd/publisher/commands/status_test.go

```go path="/cmd/publisher/commands/status_test.go" 
package commands_test

import (
	"strings"
	"testing"

	"github.com/modelcontextprotocol/registry/cmd/publisher/commands"
)

func TestStatusCommand_Validation(t *testing.T) {
	tests := []struct {
		name        string
		args        []string
		expectError bool
		errorSubstr string
	}{
		{
			name:        "missing --status flag",
			args:        []string{"io.github.user/my-server", "1.0.0"},
			expectError: true,
			errorSubstr: "--status flag is required",
		},
		{
			name:        "invalid status value",
			args:        []string{"--status", "invalid", "io.github.user/my-server", "1.0.0"},
			expectError: true,
			errorSubstr: "invalid status 'invalid'",
		},
		{
			name:        "missing server name",
			args:        []string{"--status", "deprecated"},
			expectError: true,
			errorSubstr: "server name is required",
		},
		{
			name:        "missing version without --all-versions",
			args:        []string{"--status", "deprecated", "io.github.user/my-server"},
			expectError: true,
			errorSubstr: "version is required unless --all-versions",
		},
		{
			name:        "valid args passes validation",
			args:        []string{"--status", "deprecated", "io.github.user/my-server", "1.0.0"},
			expectError: false,
		},
		{
			name:        "valid args with --all-versions passes validation",
			args:        []string{"--status", "deprecated", "--all-versions", "io.github.user/my-server"},
			expectError: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := commands.StatusCommand(tt.args)

			if tt.expectError {
				if err == nil {
					t.Errorf("Expected error but got none")
					return
				}
				if !strings.Contains(err.Error(), tt.errorSubstr) {
					t.Errorf("Expected error containing '%s', got: %v", tt.errorSubstr, err)
				}
			} else if err != nil {
				// For valid args, we expect it to pass validation
				// It may fail later at auth or API level, which is acceptable
				// Just check it's not a validation error
				if strings.Contains(err.Error(), "invalid status") ||
					strings.Contains(err.Error(), "server name is required") ||
					strings.Contains(err.Error(), "version is required unless") ||
					strings.Contains(err.Error(), "--status flag is required") {
					t.Errorf("Validation failed unexpectedly: %v", err)
				}
			}
		})
	}
}

func TestStatusCommand_ServerNameValidation(t *testing.T) {
	tests := []struct {
		name       string
		serverName string
	}{
		{
			name:       "valid github server name",
			serverName: "io.github.user/my-server",
		},
		{
			name:       "valid domain server name",
			serverName: "com.example/my-server",
		},
		{
			name:       "server name with dashes",
			serverName: "io.github.user/my-cool-server",
		},
		{
			name:       "server name with underscores",
			serverName: "io.github.user/my_server",
		},
		{
			name:       "server name with dots",
			serverName: "io.github.user/my.server",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			args := []string{"--status", "deprecated", tt.serverName, "1.0.0"}
			err := commands.StatusCommand(args)

			// Should pass validation (server name format is not validated by CLI)
			if err != nil && strings.Contains(err.Error(), "server name is required") {
				t.Errorf("Server name '%s' was rejected", tt.serverName)
			}
		})
	}
}

func TestStatusCommand_VersionValidation(t *testing.T) {
	tests := []struct {
		name    string
		version string
	}{
		{
			name:    "semver version",
			version: "1.0.0",
		},
		{
			name:    "semver with patch",
			version: "1.2.3",
		},
		{
			name:    "semver with prerelease",
			version: "1.0.0-alpha",
		},
		{
			name:    "semver with build metadata",
			version: "1.0.0+20130313144700",
		},
		{
			name:    "semver with prerelease and build",
			version: "1.0.0-beta.1+exp.sha.5114f85",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			args := []string{"--status", "deprecated", "io.github.user/my-server", tt.version}
			err := commands.StatusCommand(args)

			// Should pass validation (version format is not validated by CLI)
			if err != nil && strings.Contains(err.Error(), "version is required") {
				t.Errorf("Version '%s' was rejected", tt.version)
			}
		})
	}
}

func TestStatusCommand_AllVersionsFlag(t *testing.T) {
	tests := []struct {
		name        string
		args        []string
		expectError bool
		errorSubstr string
	}{
		{
			name:        "all-versions without version arg passes validation",
			args:        []string{"--status", "deprecated", "--all-versions", "io.github.user/my-server"},
			expectError: false,
		},
		{
			name:        "all-versions with extra version arg still works",
			args:        []string{"--status", "deprecated", "--all-versions", "io.github.user/my-server", "1.0.0"},
			expectError: false,
		},
		{
			name:        "missing server name with all-versions",
			args:        []string{"--status", "deprecated", "--all-versions"},
			expectError: true,
			errorSubstr: "server name is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := commands.StatusCommand(tt.args)

			if tt.expectError {
				if err == nil {
					t.Errorf("Expected error but got none")
					return
				}
				if !strings.Contains(err.Error(), tt.errorSubstr) {
					t.Errorf("Expected error containing '%s', got: %v", tt.errorSubstr, err)
				}
			} else if err != nil {
				// Should pass validation
				// Just check it's not a validation error
				if strings.Contains(err.Error(), "invalid status") ||
					strings.Contains(err.Error(), "server name is required") ||
					strings.Contains(err.Error(), "version is required unless") ||
					strings.Contains(err.Error(), "--status flag is required") {
					t.Errorf("Validation failed unexpectedly: %v", err)
				}
			}
		})
	}
}

func TestStatusCommand_FlagCombinations(t *testing.T) {
	tests := []struct {
		name string
		args []string
	}{
		{
			name: "status with message",
			args: []string{"--status", "deprecated", "--message", "Please upgrade to v2", "io.github.user/my-server", "1.0.0"},
		},
		{
			name: "all-versions with message",
			args: []string{"--status", "deprecated", "--all-versions", "--message", "All versions deprecated", "io.github.user/my-server"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := commands.StatusCommand(tt.args)
			// All these should pass CLI validation
			// They may fail at auth or API level which is acceptable
			if err != nil {
				// Just check it's not a validation error we can detect
				if strings.Contains(err.Error(), "invalid status") ||
					strings.Contains(err.Error(), "server name is required") ||
					strings.Contains(err.Error(), "version is required unless") ||
					strings.Contains(err.Error(), "--status flag is required") {
					t.Errorf("Validation failed unexpectedly: %v", err)
				}
			}
		})
	}
}

func TestStatusCommand_MissingStatus(t *testing.T) {
	// Test various ways status flag can be missing
	tests := []struct {
		name string
		args []string
	}{
		{
			name: "no status flag at all",
			args: []string{"io.github.user/my-server", "1.0.0"},
		},
		{
			name: "empty status value",
			args: []string{"--status", "", "io.github.user/my-server", "1.0.0"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := commands.StatusCommand(tt.args)

			if err == nil {
				t.Errorf("Expected error for missing status but got none")
				return
			}
			if !strings.Contains(err.Error(), "--status flag is required") {
				t.Errorf("Expected '--status flag is required' error, got: %v", err)
			}
		})
	}
}

func TestStatusCommand_YesFlag(t *testing.T) {
	tests := []struct {
		name string
		args []string
	}{
		{
			name: "all-versions with --yes flag",
			args: []string{"--status", "deprecated", "--all-versions", "--yes", "io.github.user/my-server"},
		},
		{
			name: "all-versions with -y shorthand",
			args: []string{"--status", "deprecated", "--all-versions", "-y", "io.github.user/my-server"},
		},
		{
			name: "yes flag with single version (flag accepted but not used)",
			args: []string{"--status", "deprecated", "--yes", "io.github.user/my-server", "1.0.0"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := commands.StatusCommand(tt.args)
			// All these should pass CLI validation
			// They may fail at auth or API level which is acceptable
			if err != nil {
				// Just check it's not a validation error
				if strings.Contains(err.Error(), "invalid status") ||
					strings.Contains(err.Error(), "server name is required") ||
					strings.Contains(err.Error(), "version is required unless") ||
					strings.Contains(err.Error(), "--status flag is required") ||
					strings.Contains(err.Error(), "flag provided but not defined") {
					t.Errorf("Validation failed unexpectedly: %v", err)
				}
			}
		})
	}
}

```

## /cmd/publisher/commands/testutil_test.go

```go path="/cmd/publisher/commands/testutil_test.go" 
package commands_test

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/modelcontextprotocol/registry/internal/validators"
	apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
	"github.com/stretchr/testify/require"
)

// SetupMockRegistryServer creates an httptest.Server that mocks the registry API
func SetupMockRegistryServer(t *testing.T, publishHandler func(w http.ResponseWriter, r *http.Request), validateHandler func(w http.ResponseWriter, r *http.Request)) *httptest.Server {
	t.Helper()

	mux := http.NewServeMux()

	// Default handlers if not provided
	if publishHandler == nil {
		publishHandler = func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusCreated)
			response := apiv0.ServerResponse{
				Server: apiv0.ServerJSON{
					Name:    "com.example/test",
					Version: "1.0.0",
				},
			}
			_ = json.NewEncoder(w).Encode(response)
		}
	}

	if validateHandler == nil {
		validateHandler = func(w http.ResponseWriter, _ *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			result := validators.ValidationResult{Valid: true}
			_ = json.NewEncoder(w).Encode(result)
		}
	}

	mux.HandleFunc("/v0/publish", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		publishHandler(w, r)
	})

	mux.HandleFunc("/v0/validate", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		validateHandler(w, r)
	})

	server := httptest.NewServer(mux)
	t.Cleanup(server.Close)

	return server
}

// SetupTestToken creates a token file pointing to the test server.
// It overrides $HOME to a temp directory so tests don't touch real credentials.
func SetupTestToken(t *testing.T, registryURL, token string) string {
	t.Helper()

	// Override $HOME so tokenFilePath() resolves to a temp directory
	tempHome := t.TempDir()
	t.Setenv("HOME", tempHome)

	dir := filepath.Join(tempHome, ".config", "mcp-publisher")
	require.NoError(t, os.MkdirAll(dir, 0700))

	tokenPath := filepath.Join(dir, "token.json")
	tokenData := map[string]string{
		"token":    token,
		"registry": registryURL,
	}

	data, err := json.Marshal(tokenData)
	require.NoError(t, err)

	err = os.WriteFile(tokenPath, data, 0600)
	require.NoError(t, err)

	return tokenPath
}

// CreateTestServerJSON creates a server.json file in a temp directory and changes to it
func CreateTestServerJSON(t *testing.T, serverJSON apiv0.ServerJSON) (string, string) {
	t.Helper()

	tempDir, err := os.MkdirTemp("", "mcp-publisher-test")
	require.NoError(t, err)
	t.Cleanup(func() { os.RemoveAll(tempDir) })

	jsonData, err := json.MarshalIndent(serverJSON, "", "  ")
	require.NoError(t, err)

	serverFile := filepath.Join(tempDir, "server.json")
	err = os.WriteFile(serverFile, jsonData, 0600)
	require.NoError(t, err)

	// Change to temp directory
	originalDir, err := os.Getwd()
	require.NoError(t, err)
	t.Cleanup(func() { _ = os.Chdir(originalDir) })

	err = os.Chdir(tempDir)
	require.NoError(t, err)

	return tempDir, serverFile
}

```

## /cmd/publisher/commands/validate.go

```go path="/cmd/publisher/commands/validate.go" 
package commands

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"github.com/modelcontextprotocol/registry/internal/validators"
	apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
	"github.com/modelcontextprotocol/registry/pkg/model"
)

// printSchemaValidationErrors prints nicely formatted error messages for schema validation issues
// (empty schema or non-current schema) with migration guidance to stdout.
// Returns the formatted error message string if any schema errors were printed, empty string otherwise.
func printSchemaValidationErrors(result *validators.ValidationResult, serverJSON *apiv0.ServerJSON) string {
	currentSchemaURL := model.CurrentSchemaURL
	migrationURL := "https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/server-json/CHANGELOG.md"
	checklistURL := migrationURL + "#migration-checklist-for-publishers"

	var formattedMsg strings.Builder

	for _, issue := range result.Issues {
		switch issue.Reference {
		case "schema-field-required":
			// Empty/missing schema
			_, _ = fmt.Fprintf(os.Stdout, "$schema field is required.\n")
			_, _ = fmt.Fprintln(os.Stdout)
			_, _ = fmt.Fprintf(os.Stdout, "Expected current schema: %s\n", currentSchemaURL)
			_, _ = fmt.Fprintln(os.Stdout)
			_, _ = fmt.Fprintln(os.Stdout, "Run 'mcp-publisher init' to create a new server.json with the correct schema, or update your existing server.json file.")
			_, _ = fmt.Fprintln(os.Stdout)
			_, _ = fmt.Fprintf(os.Stdout, "📋 Migration checklist: %s\n", checklistURL)
			_, _ = fmt.Fprintf(os.Stdout, "📖 Full changelog with examples: %s\n", migrationURL)
			_, _ = fmt.Fprintln(os.Stdout)

			// Build formatted error message
			_, _ = fmt.Fprintf(&formattedMsg, "$schema field is required. Expected current schema: %s. 📋 Migration checklist: %s 📖 Full changelog with examples: %s", currentSchemaURL, checklistURL, migrationURL)
			return formattedMsg.String() // Only one schema error at a time

		case "schema-version-deprecated":
			// Non-current schema
			if issue.Severity == validators.ValidationIssueSeverityWarning {
				// Warning format (for validate command)
				_, _ = fmt.Fprintf(os.Stdout, "⚠️  Deprecated schema detected: %s\n", serverJSON.Schema)
			} else {
				// Error format (for publish command)
				_, _ = fmt.Fprintf(os.Stdout, "deprecated schema detected: %s.\n", serverJSON.Schema)
			}
			_, _ = fmt.Fprintln(os.Stdout)
			_, _ = fmt.Fprintf(os.Stdout, "Expected current schema: %s\n", currentSchemaURL)
			_, _ = fmt.Fprintln(os.Stdout)
			_, _ = fmt.Fprintln(os.Stdout, "Migrate to the current schema format for new servers.")
			_, _ = fmt.Fprintln(os.Stdout)
			_, _ = fmt.Fprintf(os.Stdout, "📋 Migration checklist: %s\n", checklistURL)
			_, _ = fmt.Fprintf(os.Stdout, "📖 Full changelog with examples: %s\n", migrationURL)
			_, _ = fmt.Fprintln(os.Stdout)

			// Build formatted error message - include the original issue message for test compatibility
			_, _ = fmt.Fprintf(&formattedMsg, "%s. deprecated schema detected: %s. Expected current schema: %s. Migrate to the current schema format for new servers. 📋 Migration checklist: %s 📖 Full changelog with examples: %s", issue.Message, serverJSON.Schema, currentSchemaURL, checklistURL, migrationURL)
			return formattedMsg.String() // Only one schema error at a time

		case "schema-version-extraction-error":
			// Invalid schema URL format - also include migration links for consistency
			// Build formatted error message with migration links
			_, _ = fmt.Fprintf(&formattedMsg, "%s. 📋 Migration checklist: %s 📖 Full changelog with examples: %s", issue.Message, checklistURL, migrationURL)
			return formattedMsg.String()
		}
	}

	return ""
}

// printValidationIssues prints schema validation errors and all other validation issues.
// Returns the formatted error message string for schema validation errors (empty string if none).
func printValidationIssues(result *validators.ValidationResult, serverJSON *apiv0.ServerJSON) string {
	// Print schema validation errors/warnings with friendly messages
	formattedErrorMsg := printSchemaValidationErrors(result, serverJSON)

	if result.Valid {
		return formattedErrorMsg
	}

	// Print all issues
	_, _ = fmt.Fprintf(os.Stdout, "❌ Validation failed with %d issue(s):\n", len(result.Issues))
	_, _ = fmt.Fprintln(os.Stdout)

	// Track which schema issues we've already printed to avoid duplicates
	issueNum := 1

	for _, issue := range result.Issues {
		// Skip schema issues that were already printed (they're printed by printSchemaValidationErrors above)
		if issue.Reference == "schema-field-required" || issue.Reference == "schema-version-deprecated" {
			continue
		}

		// Print other issues normally
		_, _ = fmt.Fprintf(os.Stdout, "%d. [%s] %s (%s)\n", issueNum, issue.Severity, issue.Path, issue.Type)
		_, _ = fmt.Fprintf(os.Stdout, "   %s\n", issue.Message)
		if issue.Reference != "" {
			_, _ = fmt.Fprintf(os.Stdout, "   Reference: %s\n", issue.Reference)
		}
		_, _ = fmt.Fprintln(os.Stdout)
		issueNum++
	}

	return formattedErrorMsg
}

func ValidateCommand(args []string) error {
	// Parse arguments
	serverFile := "server.json"

	for _, arg := range args {
		if arg == "--help" || arg == "-h" {
			_, _ = fmt.Fprintln(os.Stdout, "Usage: mcp-publisher validate [file]")
			_, _ = fmt.Fprintln(os.Stdout)
			_, _ = fmt.Fprintln(os.Stdout, "Validate a server.json file without publishing.")
			_, _ = fmt.Fprintln(os.Stdout)
			_, _ = fmt.Fprintln(os.Stdout, "Arguments:")
			_, _ = fmt.Fprintln(os.Stdout, "  file    Path to server.json file (default: ./server.json)")
			_, _ = fmt.Fprintln(os.Stdout)
			_, _ = fmt.Fprintln(os.Stdout, "The validate command performs exhaustive validation, reporting all issues at once.")
			_, _ = fmt.Fprintln(os.Stdout, "It validates JSON syntax, schema compliance, and semantic rules.")
			return nil
		}
		if !strings.HasPrefix(arg, "-") {
			serverFile = arg
		}
	}

	// Read server file
	serverData, err := os.ReadFile(serverFile)
	if err != nil {
		if os.IsNotExist(err) {
			return fmt.Errorf("%s not found, please check the file path", serverFile)
		}
		return fmt.Errorf("failed to read %s: %w", serverFile, err)
	}

	// Validate JSON
	var serverJSON apiv0.ServerJSON
	if err := json.Unmarshal(serverData, &serverJSON); err != nil {
		return fmt.Errorf("invalid JSON: %w", err)
	}

	// Get registry URL (same pattern as publish)
	registryURL := DefaultRegistryURL
	// Try to read registry URL from token file (if it exists)
	if tokenPath, err := tokenFilePath(); err == nil {
		if tokenData, err := os.ReadFile(tokenPath); err == nil {
			var tokenInfo map[string]string
			if err := json.Unmarshal(tokenData, &tokenInfo); err == nil {
				if url := tokenInfo["registry"]; url != "" {
					registryURL = url
				}
			}
		}
	}

	// Validate via API
	_, _ = fmt.Fprintf(os.Stdout, "Validating against %s...\n", registryURL)
	result, err := validateViaAPI(registryURL, serverData)
	if err != nil {
		return fmt.Errorf("validation failed: %w", err)
	}

	// Print validation results using shared formatting logic
	formattedErrorMsg := printValidationIssues(result, &serverJSON)

	if result.Valid {
		_, _ = fmt.Fprintln(os.Stdout, "✅ server.json is valid")
		return nil
	}

	// Return error with formatted message if available
	if formattedErrorMsg != "" {
		return fmt.Errorf("%s", formattedErrorMsg)
	}

	return fmt.Errorf("validation failed")
}

// validateViaAPI calls the /validate endpoint on the registry
func validateViaAPI(registryURL string, serverData []byte) (*validators.ValidationResult, error) {
	// Parse the server JSON data to ensure it's valid JSON
	var serverJSON apiv0.ServerJSON
	err := json.Unmarshal(serverData, &serverJSON)
	if err != nil {
		return nil, fmt.Errorf("error parsing server.json file: %w", err)
	}

	// Convert to JSON
	jsonData, err := json.Marshal(serverJSON)
	if err != nil {
		return nil, fmt.Errorf("error serializing request: %w", err)
	}

	// Ensure URL ends with / and add validate endpoint
	if !strings.HasSuffix(registryURL, "/") {
		registryURL += "/"
	}
	validateURL := registryURL + "v0/validate"

	// Create and send request
	req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, validateURL, bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("error creating request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("error sending request: %w", err)
	}
	defer resp.Body.Close()

	// Read response
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("error reading response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("server returned status %d: %s", resp.StatusCode, body)
	}

	// Parse response - Huma returns ValidationResult directly
	var result validators.ValidationResult
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("error parsing response: %w", err)
	}

	return &result, nil
}

```

## /cmd/publisher/commands/validate_test.go

```go path="/cmd/publisher/commands/validate_test.go" 
package commands_test

import (
	"encoding/json"
	"io"
	"net/http"
	"os"
	"testing"

	"github.com/modelcontextprotocol/registry/cmd/publisher/commands"
	"github.com/modelcontextprotocol/registry/internal/validators"
	apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
	"github.com/modelcontextprotocol/registry/pkg/model"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestValidateCommand_Success(t *testing.T) {
	validateCallCount := 0

	server := SetupMockRegistryServer(t,
		nil, // No publish handler
		func(w http.ResponseWriter, _ *http.Request) {
			validateCallCount++
			w.Header().Set("Content-Type", "application/json")

			result := validators.ValidationResult{
				Valid:  true,
				Issues: []validators.ValidationIssue{},
			}

			_ = json.NewEncoder(w).Encode(result)
		},
	)

	SetupTestToken(t, server.URL, "test-token")

	serverJSON := apiv0.ServerJSON{
		Schema:      model.CurrentSchemaURL,
		Name:        "com.example/test-server",
		Description: "A test server",
		Version:     "1.0.0",
	}
	CreateTestServerJSON(t, serverJSON)

	err := commands.ValidateCommand([]string{})

	assert.NoError(t, err)
	assert.Equal(t, 1, validateCallCount, "validate endpoint should be called")
}

func TestValidateCommand_WithErrors(t *testing.T) {
	validateCallCount := 0

	server := SetupMockRegistryServer(t,
		nil,
		func(w http.ResponseWriter, _ *http.Request) {
			validateCallCount++
			w.Header().Set("Content-Type", "application/json")

			result := validators.ValidationResult{
				Valid: false,
				Issues: []validators.ValidationIssue{
					{
						Type:      validators.ValidationIssueTypeSemantic,
						Path:      "version",
						Message:   "version must be a specific version, not a range",
						Severity:  validators.ValidationIssueSeverityError,
						Reference: "semantic-version-range",
					},
				},
			}

			_ = json.NewEncoder(w).Encode(result)
		},
	)

	SetupTestToken(t, server.URL, "test-token")

	serverJSON := apiv0.ServerJSON{
		Schema:      model.CurrentSchemaURL,
		Name:        "com.example/test-server",
		Description: "A test server",
		Version:     "^1.0.0", // Invalid
	}
	CreateTestServerJSON(t, serverJSON)

	err := commands.ValidateCommand([]string{})

	require.Error(t, err)
	assert.Contains(t, err.Error(), "validation failed")
	assert.Equal(t, 1, validateCallCount, "validate endpoint should be called")
}

func TestValidateCommand_DeprecatedSchema(t *testing.T) {
	server := SetupMockRegistryServer(t,
		nil,
		func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")

			body, _ := io.ReadAll(r.Body)
			var req apiv0.ServerJSON
			_ = json.Unmarshal(body, &req)

			result := validators.ValidationResult{
				Valid: false,
				Issues: []validators.ValidationIssue{
					{
						Type:      validators.ValidationIssueTypeSemantic,
						Path:      "schema",
						Message:   "schema version 2025-07-09 is not the current version",
						Severity:  validators.ValidationIssueSeverityWarning,
						Reference: "schema-version-deprecated",
					},
				},
			}

			_ = json.NewEncoder(w).Encode(result)
		},
	)

	SetupTestToken(t, server.URL, "test-token")

	serverJSON := apiv0.ServerJSON{
		Schema:      "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
		Name:        "com.example/test-server",
		Description: "A test server",
		Version:     "1.0.0",
	}
	CreateTestServerJSON(t, serverJSON)

	err := commands.ValidateCommand([]string{})

	require.Error(t, err)
	assert.Contains(t, err.Error(), "schema version 2025-07-09")
	assert.Contains(t, err.Error(), "Migration checklist:")
}

func TestValidateCommand_NoServerFile(t *testing.T) {
	server := SetupMockRegistryServer(t, nil, nil)
	SetupTestToken(t, server.URL, "test-token")

	// Don't create server.json
	tempDir, err := os.MkdirTemp("", "mcp-publisher-test")
	require.NoError(t, err)
	defer os.RemoveAll(tempDir)

	originalDir, err := os.Getwd()
	require.NoError(t, err)
	defer func() { _ = os.Chdir(originalDir) }()

	_ = os.Chdir(tempDir)

	err = commands.ValidateCommand([]string{})

	require.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestValidateCommand_InvalidJSON(t *testing.T) {
	server := SetupMockRegistryServer(t, nil, nil)
	SetupTestToken(t, server.URL, "test-token")

	tempDir, err := os.MkdirTemp("", "mcp-publisher-test")
	require.NoError(t, err)
	defer os.RemoveAll(tempDir)

	originalDir, err := os.Getwd()
	require.NoError(t, err)
	defer func() { _ = os.Chdir(originalDir) }()

	_ = os.Chdir(tempDir)

	// Create invalid JSON file
	err = os.WriteFile("server.json", []byte("{ invalid json }"), 0600)
	require.NoError(t, err)

	err = commands.ValidateCommand([]string{})

	require.Error(t, err)
	assert.Contains(t, err.Error(), "invalid JSON")
}

```

## /cmd/publisher/main.go

```go path="/cmd/publisher/main.go" 
package main

import (
	"fmt"
	"log"
	"os"

	"github.com/modelcontextprotocol/registry/cmd/publisher/commands"
)

// Version info for the MCP Publisher tool
// These variables are injected at build time via ldflags by goreleaser
var (
	// Version is the current version of the MCP Publisher tool
	Version = "dev"

	// BuildTime is the time at which the binary was built
	BuildTime = "unknown"

	// GitCommit is the git commit that was compiled
	GitCommit = "unknown"
)

func main() {
	if len(os.Args) < 2 {
		printUsage()
		os.Exit(1)
	}

	// Check for help flag for subcommands
	if len(os.Args) >= 3 && (os.Args[2] == "--help" || os.Args[2] == "-h") {
		printCommandHelp(os.Args[1])
		return
	}

	var err error
	switch os.Args[1] {
	case "init":
		err = commands.InitCommand()
	case "login":
		err = commands.LoginCommand(os.Args[2:])
	case "logout":
		err = commands.LogoutCommand()
	case "publish":
		err = commands.PublishCommand(os.Args[2:])
	case "status":
		err = commands.StatusCommand(os.Args[2:])
	case "validate":
		err = commands.ValidateCommand(os.Args[2:])
	case "--version", "-v", "version":
		log.Printf("mcp-publisher %s (commit: %s, built: %s)", Version, GitCommit, BuildTime)
		return
	case "--help", "-h", "help":
		printUsage()
	default:
		fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", os.Args[1])
		printUsage()
		os.Exit(1)
	}

	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
}

func printUsage() {
	_, _ = fmt.Fprintln(os.Stdout, "MCP Registry Publisher Tool")
	_, _ = fmt.Fprintln(os.Stdout)
	_, _ = fmt.Fprintln(os.Stdout, "Usage:")
	_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher <command> [arguments]")
	_, _ = fmt.Fprintln(os.Stdout)
	_, _ = fmt.Fprintln(os.Stdout, "Commands:")
	_, _ = fmt.Fprintln(os.Stdout, "  init          Create a server.json file template")
	_, _ = fmt.Fprintln(os.Stdout, "  login         Authenticate with the registry")
	_, _ = fmt.Fprintln(os.Stdout, "  logout        Clear saved authentication")
	_, _ = fmt.Fprintln(os.Stdout, "  publish       Publish server.json to the registry")
	_, _ = fmt.Fprintln(os.Stdout, "  status        Update the status of a server version")
	_, _ = fmt.Fprintln(os.Stdout, "  validate      Validate server.json without publishing")
	_, _ = fmt.Fprintln(os.Stdout)
	_, _ = fmt.Fprintln(os.Stdout, "Use 'mcp-publisher <command> --help' for more information about a command.")
}

func printCommandHelp(command string) {
	switch command {
	case "init":
		_, _ = fmt.Fprintln(os.Stdout, "Create a server.json file template")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Usage:")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher init")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "This command creates a server.json file in the current directory with")
		_, _ = fmt.Fprintln(os.Stdout, "auto-detected values from your project (package.json, git remote, etc.).")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "After running init, edit the generated server.json to customize your")
		_, _ = fmt.Fprintln(os.Stdout, "server's metadata before publishing.")

	case "login":
		_, _ = fmt.Fprintln(os.Stdout, "Authenticate with the registry")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Usage:")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher login <method> [options]")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Methods:")
		_, _ = fmt.Fprintln(os.Stdout, "  github        Interactive GitHub authentication")
		_, _ = fmt.Fprintln(os.Stdout, "  github-oidc   GitHub Actions OIDC authentication")
		_, _ = fmt.Fprintln(os.Stdout, "  dns           DNS-based authentication (requires --domain)")
		_, _ = fmt.Fprintln(os.Stdout, "  http          HTTP-based authentication (requires --domain)")
		_, _ = fmt.Fprintln(os.Stdout, "  none          Anonymous authentication (for testing)")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Examples:")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher login github")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher login dns --domain example.com --private-key <key>")

	case "logout":
		_, _ = fmt.Fprintln(os.Stdout, "Clear saved authentication")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Usage:")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher logout")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "This command removes the saved authentication token from your system.")

	case "publish":
		_, _ = fmt.Fprintln(os.Stdout, "Publish server.json to the registry")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Usage:")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher publish [server.json]")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Arguments:")
		_, _ = fmt.Fprintln(os.Stdout, "  server.json   Path to the server.json file (default: ./server.json)")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "You must be logged in before publishing. Run 'mcp-publisher login' first.")

	case "status":
		_, _ = fmt.Fprintln(os.Stdout, "Update the status of a server version")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Usage:")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher status --status <active|deprecated|deleted> [flags] <server-name> [version]")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Flags (must come before positional arguments):")
		_, _ = fmt.Fprintln(os.Stdout, "  --status string            New status: active, deprecated, or deleted (required)")
		_, _ = fmt.Fprintln(os.Stdout, "  --message string           Optional message explaining the status change")
		_, _ = fmt.Fprintln(os.Stdout, "  --all-versions             Apply status change to all versions of the server")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Arguments:")
		_, _ = fmt.Fprintln(os.Stdout, "  server-name   Full server name (e.g., io.github.user/my-server)")
		_, _ = fmt.Fprintln(os.Stdout, "  version       Server version to update (required unless --all-versions is set)")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "Examples:")
		_, _ = fmt.Fprintln(os.Stdout, "  # Deprecate a specific version")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher status --status deprecated --message \"Please upgrade to 2.0.0\" \\")
		_, _ = fmt.Fprintln(os.Stdout, "    io.github.user/my-server 1.0.0")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "  # Delete a version with security issues")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher status --status deleted --message \"Critical security vulnerability\" \\")
		_, _ = fmt.Fprintln(os.Stdout, "    io.github.user/my-server 1.0.0")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "  # Restore a version to active")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher status --status active io.github.user/my-server 1.0.0")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "  # Deprecate all versions")
		_, _ = fmt.Fprintln(os.Stdout, "  mcp-publisher status --status deprecated --all-versions --message \"Project archived\" \\")
		_, _ = fmt.Fprintln(os.Stdout, "    io.github.user/my-server")
		_, _ = fmt.Fprintln(os.Stdout)
		_, _ = fmt.Fprintln(os.Stdout, "You must be logged in before updating status. Run 'mcp-publisher login' first.")

	default:
		fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
		printUsage()
	}
}

```

## /cmd/registry/main.go

```go path="/cmd/registry/main.go" 
package main

import (
	"context"
	"errors"
	"flag"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/modelcontextprotocol/registry/internal/api"
	v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
	"github.com/modelcontextprotocol/registry/internal/config"
	"github.com/modelcontextprotocol/registry/internal/database"
	"github.com/modelcontextprotocol/registry/internal/importer"
	"github.com/modelcontextprotocol/registry/internal/service"
	"github.com/modelcontextprotocol/registry/internal/telemetry"
)

// Version info for the MCP Registry application
// These variables are injected at build time via ldflags
var (
	// Version is the current version of the MCP Registry application
	Version = "dev"

	// BuildTime is the time at which the binary was built
	BuildTime = "unknown"

	// GitCommit is the git commit that was compiled
	GitCommit = "unknown"
)

func main() {
	// Parse command line flags
	showVersion := flag.Bool("version", false, "Display version information")
	flag.Parse()

	// Show version information if requested
	if *showVersion {
		log.Printf("MCP Registry %s\n", Version)
		log.Printf("Git commit: %s\n", GitCommit)
		log.Printf("Build time: %s\n", BuildTime)
		return
	}

	log.Printf("Starting MCP Registry Application v%s (commit: %s)", Version, GitCommit)

	var (
		registryService service.RegistryService
		db              database.Database
		err             error
	)

	// Initialize configuration
	cfg := config.NewConfig()

	// Connect to PostgreSQL with bounded retry. The DB endpoint can flap briefly
	// during voluntary disruptions (node drains, rollouts) and without retry the
	// pod exits cleanly, restarts, then races with the still-recovering service
	// — turning a transient blip into a multi-minute outage.
	const (
		maxStartupAttempts = 8
		perAttemptTimeout  = 10 * time.Second
		initialBackoff     = 1 * time.Second
		maxBackoff         = 8 * time.Second
	)
	backoff := initialBackoff
	for attempt := 1; ; attempt++ {
		attemptCtx, attemptCancel := context.WithTimeout(context.Background(), perAttemptTimeout)
		db, err = database.NewPostgreSQL(attemptCtx, cfg.DatabaseURL)
		attemptCancel()
		if err == nil {
			break
		}
		if attempt >= maxStartupAttempts {
			log.Printf("Failed to connect to PostgreSQL after %d attempts: %v", attempt, err)
			return
		}
		log.Printf("PostgreSQL not reachable (attempt %d/%d): %v, retrying in %s", attempt, maxStartupAttempts, err, backoff)
		time.Sleep(backoff)
		backoff = min(backoff*2, maxBackoff)
	}

	// Store the PostgreSQL instance for later cleanup
	defer func() {
		if err := db.Close(); err != nil {
			log.Printf("Error closing PostgreSQL connection: %v", err)
		} else {
			log.Println("PostgreSQL connection closed successfully")
		}
	}()

	registryService = service.NewRegistryService(db, cfg)

	// Import seed data if seed source is provided
	if cfg.SeedFrom != "" {
		log.Printf("Importing data from %s...", cfg.SeedFrom)
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
		defer cancel()

		importerService := importer.NewService(registryService)
		if err := importerService.ImportFromPath(ctx, cfg.SeedFrom); err != nil {
			log.Printf("Failed to import seed data: %v", err)
		}
	}

	shutdownTelemetry, metrics, err := telemetry.InitMetrics(cfg.Version)
	if err != nil {
		log.Printf("Failed to initialize metrics: %v", err)
		return
	}

	defer func() {
		if err := shutdownTelemetry(context.Background()); err != nil {
			log.Printf("Failed to shutdown telemetry: %v", err)
		}
	}()

	// Prepare version information
	versionInfo := &v0.VersionBody{
		Version:   Version,
		GitCommit: GitCommit,
		BuildTime: BuildTime,
	}

	// Initialize HTTP server
	server := api.NewServer(cfg, registryService, metrics, versionInfo)

	// Start server in a goroutine so it doesn't block signal handling
	go func() {
		if err := server.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Printf("Failed to start server: %v", err)
			os.Exit(1)
		}
	}()

	// Wait for interrupt signal to gracefully shutdown the server
	quit := make(chan os.Signal, 1)

	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Println("Shutting down server...")

	// Create context with timeout for shutdown
	sctx, scancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer scancel()

	// Gracefully shutdown the server
	if err := server.Shutdown(sctx); err != nil {
		log.Printf("Server forced to shutdown: %v", err)
	}

	log.Println("Server exiting")
}

```

## /data/seed.json

```json path="/data/seed.json" 
[
  {
    "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
    "name": "io.github.domdomegg/airtable-mcp-server",
    "description": "Read and write access to Airtable database schemas, tables, and records.",
    "repository": {
      "url": "https://github.com/domdomegg/airtable-mcp-server.git",
      "source": "github"
    },
    "version": "1.7.2",
    "icons": [
      {
        "src": "https://airtable.com/images/favicon/favicon-32x32.png",
        "mimeType": "image/png",
        "sizes": [
          "32x32"
        ]
      }
    ],
    "packages": [
      {
        "registryType": "npm",
        "identifier": "airtable-mcp-server",
        "version": "1.7.2",
        "runtimeHint": "npx",
        "transport": {
          "type": "stdio"
        },
        "environmentVariables": [
          {
            "description": "Airtable personal access token (e.g., pat123.abc123). Create at https://airtable.com/create/tokens/new with scopes: schema.bases:read, data.records:read, and optionally schema.bases:write and data.records:write.",
            "isRequired": true,
            "isSecret": true,
            "name": "AIRTABLE_API_KEY"
          }
        ]
      },
      {
        "registryType": "oci",
        "identifier": "docker.io/domdomegg/airtable-mcp-server:1.7.2",
        "runtimeHint": "docker",
        "transport": {
          "type": "stdio"
        },
        "environmentVariables": [
          {
            "description": "Airtable personal access token (e.g., pat123.abc123). Create at https://airtable.com/create/tokens/new with scopes: schema.bases:read, data.records:read, and optionally schema.bases:write and data.records:write.",
            "isRequired": true,
            "isSecret": true,
            "name": "AIRTABLE_API_KEY"
          }
        ]
      },
      {
        "registryType": "mcpb",
        "identifier": "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.2/airtable-mcp-server.mcpb",
        "fileSha256": "8220de07a08ebe908f04da139ea03dbfe29758141347e945da60535fb7bcca20",
        "transport": {
          "type": "stdio"
        }
      }
    ]
  },
  {
    "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
    "name": "io.github.domdomegg/airtable-mcp-server",
    "description": "Read and write access to Airtable database schemas, tables, and records.",
    "repository": {
      "url": "https://github.com/domdomegg/airtable-mcp-server.git",
      "source": "github"
    },
    "version": "1.7.3",
    "packages": [
      {
        "registryType": "npm",
        "identifier": "airtable-mcp-server",
        "version": "1.7.3",
        "runtimeHint": "npx",
        "transport": {
          "type": "stdio"
        },
        "environmentVariables": [
          {
            "description": "Airtable personal access token (e.g., pat123.abc123). Create at https://airtable.com/create/tokens/new with scopes: schema.bases:read, data.records:read, and optionally schema.bases:write and data.records:write.",
            "isRequired": true,
            "isSecret": true,
            "name": "AIRTABLE_API_KEY"
          }
        ]
      },
      {
        "registryType": "oci",
        "identifier": "docker.io/domdomegg/airtable-mcp-server:1.7.3",
        "runtimeHint": "docker",
        "transport": {
          "type": "stdio"
        },
        "environmentVariables": [
          {
            "description": "Airtable personal access token (e.g., pat123.abc123). Create at https://airtable.com/create/tokens/new with scopes: schema.bases:read, data.records:read, and optionally schema.bases:write and data.records:write.",
            "isRequired": true,
            "isSecret": true,
            "name": "AIRTABLE_API_KEY"
          }
        ]
      },
      {
        "registryType": "mcpb",
        "identifier": "https://github.com/domdomegg/airtable-mcp-server/releases/download/v1.7.3/airtable-mcp-server.mcpb",
        "fileSha256": "0f28a9129cfebd262dfb77854c872355d21401bb3e056575b3027081f5d570ca",
        "transport": {
          "type": "stdio"
        }
      }
    ]
  },
  {
    "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
    "name": "io.github.domdomegg/time-mcp-nuget",
    "description": "Get the current UTC time in RFC 3339 format.",
    "repository": {
      "url": "https://github.com/domdomegg/time-mcp-nuget.git",
      "source": "github"
    },
    "version": "1.0.8",
    "packages": [
      {
        "registryType": "nuget",
        "identifier": "TimeMcpServer",
        "version": "1.0.8",
        "runtimeHint": "dnx",
        "transport": {
          "type": "stdio"
        }
      }
    ]
  },
  {
    "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
    "name": "io.github.domdomegg/time-mcp-pypi",
    "description": "Get the current UTC time in RFC 3339 format.",
    "repository": {
      "url": "https://github.com/domdomegg/time-mcp-pypi.git",
      "source": "github"
    },
    "version": "1.0.6",
    "packages": [
      {
        "registryType": "pypi",
        "identifier": "time-mcp-pypi",
        "version": "1.0.6",
        "runtimeHint": "python",
        "transport": {
          "type": "stdio"
        }
      }
    ]
  }
]

```

## /deploy/.gitignore

```gitignore path="/deploy/.gitignore" 
# Pulumi
*.pyc
.pulumi/
Pulumi.*.yaml.bak

# Go
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
vendor/

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Build artifacts
infra

# Secrets
passphrase.prod.txt
passphrase.staging.txt

```

## /deploy/Makefile

``` path="/deploy/Makefile" 
.PHONY: help build local-login local-preview local-up staging-login staging-preview staging-up prod-login prod-preview prod-up

# Default target
help: ## Show this help message
	@echo "Available targets:"
	@grep -E '^[a-zA-Z_-]+:.*?## .*$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "  %-20s %s\n", $1, $2}'

# Build the Go binary
build: ## Build the Pulumi Go program
	go build

# Local stack commands
local-login: ## Login to local Pulumi backend
	pulumi login --local
	PULUMI_CONFIG_PASSPHRASE="" pulumi stack select local --create

local-preview: build local-login ## Preview local infrastructure changes
	PULUMI_CONFIG_PASSPHRASE="" pulumi preview --stack local

local-up: build local-login ## Deploy local infrastructure
	PULUMI_CONFIG_PASSPHRASE="" pulumi up --yes --stack local

local-destroy: local-login ## Destroy local infrastructure
	pulumi stack rm local --force --yes --preserve-config
	echo "Make sure to also delete your k8s cluster, e.g. minikube delete"

# Staging stack commands
staging-login: ## Login to staging Pulumi backend
	pulumi login gs://mcp-registry-staging-pulumi-state

staging-preview: build staging-login ## Preview staging infrastructure changes
	PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.staging.txt pulumi preview --stack gcpStaging

staging-up: build staging-login ## Deploy staging infrastructure
	PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.staging.txt pulumi up --yes --stack gcpStaging

# Production stack commands
prod-login: ## Login to production Pulumi backend
	pulumi login gs://mcp-registry-prod-pulumi-state

prod-preview: build prod-login ## Preview production infrastructure changes
	PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi preview --stack gcpProd

prod-up: build prod-login ## Deploy production infrastructure
	PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi up --yes --stack gcpProd
```

## /deploy/Pulumi.gcpProd.yaml

```yaml path="/deploy/Pulumi.gcpProd.yaml" 
config:
  mcp-registry:environment: prod
  mcp-registry:provider: gcp
  mcp-registry:imageTag: 1.7.9  # Set specific image tag for production (change this to deploy different versions)
  gcp:project: mcp-registry-prod
  mcp-registry:githubClientId: Iv23liUydBbI7Z2Q9bOZ
  mcp-registry:githubClientSecret:
    secure: v1:mSVikc0wDjoN8jCF:ytoI2gZ5WRJN3Fd6s5SRd2fnzirqtFBPSdIshIa2RfnF0OdtDpmucfs5KRw3HoJVGfDTbkrG+Sk=
  gcp:credentials:
    secure: v1:hyZWlpeMTFDnMcz2:X2bc8Gy8Gq5O83re7/uVZKX4phPHD0AuAwQgGxvT/5Tkg5slsb48dxX8QwZowOBNR7+7rfiDKijciAvbXqevzKgRPkTnEyxEbG1q+GuDfioGBex8yWPzBuC31xnAUMXxHoZqcBgzgVRU7kGIgyWwVWR420hAtDz0ISiliYtWyL6GY+VmPncfq+THMEDHCf6kySwt8rbNg5wPuGSEbS6VYhlveM+v9X5Tn7e8kGlUOIHcccMyhTIXcEk5AjhsQCg78CzAW7Y7PWj4JdZQGk88vSe+tJAlNdvkfyrlyrxP3/Rto84y9OhfrIrhkLVTDywRC1fI6sA73o8esk9EAtnw9HxySYqAYk1HwegwIPcob3YeC8Xa817NCg4vXqAeRIVp5iOuvD/tPaXMGvQpFOjvYUiz941UdIbE2F87ujLu2/JydAkAEFaZzc0iSHna9Ih+ss/I/00jrpSxOLoIOHLmiEuhN7XWyQRqWTHapS8TTI5cnCJdwC7ZIQQHAsBhXQiEVBnaZO2bdrwCwD1NyRaFDXs7egDwDbOF49qkO8D2KqPeWgC7LrD2/xfy4EqxRiLCmljIA1CGfGm+vqngl6LYbC3lnFJlZl3Y/ZssmDZt/71Duc7EENRNB9alYMhzSsP2/e7nxKxD20hdUmc90vtXpLCUO5oAoixclw1utWkxCQ4rLh7KHkFYitaL2S2GflOuzikCgY9g6NqhC5G+8tROGSSXXSkH/ERRAUcshqOGwlA1X3Il7Xnh5TkjH35t8JTPvKRKaBfIyXbAvetxk1fKpLfjMr9WXTxql9WyhWNQ+R6d6G1wIYeQ2d/75ANhwLZ5gPHWsr5Q6h2ezLBXoHePV2ISDjwa5ufi1Rf8ufvO8nuKJrBfV4F5TczWdOen8fuQTyvz16AMPk9oSvVe+davY9ABuK5KQweok267Kli/JbqAl5k48lbR70W6tyxTtsyOBw9j1AlmblT8KN36MsTO2zd3wcPhWWbrC4UlPPcUX6cmDUkhqvtygOxhdKq3NiVkqhKrXySeA+5hkjO5atHMmR0FH5g0GXIVHdltxUukCVb2Ur7m3Dn5t0euOW2TuHmfwxy5Azyt4uDmlsrByDqFS0sqvlBiCG/h8awqGXPbAKUtm6ASND1yjugyKF8Qs6G9BmW1lGNJvGlRGgfguHVy3za7DO4dKO1MKlOwxmg4Bx3GUr316/3gZU8bd4v5a7nWqOR9Hinz2dZ6MlcXl9YeLK+gbiyVHSDXp5Vwtwydp/mWfxBE5CAOoxbE+7R+tSlNfW9WIWRVRNuGoagFQoE/1dZr+iKnK4SqNYErLc7oFhHkgSBTi+RfqBMV88aEI3Vbzh5i/16Skh3LEmj0cIDCHCjw8z5tZUMH4Vg5FUUTZHwz/LsFwN8G6zijy1IA/DKLoYYCgv1M89Ih/s2UFC7ckzQ9swRL6/HfuS6dKOv3vtuZ7yMgslPKACwVZ/W+woA++S2CLwUTgxJUbt6wNXq6qkWaEe8ITRT+U5INCMELMAFFiNEYmRZXuNS5kMufBT1FtYGCAF/VGP9dtk+skRUxK2nhbz7qmeqHAr4QL8pQm4LlZtixSQftaoTWDmHXphVNh/k9MjgOL1rXrahPLxy7M02ijOEZtT8v61A0fwcWvy/ddB8Y3E3Q1RcA/W6OthnmwiWZl5bkt8izKrQyu3MKfQmWeb7FZCu2YluO0bx2NFvCsxbtc4KN2xFU5lzRZG7pQ6q1rBwTgyTHjHKZmR/UDP20682O5IAx76fTtTHToPK0fj/MM97feTh5x1qDDnOTX/WHb79XB4Djy3jQd5S3ql9nVp8mJbyw4hG/p6h5wIN0pAjErg9ghh3PcVWK0vYJQzamBcCC7ZBoNpCEz64yiEorn56ahB3FT7hCi/WYiLU/AIPI5mEagG6dIKrqfrpQwEuM0tKRNTl60jUV2tX3fptJD+krnkqN2r1Vijiqu3+2RzGrlgEVDgoPg9cPCC3dfEpDqtFZF/OHKpRiZ2PFmARAFVgLf/aOOJ94VOAwW5LNEP/s29cbivlyJFwSwDXW/0aN8N8Fo4W12ZUdul8XhGeiwGD3omuGqvZ/U1hVxV3FYctViR3kmB1O51j0SV8OUUrjSGlNiI3HjMqJLmLAdCngVtWU8Op9VS2uM1WdJWpP0Bsf9qH/RdgViSfO/siyvn7VA1NGbEkSY3os/KSqjmIAwKrIqMyaVvTlpund2Y55QVPVQsJ/g2tYJyBsvQQHJ+PG7V1EL0pxYGlDvzWYkgz+Yh3CC8Z9grNP4sD/3LcGer2AaMTwGRvKd5A0F7d4v7Fv+B5c9vLlW5TQPYJFVgXRpSoGaYDqmbV0Z/LN6bIkqH5poD1etYk5BVxEsZTVZXfI8a/dkpq2YjKc8C+tvquLyR+l9ykFZ8e1yaqd165jhj8aymskKiiz7LqJ4haBU1IiBAvY8fRp/hgQxtZKN8h7zI/VS1sQt75mTwgh/dx7Gzsfwhb2wuN0DUm8zYbqdOI4I8HonkgbgMiUKLSgoDqHpXzEtVglTixnnA2zUTZwJ0bzfCkvkheKM7IZ3h53Fbg1EqCrtUVgU6rIhiaM4uZVpTK/v0Ws7GbECpNF4HS7Ge5b+z9Ajzmf3VMOoZytJCbZWCN+Rtgfu0hODRyv2+BrKyz3r3aUAEA+2bClC6yPBjpY/VuyA2ulgoe5S2ZdZ69o8vH2aokoGHkZvqgMoKEMNR9enU/9qzoV+Gl+y1Y6qB0bKOHqU4ywBaVme2tUFm1djRSIyec3oVFwOi2fADwklrOd3hBAxdgYNHWrhhHF7G+L9pIb5E/TRTlO3s0wbCuo+F/CNX5Fx6lejcAyXsri9mS4+Qs0znFXt54hdjPoKaJiwDahJg86VKlav5cm1GsQAzpm0ITC4Ck1czNK1CJBfgp9eiofDm85cI6Ha7QsU2/O9Et7C9/+/sFJMgfaYzHokKG3cCFKuxSPpOg+tbyguxXTN5j+chKDATM0JNgxp1iHlDVcVlxqviIae7y5bcllRja4xvUG9RB1zoWAGf9OLIbd0AnPdC4cwXXQxkRJyKfXS8WaahY629zq6Kmbtlh+viEIqCCtqIBDsaDfjYOI+BTJ2UVprAt4R4M7hWLgyg3bYD8UTjLsyr3StkpfBtlGOYIbCwk/Ym97xvEz3IdfEqNY8Uaa6w1tYvePbOlzLVyu10AwVQc1wGvCYmUbQER6zhLaxrNoHjHv1olcjr5nXCIYl32m4H8hvODeCCGIU682f2nbkco498KA9lx+PFHLzTkJ5XY5KoJbnFMg6EIS9A4gaFIxhFL6QsOCKXM2qT10DSOH8VpcazFHyfJIjcQLXPyN4n66QPa6fMKJcoEIjsRkZtTLtOn3RcGnl2SG6o5iQ+OWnIxt3EQcaWNChW1jwKEO4rpyLlbFk51u1YU+1OywvyAmsXu5WAU4BANUfhCDB/Lt7/P6ucyQEN67ioorg71YtfGsQuyfy1VZb2Daasqup/giY+jMtz1NHKtwiCSnY6A0lYPz6vTHUlJiX9MXZh8xJENhAVFsD2SI2FyIkKTdh5Z9H8EbZoa6KFewGZ5GWOk3j9F4ITHvtvveYHi4OtBl7uDh0lNK9ZYEGyR44Wy46Cj4wy27pl6a0H09vFfJTePBudHqsMXgKuTxuGdp6Lk5CbCkASiW1pqLiCOd5nSp/iOQ2ntlOkfmd9XYxZ61Qr/zimFcU998B/qjdipwBsIndp0Fmv44czodto9qmBQi4Ei/ydHbe+eriBUHMSyHbtD3UQ/35EU0zmyA2QMBKnnd/zkL2MmDKDfhaSjOMUZe7rv5njXQNC8m3TT6CNfe+HXrx7wWFGMKKA8ojt1SXexQjCsM4gA4SMzKC9nZaEcyHCXCi2yzGdVtXqj+bzTgqG6huBuhFfeA+B5o03yy1DUy4B1mNXbra+IY1Qgs9fQ9hIoDKl/0TxCOTBDVXIGfBdam7xTPDC72uKlFSou9LTB1VMmqxsRGIWvKOBoIdC+iwcPB27lGBlYEzhT0dQd9erPLre38Cah/cZNz59PlIj8/zhKnIpKBrTSJRzi0qX65UZUjDi7CuwHOEId/h2xZlv01cr61a0NdL98wyF4QPCp8MzywEFNoSfwLHYNsNcxdIyVW32dQxgC+KfEytHriG8RwTNwuqAhj0NUiJuDUsSNAVFB+CXoh/AylDi3NBAY33/pDOc7PRz2CLJNju93DCmU9inJnW+OUCA5nJHRHsg==
  mcp-registry:jwtPrivateKey:
    secure: v1:0QwJl5e504ECQfjb:j8b4v1KxxqS8g9E8JTL0Wq5EEDbk/xPUglyg4/hyuL8go+pH2EE6skjf+7D5aAsld1SDRGlhXQBOZIVnjLBffsVN7cBhVlKy2qwTCC6QH5Q=
  mcp-registry:googleOauthClientSecret:
    secure: v1:6AUQwlFpA6TjYPTa:MUqOWbXufz2tr2s+OvapEsAWFl3VbDE0VxDRTcJ1qbAQM/phmDj/BVCutozvlolmurHD
encryptionsalt: v1:0funtAX4m9k=:v1:yMCnBXyBO+q4+/yy:AqWJTzwwIWXmUK0JGCzqbeg0RUr8Jw==

```

## /deploy/Pulumi.gcpStaging.yaml

```yaml path="/deploy/Pulumi.gcpStaging.yaml" 
config:
  mcp-registry:environment: staging
  mcp-registry:provider: gcp
  gcp:project: mcp-registry-staging
  mcp-registry:githubClientId: Iv23liB2r5oUw6PxiMOC
  mcp-registry:githubClientSecret:
    secure: v1:WRAquCFbMSoG7+iD:LCiTHniEP6eCHeE440q+lI/KqyYvK+0gZ4s4NseM1pN9JgWFt31wRttiQ7nWeHQpf0QEkZFALZY=
  gcp:credentials:
    secure: v1:RaHpGsBp37XO/EhJ:Dlk6YtSghGCtEKUUbxGr6KZvNFbttpPWUB79meTCy6gnV8xSKCys9HNaIjmSHfJeBEaqHsF8qZLL5coFU7Bd8b2ScthFCCPLx9Ra7/TuJx44oiQxgZwWm1h1epTFWrjCAAZlO7fLDnvtiGx/ErpY44U08uclx22RdWlbUu6d4ytFr/1SR3dmTUoM9kcmqFOL2Z12N3YCEMlBI0ant4iU0wv6PjP5JPAGeVhCg96oPvmCflrbhyjGWWLFIl+7oaEC2AnX6xBIk/s6yf9+kpFVLmQNE67TKk3ENMmJNxR8hXcc9mf//sdq2AgLViR8WiBMmzp0j/DA+oaS4AggsG9TTsGOe4YW+W9qiybZdJWzDUe5XQ76mZUFmOHlKkSnHE/jPPDoPGGqcqhbXQ8LXJJVJVthzYstoxMCnpTI8IRrnax38+nJAZnOW34mjaEFqu0PxNIyt8tuCn9jYYyYCtVs+8fbJb3yKWSUh+K+Oe/y1U5Lvtlox0r0kQ3t/vpKYolg2v+haab27FguagSo6jrqMC7CNL8Kx5k9nxLHJeLd41GU8ufep6CZRL+XFFcOpknDWlQl83RMlabXwbMM12Yj7wcpnPq7DStG37os2laLaaXDZbyXEyZc6HmZgWqbdH7Fs4Itn5l8dZgoHrJZCU5Xk2qixCQdTZf1WK1tnTVuasW6zUkMPai+np9yKTiy4yY8exGd4vZjARzFBZgoIeqUcaVcbFoABbGQSZmADNmLF3sIK4cEA24YGr4BhqC7buOkiUTg6U78jD7U6LFA9/rfITCry/wXlEJlA11Bdfc9OGlG3e6jTUy5rRM4C8sgVcpL/qHZlnIlSkyF2u6WSdnr4dBHHqFSWh+g0J4tO6xT8/x3XgONFLHnw+5o57oLVgY7rxa9V6tmQsOqfcGJFTCqmB4HJ+c9RQ62sXSkQdgKSkIujyEDRoxPiZCDkvJEYnjoJHMM5aA+L0HrcfZ1WN4uAUwzfJtJJjMjtLhsbF5tXinfVIvCPa9gxATjtUFUX9nFsWvdaW7/mwe+nyXXPbrhICuXckgSHp/ImyHubzYLlNZbH5vrgGPROvxCJP1ldorMGLPzBZN6vEP9xYYAIbi0jrftihbAXPtbT9+/VbYPtrtXomA56haD6BSO0ZDaR80di6Vmj8EPv1PkLO8MUVmUrnsxj+uOSmcfyzOx0nu7YXvXCgwqmIUK+f1BGebXuR7a6JT9LzKWKkQylSl759y0pl9i2lMS64e1kdU7XfhuPZnFZgebWB6ajcP+hXoAvpS/MSretsVnyPMdubSiPlIHNJfe4XoUzGOMGaB1NOU4tRnLsfe+J5+VreIKwU+c6/E5jKS86G2XTo2I0ZdBuarS7QGEwSVddHxm0ev51iQB9+wKbFLWFxV8bl3CwRqgF7xLYDx8DhA7SOku09lMB2xKq0Ba2spuHAM6vSEm0GMiVl01eKfgc3KAhHELmNkakFjRUsW7t2Z3ZKAVR5WYGiInZO/jqSKFLLRfrddin0rcl0Q3pkncJbbJqnpSxbQA3oofGA7SD6nFinpP7cBf+qOPyGHBh6zng8sTtAKBAtpGoQ6RVDDIvSaxri+UvGvi+cNdIkLF7Zpf5Ckk2fY8ez348zaJtvgv6Fbfs+nvNtdpX7+vy9yFYT2WJAd131+r6BPF795H1Spz35ZinIghoGxPDlA4Q9bKXQn/yUTDUsHW8H0hQnfYVCSjCh0t90QeZGdDzWDKFubTsH9OeKQUrX5CrVBvJZ3qwf94LU0DeYLR5NsYcbj5d3TNMyl8Ss371u7qIwFXgT25tsFiPbo4Cmi1bRf+rB4yB9Xj/HeV7IuLs7zOMcPDP2D7D+dX2A2rab5F/KAFBxXKSeWPgKp6II+UtvN/fsBkk3eFJ3Yg+HYzqxrKKcfitOy4vUHRIGo45rFthSqxBQ8YfACpZyata2Agm+1CkNuNzq6G1U3rcTDJ5k7sSfdp8eCsc77mpSKpPDWGjkJO9X7UjDt14kqP/FxONm5lhI9/MHEk7F0yjjUUw6oa0XaNn4li6odZbPRtLcNBfRgOGEVs61fydOS7H51tG65MfIYLx93h8stItPsiGmYmk2zGH/dS3n0IEUSZPcM+b8qY4KoznqyAjyCjvpQI5rqa1M5lvO6XNHrhBJPOZtSeuMmOedb8NGHuNtxPGsLq05kN0CIYgjnwW+PqUlPWknsOKrwhtmkyVQIrKx55BAng7D32779JsWQrdppJESuPDwcaKq943euopdXbNp94HhcmGWecpQQrRyINvitRmE7OTxB0ksBsMCv1fL5SyZS2YtSra3d9ITaWb/T742KgJV+1mZfbexJvO2nCJEKtLjjJ40EXWIhXiTis1NO25eO+Zm9BWNQ7ekXi6MNJEORY1SscWwfT46twjWmLwUldv/KHNGtgoCGvNsvzoLWuLX0h1iY1JZBnxR+bBj02Am4T+Fc/kmTa0x+MN1ax4Jva5/1YQ2t1oLcz7bb6Lum8FwLXvJM4gCtO5lURFcIeNPCPggjZO7zFTd6zGU2RWELBTIkrxbLJ9JP4gk3gTzrXBhWIzgkhSjDt6ZKf3nXKF08+atjkVbeWbTox1+vGdg+KaSItqFCVCtJixv8zEV3Ad+DppBFn9DkyPKb1k9ZJxyTrRTPzIrp5mLV6PD+VPBPc/zJcBkn40JvLcqZhIdQVtbadmVxP4+5vaZFl/C/Jrfp4YzdDHGoLW1g5RAMwbzpm2v7O06pfPYaXn/lI4UBYKq1i7aCEzXp9bEtJwb7KNfhsQnzzAmSousNiJnZUz6NovkydFC8F1YPCrFRVJpkpOP2WUfg9uWnMFuNZjBmsGky0ANj+1nUewXU+JpIPkgZF9C5WltnUB6jCon5zBNzNW5ewBPLM3Zea5Uk3NjUTCFuaC21So5kZX7XAZ2T9eBOMYntzan6EZUggN1sj5ofKGa5Shb/q53UuUO6kH7pMii35nlvpbK95yR16snUqhOMOuu+QhdqpQmvTZ+mLejXs18B1dm62qMiwDRHAB9NdgKFS7oqFE7BXXKl3XJLFywv9akXLsd9sd8EzVE9gT+HujbT+r30M0fNjaCt/Ik7A/bqMXQtkfyow+kLrHu4YvKXkukwySiisRtXqFBXNPbgPDN0uEwRaThe2iVYoZWjTO6bgVK+TVxyec+ACqlc9sZUj1n3SdXpRMIZziqku03UidqXFfut4+VFU3Xc+2XL9SZnWPEzzEr3aW2wTtZZPj9ebiJWkoEc4edZBYoGTb9pco6FbfV5FjJ+CwL4LoNRLicnXkII5wOMJuSm6c9bNqksxZgFLMA/JWAtoPsIWWntPFjUMR47ioWmvq7DeDXlVz546FMAbIdtHNMoM/PbanEsZzBfX8qWHr0l9igKZq4CsqbPa26B5JtQUHlOu+9DvqJd29iawLwS5QZexOlvj+3+LHBiuM1zGipYJ/8+pwpeNhvzB5lbbR8ZV0W1zFbyPmYkRE/R4lXdnfD6njSp1wbWoHNdXFQ8PR+t3LuGygUImRHGUCSn9pAEVCbu4WTWwCGgIyO1t+yyGwkWaHSpwaeLAcxeglN6z5p3mDt6WVySHC/mFkfpBywAxOyLFQJu09sMqe/1DN9Wc7EVDiQfjcItywWdjj1VXRURTLcf4P71TH/gfyGnG7K4Mi76geMaeUEfbQIvF33L5tppUnI3i39PoYO/dVvxLRLTed2XA3Ph+eK0dQCyyIhEg92dFrH/yP6LnyG0QHxvFrMUpKxi3E7jTbE/wyfAwNPKHUNA7NPTDovc0itnE3yc/JEy5o3RO6AMXsR5YcUxwgdYu6OoEBKIs9rlrdfQw2CTFbjM6kP7gqK8vhZbpnP4RQYPQOuDzu0B0EnULBocN9SCdv+dSFPOU5RPrsQYRgoXKmqhTc57VndgMD1p0KVWDjnzbVm8GV2OR16L+repVpqLCxGf7xxp0tenWAlZAMVBPbRltER+zTul5fe85f/PNFUI0dgaIPkKFJcOMoayNFr5VBSt+em+mWLIoyNLeEygBkyAJ7sa2Th9fMplBfq+FXyDNRj2mzP3G62BA3m1Ojx3hvDsJNi8GClTgKzlG+eXAc2YgjknS9/59scVvKRlaMOu7qDwOs0ImiCQcT3LQKhDZQxJ6wgPaR76bzX9SoDDiReWdqgTF68GQg3q6L+La8p+Nhm0j1Y4NGfLRhVka70WUNyanthA5FxuWpmbmttcJ4Nf3jw44IACQ1OGVja6KYA69oj/tTpKltQ==
  mcp-registry:jwtPrivateKey:
    secure: v1:0NSeI0qWrdcHeVfh:P56IEP/700/e839TSbF7Ns2j2orZnD5cRNXohjxCPOKyIDYn+0bmiTuk8pyFnFk8WibS/w7M2FFgv3/BL0Djo0XMVe9tq+7HjLN4tPEJnCU=
  mcp-registry:googleOauthClientSecret:
    secure: v1:MfA48Ri4VssQwUi7:TUF9sBGIgTnBUhePfEF7L+h1VVn5e6j2uGLbOwbGbjyBO9Z72SKC+z/21xHmJtqUSr2t
encryptionsalt: v1:EKBwmTmss1c=:v1:JLd4a7cM0X8Jroh+:z9/q6RSCEFTDzMV6X6h5Tpbw0tnpkA==

```

## /deploy/Pulumi.local.yaml

```yaml path="/deploy/Pulumi.local.yaml" 
config:
  mcp-registry:environment: local
  mcp-registry:provider: local
  mcp-registry:githubClientId: Iv23licy3GSiM9Km5jtd
  mcp-registry:githubClientSecret: 0e8db54879b02c29adef51795586f3c510a9341d
  mcp-registry:jwtPrivateKey: bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c
  mcp-registry:googleOauthClientSecret:
    secure: v1:DUSCqfRvRSVmSihj:JIYznEU+T1vbmVF8MkdSUtqUM50498sxBWsfwNaS1r5jkF/+gtltWfJoKWb1OLgcX8lb
encryptionsalt: v1:ijIHaqhbXVA=:v1:7voX1Kv+Bunz33iN:fyVHMOhlGIymzJ+ILgUBy3ExTwUUnA==

```

## /deploy/Pulumi.yaml

```yaml path="/deploy/Pulumi.yaml" 
name: mcp-registry-infra
runtime:
  name: go
  options:
    binary: ./infra
description: MCP Registry Kubernetes Infrastructure
```

## /deploy/README.md

# MCP Registry Kubernetes Deployment

This directory contains Pulumi infrastructure code to deploy the MCP Registry service to a Kubernetes cluster. It supports deploying the infrastructure locally (using an existing kubeconfig, e.g. with minikube) or to Google Cloud Platform (GCP).

## Quick Start

### Local Development

Pre-requisites:
- [Pulumi CLI installed](https://www.pulumi.com/docs/iac/download-install/)
- Access to a Kubernetes cluster via kubeconfig. You can run a cluster locally with [minikube](https://minikube.sigs.k8s.io/docs/start/).

1. Ensure your kubeconfig is configured at the cluster you want to use. For minikube, run `minikube start && minikube tunnel`.
2. Run `make local-up` to deploy the stack.
3. Access the repository via the ingress load balancer. You can find its external IP with `kubectl get svc ingress-nginx-controller -n ingress-nginx`. Then run `curl -H "Host: local.registry.modelcontextprotocol.io" -k https://<EXTERNAL-IP>/v0/ping` to check that the service is up.

#### To change config

The stack is configured out of the box for local development. But if you want to make changes, run commands like:

```bash
PULUMI_CONFIG_PASSPHRASE="" pulumi config set mcp-registry:environment local
PULUMI_CONFIG_PASSPHRASE="" pulumi config set mcp-registry:githubClientSecret --secret <some-secret-value>
```

#### To delete the stack

`make local-destroy` and deleting the cluster (with minikube: `minikube delete`) will reset you back to a clean state.

### Production Deployment (GCP)

**Note:** Deployments are automatically handled by GitHub Actions with separate workflows for staging and production:

- **Staging:** All merges to the `main` branch automatically build and deploy the latest code to the `staging` environment via [deploy-staging.yml](../.github/workflows/deploy-staging.yml). Staging always uses the `:main` Docker image tag.

- **Production:** Deployment requires explicit configuration of the Docker image tag in `Pulumi.gcpProd.yaml` and is triggered automatically via [deploy-production.yml](../.github/workflows/deploy-production.yml) when this config file is pushed to `main`. This GitOps-style approach provides manual control and an audit trail for production versions.

**To deploy a specific version to production:**

1. Cut a release (if deploying a new version):
   ```bash
   # Via GitHub UI: https://github.com/modelcontextprotocol/registry/releases
   # Or via gh CLI:
   gh release create v1.2.3 --generate-notes
   ```
   This builds Docker images tagged as `1.2.3` and `latest` (note: image tags do not include the 'v' prefix)

2. Update the production image tag in `Pulumi.gcpProd.yaml`:
   ```bash
   # Edit deploy/Pulumi.gcpProd.yaml
   # Change line: mcp-registry:imageTag: 1.2.3  (note: no 'v' prefix for Docker image tags)
   git add deploy/Pulumi.gcpProd.yaml
   git commit -m "Deploy version 1.2.3 to production"
   git push
   ```

3. The production deployment workflow will automatically trigger and deploy the specified version

**Manual Override:** The steps below are preserved if a manual deployment override is needed.

Pre-requisites:
- [Pulumi CLI installed](https://www.pulumi.com/docs/iac/download-install/)
- A Google Cloud Platform (GCP) account
- A GCP Service Account with appropriate permissions

1. Create a project: `gcloud projects create mcp-registry-prod`
2. Set the project: `gcloud config set project mcp-registry-prod`
3. Enable the bootstrap APIs needed before Pulumi can run. The remaining APIs
   (`cloudresourcemanager`, `compute`, `container`, `logging`, `monitoring`) are
   adopted as Pulumi-managed `projects.Service` resources by `ensureRequiredAPIs`
   in `pkg/providers/gcp/provider.go`, so they self-heal against drift but still
   need to be reachable on the very first deploy:
   ```bash
   gcloud services enable storage.googleapis.com         # for the Pulumi state bucket below
   gcloud services enable cloudresourcemanager.googleapis.com  # for the IAM bindings Pulumi creates
   gcloud services enable container.googleapis.com       # for the GKE cluster
   ```
4. Create the Pulumi service account and grant it the roles required to manage
   the deploy. `projectIamAdmin` is needed because the deploy creates IAM bindings
   on the project (for the default compute SA). The other four are general
   resource management:
   ```bash
   gcloud iam service-accounts create pulumi-svc
   sleep 10
   gcloud projects add-iam-policy-binding mcp-registry-prod --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/container.admin"
   gcloud projects add-iam-policy-binding mcp-registry-prod --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/compute.admin"
   gcloud projects add-iam-policy-binding mcp-registry-prod --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/storage.admin"
   gcloud projects add-iam-policy-binding mcp-registry-prod --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/storage.hmacKeyAdmin"
   gcloud projects add-iam-policy-binding mcp-registry-prod --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/resourcemanager.projectIamAdmin"
   gcloud iam service-accounts add-iam-policy-binding $(gcloud projects describe mcp-registry-prod --format="value(projectNumber)")-compute@developer.gserviceaccount.com --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/iam.serviceAccountUser"
   gcloud iam service-accounts keys create sa-key.json --iam-account=pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com
   ```
5. Create a GCS bucket for Pulumi state: `gsutil mb gs://mcp-registry-prod-pulumi-state`
6. Set Pulumi's backend to GCS: `pulumi login gs://mcp-registry-prod-pulumi-state`
7. Get the passphrase file `passphrase.prod.txt` from the registry maintainers
8. Init the GCP stack: `PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi stack init gcpProd`
9. Set the GCP credentials in Pulumi config:
    ```bash
    # Base64 encode the service account key and set it
    pulumi config set --secret gcp:credentials "$(base64 < sa-key.json)"
    ```
10. Deploy: `make prod-up`
11. Access the repository via the ingress load balancer. You can find its external IP with: `kubectl get svc ingress-nginx-controller -n ingress-nginx`.
   Then run `curl -H "Host: prod.registry.modelcontextprotocol.io" -k https://<EXTERNAL-IP>/v0/ping` to check that the service is up.


## Structure

```
├── main.go                 # Pulumi program entry point
├── Pulumi.yaml             # Project configuration
├── Pulumi.local.yaml       # Local stack configuration
├── Pulumi.gcpProd.yaml     # GCP production stack configuration
├── Pulumi.gcpStaging.yaml  # GCP staging stack configuration
├── Makefile                # Build and deployment targets
├── go.mod                  # Go module dependencies
├── go.sum                  # Go module checksums
└── pkg/                    # Infrastructure packages
    ├── k8s/                # Kubernetes deployment components
    │   ├── backup.go          # Database backup configuration
    │   ├── cert_manager.go    # SSL certificate management
    │   ├── deploy.go          # Deployment orchestration
    │   ├── ingress.go         # Ingress controller setup
    │   ├── monitoring.go      # Metrics and monitoring setup
    │   ├── postgres.go        # PostgreSQL database deployment
    │   └── registry.go        # MCP Registry deployment
    └── providers/          # Kubernetes cluster providers
        ├── types.go           # Provider interface definitions
        ├── gcp/               # Google Kubernetes Engine provider
        └── local/             # Local kubeconfig provider
```

### Architecture Overview

#### Deployment Flow
1. Pulumi program starts in `main.go`
2. Configuration is loaded from Pulumi config files
3. Provider factory creates appropriate cluster provider (GCP or local)
4. Cluster provider sets up Kubernetes access
5. `k8s.DeployAll()` orchestrates complete deployment:
   - Certificate manager for SSL/TLS
   - Ingress controller for external access
   - Database for data persistence
   - Backup infrastructure for database
   - Monitoring and metrics collection
   - MCP Registry application

## Configuration

| Parameter | Description | Required |
|-----------|-------------|----------|
| `environment` | Deployment environment (local/staging/prod) | Yes |
| `provider` | Kubernetes provider (local/gcp) | No (default: local) |
| `githubClientId` | GitHub OAuth Client ID | Yes |
| `githubClientSecret` | GitHub OAuth Client Secret | Yes |
| `imageTag` | Docker image tag for production environment | Yes (prod only) |
| `gcpProjectId` | GCP Project ID (required when provider=gcp) | No |
| `gcpRegion` | GCP Region (default: us-central1) | No |

## Database Backups

The deployment uses [K8up](https://k8up.io/) (a Kubernetes backup operator) that uses [Restic](https://restic.net/) under the hood.

When running locally they are stored in a Minio bucket. In staging and production, backups are stored in a GCS bucket.

### Accessing Backup Files

#### Local Development (MinIO)

```bash
# Expose MinIO web console
kubectl port-forward -n minio svc/minio 9000:9000 9001:9001
```

Then open [localhost:9001](http://localhost:9001), login with username `minioadmin` and password `minioadmin`, and navigate to the k8up-backups bucket.

##### Staging and Production (GCS)

- [Staging](https://console.cloud.google.com/storage/browser/mcp-registry-staging-backups?project=mcp-registry-staging)
- [Production](https://console.cloud.google.com/storage/browser/mcp-registry-prod-backups?project=mcp-registry-prod)

#### Decrypting and Restoring Backups

Backups are encrypted using Restic. To access the backup data:

1. **Download the backup files from the bucket:**
   ```bash
   # Local (MinIO) - ensure port-forward is active: kubectl port-forward -n minio svc/minio 9000:9000 9001:9001
   AWS_ACCESS_KEY_ID=minioadmin AWS_SECRET_ACCESS_KEY=minioadmin \
     aws --endpoint-url http://localhost:9000 s3 sync s3://k8up-backups/ ./backup-files/
   
   # GCS (staging/production)
   gsutil -m cp -r gs://mcp-registry-{staging|prod}-backups/* ./backup-files/
   ```
2. **[Install Restic](https://restic.readthedocs.io/en/latest/020_installation.html)**
3. **Restore the backup:**
   ```bash
   RESTIC_PASSWORD=password restic -r ./backup-files restore latest --target ./restored-files
   ```
   PostgreSQL data will be in `./restored-files/data/registry-pg-1/pgdata/`

## Troubleshooting

To configure kubectl to access an existing GKE cluster:

```bash
# Login
gcloud auth login
gcloud auth application-default login

# For production
gcloud container clusters get-credentials mcp-registry-prod --zone us-central1-b --project mcp-registry-prod

# For staging
gcloud container clusters get-credentials mcp-registry-staging --zone us-central1-b --project mcp-registry-staging
```

### Check Status

```bash
kubectl get pods
kubectl get deployment
kubectl get svc
kubectl get ingress
kubectl get svc -n ingress-nginx
```

### View Logs

```bash
kubectl logs -l app=mcp-registry
kubectl logs -l app=postgres
```

### Check Backup Status
```bash
kubectl describe schedule.k8up.io 
kubectl get backup
```


## /deploy/go.mod

```mod path="/deploy/go.mod" 
module github.com/modelcontextprotocol/registry/deploy/infra

go 1.26

require (
	github.com/pulumi/pulumi-gcp/sdk/v8 v8.41.1
	github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.31.1
	github.com/pulumi/pulumi/sdk/v3 v3.244.0
	gopkg.in/yaml.v2 v2.4.0
)

require (
	dario.cat/mergo v1.0.0 // indirect
	github.com/BurntSushi/toml v1.6.0 // indirect
	github.com/Microsoft/go-winio v0.6.2 // indirect
	github.com/ProtonMail/go-crypto v1.1.6 // indirect
	github.com/agext/levenshtein v1.2.3 // indirect
	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
	github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
	github.com/atotto/clipboard v0.1.4 // indirect
	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
	github.com/blang/semver v3.5.1+incompatible // indirect
	github.com/cenkalti/backoff/v5 v5.0.3 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/charmbracelet/bubbles v1.0.0 // indirect
	github.com/charmbracelet/bubbletea v1.3.10 // indirect
	github.com/charmbracelet/colorprofile v0.4.3 // indirect
	github.com/charmbracelet/lipgloss v1.1.0 // indirect
	github.com/charmbracelet/x/ansi v0.11.6 // indirect
	github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
	github.com/charmbracelet/x/term v0.2.2 // indirect
	github.com/cheggaaa/pb v1.0.29 // indirect
	github.com/clipperhouse/displaywidth v0.11.0 // indirect
	github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
	github.com/cloudflare/circl v1.6.3 // indirect
	github.com/cyphar/filepath-securejoin v0.6.1 // indirect
	github.com/djherbis/times v1.5.0 // indirect
	github.com/emirpasic/gods v1.18.1 // indirect
	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
	github.com/go-git/go-billy/v5 v5.9.0 // indirect
	github.com/go-git/go-git/v5 v5.19.1 // indirect
	github.com/go-logr/logr v1.4.3 // indirect
	github.com/go-logr/stdr v1.2.2 // indirect
	github.com/gogo/protobuf v1.3.2 // indirect
	github.com/golang/glog v1.2.5 // indirect
	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
	github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
	github.com/hashicorp/errwrap v1.1.0 // indirect
	github.com/hashicorp/go-multierror v1.1.1 // indirect
	github.com/hashicorp/go-version v1.8.0 // indirect
	github.com/hashicorp/hcl/v2 v2.22.0 // indirect
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/kevinburke/ssh_config v1.2.0 // indirect
	github.com/klauspost/compress v1.18.0 // indirect
	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
	github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/mattn/go-localereader v0.0.1 // indirect
	github.com/mattn/go-runewidth v0.0.21 // indirect
	github.com/mitchellh/go-ps v1.0.0 // indirect
	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
	github.com/muesli/cancelreader v0.2.2 // indirect
	github.com/muesli/termenv v0.16.0 // indirect
	github.com/opentracing/basictracer-go v1.1.0 // indirect
	github.com/opentracing/opentracing-go v1.2.0 // indirect
	github.com/pgavlin/fx v0.1.6 // indirect
	github.com/pgavlin/fx/v2 v2.0.12 // indirect
	github.com/pjbgf/sha1cd v0.6.0 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	github.com/pkg/term v1.1.0 // indirect
	github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect
	github.com/pulumi/esc v0.24.0 // indirect
	github.com/rivo/uniseg v0.4.7 // indirect
	github.com/rogpeppe/go-internal v1.14.1 // indirect
	github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect
	github.com/sergi/go-diff v1.4.0 // indirect
	github.com/skeema/knownhosts v1.3.1 // indirect
	github.com/spf13/cast v1.4.1 // indirect
	github.com/spf13/cobra v1.10.2 // indirect
	github.com/spf13/pflag v1.0.10 // indirect
	github.com/texttheater/golang-levenshtein v1.0.1 // indirect
	github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
	github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
	github.com/xanzy/ssh-agent v0.3.3 // indirect
	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
	github.com/zclconf/go-cty v1.13.2 // indirect
	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
	go.opentelemetry.io/collector/featuregate v1.53.0 // indirect
	go.opentelemetry.io/collector/pdata v1.53.0 // indirect
	go.opentelemetry.io/otel v1.43.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
	go.opentelemetry.io/otel/metric v1.43.0 // indirect
	go.opentelemetry.io/otel/sdk v1.43.0 // indirect
	go.opentelemetry.io/otel/trace v1.43.0 // indirect
	go.opentelemetry.io/proto/otlp v1.10.0 // indirect
	go.uber.org/atomic v1.11.0 // indirect
	go.uber.org/multierr v1.11.0 // indirect
	golang.org/x/crypto v0.50.0 // indirect
	golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
	golang.org/x/mod v0.35.0 // indirect
	golang.org/x/net v0.53.0 // indirect
	golang.org/x/sync v0.20.0 // indirect
	golang.org/x/sys v0.43.0 // indirect
	golang.org/x/term v0.42.0 // indirect
	golang.org/x/text v0.36.0 // indirect
	golang.org/x/tools v0.44.0 // indirect
	google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
	google.golang.org/grpc v1.80.0 // indirect
	google.golang.org/protobuf v1.36.11 // indirect
	gopkg.in/warnings.v0 v0.1.2 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
	lukechampine.com/frand v1.5.1 // indirect
)

```

## /deploy/main.go

```go path="/deploy/main.go" 
package main

import (
	"fmt"

	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"

	"github.com/modelcontextprotocol/registry/deploy/infra/pkg/k8s"
	"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
	"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers/gcp"
	"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers/local"
)

// createProvider creates the appropriate cluster provider based on configuration
func createProvider(ctx *pulumi.Context) (providers.ClusterProvider, error) {
	conf := config.New(ctx, "mcp-registry")
	providerName := conf.Get("provider")
	if providerName == "" {
		providerName = "local" // Default to local provider
	}

	switch providerName {
	case "gcp":
		return &gcp.Provider{}, nil
	case "local":
		return &local.Provider{}, nil
	default:
		return nil, fmt.Errorf("unsupported provider: %s", providerName)
	}
}

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		// Get configuration
		conf := config.New(ctx, "mcp-registry")
		environment := conf.Require("environment")

		// Create provider
		provider, err := createProvider(ctx)
		if err != nil {
			return err
		}

		// Create cluster
		cluster, err := provider.CreateCluster(ctx, environment)
		if err != nil {
			return err
		}

		// Create backup storage
		storage, err := provider.CreateBackupStorage(ctx, cluster, environment)
		if err != nil {
			return err
		}

		// Deploy to Kubernetes
		_, err = k8s.DeployAll(ctx, cluster, storage, environment)
		if err != nil {
			return err
		}

		// Export outputs
		ctx.Export("clusterName", cluster.Name)

		return nil
	})
}

```

## /deploy/pkg/k8s/backup.go

```go path="/deploy/pkg/k8s/backup.go" 
package k8s

import (
	"fmt"

	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions"
	corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
	metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/yaml"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"

	"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
)

// DeployK8up installs the k8up backup operator and configures scheduled backups
func DeployK8up(ctx *pulumi.Context, cluster *providers.ProviderInfo, environment string, storage *providers.BackupStorageInfo) error {
	if storage == nil {
		ctx.Log.Info("No backup storage configured, skipping k8up deployment", nil)
		return nil
	}

	// Install the k8up CRDs before the helm chart
	// Related: https://github.com/k8up-io/k8up/issues/1050
	k8upCRDs, err := yaml.NewConfigFile(ctx, "k8up-crds", &yaml.ConfigFileArgs{
		File: "https://github.com/k8up-io/k8up/releases/download/k8up-4.8.4/k8up-crd.yaml",
	}, pulumi.Provider(cluster.Provider))
	if err != nil {
		return fmt.Errorf("failed to install k8up CRDs: %w", err)
	}

	// Install k8up operator
	k8upValues := pulumi.Map{
		"k8up": pulumi.Map{
			"backupCommandAnnotation": pulumi.String("k8up.io/backup-command"),
			"fileExtensionAnnotation": pulumi.String("k8up.io/file-extension"),
		},
	}

	k8up, err := helm.NewChart(ctx, "k8up", helm.ChartArgs{
		Chart:   pulumi.String("k8up"),
		Version: pulumi.String("4.8.4"),
		FetchArgs: helm.FetchArgs{
			Repo: pulumi.String("https://k8up-io.github.io/k8up"),
		},
		Values: k8upValues,
	}, pulumi.Provider(cluster.Provider), pulumi.DependsOn([]pulumi.Resource{k8upCRDs}))
	if err != nil {
		return fmt.Errorf("failed to install k8up: %w", err)
	}

	// Create restic repository password secret
	repoPassword, err := corev1.NewSecret(ctx, "k8up-repo-password", &corev1.SecretArgs{
		Metadata: &metav1.ObjectMetaArgs{
			Name:      pulumi.String("k8up-repo-password"),
			Namespace: pulumi.String("default"),
			Labels: pulumi.StringMap{
				"k8up.io/backup": pulumi.String("true"),
			},
		},
		Type: pulumi.String("Opaque"),
		StringData: pulumi.StringMap{
			"password": pulumi.String("password"), // In production we use GCS, which is already encrypted
		},
	}, pulumi.Provider(cluster.Provider))
	if err != nil {
		return fmt.Errorf("failed to create repository password secret: %w", err)
	}

	// Determine schedule based on environment
	backupSchedule := "46 4 * * *" // Daily at 4:46 AM
	pruneSchedule := "46 5 * * *"  // Daily at 5:46 AM
	keepDaily := 28                // Keep daily backups for 28 days

	if environment == "local" || environment == "dev" {
		backupSchedule = "* * * * *"  // Every minute for testing
		pruneSchedule = "*/5 * * * *" // Every 5 minutes
		keepDaily = 1
	}

	// Create Schedule for automated backups
	_, err = apiextensions.NewCustomResource(ctx, "k8up-schedule", &apiextensions.CustomResourceArgs{
		ApiVersion: pulumi.String("k8up.io/v1"),
		Kind:       pulumi.String("Schedule"),
		Metadata: &metav1.ObjectMetaArgs{
			Name:      pulumi.String("backup-schedule"),
			Namespace: pulumi.String("default"),
			Labels: pulumi.StringMap{
				"environment": pulumi.String(environment),
			},
		},
		OtherFields: map[string]any{
			"spec": map[string]any{
				"backend": map[string]any{
					"repoPasswordSecretRef": map[string]any{
						"name": repoPassword.Metadata.Name().Elem(),
						"key":  "password",
					},
					"s3": map[string]any{
						"endpoint": storage.Endpoint,
						"bucket":   storage.BucketName,
						"accessKeyIDSecretRef": map[string]any{
							"name": storage.Credentials.Metadata.Name().Elem(),
							"key":  "AWS_ACCESS_KEY_ID",
						},
						"secretAccessKeySecretRef": map[string]any{
							"name": storage.Credentials.Metadata.Name().Elem(),
							"key":  "AWS_SECRET_ACCESS_KEY",
						},
					},
				},
				"backup": map[string]any{
					"schedule": backupSchedule,
					"podSecurityContext": map[string]any{
						"runAsUser": 0, // Run as root to access all files
					},
					"successfulJobsHistoryLimit": 3,
					"failedJobsHistoryLimit":     3,
				},
				"prune": map[string]any{
					"schedule": pruneSchedule,
					"retention": map[string]any{
						"keepDaily": keepDaily,
					},
					"successfulJobsHistoryLimit": 1,
					"failedJobsHistoryLimit":     1,
				},
			},
		},
	}, pulumi.Provider(cluster.Provider), pulumi.DependsOn([]pulumi.Resource{k8up, storage.Credentials, repoPassword}))
	if err != nil {
		return fmt.Errorf("failed to create k8up schedule: %w", err)
	}

	return nil
}

```

## /deploy/pkg/k8s/cert_manager.go

```go path="/deploy/pkg/k8s/cert_manager.go" 
package k8s

import (
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes"
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions"
	v1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
	metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"

	"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
)

// SetupCertManager sets up cert-manager for TLS certificates
func SetupCertManager(ctx *pulumi.Context, cluster *providers.ProviderInfo) error {
	// Create namespace for cert-manager
	certManagerNamespace, err := v1.NewNamespace(ctx, "cert-manager", &v1.NamespaceArgs{
		Metadata: &metav1.ObjectMetaArgs{
			Name: pulumi.String("cert-manager"),
		},
	}, pulumi.Provider(cluster.Provider))
	if err != nil {
		return err
	}

	// Install cert-manager for TLS certificates
	certManager, err := helm.NewChart(ctx, "cert-manager", helm.ChartArgs{
		Chart:   pulumi.String("cert-manager"),
		Version: pulumi.String("v1.18.2"),
		FetchArgs: helm.FetchArgs{
			Repo: pulumi.String("https://charts.jetstack.io"),
		},
		Namespace: certManagerNamespace.Metadata.Name().Elem(),
		Values: pulumi.Map{
			"installCRDs": pulumi.Bool(true),
			"ingressShim": pulumi.Map{
				"defaultIssuerName": pulumi.String("letsencrypt-prod"),
				"defaultIssuerKind": pulumi.String("ClusterIssuer"),
			},
		},
	}, pulumi.Provider(cluster.Provider))
	if err != nil {
		return err
	}

	_, err = apiextensions.NewCustomResource(ctx, "letsencrypt-prod", &apiextensions.CustomResourceArgs{
		ApiVersion: pulumi.String("cert-manager.io/v1"),
		Kind:       pulumi.String("ClusterIssuer"),
		Metadata: &metav1.ObjectMetaArgs{
			Name: pulumi.String("letsencrypt-prod"),
		},
		OtherFields: kubernetes.UntypedArgs{
			"spec": pulumi.Map{
				"acme": pulumi.Map{
					"server": pulumi.String("https://acme-v02.api.letsencrypt.org/directory"),
					"email":  pulumi.String("admin@modelcontextprotocol.io"),
					"privateKeySecretRef": pulumi.Map{
						"name": pulumi.String("letsencrypt-prod-key"),
					},
					"solvers": pulumi.Array{
						pulumi.Map{
							"http01": pulumi.Map{
								"ingress": pulumi.Map{
									"ingressClassName": pulumi.String("nginx"),
								},
							},
						},
					},
				},
			},
		},
	}, pulumi.Provider(cluster.Provider), pulumi.DependsOnInputs(certManager.Ready))
	if err != nil {
		return err
	}

	return nil
}

```

## /deploy/pkg/k8s/deploy.go

```go path="/deploy/pkg/k8s/deploy.go" 
package k8s

import (
	corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"

	"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
)

// DeployAll orchestrates the complete deployment of the MCP Registry to Kubernetes
func DeployAll(ctx *pulumi.Context, cluster *providers.ProviderInfo, backupStorage *providers.BackupStorageInfo, environment string) (service *corev1.Service, err error) {
	// Setup cert-manager
	err = SetupCertManager(ctx, cluster)
	if err != nil {
		return nil, err
	}

	// Setup ingress controller
	ingressNginx, err := SetupIngressController(ctx, cluster, environment)
	if err != nil {
		return nil, err
	}

	// Deploy PostgreSQL databases
	pgCluster, err := DeployPostgresDatabases(ctx, cluster, environment)
	if err != nil {
		return nil, err
	}

	// Deploy k8up backup operator
	err = DeployK8up(ctx, cluster, environment, backupStorage)
	if err != nil {
		return nil, err
	}

	// Deploy MCP Registry
	service, err = DeployMCPRegistry(ctx, cluster, environment, ingressNginx, pgCluster)
	if err != nil {
		return nil, err
	}

	// Deploy monitoring stack
	err = DeployMonitoringStack(ctx, cluster, environment, ingressNginx)
	if err != nil {
		return nil, err
	}

	return service, nil
}

```

## /deploy/pkg/providers/types.go

```go path="/deploy/pkg/providers/types.go" 
package providers

import (
	"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes"
	corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

// ProviderInfo represents the information returned by a cluster provider
type ProviderInfo struct {
	Name     pulumi.StringOutput
	Provider *kubernetes.Provider
}

// BackupStorageInfo represents S3-compatible backup storage configuration
type BackupStorageInfo struct {
	Endpoint    string // S3 endpoint (e.g., https://storage.googleapis.com or http://minio:9000)
	BucketName  string
	Credentials *corev1.Secret // Should contain AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
}

// ClusterProvider defines the interface that all cluster providers must implement
type ClusterProvider interface {
	// CreateCluster creates a Kubernetes cluster and returns provider info
	CreateCluster(ctx *pulumi.Context, environment string) (*ProviderInfo, error)

	// CreateBackupStorage creates backup storage infrastructure
	CreateBackupStorage(ctx *pulumi.Context, cluster *ProviderInfo, environment string) (*BackupStorageInfo, error)
}

```

## /docs/README.md

# MCP Registry Documentation

The MCP registry provides MCP clients with a list of MCP servers, like an app store for MCP servers.

## I want to...

- **📤 Publish my MCP server** → [Publishing Guide](modelcontextprotocol-io/quickstart.mdx)
- **📥 Consume registry data** → [API Usage Guide](modelcontextprotocol-io/registry-aggregators.mdx)
- **🔌 Understand the registry's purpose** → [Ecosystem vision](design/ecosystem-vision.md)
- **📋 Look up specific information** → [server.json spec](reference/server-json/generic-server-json.md) | [API spec](reference/api/generic-registry-api.md) | [CLI reference](reference/cli/commands.md)
- **🤝 Add reference to my community project** → [Community Projects](community-projects.md)

## Documentation Index

- 📄 [Public-facing docs](./modelcontextprotocol-io/) - Published on modelcontextprotocol.io
- 🏗️ [Design documentation](./design/) - Architecture, vision, and roadmap
- 📖 [Reference](./reference/) - Technical specifications
- 🔧 [Contributing guides](./contributing/) - How to contribute
- 🔒 [Administration](./administration/) - Admin operations


## /docs/design/dev-summit-2025-05-registry-goals-presentation.pdf

Binary file available at https://raw.githubusercontent.com/modelcontextprotocol/registry/refs/heads/main/docs/design/dev-summit-2025-05-registry-goals-presentation.pdf

## /docs/design/dev-summit-2025-10-registry-status-presentation.pdf

Binary file available at https://raw.githubusercontent.com/modelcontextprotocol/registry/refs/heads/main/docs/design/dev-summit-2025-10-registry-status-presentation.pdf


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!