Compare commits
117 changed files with 3827 additions and 5241 deletions
|
|
@ -11,11 +11,6 @@ MCP_REGISTRY_DATABASE_URL=postgres://username:password@localhost:5432/mcp-regist
|
|||
# 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
|
||||
|
||||
# GitHub OAuth configuration
|
||||
# These creds are for local development with the 'MCP Registry Login (Local)' GitHub App
|
||||
# They don't provide any real privileged access, hence why it's okay that they're here
|
||||
|
|
|
|||
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
|
@ -9,5 +9,5 @@ To publish an MCP server to the registry, use the `mcp-publisher` CLI tool. See
|
|||
## 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 check` to run lint, unit tests, and integration tests
|
||||
- Run `make dev-compose` to start the local development environment
|
||||
22
.github/dependabot.yml
vendored
22
.github/dependabot.yml
vendored
|
|
@ -3,28 +3,28 @@ updates:
|
|||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
groups:
|
||||
go-dependencies:
|
||||
opentelemetry:
|
||||
patterns:
|
||||
- "*"
|
||||
- "go.opentelemetry.io/*"
|
||||
schedule:
|
||||
interval: weekly
|
||||
interval: daily
|
||||
- package-ecosystem: gomod
|
||||
directory: /deploy
|
||||
groups:
|
||||
deploy-go-dependencies:
|
||||
opentelemetry:
|
||||
patterns:
|
||||
- "*"
|
||||
- "go.opentelemetry.io/*"
|
||||
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"
|
||||
|
|
|
|||
4
.github/workflows/cancel-pulumi-lock.yml
vendored
4
.github/workflows/cancel-pulumi-lock.yml
vendored
|
|
@ -24,10 +24,10 @@ jobs:
|
|||
environment: ${{ inputs.environment }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Setup Pulumi
|
||||
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
|
||||
uses: pulumi/actions@8582a9e8cc630786854029b4e09281acd6794b58
|
||||
with:
|
||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
||||
|
||||
|
|
|
|||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
|
@ -16,18 +16,18 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Run lint
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a
|
||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20
|
||||
with:
|
||||
version: v2.13.1
|
||||
version: v2.11.4
|
||||
|
||||
- name: Validate schemas and examples
|
||||
run: make validate
|
||||
|
|
@ -48,16 +48,16 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Set up ko
|
||||
uses: ko-build/setup-ko@v0.10
|
||||
uses: ko-build/setup-ko@v0.9
|
||||
|
||||
- name: Run all tests
|
||||
run: make test-all
|
||||
|
|
|
|||
2
.github/workflows/claude.yml
vendored
2
.github/workflows/claude.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
|
|
|||
151
.github/workflows/close-invalid-publish-prs.yml
vendored
151
.github/workflows/close-invalid-publish-prs.yml
vendored
|
|
@ -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"
|
||||
6
.github/workflows/deploy-production.yml
vendored
6
.github/workflows/deploy-production.yml
vendored
|
|
@ -23,16 +23,16 @@ jobs:
|
|||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Setup Pulumi
|
||||
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
|
||||
uses: pulumi/actions@8582a9e8cc630786854029b4e09281acd6794b58
|
||||
with:
|
||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
||||
|
||||
|
|
|
|||
14
.github/workflows/deploy-staging.yml
vendored
14
.github/workflows/deploy-staging.yml
vendored
|
|
@ -22,19 +22,19 @@ jobs:
|
|||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Set up ko
|
||||
uses: ko-build/setup-ko@v0.10
|
||||
uses: ko-build/setup-ko@v0.9
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
|
@ -70,16 +70,16 @@ jobs:
|
|||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Setup Pulumi
|
||||
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
|
||||
uses: pulumi/actions@8582a9e8cc630786854029b4e09281acd6794b58
|
||||
with:
|
||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
||||
|
||||
|
|
|
|||
96
.github/workflows/detect-invalid-publish-prs.yml
vendored
96
.github/workflows/detect-invalid-publish-prs.yml
vendored
|
|
@ -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
|
||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
|
|
@ -14,24 +14,24 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
|
||||
- name: Install Syft
|
||||
uses: anchore/sbom-action/download-syft@v0.24.2
|
||||
uses: anchore/sbom-action/download-syft@v0.24.0
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94
|
||||
uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: v2.12.0
|
||||
|
|
@ -45,19 +45,19 @@ jobs:
|
|||
needs: goreleaser
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: Set up ko
|
||||
uses: ko-build/setup-ko@v0.10
|
||||
uses: ko-build/setup-ko@v0.9
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
|
|
|||
4
.github/workflows/sync-schema.yml
vendored
4
.github/workflows/sync-schema.yml
vendored
|
|
@ -15,10 +15,10 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout static repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: modelcontextprotocol/static
|
||||
path: static-repo
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
0
CHANGES.md
Normal file
0
CHANGES.md
Normal file
|
|
@ -32,8 +32,7 @@ See the [README](README.md#quick-start) for prerequisites and instructions on ru
|
|||
### Running checks
|
||||
|
||||
```bash
|
||||
# Run lint, schema validation, and all tests
|
||||
# Note: this also runs `make dev-down`, stopping any running dev-compose environment
|
||||
# Run lint, unit tests and integration tests
|
||||
make check
|
||||
```
|
||||
|
||||
|
|
|
|||
19
README.md
19
README.md
|
|
@ -10,11 +10,11 @@ The MCP registry provides MCP clients with a list of MCP servers, like an app st
|
|||
|
||||
**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:
|
||||
- **Radoslav (Rado) Dimitrov** (Stacklok) [@rdimitrov](https://github.com/rdimitrov) - WG Lead
|
||||
Current key maintainers:
|
||||
- **Adam Jones** (Anthropic) [@domdomegg](https://github.com/domdomegg)
|
||||
- **Tadas Antanavicius** (PulseMCP) [@tadasant](https://github.com/tadasant)
|
||||
- **Bob Dickinson** (TeamSpark) [@BobDickinson](https://github.com/BobDickinson)
|
||||
- **Preeti (Pree) Dewani** (Ravenmail) [@pree-dew](https://github.com/pree-dew)
|
||||
- **Toby Padilla** (GitHub) [@toby](https://github.com/toby)
|
||||
- **Radoslav (Rado) Dimitrov** (Stacklok) [@rdimitrov](https://github.com/rdimitrov)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
@ -32,9 +32,9 @@ Often (but not always) ideas flow through this pipeline:
|
|||
#### Pre-requisites
|
||||
|
||||
- **Docker**
|
||||
- **Go** - version per the `go` directive in [go.mod](./go.mod); the Go toolchain fetches it for you
|
||||
- **Go 1.24.x**
|
||||
- **ko** - Container image builder for Go ([installation instructions](https://ko.build/install/))
|
||||
- **golangci-lint** - version per the `golangci-lint-action` pin in [ci.yml](./.github/workflows/ci.yml), so local lint matches CI
|
||||
- **golangci-lint v2.4.0**
|
||||
|
||||
#### Running the server
|
||||
|
||||
|
|
@ -94,8 +94,7 @@ See [the publisher guide](./docs/modelcontextprotocol-io/quickstart.mdx) for mor
|
|||
#### Other commands
|
||||
|
||||
```bash
|
||||
# Run lint, schema validation, and all tests
|
||||
# Note: this also runs `make dev-down`, stopping any running dev-compose environment
|
||||
# Run lint, unit tests and integration tests
|
||||
make check
|
||||
```
|
||||
|
||||
|
|
@ -111,8 +110,7 @@ For Claude and other AI tools: Always prefer make targets over custom commands w
|
|||
|
||||
```
|
||||
├── cmd/ # Application entry points
|
||||
│ ├── publisher/ # Server publishing tool
|
||||
│ └── registry/ # Registry API server
|
||||
│ └── publisher/ # Server publishing tool
|
||||
├── data/ # Seed data
|
||||
├── deploy/ # Deployment configuration (Pulumi)
|
||||
├── docs/ # Documentation
|
||||
|
|
@ -121,7 +119,6 @@ For Claude and other AI tools: Always prefer make targets over custom commands w
|
|||
│ ├── auth/ # Authentication (GitHub OAuth, JWT, namespace blocking)
|
||||
│ ├── config/ # Configuration management
|
||||
│ ├── database/ # Data persistence (PostgreSQL)
|
||||
│ ├── importer/ # Seed data import
|
||||
│ ├── service/ # Business logic
|
||||
│ ├── telemetry/ # Metrics and monitoring
|
||||
│ └── validators/ # Input validation
|
||||
|
|
|
|||
|
|
@ -14,12 +14,9 @@ make publisher
|
|||
make dev-compose # Start local registry
|
||||
./bin/mcp-publisher init
|
||||
./bin/mcp-publisher login none --registry=http://localhost:8080
|
||||
./bin/mcp-publisher publish
|
||||
./bin/mcp-publisher publish --registry=http://localhost:8080
|
||||
```
|
||||
|
||||
`publish` takes the registry URL from the token saved by `login`, so it accepts no `--registry`
|
||||
flag — passing one would be read as the `server.json` path.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Commands
|
||||
|
|
@ -27,7 +24,6 @@ flag — passing one would be read as the `server.json` path.
|
|||
- **`login`** - Handle authentication (github, dns, http, none)
|
||||
- **`publish`** - Validate and upload servers to registry
|
||||
- **`status`** - Update server lifecycle status (active, deprecated, deleted)
|
||||
- **`validate`** - Validate server.json without publishing
|
||||
- **`logout`** - Clear stored credentials
|
||||
|
||||
### Authentication Providers
|
||||
|
|
@ -48,3 +44,4 @@ Optional: enables `dns` and `http` methods to sign out-of-process without direct
|
|||
- **`main.go`** - CLI setup and command routing
|
||||
- **`commands/`** - Command implementations with auto-detection logic
|
||||
- **`auth/`** - Authentication provider implementations
|
||||
- **`build.sh`** - Cross-platform build script
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ func (d Signer) GetSignedTimestamp(ctx context.Context) (*string, []byte, error)
|
|||
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), //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
|
||||
Y: new(big.Int).SetBytes(keyResp.Key.Y), //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
|
||||
X: new(big.Int).SetBytes(keyResp.Key.X),
|
||||
Y: new(big.Int).SetBytes(keyResp.Key.Y),
|
||||
})
|
||||
|
||||
timestamp := auth.GetTimestamp()
|
||||
|
|
|
|||
|
|
@ -164,10 +164,10 @@ func parseRawPrivateKey(curve elliptic.Curve, privateKeyBytes []byte) (*ecdsa.Pr
|
|||
return &ecdsa.PrivateKey{
|
||||
PublicKey: ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: x, //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
|
||||
Y: y, //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
|
||||
X: x,
|
||||
Y: y,
|
||||
},
|
||||
D: d, //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
|
||||
D: d,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,31 +17,6 @@ const (
|
|||
GitHubAccessTokenURL = "https://github.com/login/oauth/access_token" // #nosec:G101
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultPollInterval is the initial device-flow polling interval in
|
||||
// seconds, per RFC 8628 §3.5.
|
||||
defaultPollInterval = 5
|
||||
// maxPollInterval caps how large the polling interval may grow. RFC 8628
|
||||
// §3.5 mandates a 5-second increase on each slow_down but leaves the
|
||||
// maximum to the implementation; this prevents a misbehaving auth server
|
||||
// from growing the interval unboundedly.
|
||||
maxPollInterval = 60
|
||||
// defaultExpiresIn is the device-code lifetime in seconds used when the
|
||||
// device-code response omits expires_in.
|
||||
defaultExpiresIn = 900
|
||||
// maxExpiresIn caps the device-code lifetime a response may claim. It sits
|
||||
// well past GitHub's documented 15 minutes; the cap stops a misbehaving or
|
||||
// hostile response from pushing the deadline (and the polling loop) far
|
||||
// into the future.
|
||||
maxExpiresIn = 3600
|
||||
// incorrectDeviceCodeGraceRetries is the total number of extra polls a
|
||||
// single login may spend on incorrect_device_code before giving up — a
|
||||
// budget for the whole login, not per occurrence. cli/cli#9302 reports the
|
||||
// same error for a code that had just been issued; a genuinely invalid code
|
||||
// still fails, only a few seconds later.
|
||||
incorrectDeviceCodeGraceRetries = 2
|
||||
)
|
||||
|
||||
// DeviceCodeResponse represents the response from GitHub's device code endpoint
|
||||
type DeviceCodeResponse struct {
|
||||
DeviceCode string `json:"device_code"`
|
||||
|
|
@ -57,38 +32,6 @@ type AccessTokenResponse struct {
|
|||
TokenType string `json:"token_type"`
|
||||
Scope string `json:"scope"`
|
||||
Error string `json:"error,omitempty"`
|
||||
// ErrorDescription and ErrorURI accompany Error in GitHub's responses;
|
||||
// surfacing them turns opaque failures like "incorrect_device_code" into
|
||||
// diagnosable reports.
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
ErrorURI string `json:"error_uri,omitempty"`
|
||||
}
|
||||
|
||||
// errorDetail renders Error together with any error_description and error_uri
|
||||
// GitHub returned, so failures reach the user diagnosable rather than opaque.
|
||||
func (r AccessTokenResponse) errorDetail() string {
|
||||
detail := r.Error
|
||||
if r.ErrorDescription != "" {
|
||||
detail += ": " + r.ErrorDescription
|
||||
}
|
||||
if r.ErrorURI != "" {
|
||||
detail += " (" + r.ErrorURI + ")"
|
||||
}
|
||||
return detail
|
||||
}
|
||||
|
||||
func decodeAccessTokenResponse(statusCode int, body []byte) (AccessTokenResponse, error) {
|
||||
var tokenResp AccessTokenResponse
|
||||
// OAuth errors may use HTTP 400 (RFC 6749 §5.2), not just HTTP 200.
|
||||
if statusCode != http.StatusOK && statusCode != http.StatusBadRequest {
|
||||
return tokenResp, fmt.Errorf("token endpoint returned %d: %s", statusCode, body)
|
||||
}
|
||||
|
||||
err := json.Unmarshal(body, &tokenResp)
|
||||
if statusCode == http.StatusBadRequest && (err != nil || tokenResp.Error == "") {
|
||||
return tokenResp, fmt.Errorf("token endpoint returned %d: %s", statusCode, body)
|
||||
}
|
||||
return tokenResp, err
|
||||
}
|
||||
|
||||
// RegistryTokenResponse represents the response from registry's token exchange endpoint
|
||||
|
|
@ -103,24 +46,6 @@ type GitHubATProvider struct {
|
|||
registryURL string
|
||||
providedToken string // Token provided via --token flag or MCP_GITHUB_TOKEN env var
|
||||
githubToken string // In-memory GitHub token set by Login()
|
||||
|
||||
// accessTokenURL is the GitHub access-token polling endpoint. It is a field
|
||||
// (rather than the package constant) so tests can point it at a mock server.
|
||||
accessTokenURL string
|
||||
// deviceCodeURL is the GitHub device-code endpoint, a field for the same
|
||||
// reason as accessTokenURL.
|
||||
deviceCodeURL string
|
||||
// pollInterval is the initial polling interval in seconds. Defaults to
|
||||
// defaultPollInterval; overridable in tests to avoid real delays. Updated
|
||||
// from the device-code response's interval when GitHub returns one.
|
||||
pollInterval int
|
||||
// expiresIn is the device-code lifetime in seconds. Defaults to
|
||||
// defaultExpiresIn; updated from the device-code response's expires_in
|
||||
// when GitHub returns one.
|
||||
expiresIn int
|
||||
// sleep abstracts time.Sleep so tests can run without real delays and
|
||||
// assert the back-off sequence. Defaults to time.Sleep.
|
||||
sleep func(time.Duration)
|
||||
}
|
||||
|
||||
// ServerHealthResponse represents the response from the health endpoint
|
||||
|
|
@ -137,13 +62,8 @@ func NewGitHubATProvider(registryURL, token string) Provider {
|
|||
}
|
||||
|
||||
return &GitHubATProvider{
|
||||
registryURL: registryURL,
|
||||
providedToken: token,
|
||||
accessTokenURL: GitHubAccessTokenURL,
|
||||
deviceCodeURL: GitHubDeviceCodeURL,
|
||||
pollInterval: defaultPollInterval,
|
||||
expiresIn: defaultExpiresIn,
|
||||
sleep: time.Sleep,
|
||||
registryURL: registryURL,
|
||||
providedToken: token,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +149,7 @@ func (g *GitHubATProvider) requestDeviceCode(ctx context.Context) (string, strin
|
|||
return "", "", "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, g.deviceCodeURL, bytes.NewBuffer(jsonData))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GitHubDeviceCodeURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
|
@ -258,21 +178,6 @@ func (g *GitHubATProvider) requestDeviceCode(ctx context.Context) (string, strin
|
|||
return "", "", "", err
|
||||
}
|
||||
|
||||
// Per RFC 8628 §3.2 interval and expires_in are bound to this device code,
|
||||
// so reset any pacing left over from a previous code before adopting them.
|
||||
// Clamp both: a hostile or buggy response must not grow the interval past
|
||||
// maxPollInterval (the invariant maxPollInterval exists for) nor push the
|
||||
// deadline unboundedly into the future, and clamping also keeps the
|
||||
// derived time.Duration well clear of int64 overflow.
|
||||
g.pollInterval = defaultPollInterval
|
||||
g.expiresIn = defaultExpiresIn
|
||||
if deviceCodeResp.Interval > 0 {
|
||||
g.pollInterval = min(deviceCodeResp.Interval, maxPollInterval)
|
||||
}
|
||||
if deviceCodeResp.ExpiresIn > 0 {
|
||||
g.expiresIn = min(deviceCodeResp.ExpiresIn, maxExpiresIn)
|
||||
}
|
||||
|
||||
return deviceCodeResp.DeviceCode, deviceCodeResp.UserCode, deviceCodeResp.VerificationURI, nil
|
||||
}
|
||||
|
||||
|
|
@ -293,28 +198,13 @@ func (g *GitHubATProvider) pollForToken(ctx context.Context, deviceCode string)
|
|||
return "", err
|
||||
}
|
||||
|
||||
// Pacing comes from the device-code response (via requestDeviceCode),
|
||||
// falling back to GitHub's documented defaults.
|
||||
interval := g.pollInterval
|
||||
expiresIn := g.expiresIn
|
||||
// Zero-value fallback: providers built directly rather than through
|
||||
// requestDeviceCode (the test seam) carry no lifetime. Keep this guard.
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = defaultExpiresIn
|
||||
}
|
||||
// Default polling interval and expiration time
|
||||
interval := 5 // seconds
|
||||
expiresIn := 900 // 15 minutes
|
||||
deadline := time.Now().Add(time.Duration(expiresIn) * time.Second)
|
||||
|
||||
graceRetries := 0
|
||||
// serverErrorInterval grows the wait between consecutive 5xx retries and is
|
||||
// reset by any other response.
|
||||
serverErrorInterval := interval
|
||||
// lastErr holds the most recent retryable failure so a deadline reached
|
||||
// mid-retry reports that diagnostic instead of a bare "timed out". It is
|
||||
// cleared whenever polling recovers, so a genuine user timeout after a
|
||||
// recovered blip still reports plainly.
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, g.accessTokenURL, bytes.NewBuffer(jsonData))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, GitHubAccessTokenURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -333,54 +223,20 @@ func (g *GitHubATProvider) pollForToken(ctx context.Context, deviceCode string)
|
|||
return "", err
|
||||
}
|
||||
|
||||
// A transient 5xx must not abandon a login the user may already have
|
||||
// approved in the browser. Each consecutive 5xx grows the wait by 5s up
|
||||
// to maxPollInterval, so a sustained outage costs a couple of dozen
|
||||
// requests rather than hundreds; the deadline bounds the number of
|
||||
// these retries.
|
||||
if resp.StatusCode >= http.StatusInternalServerError {
|
||||
lastErr = fmt.Errorf("token endpoint returned %d", resp.StatusCode)
|
||||
serverErrorInterval = min(serverErrorInterval+5, maxPollInterval)
|
||||
g.sleep(time.Duration(serverErrorInterval) * time.Second)
|
||||
continue
|
||||
}
|
||||
serverErrorInterval = interval
|
||||
|
||||
tokenResp, err := decodeAccessTokenResponse(resp.StatusCode, body)
|
||||
var tokenResp AccessTokenResponse
|
||||
err = json.Unmarshal(body, &tokenResp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Per RFC 8628 §3.5, both authorization_pending and slow_down indicate
|
||||
// the client should keep polling. slow_down additionally requires that
|
||||
// the polling interval be increased by 5 seconds.
|
||||
if tokenResp.Error == "authorization_pending" || tokenResp.Error == "slow_down" {
|
||||
if tokenResp.Error == "slow_down" {
|
||||
interval += 5
|
||||
if interval > maxPollInterval {
|
||||
interval = maxPollInterval
|
||||
}
|
||||
}
|
||||
lastErr = nil
|
||||
g.sleep(time.Duration(interval) * time.Second)
|
||||
if tokenResp.Error == "authorization_pending" {
|
||||
// User hasn't authorized yet, wait and retry
|
||||
time.Sleep(time.Duration(interval) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
if tokenResp.Error != "" {
|
||||
failure := fmt.Errorf("token request failed: %s", tokenResp.errorDetail())
|
||||
|
||||
// incorrect_device_code has been reported for codes that were in
|
||||
// fact valid (cli/cli#9302; cause never confirmed), so spend a
|
||||
// couple of grace polls before abandoning a login the user may
|
||||
// have approved.
|
||||
if tokenResp.Error == "incorrect_device_code" && graceRetries < incorrectDeviceCodeGraceRetries {
|
||||
graceRetries++
|
||||
lastErr = failure
|
||||
g.sleep(time.Duration(interval) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
return "", failure
|
||||
return "", fmt.Errorf("token request failed: %s", tokenResp.Error)
|
||||
}
|
||||
|
||||
if tokenResp.AccessToken != "" {
|
||||
|
|
@ -391,9 +247,6 @@ func (g *GitHubATProvider) pollForToken(ctx context.Context, deviceCode string)
|
|||
return "", fmt.Errorf("failed to obtain access token")
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return "", fmt.Errorf("device code authorization timed out; last response: %w", lastErr)
|
||||
}
|
||||
return "", fmt.Errorf("device code authorization timed out")
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue