Compare commits

..
236 changed files with 20082 additions and 28840 deletions

1
.dockerignore Normal file
View file

@ -0,0 +1 @@
.db

View file

@ -8,13 +8,7 @@ MCP_REGISTRY_VERSION=dev
MCP_REGISTRY_DATABASE_URL=postgres://username:password@localhost:5432/mcp-registry
# Path or URL to import seed data (supports local files and HTTP URLs)
# For offline development, use: data/seed.json
MCP_REGISTRY_SEED_FROM=https://registry.modelcontextprotocol.io/v0/servers
# Validate seed data against the registry rules on import (default: true)
# Set to false for offline development when seeding from a local file, since
# data/seed.json is not guaranteed to pass validation
MCP_REGISTRY_ENABLE_REGISTRY_VALIDATION=true
MCP_REGISTRY_SEED_FROM=data/seed.json
# GitHub OAuth configuration
# These creds are for local development with the 'MCP Registry Login (Local)' GitHub App
@ -32,14 +26,6 @@ MCP_REGISTRY_JWT_PRIVATE_KEY=bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb
# This should be disabled in prod
MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH=false
# GitHub OIDC token exchange (for `mcp-publisher login github-oidc`)
# Expected `aud` claim on incoming GitHub Actions OIDC tokens. Must equal the
# scheme + host that publishers pass via `--registry` (e.g. `https://registry.example.com`).
# The publisher CLI derives the audience from `--registry` automatically; the server
# rejects all OIDC exchanges if this is unset, so set it on every deployment that
# accepts GitHub Actions publishes.
MCP_REGISTRY_GITHUB_OIDC_AUDIENCE=
# Google Cloud Identity OIDC configuration for admin access
# Enable OIDC authentication for @modelcontextprotocol.io admin accounts
MCP_REGISTRY_OIDC_ENABLED=false

View file

@ -1,13 +0,0 @@
# 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, schema validation, and all tests (this also stops any running dev-compose environment)
- Run `make dev-compose` to start the local development environment

View file

@ -2,29 +2,17 @@ version: 2
updates:
- package-ecosystem: gomod
directory: /
groups:
go-dependencies:
patterns:
- "*"
schedule:
interval: weekly
- package-ecosystem: gomod
directory: /deploy
groups:
deploy-go-dependencies:
patterns:
- "*"
schedule:
interval: weekly
interval: daily
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
actions:
patterns:
- "*"
schedule:
interval: weekly
- package-ecosystem: docker
directory: /
interval: daily
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: weekly
interval: "daily"

View file

@ -24,10 +24,10 @@ jobs:
environment: ${{ inputs.environment }}
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Setup Pulumi
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
uses: pulumi/actions@cc7494be991dba0978f7ffafaf995b0449a0998e
with:
pulumi-version: ${{ env.PULUMI_VERSION }}
@ -58,19 +58,15 @@ jobs:
- 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
echo "${{ secrets.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
echo "${{ secrets.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
PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi cancel --stack gcpProd --yes

View file

@ -16,18 +16,32 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version: 'stable'
cache: true
go-version-file: 'go.mod'
- name: Cache Go modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.sha }}-${{ hashFiles('**/go.sum', '.golangci.yml') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
run: go mod download
- name: Install golangci-lint
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.4.0
- name: Run lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a
with:
version: v2.13.1
run: make lint
- name: Validate schemas and examples
run: make validate
@ -35,12 +49,10 @@ jobs:
- name: Build application
run: make build
- name: Run govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-input: 'stable'
go-package: ./...
repo-checkout: false
- name: Check for vulnerabilities
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
# All Tests
tests:
@ -48,22 +60,31 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version: 'stable'
cache: true
go-version-file: 'go.mod'
- name: Set up ko
uses: ko-build/setup-ko@v0.10
- name: Cache Go modules
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
run: go mod download
- name: Run all tests
run: make test-all
- name: Upload coverage artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: coverage-report
path: |

View file

@ -26,13 +26,13 @@ jobs:
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
@ -42,7 +42,10 @@ jobs:
# 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."
# Allow Claude to run bash
# This should be safe given the repo is already public
allowed_tools: "Bash"
custom_instructions: |
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.

View file

@ -1,151 +0,0 @@
name: Close invalid publish PRs
# Stage 2 of 2 (see detect-invalid-publish-prs.yml for stage 1).
#
# Runs in the trusted base-repo context (triggered by workflow_run, not by the
# PR itself), so it has a write token. It picks up the PR number flagged by
# stage 1 and, after independently re-validating the PR from the API, posts a
# comment pointing at the publishing docs, labels it `invalid`, and closes it.
#
# Trust boundary: the ONLY thing crossing from the untrusted stage is the PR
# number (sanitized to digits below). We re-derive every decision (state,
# author, changed files) here, so a wrong/stale artifact can never cause us to
# close a PR that doesn't actually match.
on:
workflow_run:
workflows: ["Detect invalid publish PRs"]
types: [completed]
permissions:
contents: read
pull-requests: write
issues: write # labels on PRs go through the issues API
actions: read # needed to list/download the stage-1 artifact
jobs:
close:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RUN_ID: ${{ github.event.workflow_run.id }}
COMMENT_BODY: |
Hi @__AUTHOR__ 👋 — thanks for your interest in the MCP Registry!
It looks like this PR is trying to publish an MCP server by adding or editing server files in this repository (under `servers/`, `data/servers/`, or in `data/seed.json`). That isn't how servers get published, so I'm closing this PR automatically.
**Servers are published with the [`mcp-publisher`](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx) CLI**, not by opening a pull request against this repo. The CLI verifies that you own your namespace and submits your `server.json` directly to the live registry API.
To publish your server, follow the **[Publishing Quickstart](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx)**. In short:
```bash
# Build the publisher CLI
make publisher
# Authenticate (e.g. via GitHub) and publish your server.json
./bin/mcp-publisher login github
./bin/mcp-publisher publish
```
Note: `data/seed.json` is seed data for **local development only** — adding entries there does not publish anything to the registry.
If you believe this was closed in error, please leave a comment and a maintainer will take a look. 🙇
_This is an automated message._
steps:
- name: Check for flagged-pr artifact
id: check
run: |
set -euo pipefail
names="$(gh api "repos/$GH_REPO/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[].name' || true)"
if printf '%s\n' "$names" | grep -qx "flagged-pr"; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "No flagged-pr artifact on run $RUN_ID; nothing to do."
echo "found=false" >> "$GITHUB_OUTPUT"
fi
- name: Download flagged PR number
if: steps.check.outputs.found == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: flagged-pr
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Re-validate and close
if: steps.check.outputs.found == 'true'
run: |
set -euo pipefail
# Sanitize: the only cross-boundary input. Keep digits only.
PR_NUMBER="$(tr -dc '0-9' < pr-number.txt)"
if [ -z "$PR_NUMBER" ]; then
echo "No valid PR number in artifact; skipping."
exit 0
fi
echo "Re-validating PR #$PR_NUMBER"
state="$(gh pr view "$PR_NUMBER" --json state --jq '.state')"
if [ "$state" != "OPEN" ]; then
echo "PR #$PR_NUMBER is $state, not OPEN; skipping."
exit 0
fi
# Use the REST endpoint: `gh pr view --json` does not expose an
# authorAssociation field, so calling it there exits non-zero and,
# under `set -euo pipefail`, aborts the job before we ever close.
assoc="$(gh api "repos/$GH_REPO/pulls/$PR_NUMBER" --jq '.author_association')"
case "$assoc" in
MEMBER|OWNER|COLLABORATOR)
echo "Author association '$assoc' is trusted; skipping."
exit 0
;;
esac
# Idempotency: if a maintainer reopens after we labeled it, leave it be.
# Capture into a var first so a transient gh failure can't fail open
# (an errored fetch piped to grep would otherwise look "not labeled").
labels="$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name' || true)"
if printf '%s\n' "$labels" | grep -qx "invalid"; then
echo "PR #$PR_NUMBER already labeled 'invalid'; skipping."
exit 0
fi
# Paginated REST endpoint (gh pr view --json files caps at 100, no pagination).
mapfile -t FILES < <(gh api --paginate "repos/$GH_REPO/pulls/$PR_NUMBER/files" --jq '.[].filename')
if [ "${#FILES[@]}" -eq 0 ]; then
echo "No files for PR #$PR_NUMBER; skipping."
exit 0
fi
# Keep this list in sync with the copy in stage 1.
publish_attempt=0
other=0
for f in "${FILES[@]}"; do
if [[ "$f" == servers/* \
|| "$f" == data/servers/* \
|| "$f" == data/data/servers/* \
|| "$f" =~ ^data/[^/]*server\.json$ \
|| "$f" == "data/seed.json" \
|| "$f" == "server.json" \
|| "$f" == "servers.json" ]]; then
publish_attempt=1
else
other=1
fi
done
if [ "$publish_attempt" -ne 1 ] || [ "$other" -ne 0 ]; then
echo "PR #$PR_NUMBER no longer matches the publish-attempt pattern; skipping."
exit 0
fi
author="$(gh pr view "$PR_NUMBER" --json author --jq '.author.login')"
body="${COMMENT_BODY//__AUTHOR__/$author}"
echo "Commenting on, labeling, and closing PR #$PR_NUMBER"
gh pr comment "$PR_NUMBER" --body "$body"
gh pr edit "$PR_NUMBER" --add-label "invalid" || echo "Could not add label; continuing."
gh pr close "$PR_NUMBER"

View file

@ -1,56 +0,0 @@
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
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

View file

@ -1,103 +0,0 @@
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
with:
go-version: 'stable'
cache: true
- name: Set up ko
uses: ko-build/setup-ko@v0.10
- name: Log in to Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
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

132
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,132 @@
name: Deploy
on:
push:
branches:
- main
permissions:
contents: read
env:
PULUMI_VERSION: "3.188.0"
jobs:
docker-push:
name: Build Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Log in to Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=main-{{date 'YYYYMMDD'}}-,enable={{is_default_branch}}
type=raw,value=main,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
environment: staging
needs: docker-push
concurrency:
group: deploy-staging
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Setup Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version-file: 'go.mod'
- name: Setup Pulumi
uses: pulumi/actions@cc7494be991dba0978f7ffafaf995b0449a0998e
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
run: |
echo "${{ secrets.PULUMI_STAGING_PASSPHRASE }}" > passphrase.staging.txt
make staging-up
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
environment: production
needs: deploy-staging
concurrency:
group: deploy-production
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Setup Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version-file: 'go.mod'
- name: Setup Pulumi
uses: pulumi/actions@cc7494be991dba0978f7ffafaf995b0449a0998e
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
run: |
echo "${{ secrets.PULUMI_PROD_PASSPHRASE }}" > passphrase.prod.txt
make prod-up

View file

@ -1,96 +0,0 @@
name: Detect invalid publish PRs
# Stage 1 of 2 (see close-invalid-publish-prs.yml for stage 2).
#
# Some contributors try to "publish" a server by opening a PR that adds files
# under servers/ or edits data/seed.json. That is not how publishing works -
# servers are published with the mcp-publisher CLI against the live registry
# API. This job only DETECTS such PRs. It runs from the (untrusted) PR context
# with a read-only token, so it cannot comment on or close anything. When it
# finds a match it records just the PR number as an artifact; the privileged
# stage-2 workflow picks that up via workflow_run and acts on it.
#
# We never check out or run the PR's code - we only read changed file paths.
on:
pull_request:
types: [opened, reopened]
permissions:
contents: read
pull-requests: read
jobs:
detect:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
steps:
- name: Classify PR
id: classify
run: |
set -euo pipefail
match=false
# Maintainers may legitimately touch these files (e.g. dev seed data),
# so never flag their PRs.
case "$AUTHOR_ASSOCIATION" in
MEMBER|OWNER|COLLABORATOR)
echo "Author association '$AUTHOR_ASSOCIATION' is trusted; not flagging."
;;
*)
# Use the paginated REST endpoint: `gh pr view --json files` caps
# at 100 files with no pagination, which could truncate a large PR
# and cause a false match.
mapfile -t FILES < <(gh api --paginate "repos/$GH_REPO/pulls/$PR_NUMBER/files" --jq '.[].filename')
if [ "${#FILES[@]}" -gt 0 ]; then
# Flag only if the PR touches *exclusively* publish-attempt files
# and nothing else, so that legit PRs touching seed data
# alongside real code are left alone.
#
# Keep this list in sync with the copy in stage 2. It stays an
# explicit list rather than a broad `**/server.json` glob:
# because we require *every* file to match, an over-broad
# pattern would auto-close a legitimate single-file PR.
# data/servers/** is here because #1524 put its server.json
# there - a natural guess next to data/seed.json - and slipped
# through the original servers/**-only list.
publish_attempt=0
other=0
for f in "${FILES[@]}"; do
if [[ "$f" == servers/* \
|| "$f" == data/servers/* \
|| "$f" == data/data/servers/* \
|| "$f" =~ ^data/[^/]*server\.json$ \
|| "$f" == "data/seed.json" \
|| "$f" == "server.json" \
|| "$f" == "servers.json" ]]; then
publish_attempt=1
else
echo "Non-publish file changed: $f"
other=1
fi
done
if [ "$publish_attempt" -eq 1 ] && [ "$other" -eq 0 ]; then
match=true
fi
fi
;;
esac
echo "PR #$PR_NUMBER match=$match"
echo "match=$match" >> "$GITHUB_OUTPUT"
if [ "$match" = "true" ]; then
echo "$PR_NUMBER" > pr-number.txt
fi
- name: Record flagged PR number
if: steps.classify.outputs.match == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4
with:
name: flagged-pr
path: pr-number.txt
retention-days: 1

View file

@ -14,24 +14,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version: 'stable'
cache: true
go-version-file: 'go.mod'
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62
- name: Install Syft
uses: anchore/sbom-action/download-syft@v0.24.2
uses: anchore/sbom-action/download-syft@v0.20.6
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
with:
distribution: goreleaser
version: v2.12.0
@ -39,48 +38,40 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ko-push:
name: Build and Push Image with ko
docker:
runs-on: ubuntu-latest
needs: goreleaser
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
with:
go-version: 'stable'
cache: true
- name: Set up ko
uses: ko-build/setup-ko@v0.10
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Log in to Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
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: Extract metadata
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
- 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
- name: Build and push Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,435 +0,0 @@
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

View file

@ -1,69 +0,0 @@
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@v7
- name: Checkout static repo
uses: actions/checkout@v7
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

2
.gitignore vendored
View file

@ -12,4 +12,4 @@ validate-schemas
coverage.out
coverage.html
deploy/infra/infra
registry
registry

View file

@ -22,7 +22,7 @@ linters:
- gocyclo
- godox
- gomoddirectives
- gomodguard_v2
- gomodguard
- goprintffuncname
- gosec
- grouper
@ -55,10 +55,6 @@ linters:
max-complexity: 20
dupl:
threshold: 300
goconst:
# Repeated literals in tests are fixture data ("1.0.0", "Test server", ...);
# hoisting them into constants hurts readability more than it helps.
ignore-tests: true
gocognit:
min-complexity: 51
nestif:
@ -70,18 +66,11 @@ linters:
- 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:

View file

@ -95,35 +95,37 @@ signs:
cmd: cosign
args:
- "sign-blob"
- "--bundle=${signature}" # cosign v3+: bundles signature and certificate together
- "--output-signature=${signature}"
- "--output-certificate=${certificate}"
- "${artifact}"
- "--yes" # needed on cosign 2.0.0+
artifacts: archive
output: true
signature: "${artifact}.sigstore.json"
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
# Also sign checksums file for additional verification
- id: checksums
cmd: cosign
args:
- "sign-blob"
- "--bundle=${signature}" # cosign v3+: bundles signature and certificate together
- "--output-signature=${signature}"
- "--output-certificate=${certificate}"
- "${artifact}"
- "--yes"
artifacts: checksum
output: true
signature: "${artifact}.sigstore.json"
certificate: '{{ trimsuffix .Env.artifact ".txt" }}.pem'
# This section defines the release format.
archives:
# Registry server archive
- id: registry
name_template: "registry_{{ .Os }}_{{ .Arch }}"
name_template: "registry_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
ids:
- registry
# Publisher CLI archive
- id: publisher
name_template: "mcp-publisher_{{ .Os }}_{{ .Arch }}"
name_template: "mcp-publisher_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
ids:
- publisher

View file

@ -1,52 +0,0 @@
# 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

View file

@ -1,32 +0,0 @@
# 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

Some files were not shown because too many files have changed in this diff Show more